Introdução Técnica
Em aplicativos de exercício físico, a funcionalidade de reprodução de trajetória é crucial para melhorar a experiência do usuário. Este recurso permite visualizar dinamicamente a rota percorrida durante uma atividade física, adicionando interatividade e imersão. Neste artigo, exploraremos como desenvolver um efeito de reprodução de trajetória semelhante ao observado em aplicativos populares, utilizando o sistema HarmonyOS.
Funcionalidades Principais
Para implementar a reprodução de trajetória, é necessário abordar dois copmonentes técnicos essenciais:
- Reprodução Dinâmica da Trajetória: Uso de temporizadores e interpolação linear para simular o movimento ao longo da rota.
- Interação com o Mapa: Atualização contínua do centro do mapa e ângulo de rotação para manter o foco no ponto atual.
Implementação da Reprodução Dinâmica
Lógica de Reprodução
A reprodução da trajetória é controlada por um temporizador principal que avança pelos pontos da rota. Para cada segmento, um temporizador secundário gerencia a animação suave do marcader móvel.
private iniciarReproducao() {
// Verifica se já está em reprodução e interrompe, se necessário
if (this.temporizadorPrincipal) {
this.controladorMapa?.removerSobreposicao(this.polilinha);
clearInterval(this.temporizadorPrincipal);
this.temporizadorPrincipal = undefined;
if (this.temporizadorAnimacao) {
clearInterval(this.temporizadorAnimacao);
}
if (this.marcadorMovel) {
this.controladorMapa?.removerSobreposicao(this.marcadorMovel);
this.marcadorMovel = undefined;
}
this.indicePontoAtual = 0;
return;
}
// Cria o marcador móvel na posição inicial
this.marcadorMovel = new Marcador({
posicao: this.pontosRota[0],
icone: new EntidadeImagem("rawfile://imagens/marcador_inicio.png"),
evitarColisao: EnumSistema.ComportamentoColisao.NAO_COLIDIR,
localizacao: EnumSistema.Localizacao.CENTRO
});
this.controladorMapa?.adicionarSobreposicao(this.marcadorMovel);
// Inicia a reprodução com intervalo de 100ms
this.temporizadorPrincipal = setInterval(() => {
this.indicePontoAtual++;
if (this.indicePontoAtual >= this.pontosRota.length) {
clearInterval(this.temporizadorPrincipal);
this.temporizadorPrincipal = undefined;
this.indicePontoAtual = 0;
if (this.marcadorMovel) {
this.controladorMapa?.removerSobreposicao(this.marcadorMovel);
this.marcadorMovel = undefined;
}
return;
}
// Executa a animação de movimento suave entre pontos adjacentes
if (this.marcadorMovel && this.indicePontoAtual < this.pontosRota.length - 1) {
const pontoAtual = this.pontosRota[this.indicePontoAtual];
const pontoProximo = this.pontosRota[this.indicePontoAtual + 1];
let progressoAnimacao = 0;
if (this.temporizadorAnimacao) {
clearInterval(this.temporizadorAnimacao);
}
this.temporizadorAnimacao = setInterval(() => {
progressoAnimacao += 0.1;
if (progressoAnimacao >= 1) {
clearInterval(this.temporizadorAnimacao);
this.temporizadorAnimacao = undefined;
this.marcadorMovel?.definirPosicao(new Coordenada(pontoProximo.lat, pontoProximo.lng));
} else {
const latInterpolada = pontoAtual.lat + (pontoProximo.lat - pontoAtual.lat) * progressoAnimacao;
const lngInterpolada = pontoAtual.lng + (pontoProximo.lng - pontoAtual.lng) * progressoAnimacao;
this.marcadorMovel?.definirPosicao(new Coordenada(latInterpolada, lngInterpolada));
}
}, 10);
}
// Atualiza a polilinha da trajetória até o ponto atual
const pontosAtuais = this.pontosRota.slice(0, this.indicePontoAtual + 1);
const coresGradiente = FerramentaGradiente.obterCoresRota(this.gravacao!.pontos.slice(0, this.indicePontoAtual + 1), 100);
if (this.polilinha) {
this.controladorMapa?.removerSobreposicao(this.polilinha);
this.polilinha.remover();
this.polilinha.destruir();
}
this.polilinha = new Polilinha({
pontos: pontosAtuais,
espessura: 5,
juncao: EnumSistema.TipoJuncaoLinha.REDONDO,
terminal: EnumSistema.TipoTerminalLinha.REDONDO,
gradiente: true,
listaCores: coresGradiente!
});
this.controladorMapa?.adicionarSobreposicao(this.polilinha);
// Ajusta a visualização do mapa
this.atualizarVisualizacaoMapa();
}, 100);
}
Efeito de Animação
A interpolação linear entre coordenadas consecutivas cria uma transição suave para o marcador. O progresso é incremnetado a cada 10ms, resultando em um movimento fluido.
private executarAnimacao(pontoAtual, pontoProximo) {
let progresso = 0;
if (this.temporizadorAnimacao) {
clearInterval(this.temporizadorAnimacao);
}
this.temporizadorAnimacao = setInterval(() => {
progresso += 0.1;
if (progresso >= 1) {
clearInterval(this.temporizadorAnimacao);
this.marcadorMovel?.definirPosicao(new Coordenada(pontoProximo.lat, pontoProximo.lng));
} else {
const lat = pontoAtual.lat + (pontoProximo.lat - pontoAtual.lat) * progresso;
const lng = pontoAtual.lng + (pontoProximo.lng - pontoAtual.lng) * progresso;
this.marcadorMovel?.definirPosicao(new Coordenada(lat, lng));
}
}, 10);
}
Interação com o Mapa
Para manter o usuário orientado, o mapa é atualizado dinamicamente. O centro é deslocado para o ponto atual e a rotação é calculada com base na direção entre pontos adjacentes.
private atualizarVisualizacaoMapa() {
let anguloRotacao = 0;
if (this.indicePontoAtual < this.pontosRota.length - 1) {
const pontoAtual = this.pontosRota[this.indicePontoAtual];
const pontoProximo = this.pontosRota[this.indicePontoAtual + 1];
anguloRotacao = Math.atan2(
pontoProximo.lat - pontoAtual.lat,
pontoProximo.lng - pontoAtual.lng
) * 180 / Math.PI;
anguloRotacao = (anguloRotacao + 360) % 360;
anguloRotacao = (360 - anguloRotacao + 90) % 360;
}
this.controladorMapa?.statusMapa
.definirRotacao(anguloRotacao)
.definirInclinacao(90)
.definirCentro(new Coordenada(this.pontosRota[this.indicePontoAtual].lat, this.pontosRota[this.indicePontoAtual].lng))
.atualizar();
}