Esta página aborda 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:
- 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 substituto: uma demonstração de substituto.
- Composições 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 salto 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 um efeito personalizado, considere o seguinte:
- Quais recursos do dispositivo são necessários para o efeito
- O que fazer quando o dispositivo não consegue reproduzir o efeito
A referência da API háptica do Android (link em inglês) fornece detalhes sobre como verificar o suporte para componentes envolvidos na háptica, para que seu app possa oferecer uma experiência geral consistente.
Dependendo do seu caso de uso, talvez seja necessário desativar os efeitos personalizados ou fornecer alternativas com base em diferentes recursos.
Planeje as seguintes classes de recursos de dispositivos de alto nível:
Se você estiver usando primitivos táteis: dispositivos que oferecem suporte a essas primitivas necessárias para os efeitos personalizados. Consulte a próxima seção para mais detalhes sobre primitivos.
Dispositivos com controle de amplitude.
Dispositivos com suporte a vibração básica (ativada/desativada), ou seja, aqueles sem controle de amplitude.
Se a escolha do efeito háptico do app considerar essas categorias, a experiência do usuário háptica vai permanecer previsível em qualquer dispositivo.
Uso de primitivas hápticas
O Android inclui várias primitivas hápticas 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, considerando também 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 mais bem 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
As seções a seguir fornecem vários exemplos de padrões de vibração:
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 gradual 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 repetitivo
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 reconhecimento. Exemplos desses eventos incluem ligações recebidas e alarmes acionados.
Padrão com substituto
Controlar a amplitude de uma vibração é um recurso dependente de hardware. Reproduzir uma forma de onda em um dispositivo mais simples sem essa capacidade 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));
}
Criar composições de vibração
Esta seção apresenta maneiras de compor vibrações em efeitos personalizados mais longos e complexos, além de explorar hápticos avançados usando recursos de hardware mais avançados. É possível usar combinações de efeitos que variam a amplitude e a frequência para criar efeitos hápticos mais complexos em dispositivos com atuadores hápticos que têm uma largura de banda de frequência maior.
O processo para criar padrões de vibração personalizados, descrito anteriormente nesta página, explica como controlar a amplitude da vibração para criar efeitos suaves de aumento e diminuição. A resposta tátil avançada melhora esse conceito ao explorar a faixa de frequência mais ampla do vibrador do dispositivo para tornar o efeito ainda mais suave. Essas formas de onda são especialmente eficazes para criar um efeito de crescendo ou diminuendo.
As primitivas de composição, descritas anteriormente nesta página, são implementadas pelo fabricante do dispositivo. Elas oferecem uma vibração nítida, curta e agradável que se alinha aos princípios hápticos para uma resposta tátil clara. Para mais detalhes sobre essas funcionalidades e como elas funcionam, consulte Noções básicas sobre atuadores de vibração.
O Android não oferece substitutos para composições com primitivos não compatíveis. Portanto, siga estas etapas:
Antes de ativar a resposta tátil avançada, verifique se um determinado dispositivo é compatível com todas as primitivas que você está usando.
Desative o conjunto consistente de experiências não compatíveis, não apenas os efeitos que não têm uma primitiva.
Mais informações sobre como verificar o suporte do dispositivo são mostradas nas seções a seguir.
Criar 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 nítido:
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 uma 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 lacuna de 5 a 10 ms entre duas primitivas é muito curta 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 primitivas são compatíveis
As seguintes APIs 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.
Resistir (com poucos ticks)
É possível controlar a amplitude da vibração primitiva para transmitir um feedback útil a uma ação em andamento. Valores de escala com espaçamento pequeno 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 aumentada com háptica.
Figura 1. Essa forma de onda representa a aceleração de 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. Ambas 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. É possível alinhar primitivas dimensionadas 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, para mudar o foco para a parte decrescente, faça a crescente ser mais curta.
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;
}
}
Equilíbrio (com giros)
Um dos principais princípios de háptica é 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 sucessivos. Dois giros sem intervalo (0 ms entre eles) criam uma sensação de giro
rápido. Aumentar o intervalo entre giros de 10 para 50 ms causa uma sensação de giro 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.
Veja 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 de 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 de 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 de criação de 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 efeitos táteis. Exige familiaridade com hardware háptico.
O Android não oferece substitutos para efeitos de envelope. Se precisar desse suporte, siga estas etapas:
- Confira se um dispositivo específico é compatível com efeitos de envelope usando
Vibrator.areEnvelopeEffectsSupported(). - Desative o conjunto consistente de experiências que não são compatíveis 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 nítida.
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 um tom alto, com vibração de intensidade 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 de 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, na sigla em inglês) 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 frequência especificada.
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 aceleração de amplitude de 50 ms, de desligado para total, 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 refinado, permitindo que você ajuste com precisão a intensidade e a nitidez da vibração.
Isso resulta em um feedback tátil que acompanha 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 desired 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
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 ao 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 desired 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()
)
}