Nesta página, mostramos exemplos de como usar diferentes APIs hápticas para criar efeitos personalizados além das formas de onda de vibração padrão em um app Android.
Esta página inclui os seguintes exemplos:
- Composições ancoradas na linha do tempo com
VibrationEffect.Builder- Compor com predefinições: sequencie sensações táteis predefinidas.
- Compor com envelopes e predefinições: combine envelopes e predefinições em uma linha do tempo.
- Reutilizar e mover eventos: Mova e reutilize eventos de composição atuais.
- Composições repetidas: crie efeitos de linha do tempo repetidos.
- Padrões de vibração personalizados
- Padrão de aumento: um padrão que começa de forma suave.
- Padrão repetido: um padrão sem fim.
- Padrão com substituição: uma demonstração de substituição.
- Composições de primitivas de vibração
- Resistência: um efeito de arrastar com intensidade dinâmica.
- Expandir: um efeito de aumento e queda.
- Tremor: um efeito de tremor usando a primitiva
SPIN. - Rebote: um efeito de rebatida usando a primitiva
THUD.
- Forma de onda de vibração com envelopes
- Mola saltitante: um efeito de mola saltitante usando efeitos básicos de envelope.
- Lançamento de foguete: um efeito de lançamento de foguete usando efeitos de envelope de forma de onda.
Para mais exemplos, consulte Adicionar retorno tátil a eventos e siga sempre os princípios de design de háptica.
Usar substituições para lidar com a compatibilidade de dispositivos
Ao implementar efeitos hápticos personalizados, a compatibilidade do dispositivo e o comportamento de substituição dependem da plataforma da API escolhida:
VibrationEffect.Builder(recomendado): a partir do Android 16 (4º trimestre de 2026), os efeitos criados usandoVibrationEffect.Builderincluem fallback automático no nível do framework. Se um dispositivo não oferecer suporte nativo a umPresetouEnvelopebásico solicitado, o framework vai traduzir automaticamente para uma alternativa adequada no momento da reprodução da melhor maneira possível. Não é necessário verificar manualmente os recursos de dispositivos primitivos antes de reproduzir efeitos compostos comVibrationEffect.Builder.- Exceção:os envelopes de forma de onda avançados criados com
WaveformEnvelopeBuildernão oferecem suporte a fallback automático porque dependem de mapeamentos de frequência de hardware específicos (FOAM). Se não forem compatíveis, eles não serão reproduzidos.
- Exceção:os envelopes de forma de onda avançados criados com
VibrationEffect.Composition:as composições criadas com a APIstartComposition()não têm substituições automáticas. Se uma composição tiver apenas uma primitiva sem suporte, toda a vibração não será reproduzida. Verifique manualmente os recursos usandovibrator.arePrimitivesSupported().- Formas de onda com controle de amplitude:amplitudes diferentes de zero são arredondadas para 100% em dispositivos sem controle de amplitude. Verifique
vibrator.hasAmplitudeControl()e volte para um padrão ON/OFF explicitamente projetado, se necessário.
Uso de primitivas hápticas
O Android inclui várias primitivas de retorno tátil que variam em amplitude e frequência. Você pode usar uma primitiva sozinha ou várias em combinação para conseguir efeitos hápticos avançados.
- Use atrasos de 50 ms ou mais para lacunas perceptíveis entre duas primitivas, também considerando a duração da primitiva, se possível.
- Use escalas que diferem em uma proporção de 1,4 ou mais para que a diferença de intensidade seja melhor percebida.
Use escalas de 0,5, 0,7 e 1,0 para criar uma versão de baixa, média e alta intensidade de uma primitiva.
Criar padrões de vibração personalizados
Os padrões de vibração são usados com frequência em háptica atencional, como notificações
e toques. O serviço Vibrator pode reproduzir padrões de vibração longos
que mudam a amplitude da vibração ao longo do tempo. Esses efeitos são chamados de
formas de onda.
Os efeitos de forma de onda geralmente são perceptíveis, mas vibrações longas e repentinas podem assustar o usuário se forem reproduzidas em um ambiente silencioso. Aumentar muito rápido para uma amplitude de destino também pode produzir ruídos audíveis. Projete padrões de forma de onda para suavizar as transições de amplitude e criar efeitos de aumento e redução.
Exemplos de padrões de vibração
Confira alguns exemplos de padrões de vibração nas seções a seguir:
Padrão de otimização
As formas de onda são representadas como VibrationEffect com três parâmetros:
- Timings:uma matriz de durações, em milissegundos, para cada segmento de forma de onda.
- Amplitudes:a amplitude de vibração desejada para cada duração especificada no primeiro argumento, representada por um valor inteiro de 0 a 255, sendo 0 o "estado desligado" do vibrador e 255 a amplitude máxima do dispositivo.
- Índice de repetição:o índice na matriz especificada no primeiro argumento para começar a repetir a forma de onda ou -1 se o padrão precisar ser reproduzido apenas uma vez.
Este é um exemplo de forma de onda que pulsa duas vezes com uma pausa de 350 ms entre os pulsos. O primeiro pulso é um aumento suave até a amplitude máxima, e o segundo é um aumento rápido para manter a amplitude máxima. A parada no final é definida pelo valor negativo do índice de repetição.
Kotlin
val timings: LongArray = longArrayOf(
50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200)
val amplitudes: IntArray = intArrayOf(
33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255)
val repeatIndex = -1 // Don't repeat.
vibrator.vibrate(VibrationEffect.createWaveform(
timings, amplitudes, repeatIndex))
Java
long[] timings = new long[] {
50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200 };
int[] amplitudes = new int[] {
33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255 };
int repeatIndex = -1; // Don't repeat.
vibrator.vibrate(VibrationEffect.createWaveform(
timings, amplitudes, repeatIndex));
Padrão repetido
As formas de onda também podem ser reproduzidas repetidamente até serem canceladas. Para criar uma forma de onda repetida, defina um parâmetro repeat não negativo. Quando você toca uma
forma de onda repetida, a vibração continua até ser explicitamente cancelada no
serviço:
Kotlin
void startVibrating() {
val timings: LongArray = longArrayOf(50, 50, 100, 50, 50)
val amplitudes: IntArray = intArrayOf(64, 128, 255, 128, 64)
val repeat = 1 // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
timings, amplitudes, repeat)
// repeatingEffect can be used in multiple places.
vibrator.vibrate(repeatingEffect)
}
void stopVibrating() {
vibrator.cancel()
}
Java
void startVibrating() {
long[] timings = new long[] { 50, 50, 100, 50, 50 };
int[] amplitudes = new int[] { 64, 128, 255, 128, 64 };
int repeat = 1; // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
timings, amplitudes, repeat);
// repeatingEffect can be used in multiple places.
vibrator.vibrate(repeatingEffect);
}
void stopVibrating() {
vibrator.cancel();
}
Isso é muito útil para eventos intermitentes que exigem uma ação do usuário para confirmá-los. Alguns exemplos são ligações recebidas e alarmes acionados.
Padrão com substituição
Controlar a amplitude de uma vibração é um recurso dependente de hardware. Reproduzir uma forma de onda em um dispositivo mais simples sem esse recurso faz com que ele vibre na amplitude máxima para cada entrada positiva na matriz de amplitude. Se o app precisar acomodar esses dispositivos, use um padrão que não gere um efeito de zumbido quando reproduzido nessa condição ou crie um padrão ON/OFF mais simples que possa ser reproduzido como um substituto.
Kotlin
if (vibrator.hasAmplitudeControl()) {
vibrator.vibrate(VibrationEffect.createWaveform(
smoothTimings, amplitudes, smoothRepeatIdx))
} else {
vibrator.vibrate(VibrationEffect.createWaveform(
onOffTimings, onOffRepeatIdx))
}
Java
if (vibrator.hasAmplitudeControl()) {
vibrator.vibrate(VibrationEffect.createWaveform(
smoothTimings, amplitudes, smoothRepeatIdx));
} else {
vibrator.vibrate(VibrationEffect.createWaveform(
onOffTimings, onOffRepeatIdx));
}
Composições ancoradas na linha do tempo com VibrationEffect.Builder
A partir do Android 16 (4º trimestre de 2026), VibrationEffect.Builder é a API preferida para criar efeitos e composições de vibração complexos. Ele
permite criar sensações táteis expressivas sequenciando elementos táteis
discretos ao longo de uma linha do tempo absoluta com startTimeMillis.
O VibrationEffect.Builder permite combinar vários tipos de vibração:
- Predefinições:pulsos táteis predefinidos (
VibrationEffect.Preset), como cliques e tiques. - Envelopes:formas de onda contínuas dinâmicas, incluindo envelopes básicos independentes de hardware (
BasicEnvelopeBuilder) e envelopes avançados modulados por frequência (WaveformEnvelopeBuilder). - VibrationEffects atuais:composições primitivas
(
VibrationEffect.Composition), formas de onda de etapa (VibrationEffect.createWaveform), efeitos predefinidos (VibrationEffect.createPredefined) e ações únicas (VibrationEffect.createOneShot) importadas usandoaddEvents()ou cópia de construtor. - Sequências repetidas:padrões contínuos configurados com
setRepeatingEffect().
As vibrações criadas com VibrationEffect.Builder têm um fallback automático no nível da estrutura em todos os elementos (incluindo predefinições, envelopes básicos e efeitos de vibração combinados), garantindo uma experiência do usuário consistente em diferentes dispositivos sem exigir verificações manuais de capacidade. Os envelopes de forma de onda avançados criados com
WaveformEnvelopeBuilder exigem suporte de hardware e não têm suporte de
fallback automático.
Criar com predefinições
Use VibrationEffect.Preset para adicionar pulsos táteis curtos comuns e predefinidos
(como PRESET_CLICK, PRESET_TICK ou PRESET_LOW_TICK) a uma composição.
As predefinições são implementadas pelo fabricante do dispositivo para oferecer uma vibração nítida, curta e agradável que se alinha aos princípios de retorno tátil para retorno tátil claro. Para mais detalhes sobre essas funcionalidades e como elas funcionam, consulte
Noções básicas sobre atuadores de vibração.
As predefinições substituem os primitivos curtos da API VibrationEffect.Composition.
Para sensações táteis mais longas ou contínuas, como aumento e diminuição, use
formas de onda de envelope (PWLE).
Cada predefinição pode receber uma escala opcional entre 0.0f e 1.0f e é colocada em um horário de início explícito (em milissegundos) desde o início da composição.
Kotlin
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
)
val tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
)
val effect = VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build()
vibrator.vibrate(effect)
Java
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
);
VibrationEffect.Preset tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
);
VibrationEffect effect = new VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build();
vibrator.vibrate(effect);
Componha com envelopes e predefinições
Você pode combinar instâncias VibrationEffect.Envelope (criadas
usando BasicEnvelopeBuilder ou WaveformEnvelopeBuilder) com
predefinições para criar padrões hápticos avançados e de vários segmentos.
Confira um exemplo que toca um envelope de aumento e redução gradual seguido por um preset de clique nítido:
Kotlin
val basicEnvelope = VibrationEffect.Envelope.create(
VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
)
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
)
val mixedEffect = VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build()
vibrator.vibrate(mixedEffect)
Java
VibrationEffect.Envelope basicEnvelope = VibrationEffect.Envelope.create(
new VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
);
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
);
VibrationEffect mixedEffect = new VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build();
vibrator.vibrate(mixedEffect);
Reutilizar e mudar eventos
Para reutilizar ou concatenar um VibrationEffect (incluindo
VibrationEffect.Composition) atual, recupere a lista de objetos
VibrationEffect.Event usando getEvents() e adicione-os com
um deslocamento usando addEvents(startTimeShiftMillis, events) ou transmita o efeito
diretamente ao construtor VibrationEffect.Builder(effect). Ao importar instâncias VibrationEffect.Composition dessa forma, o framework converte automaticamente as primitivas em predefinições, permitindo o suporte de fallback em tempo de execução.
Kotlin
val existingEffect = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build()
// Shift and append the existing events 200ms into the new composition.
val combinedEffect = VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.events)
.build()
vibrator.vibrate(combinedEffect)
Java
VibrationEffect existingEffect = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build();
// Shift and append the existing events 200ms into the new composition.
VibrationEffect combinedEffect = new VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.getEvents())
.build();
vibrator.vibrate(combinedEffect);
Criar composições repetidas
Use setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis)
para adicionar um padrão repetido a uma composição:
Kotlin
val repeatingPattern = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build()
val repeatingEffect = VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build()
vibrator.vibrate(repeatingEffect)
Java
VibrationEffect repeatingPattern = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build();
VibrationEffect repeatingEffect = new VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build();
vibrator.vibrate(repeatingEffect);
Gerenciamento de tempo, validação e deriva
Ao criar composições com VibrationEffect.Builder, lembre-se das seguintes regras de validação e de tempo:
- Horários de início estritamente crescentes:todo elemento adicionado ao builder precisa
ter um
startTimeMillisestritamente maior ou igual ao horário de início do elemento anterior. - Validação no momento da build:o builder realiza a validação de melhor esforço no momento
build()usando durações de elementos conhecidas ou um mínimo de 1 ms para predefinições. Se uma sobreposição impossível for detectada, umIllegalArgumentExceptionserá gerado. - Mudança sequencial de reprodução:se um elemento de vibração anterior ainda estiver sendo reproduzido fisicamente quando chegar a hora de início do próximo elemento, o framework vai mudar automaticamente o próximo elemento para o primeiro intervalo de tempo disponível. Isso garante que os eventos não se sobreponham e que nenhuma vibração seja descartada, embora possa introduzir um pequeno desvio de tempo se os eventos forem programados muito próximos. Para minimizar o desvio, aguarde tempo suficiente (por exemplo, 50 ms ou mais) entre eventos hápticos consecutivos.
Criar composições de primitivas de vibração
Esta seção mostra como compor vibrações usando
VibrationEffect.Composition. Os primitivos de composição, descritos
anteriormente nesta página, são implementados pelo fabricante do dispositivo. Elas oferecem uma vibração nítida, curta e agradável que se alinha aos princípios de retorno tátil para um retorno tátil claro. Para mais detalhes sobre essas funcionalidades e como elas funcionam,
consulte Noções básicas sobre atuadores de vibração.
Ao contrário do VibrationEffect.Builder, a API VibrationEffect.Composition
não tem substituições automáticas para primitivos não compatíveis. Assim:
Antes de ativar o retorno tátil avançado, verifique se um determinado dispositivo é compatível com todas as primitivas que você está usando.
Desative o conjunto consistente de experiências que não são compatíveis, não apenas os efeitos que não têm uma primitiva.
- Kotlin:
val fallbackEffect = VibrationEffect.Builder(compositionEffect).build()(ou.addEvents(0L, compositionEffect.events)) - Java:
VibrationEffect fallbackEffect =new VibrationEffect.Builder(compositionEffect).build();(ou.addEvents(0L, compositionEffect.getEvents()))
Quando construído com VibrationEffect.Builder, o framework converte os
primitivos de composição em predefinições e fornece automaticamente um fallback de tempo de execução
se algum primitivo não for compatível com o dispositivo do usuário.
Efeitos de vibração compostos
É possível criar efeitos de vibração compostos com
VibrationEffect.Composition. Confira um exemplo de um efeito que aumenta lentamente seguido por um clique brusco:
Kotlin
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK
).compose()
)
Java
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
.compose());
Uma composição é criada adicionando primitivos para serem reproduzidos em sequência. Cada primitivo também é escalonável, então você pode controlar a amplitude da vibração gerada por cada um deles. A escala é definida como um valor entre 0 e 1, em que 0 corresponde a uma amplitude mínima em que essa primitiva pode ser (mal) sentida pelo usuário.
Criar variantes em primitivos de vibração
Se quiser criar uma versão fraca e forte da mesma primitiva, crie proporções de intensidade de 1, 4 ou mais para que a diferença de intensidade possa ser facilmente percebida. Não tente criar mais de três níveis de intensidade da mesma primitiva, porque eles não são perceptualmente distintos. Por exemplo, use escalas de 0,5, 0,7 e 1,0 para criar versões de baixa, média e alta intensidade de uma primitiva.
Adicionar intervalos entre primitivas de vibração
A composição também pode especificar atrasos a serem adicionados entre primitivas consecutivas. Esse atraso é expresso em milissegundos desde o fim da primitiva anterior. Em geral, uma diferença de 5 a 10 ms entre duas primitivas é muito pequena para ser detectada. Use uma lacuna da ordem de 50 ms ou mais se quiser criar uma lacuna perceptível entre duas primitivas. Confira um exemplo de composição com atrasos:
Kotlin
val delayMs = 100
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs
).compose()
)
Java
int delayMs = 100;
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f)
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs)
.compose());
Verificar quais primitivos são compatíveis
As APIs a seguir podem ser usadas para verificar o suporte do dispositivo a primitivas específicas:
Kotlin
val primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK
if (vibrator.areAllPrimitivesSupported(primitive)) {
vibrator.vibrate(VibrationEffect.startComposition()
.addPrimitive(primitive).compose())
} else {
// Play a predefined effect or custom pattern as a fallback.
}
Java
int primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK;
if (vibrator.areAllPrimitivesSupported(primitive)) {
vibrator.vibrate(VibrationEffect.startComposition()
.addPrimitive(primitive).compose());
} else {
// Play a predefined effect or custom pattern as a fallback.
}
Também é possível verificar várias primitivas e decidir quais delas compor com base no nível de suporte do dispositivo:
Kotlin
val effects: IntArray = intArrayOf(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
VibrationEffect.Composition.PRIMITIVE_TICK,
VibrationEffect.Composition.PRIMITIVE_CLICK
)
val supported: BooleanArray = vibrator.arePrimitivesSupported(primitives)
Java
int[] primitives = new int[] {
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
VibrationEffect.Composition.PRIMITIVE_TICK,
VibrationEffect.Composition.PRIMITIVE_CLICK
};
boolean[] supported = vibrator.arePrimitivesSupported(effects);
Exemplos de composições de vibração
As seções a seguir fornecem vários exemplos de composições de vibração, extraídos do app de exemplo de haptics no GitHub.
Resistência (com poucos tiques)
É possível controlar a amplitude da vibração primitiva para transmitir feedback útil a uma ação em andamento. Valores de escala muito próximos podem ser usados para criar um efeito de crescendo suave de uma primitiva. O atraso entre primitivos consecutivas também pode ser definido dinamicamente com base na interação do usuário. Isso é ilustrado no exemplo a seguir de uma animação de visualização controlada por um gesto de arrastar e aprimorada com retorno tátil.
Figura 1. Essa forma de onda representa a aceleração da saída da vibração em um dispositivo.
Kotlin
@Composable
fun ResistScreen() {
// Control variables for the dragging of the indicator.
var isDragging by remember { mutableStateOf(false) }
var dragOffset by remember { mutableStateOf(0f) }
// Only vibrates while the user is dragging
if (isDragging) {
LaunchedEffect(Unit) {
// Continuously run the effect for vibration to occur even when the view
// is not being drawn, when user stops dragging midway through gesture.
while (true) {
// Calculate the interval inversely proportional to the drag offset.
val vibrationInterval = calculateVibrationInterval(dragOffset)
// Calculate the scale directly proportional to the drag offset.
val vibrationScale = calculateVibrationScale(dragOffset)
delay(vibrationInterval)
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
vibrationScale
).compose()
)
}
}
}
Screen() {
Column(
Modifier
.draggable(
orientation = Orientation.Vertical,
onDragStarted = {
isDragging = true
},
onDragStopped = {
isDragging = false
},
state = rememberDraggableState { delta ->
dragOffset += delta
}
)
) {
// Build the indicator UI based on how much the user has dragged it.
ResistIndicator(dragOffset)
}
}
}
Java
class DragListener implements View.OnTouchListener {
// Control variables for the dragging of the indicator.
private int startY;
private int vibrationInterval;
private float vibrationScale;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startY = event.getRawY();
vibrationInterval = calculateVibrationInterval(0);
vibrationScale = calculateVibrationScale(0);
startVibration();
break;
case MotionEvent.ACTION_MOVE:
float dragOffset = event.getRawY() - startY;
// Calculate the interval inversely proportional to the drag offset.
vibrationInterval = calculateVibrationInterval(dragOffset);
// Calculate the scale directly proportional to the drag offset.
vibrationScale = calculateVibrationScale(dragOffset);
// Build the indicator UI based on how much the user has dragged it.
updateIndicator(dragOffset);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// Only vibrates while the user is dragging
cancelVibration();
break;
}
return true;
}
private void startVibration() {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
vibrationScale)
.compose());
// Continuously run the effect for vibration to occur even when the view
// is not being drawn, when user stops dragging midway through gesture.
handler.postDelayed(this::startVibration, vibrationInterval);
}
private void cancelVibration() {
handler.removeCallbacksAndMessages(null);
}
}
Expandir (com aumento e diminuição)
Há duas primitivas para aumentar a intensidade da vibração percebida: PRIMITIVE_QUICK_RISE e PRIMITIVE_SLOW_RISE. Ambos alcançam
a mesma meta, mas com durações diferentes. Há apenas uma primitiva para redução gradual, PRIMITIVE_QUICK_FALL. Essas primitivas funcionam melhor
juntas para criar um segmento de forma de onda que aumenta de intensidade e depois desaparece.
Você pode alinhar primitivas escalonadas para evitar saltos repentinos na amplitude entre
elas, o que também funciona bem para estender a duração geral do efeito.
As pessoas sempre notam mais a parte crescente do que a decrescente. Portanto, fazer a parte crescente menor que a decrescente pode ser usado para mudar a ênfase para a parte decrescente.
Confira um exemplo de aplicação dessa composição para expandir e recolher um círculo. O efeito de elevação pode aumentar a sensação de expansão durante a animação. A combinação de efeitos de aumento e diminuição ajuda a enfatizar o encolhimento no final da animação.
Figura 2: essa forma de onda representa a aceleração de saída da vibração em um dispositivo.
Kotlin
enum class ExpandShapeState {
Collapsed,
Expanded
}
@Composable
fun ExpandScreen() {
// Control variable for the state of the indicator.
var currentState by remember { mutableStateOf(ExpandShapeState.Collapsed) }
// Animation between expanded and collapsed states.
val transitionData = updateTransitionData(currentState)
Screen() {
Column(
Modifier
.clickable(
{
if (currentState == ExpandShapeState.Collapsed) {
currentState = ExpandShapeState.Expanded
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE,
0.3f
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
0.3f
).compose()
)
} else {
currentState = ExpandShapeState.Collapsed
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
).compose()
)
}
)
) {
// Build the indicator UI based on the current state.
ExpandIndicator(transitionData)
}
}
}
Java
class ClickListener implements View.OnClickListener {
private final Animation expandAnimation;
private final Animation collapseAnimation;
private boolean isExpanded;
ClickListener(Context context) {
expandAnimation = AnimationUtils.loadAnimation(context, R.anim.expand);
expandAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.3f)
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.3f)
.compose());
}
});
collapseAnimation = AnimationUtils
.loadAnimation(context, R.anim.collapse);
collapseAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
.compose());
}
});
}
@Override
public void onClick(View view) {
view.startAnimation(isExpanded ? collapseAnimation : expandAnimation);
isExpanded = !isExpanded;
}
}
Oscilação (com rotações)
Um dos principais princípios de retorno tátil é agradar os usuários. Uma maneira divertida de
introduzir um efeito de vibração inesperado e agradável é usar
PRIMITIVE_SPIN. Essa primitiva é mais eficaz quando é chamada mais de uma vez. Vários giros concatenados podem criar um efeito instável e oscilante, que pode ser ainda mais aprimorado aplicando um escalonamento um pouco aleatório em cada primitiva. Você também pode testar o intervalo entre primitivos de rotação sucessivas. Dois giros sem intervalo (0 ms entre eles) criam uma sensação de giro
intensa. Aumentar a diferença entre as rotações de 10 para 50 ms causa uma sensação de rotação mais lenta e pode ser usado para corresponder à duração de um vídeo ou animação.
Não use um intervalo maior que 100 ms, porque as rotações sucessivas não se integram bem e começam a parecer efeitos individuais.
Confira um exemplo de forma elástica que volta ao normal depois de ser arrastada para baixo e solta. A animação é aprimorada com um par de efeitos de rotação, reproduzidos com intensidades variadas que são proporcionais ao deslocamento do salto.
Figura 3. Essa forma de onda representa a aceleração da saída da vibração em um dispositivo.
Kotlin
@Composable
fun WobbleScreen() {
// Control variables for the dragging and animating state of the elastic.
var dragDistance by remember { mutableStateOf(0f) }
var isWobbling by remember { mutableStateOf(false) }
// Use drag distance to create an animated float value behaving like a spring.
val dragDistanceAnimated by animateFloatAsState(
targetValue = if (dragDistance > 0f) dragDistance else 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
),
)
if (isWobbling) {
LaunchedEffect(Unit) {
while (true) {
val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
// Use some sort of minimum displacement so the final few frames
// of animation don't generate a vibration.
if (displacement > SPIN_MIN_DISPLACEMENT) {
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement)
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement)
).compose()
)
}
// Delay the next check for a sufficient duration until the
// current composition finishes. Note that you can use
// Vibrator.getPrimitiveDurations API to calculcate the delay.
delay(VIBRATION_DURATION)
}
}
}
Box(
Modifier
.fillMaxSize()
.draggable(
onDragStopped = {
isWobbling = true
dragDistance = 0f
},
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
isWobbling = false
dragDistance += delta
}
)
) {
// Draw the wobbling shape using the animated spring-like value.
WobbleShape(dragDistanceAnimated)
}
}
// Calculate a random scale for each spin to vary the full effect.
fun nextSpinScale(displacement: Float): Float {
// Generate a random offset in the range [-0.1, +0.1] to be added to the
// vibration scale so the spin effects have slightly different values.
val randomOffset: Float = Random.Default.nextFloat() * 0.2f - 0.1f
return (displacement + randomOffset).absoluteValue.coerceIn(0f, 1f)
}
Java
class AnimationListener implements DynamicAnimation.OnAnimationUpdateListener {
private final Random vibrationRandom = new Random(seed);
private final long lastVibrationUptime;
@Override
public void onAnimationUpdate(
DynamicAnimation animation, float value, float velocity) {
// Delay the next check for a sufficient duration until the current
// composition finishes. Note that you can use
// Vibrator.getPrimitiveDurations API to calculcate the delay.
if (SystemClock.uptimeMillis() - lastVibrationUptime < VIBRATION_DURATION) {
return;
}
float displacement = calculateRelativeDisplacement(value);
// Use some sort of minimum displacement so the final few frames
// of animation don't generate a vibration.
if (displacement < SPIN_MIN_DISPLACEMENT) {
return;
}
lastVibrationUptime = SystemClock.uptimeMillis();
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement))
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement))
.compose());
}
// Calculate a random scale for each spin to vary the full effect.
float nextSpinScale(float displacement) {
// Generate a random offset in the range [-0.1,+0.1] to be added to
// the vibration scale so the spin effects have slightly different
// values.
float randomOffset = vibrationRandom.nextFloat() * 0.2f - 0.1f
return MathUtils.clamp(displacement + randomOffset, 0f, 1f)
}
}
Quicar (com baques)
Outra aplicação avançada dos efeitos de vibração é simular interações físicas. PRIMITIVE_THUD pode criar um efeito forte e reverberante, que pode ser combinado com a visualização de um impacto, em um vídeo ou animação, por exemplo, para aumentar a experiência geral.
Confira um exemplo de animação de queda de bola aprimorada com um efeito de baque reproduzido cada vez que a bola quica na parte de baixo da tela:
Figura 4. Essa forma de onda representa a aceleração da saída da vibração em um dispositivo.
Kotlin
enum class BallPosition {
Start,
End
}
@Composable
fun BounceScreen() {
// Control variable for the state of the ball.
var ballPosition by remember { mutableStateOf(BallPosition.Start) }
var bounceCount by remember { mutableStateOf(0) }
// Animation for the bouncing ball.
var transitionData = updateTransitionData(ballPosition)
val collisionData = updateCollisionData(transitionData)
// Ball is about to contact floor, only vibrating once per collision.
var hasVibratedForBallContact by remember { mutableStateOf(false) }
if (collisionData.collisionWithFloor) {
if (!hasVibratedForBallContact) {
val vibrationScale = 0.7.pow(bounceCount++).toFloat()
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD,
vibrationScale
).compose()
)
hasVibratedForBallContact = true
}
} else {
// Reset for next contact with floor.
hasVibratedForBallContact = false
}
Screen() {
Box(
Modifier
.fillMaxSize()
.clickable {
if (transitionData.isAtStart) {
ballPosition = BallPosition.End
} else {
ballPosition = BallPosition.Start
bounceCount = 0
}
},
) {
// Build the ball UI based on the current state.
BouncingBall(transitionData)
}
}
}
Java
class ClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
view.animate()
.translationY(targetY)
.setDuration(3000)
.setInterpolator(new BounceInterpolator())
.setUpdateListener(new AnimatorUpdateListener() {
boolean hasVibratedForBallContact = false;
int bounceCount = 0;
@Override
public void onAnimationUpdate(ValueAnimator animator) {
boolean valueBeyondThreshold = (float) animator.getAnimatedValue() > 0.98;
if (valueBeyondThreshold) {
if (!hasVibratedForBallContact) {
float vibrationScale = (float) Math.pow(0.7, bounceCount++);
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD,
vibrationScale)
.compose());
hasVibratedForBallContact = true;
}
} else {
// Reset for next contact with floor.
hasVibratedForBallContact = false;
}
}
});
}
}
Forma de onda de vibração com envelopes
O processo para criar padrões de vibração personalizados permite controlar a amplitude da vibração para criar efeitos suaves de aumento e diminuição. Esta seção explica como criar efeitos hápticos dinâmicos usando envelopes de forma de onda que permitem o controle preciso da amplitude e da frequência da vibração ao longo do tempo. Isso permite criar experiências hápticas mais ricas e detalhadas.
A partir do Android 16 (nível 36 da API), o sistema oferece as seguintes APIs para criar um envelope de forma de onda de vibração definindo uma sequência de pontos de controle:
BasicEnvelopeBuilder:uma abordagem acessível para criar efeitos hápticos independentes de hardware.WaveformEnvelopeBuilder:uma abordagem mais avançada para criar retorno tátil; é necessário ter familiaridade com hardware háptico.
O Android não oferece substitutos para efeitos de envelope. Se você precisar desse suporte, siga estas etapas:
- Confira se um determinado dispositivo é compatível com efeitos de envelope usando
Vibrator.areEnvelopeEffectsSupported(). - Desative o conjunto consistente de experiências sem suporte ou use padrões de vibração personalizados ou composições como alternativas de substituição.
Para criar efeitos de envelope mais básicos, use o BasicEnvelopeBuilder com estes parâmetros:
- Um valor intensity no intervalo \( [0, 1] \), que representa a intensidade percebida da vibração. Por exemplo, um valor de \( 0.5 \)é percebido como metade da intensidade máxima global que pode ser alcançada pelo dispositivo.
Um valor de nitidez no intervalo \( [0, 1] \), que representa a nitidez da vibração. Valores mais baixos significam vibrações mais suaves, enquanto valores mais altos criam uma sensação mais forte.
Um valor de duração, que representa o tempo, em milissegundos, necessário para fazer a transição do último ponto de controle (ou seja, um par de intensidade e nitidez) para o novo.
Confira um exemplo de forma de onda que aumenta a intensidade de um tom baixo para uma vibração de tom alto e força máxima em 500 ms e depois diminui para\( 0 \) (desligado) em 100 ms.
vibrator.vibrate(VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 500)
.addControlPoint(0.0f, 1.0f, 100)
.build()
)
Se você tiver um conhecimento mais avançado sobre háptica, poderá definir efeitos de envelope usando WaveformEnvelopeBuilder. Ao usar esse objeto, é possível acessar o mapeamento de frequência para aceleração de saída (FOAM) usando o VibratorFrequencyProfile.
- Um valor de amplitude no intervalo \( [0, 1] \), que representa a intensidade de vibração alcançável em uma determinada frequência, conforme determinado pelo FOAM do dispositivo. Por exemplo, um valor de \( 0.5 \) gera metade da aceleração máxima de saída que pode ser alcançada na determinada frequência.
Um valor de frequência, especificado em Hertz.
Um valor de duração, que representa o tempo, em milissegundos, necessário para fazer a transição do último ponto de controle para o novo.
O código a seguir mostra um exemplo de forma de onda que define um efeito de vibração de 400 ms. Ela começa com uma rampa de amplitude de 50 ms, de desligada a completa, a uma constante de 60 Hz. Em seguida, a frequência aumenta para 120 Hz nos próximos 100 ms e permanece nesse nível por 200 ms. Por fim, a amplitude diminui para \( 0 \), e a frequência retorna a 60 Hz nos últimos 50 ms:
vibrator.vibrate(VibrationEffect.WaveformEnvelopeBuilder()
.addControlPoint(1.0f, 60f, 50)
.addControlPoint(1.0f, 120f, 100)
.addControlPoint(1.0f, 120f, 200)
.addControlPoint(0.0f, 60f, 50)
.build()
)
As seções a seguir fornecem vários exemplos de formas de onda de vibração com envelopes.
Mola com balanço
Uma amostra anterior usa PRIMITIVE_THUD para simular interações
de rejeição física. A API de envelope básico oferece um controle muito mais preciso, permitindo que você ajuste a intensidade e a nitidez da vibração.
Isso resulta em retorno tátil que segue com mais precisão os eventos animados.
Confira um exemplo de uma mola em queda livre com a animação aprimorada com um efeito de envelope básico reproduzido sempre que a mola quica na parte de baixo da tela:
Figura 5. Um gráfico de forma de onda de aceleração de saída para uma vibração que simula uma mola saltitante.
@Composable
fun BouncingSpringAnimation() {
var springX by remember { mutableStateOf(SPRING_WIDTH) }
var springY by remember { mutableStateOf(SPRING_HEIGHT) }
var velocityX by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var velocityY by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
var sharpness by remember { mutableFloatStateOf(INITIAL_SHARPNESS) }
var intensity by remember { mutableFloatStateOf(INITIAL_INTENSITY) }
var multiplier by remember { mutableFloatStateOf(INITIAL_MULTIPLIER) }
var bottomBounceCount by remember { mutableIntStateOf(0) }
var animationStartTime by remember { mutableLongStateOf(0L) }
var isAnimating by remember { mutableStateOf(false) }
val (screenHeight, screenWidth) = getScreenDimensions(context)
LaunchedEffect(isAnimating) {
animationStartTime = System.currentTimeMillis()
isAnimating = true
while (isAnimating) {
velocityY += GRAVITY
springX += velocityX.dp
springY += velocityY.dp
// Handle bottom collision
if (springY > screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2) {
// Set the spring's y-position to the bottom bounce point, to keep it
// above the floor.
springY = screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2
// Reverse the vertical velocity and apply damping to simulate a bounce.
velocityY *= -BOUNCE_DAMPING
bottomBounceCount++
// Calculate the fade-out duration of the vibration based on the
// vertical velocity.
val fadeOutDuration =
((abs(velocityY) / GRAVITY) * FRAME_DELAY_MS).toLong()
// Create a "boing" envelope vibration effect that fades out.
vibrator.vibrate(
VibrationEffect.BasicEnvelopeBuilder()
// Starting from zero sharpness here, will simulate a smoother
// "boing" effect.
.setInitialSharpness(0f)
// Add a control point to reach the target intensity and
// sharpness very quickly.
.addControlPoint(intensity, sharpness, 20L)
// Add a control point to fade out the vibration intensity while
// maintaining sharpness.
.addControlPoint(0f, sharpness, fadeOutDuration)
.build()
)
// Decrease the intensity and sharpness of the vibration for subsequent
// bounces, and reduce the multiplier to create a fading effect.
intensity *= multiplier
sharpness *= multiplier
multiplier -= 0.1f
}
if (springX > screenWidth - SPRING_WIDTH / 2) {
// Prevent the spring from moving beyond the right edge of the screen.
springX = screenWidth - SPRING_WIDTH / 2
}
// Check for 3 bottom bounces and then slow down.
if (bottomBounceCount >= MAX_BOTTOM_BOUNCE &&
System.currentTimeMillis() - animationStartTime > 1000) {
velocityX *= 0.9f
velocityY *= 0.9f
}
delay(FRAME_DELAY_MS) // Control animation speed.
// Determine if the animation should continue based on the spring's
// position and velocity.
isAnimating = (springY < screenHeight + SPRING_HEIGHT ||
springX < screenWidth + SPRING_WIDTH)
&& (velocityX >= 0.1f || velocityY >= 0.1f)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
if (!isAnimating) {
resetAnimation()
}
}
.width(screenWidth)
.height(screenHeight)
) {
DrawSpring(mutableStateOf(springX), mutableStateOf(springY))
DrawFloor()
if (!isAnimating) {
DrawText("Tap to restart")
}
}
}
Lançamento de foguete
Um exemplo anterior mostra como usar a API de envelope básica para
simular uma reação de mola elástica. O WaveformEnvelopeBuilder desbloqueia
o controle preciso sobre toda a faixa de frequência do dispositivo, permitindo efeitos
hápticos altamente personalizados. Ao combinar isso com dados do FOAM, é possível personalizar as vibrações para recursos de frequência específicos.
Este é um exemplo que demonstra uma simulação de lançamento de foguete usando um padrão de vibração dinâmica. O efeito vai da saída de aceleração de frequência mínima compatível, 0,1 G, até a frequência de ressonância, sempre mantendo uma entrada de amplitude de 10%. Isso permite que o efeito comece com uma saída razoavelmente forte e aumente a intensidade e a nitidez percebidas, mesmo que a amplitude de condução seja a mesma. Ao atingir a ressonância, a frequência do efeito volta ao mínimo, o que é percebido como intensidade e nitidez decrescentes. Isso cria uma sensação de resistência inicial seguida de uma liberação, imitando um lançamento no espaço.
Esse efeito não é possível com a API de envelope básica, já que ela abstrai as informações específicas do dispositivo sobre a frequência de ressonância e a curva de aceleração de saída. Aumentar a nitidez pode levar a frequência equivalente além da ressonância, causando uma queda de aceleração não intencional.
Figura 6. Um gráfico de forma de onda de aceleração de saída para uma vibração que simula o lançamento de um foguete.
@Composable
fun RocketLaunchAnimation() {
val context = LocalContext.current
val screenHeight = remember { mutableFloatStateOf(0f) }
var rocketPositionY by remember { mutableFloatStateOf(0f) }
var isLaunched by remember { mutableStateOf(false) }
val animation = remember { Animatable(0f) }
val animationDuration = 3000
LaunchedEffect(isLaunched) {
if (isLaunched) {
animation.animateTo(
1.2f, // Overshoot so that the rocket goes off the screen.
animationSpec = tween(
durationMillis = animationDuration,
// Applies an easing curve with a slow start and rapid acceleration
// towards the end.
easing = CubicBezierEasing(1f, 0f, 0.75f, 1f)
)
) {
rocketPositionY = screenHeight.floatValue * value
}
animation.snapTo(0f)
rocketPositionY = 0f;
isLaunched = false;
}
}
Box(
modifier = Modifier
.fillMaxSize()
.noRippleClickable {
if (!isLaunched) {
// Play vibration with same duration as the animation, using 70% of
// the time for the rise of the vibration, to match the easing curve
// defined previously.
playVibration(vibrator, animationDuration, 0.7f)
isLaunched = true
}
}
.background(Color(context.getColor(R.color.background)))
.onSizeChanged { screenHeight.floatValue = it.height.toFloat() }
) {
drawRocket(rocketPositionY)
}
}
private fun playVibration(
vibrator: Vibrator,
totalDurationMs: Long,
riseBias: Float,
minOutputAccelerationGs: Float = 0.1f,
) {
require(riseBias in 0f..1f) { "Rise bias must be between 0 and 1." }
if (!vibrator.areEnvelopeEffectsSupported()) {
return
}
val resonantFrequency = vibrator.resonantFrequency
if (resonantFrequency.isNaN()) {
// Device doesn't have or expose a resonant frequency.
return
}
val startFrequency = vibrator.frequencyProfile?.getFrequencyRange(minOutputAccelerationGs)?.lower ?: return
if (startFrequency >= resonantFrequency) {
// Vibrator can't generate the minimum required output at lower frequencies.
return
}
val minDurationMs = vibrator.envelopeEffectInfo.minControlPointDurationMillis
val rampUpDurationMs = (riseBias * totalDurationMs).toLong() - minDurationMs
val rampDownDurationMs = totalDurationMs - rampUpDuration - minDurationMs
vibrator.vibrate(
VibrationEffect.WaveformEnvelopeBuilder()
// Quickly reach the target output at the start frequency
.addControlPoint(0.1f, startFrequency, minDurationMs)
.addControlPoint(0.1f, resonantFrequency, rampUpDurationMs)
.addControlPoint(0.1f, startFrequency, rampDownDurationMs)
// Controlled ramp down to zero to avoid ringing after the vibration.
.addControlPoint(0.0f, startFrequency, minDurationMs)
.build()
)
}
LavaBeats
Como no exemplo do lançamento de foguete, a API WaveformEnvelopeBuilder
permite o design de muitos efeitos hápticos complexos ao controlar
segmentos de amplitude e frequência em uma vibração. Outro exemplo desse tipo de design é a emulação de sensações físicas mais abstratas, como "vivacidade".
Isso pode ser feito representando biomarcadores de um sinal típico de eletrocardiograma (ECG) com segmentos de vibração de certas amplitudes e frequências. LavaBeats é um exemplo em que dois segmentos característicos de uma gravação de ECG são representados como dois pulsos separados por um atraso. O primeiro pulso característico é o complexo QRS, que aparece como um pico acentuado de alta amplitude e curta duração. O segundo pulso é a onda T, que tem amplitude menor, duração mais longa e uma forma mais suave (consulte a Figura 7).
Use o WaveformEnvelopeBuilder para construir várias repetições desses dois pulsos separados por um atraso fixo do primeiro para o segundo pulso. O primeiro pulso pode ser um sinal de chirp que começa em uma frequência baixa e termina em uma frequência mais alta em um curto período. O segundo pulso pode ser representado como um único período de uma sinusoide de baixa frequência. Podemos compor os dois pulsos em uma batida e repetir a composição várias vezes com atraso entre elas, seguindo uma taxa típica de batidas por minuto (bpm). O resultado é um efeito tátil que
se assemelha a um coração batendo.
Você pode testar LavaBeats no nosso app de exemplo de retorno tátil (link em inglês) no GitHub e sentir o efeito acompanhado de uma visualização de lâmpada de lava que pulsa no mesmo ritmo do efeito háptico. Você também pode mudar as configurações do efeito para criar
sensações de batida diferentes modificando as amplitudes, frequências, durações
e atrasos dos dois pulsos.
Figura 7. Um segmento de uma gravação de ECG com o complexo QRS e a onda T
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
private fun createEnvelopeEffect(
beatParameters: List<BeatParameter>
):VibrationEffect =
VibrationEffect.WaveformEnvelopeBuilder()
.apply {
repeat(beatParameters.getNumBeats()) {
// First pulse chirp
addControlPoint(
beatParameters.getFirstPulseAmplitude(),
beatParameters.getFirstPulseStartFreq(),
ENVELOPE_RAMP_DURATION_MILLIS,
)
addControlPoint(
beatParameters.getFirstPulseAmplitude(),
beatParameters.getFirstPulseEndFreq(),
beatParameters.getFirstPulseDurationMillis().toLong(),
)
addControlPoint(
0f,
beatParameters.getFirstPulseEndFreq(),
ENVELOPE_RAMP_DURATION_MILLIS,
)
// Delay between first and second pulse
addControlPoint(
0f,
beatParameters.getFirstPulseEndFreq(),
beatParameters.getFirstToSecondPulseDelayMillis().toLong(),
)
// Second pulse
addControlPoint(
beatParameters.getSecondPulseAmplitude(),
beatParameters.getSecondPulseFreq(),
ENVELOPE_RAMP_DURATION_MILLIS,
)
addControlPoint(
beatParameters.getSecondPulseAmplitude(),
beatParameters.getSecondPulseFreq(),
(1_000 / (2f * beatParameters.getSecondPulseFreq())).toLong(),
)
addControlPoint(
0f,
beatParameters.getSecondPulseFreq(),
ENVELOPE_RAMP_DURATION_MILLIS,
)
addControlPoint(
0f,
beatParameters.getSecondPulseFreq(),
beatParameters.getBeatDelayMillis().toLong(),
)
}
}
.build()
/** A parameter of a haptic beat effect that represents an ECG signal parameter */
@Stable
data class BeatParameter(
val description: String = "",
val value: Float = 0f,
val range: ClosedFloatingPointRange<Float> = 0f..1f,
val steps: Int = 0,
val isFrequencyType: Boolean = false,
)