Benutzerdefinierte haptische Effekte erstellen

Auf dieser Seite finden Sie Beispiele dafür, wie Sie mit verschiedenen Haptik-APIs benutzerdefinierte Effekte in einer Android-App erstellen, die über die standardmäßigen Vibrationswellenformen hinausgehen.

Auf dieser Seite finden Sie die folgenden Beispiele:

Weitere Beispiele finden Sie unter Ereignissen haptisches Feedback hinzufügen. Beachten Sie außerdem immer die Designprinzipien für Haptik.

Fallbacks zur Gerätekompatibilität verwenden

Beachten Sie bei der Implementierung benutzerdefinierter Effekte Folgendes:

  • Welche Gerätefunktionen für den Effekt erforderlich sind
  • Was tun, wenn das Gerät den Effekt nicht abspielen kann

In der Referenz für die Android-Haptik API erfahren Sie, wie Sie die Unterstützung für Komponenten prüfen, die für die Haptik Ihrer App relevant sind, damit Ihre App eine einheitliche Nutzererfahrung bietet.

Je nach Anwendungsfall können Sie benutzerdefinierte Effekte deaktivieren oder alternative benutzerdefinierte Effekte basierend auf verschiedenen potenziellen Funktionen bereitstellen.

Berücksichtigen Sie die folgenden allgemeinen Klassen von Gerätefunktionen:

  • Bei Verwendung von haptischen Primitiven: Geräte, die die für die benutzerdefinierten Effekte erforderlichen Primitives unterstützen. Weitere Informationen zu Primitiven finden Sie im nächsten Abschnitt.

  • Geräte mit Amplitudenregelung

  • Geräte mit grundlegender Vibrationsunterstützung (An/Aus), d. h. ohne Amplitudenregelung.

Wenn die Auswahl der haptischen Effekte Ihrer App diese Kategorien berücksichtigt, sollte die haptische Nutzererfahrung für jedes einzelne Gerät vorhersehbar bleiben.

Haptische Primitive verwenden

Android bietet mehrere Haptik-Primitive, die sich sowohl in Amplitude als auch in Frequenz unterscheiden. Sie können eine oder mehrere Primitive kombinieren, um ausdrucksstarke haptische Effekte zu erzielen.

  • Verwenden Sie Verzögerungen von mindestens 50 ms für erkennbare Lücken zwischen zwei Elementen. Berücksichtigen Sie dabei nach Möglichkeit auch die Dauer der Primitivelemente.
  • Verwenden Sie Skalen, die sich um ein Verhältnis von mindestens 1,4 unterscheiden, damit der Unterschied in der Intensität besser wahrgenommen wird.
  • Verwenden Sie die Skalen 0,5, 0,7 und 1,0, um eine Version mit niedriger, mittlerer und hoher Intensität eines Primitives zu erstellen.

Benutzerdefinierte Vibrationsmuster erstellen

Vibrationsmuster werden häufig für haptische Warnungen verwendet, z. B. für Benachrichtigungen und Klingeltöne. Der Dienst Vibrator kann lange Vibrationsmuster abspielen, die die Vibrationsamplitude im Laufe der Zeit ändern. Solche Effekte werden als Wellenformen bezeichnet.

Wellenformeffekte sind normalerweise wahrnehmbar, aber plötzliche lange Vibrationen können den Nutzer erschrecken, wenn sie in einer ruhigen Umgebung abgespielt werden. Wenn die Zielamplitude zu schnell erreicht wird, kann es auch zu hörbaren Brummgeräuschen kommen. Erstellen Sie Wellenformmuster, um die Amplitudenübergänge zu glätten und so Ein- und Ausblendeffekte zu erzeugen.

Beispiele für Vibrationsmuster

In den folgenden Abschnitten finden Sie einige Beispiele für Vibrationsmuster:

Steigerungsmuster

Wellenformen werden als VibrationEffect mit drei Parametern dargestellt:

  1. Timings:ein Array mit Dauern in Millisekunden für jedes Wellenformsegment.
  2. Amplituden:Die gewünschte Vibrationsamplitude für jede im ersten Argument angegebene Dauer, dargestellt als Ganzzahl zwischen 0 und 255. Dabei steht 0 für den „Aus-Zustand“ des Vibrators und 255 für die maximale Amplitude des Geräts.
  3. Wiederholindex:Der Index im im ersten Argument angegebenen Array, ab dem die Wellenform wiederholt werden soll, oder -1, wenn das Muster nur einmal wiedergegeben werden soll.

Hier ist eine Beispielwellenform, die zweimal mit einer Pause von 350 ms zwischen den Impulsen pulsiert. Der erste Impuls ist eine glatte Rampe bis zur maximalen Amplitude und der zweite ist eine schnelle Rampe, um die maximale Amplitude aufrechtzuerhalten. Das Ende wird durch den negativen Wert des Wiederholungsindexes definiert.

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));

Sich wiederholendes Muster

Sie können auch wiederholt abgespielt werden, bis sie abgebrochen werden. Um eine sich wiederholende Wellenform zu erstellen, müssen Sie einen nicht negativen repeat-Parameter festlegen. Wenn Sie eine sich wiederholende Wellenform abspielen, wird die Vibration so lange fortgesetzt, bis sie im Dienst explizit abgebrochen wird:

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();
}

Das ist sehr nützlich für gelegentliche Ereignisse, die eine Nutzeraktion erfordern, um sie zu bestätigen. Beispiele für solche Ereignisse sind eingehende Anrufe und ausgelöste Wecker.

Muster mit Fallback

Die Steuerung der Amplitude einer Vibration ist eine hardwareabhängige Funktion. Wenn Sie eine Wellenform auf einem Low-End-Gerät ohne diese Funktion abspielen, vibriert das Gerät für jeden positiven Eintrag im Amplitudenarray mit der maximalen Amplitude. Wenn Ihre App auf solchen Geräten ausgeführt werden muss, verwenden Sie entweder ein Muster, das in diesem Zustand keinen Brummeffekt erzeugt, oder entwerfen Sie ein einfacheres EIN/AUS-Muster, das stattdessen als Fallback verwendet werden kann.

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));
}

Vibrationskompositionen erstellen

In diesem Abschnitt erfahren Sie, wie Sie Vibrationen zu längeren und komplexeren benutzerdefinierten Effekten zusammenstellen. Außerdem werden erweiterte Hardwarefunktionen für eine noch bessere Haptik vorgestellt. Sie können Kombinationen von Effekten mit unterschiedlicher Amplitude und Frequenz verwenden, um komplexere haptische Effekte auf Geräten mit haptischen Aktuatoren mit einer breiteren Frequenzbandbreite zu erzeugen.

Im Abschnitt zum Erstellen benutzerdefinierter Vibrationsmuster auf dieser Seite wird beschrieben, wie Sie die Vibrationsamplitude steuern, um eine gleichmäßige Steigerung und Abnahme der Vibration zu erzielen. Die Funktion „Rich Haptics“ verbessert dieses Konzept, indem der breitere Frequenzbereich des Vibrationsmotors genutzt wird, um den Effekt noch weicher zu machen. Diese Wellenformen eignen sich besonders gut, um einen Crescendo- oder Diminuendo-Effekt zu erzeugen.

Die Kompositionsgrundlagen, die weiter oben auf dieser Seite beschrieben wurden, werden vom Gerätehersteller implementiert. Sie bieten eine knackige, kurze und angenehme Vibration, die den Haptikprinzipien für eine klare Haptik entspricht. Weitere Informationen zu diesen Funktionen und ihrer Funktionsweise finden Sie im Artikel Vibrationsaktoren – Einführung.

Android bietet keine Fallbacks für Kompositionen mit nicht unterstützten Primitiven. Führen Sie daher die folgenden Schritte aus:

  1. Bevor Sie die erweiterten Haptikfunktionen aktivieren, prüfen Sie, ob ein bestimmtes Gerät alle von Ihnen verwendeten Primitive unterstützt.

  2. Deaktivieren Sie alle nicht unterstützten Funktionen, nicht nur die Effekte, für die ein Primitive fehlt.

Weitere Informationen dazu, wie du die Unterstützung für dein Gerät prüfen kannst, findest du in den folgenden Abschnitten.

Zusammengesetzte Vibrationseffekte erstellen

Mit VibrationEffect.Composition können Sie zusammengesetzte Vibrationseffekte erstellen. Hier ein Beispiel für einen langsam ansteigenden Effekt, gefolgt von einem scharfen Klickeffekt:

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());

Eine Komposition wird erstellt, indem Primitive hinzugefügt werden, die in einer bestimmten Reihenfolge abgespielt werden sollen. Jede Primitive ist außerdem skalierbar, sodass Sie die Amplitude der von ihnen erzeugten Vibration steuern können. Die Skala ist als Wert zwischen 0 und 1 definiert. Dabei entspricht 0 einer minimalen Amplitude, bei der dieses Element vom Nutzer (kaum) wahrgenommen werden kann.

Varianten in Vibrationsprimitiven erstellen

Wenn Sie eine schwache und eine starke Version desselben Symbols erstellen möchten, sollten Sie ein Stärkeverhältnis von mindestens 1,4 festlegen, damit der Unterschied in der Intensität leicht erkennbar ist. Versuchen Sie nicht, mehr als drei Intensitätsstufen desselben Primitives zu erstellen, da sie nicht wahrnehmbar voneinander zu unterscheiden sind. Verwenden Sie beispielsweise die Skalen 0,5, 0,7 und 1,0, um Versionen mit niedriger, mittlerer und hoher Intensität einer primitiven Form zu erstellen.

Lücken zwischen Vibrationsprimitiven einfügen

In der Komposition können auch Verzögerungen zwischen aufeinanderfolgenden Primitiven angegeben werden. Diese Verzögerung wird in Millisekunden seit dem Ende des vorherigen Primitives angegeben. Im Allgemeinen ist eine Lücke von 5 bis 10 ms zwischen zwei Primitiven zu kurz, um erkannt zu werden. Verwenden Sie eine Lücke von etwa 50 ms oder länger, wenn Sie zwischen zwei Elementen eine deutlich sichtbare Lücke schaffen möchten. Hier ist ein Beispiel für eine Komposition mit Verzögerungen:

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());

Unterstützte Primitive prüfen

Mit den folgenden APIs können Sie die Geräteunterstützung für bestimmte Primitive prüfen:

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.
}

Es ist auch möglich, mehrere Primitive zu prüfen und dann anhand der Geräteunterstützungsebene zu entscheiden, welche davon erstellt werden sollen:

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);

Beispiele für Vibrationszusammensetzungen

In den folgenden Abschnitten finden Sie mehrere Beispiele für Vibrationskompositionen aus der Beispiel-App für Haptik auf GitHub.

Widerstand (mit wenigen Zählern)

Sie können die Amplitude der primitiven Vibration steuern, um nützliches Feedback zu einer laufenden Aktion zu geben. Mit eng beieinander liegenden Skalenwerten lässt sich ein glatter Crescendo-Effekt eines Primitives erzeugen. Die Verzögerung zwischen aufeinanderfolgenden Primitiven kann auch dynamisch basierend auf der Nutzerinteraktion festgelegt werden. Das folgende Beispiel einer Ansichtsanimation, die durch eine Ziegesten gesteuert und mit Haptik ergänzt wird, veranschaulicht dies.

Animation eines Kreises, der nach unten gezogen wird.
Darstellung der Wellenform der Eingabevibration.

Abbildung 1: Diese Wellenform stellt die Ausgangsbeschleunigung der Vibration auf einem Gerät dar.

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);
    }
}

Maximieren (mit Steigerung und Abfall)

Es gibt zwei Primitive, mit denen die wahrgenommene Vibrationsintensität erhöht werden kann: PRIMITIVE_QUICK_RISE und PRIMITIVE_SLOW_RISE. Beide erreichen dasselbe Ziel, aber mit unterschiedlicher Dauer. Es gibt nur ein Primitive für das Ausblenden: PRIMITIVE_QUICK_FALL. Diese Primitive eignen sich gut, um ein Wellenformsegment zu erstellen, das an Intensität zunimmt und dann wieder abnimmt. Sie können skalierte Primitive ausrichten, um plötzliche Amplitudensprünge zwischen ihnen zu vermeiden. Das eignet sich auch gut, um die Gesamtdauer des Effekts zu verlängern. Menschen nehmen den steigenden Teil immer stärker wahr als den fallenden. Wenn Sie den steigenden Teil kürzer als den fallenden machen, können Sie den Schwerpunkt auf den fallenden Teil verlagern.

Hier ein Beispiel für die Anwendung dieser Komposition zum Maximieren und Minimieren eines Kreises. Der Aufwärtseffekt kann das Gefühl der Ausdehnung während der Animation verstärken. Die Kombination aus Auf- und Abstiegseffekten unterstreicht das Zusammenfallen am Ende der Animation.

Animation eines sich vergrößernden Kreises.
Darstellung der Wellenform der Eingabevibration.

Abbildung 2: Diese Wellenform stellt die Ausgangsbeschleunigung der Vibration auf einem Gerät dar.

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;
    }
}

Wackeln (mit Drehungen)

Eines der wichtigsten Haptikprinzipien besteht darin, die Nutzer zu begeistern. Mit PRIMITIVE_SPIN können Sie einen angenehmen, unerwarteten Vibrationseffekt einfügen. Dieses Primitive ist am effektivsten, wenn es mehrmals aufgerufen wird. Mehrere zusammengesetzte Drehungen können einen wackeligen und instabilen Effekt erzeugen, der durch eine etwas zufällige Skalierung der einzelnen Primitiven noch verstärkt werden kann. Sie können auch mit dem Abstand zwischen aufeinanderfolgenden Spin-Primitiven experimentieren. Zwei Umdrehungen ohne Lücke (0 ms dazwischen) sorgen für ein schnelles Drehgefühl. Wenn Sie die Pause zwischen den einzelnen Umdrehungen von 10 auf 50 ms erhöhen, wirkt das Bild lockerer. So lässt sich die Dauer eines Videos oder einer Animation anpassen.

Verwenden Sie keine Lücke, die länger als 100 ms ist, da sich die aufeinanderfolgenden Drehungen sonst nicht mehr gut ineinanderfügen und wie einzelne Effekte wirken.

Hier ist ein Beispiel für eine elastische Form, die nach dem Ziehen nach unten wieder in ihre Ausgangsposition zurückkehrt. Die Animation wird durch zwei Dreheffekte ergänzt, die mit unterschiedlicher Intensität abgespielt werden, die der Sprungverschiebung proportional ist.

Animation einer elastischen Form, die hüpft
Darstellung der Eingabevibrationswellenform

Abbildung 3: Diese Wellenform stellt die Ausgangsbeschleunigung der Vibration auf einem Gerät dar.

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)
    }
}

Abprallen (mit Knallgeräuschen)

Eine weitere erweiterte Anwendung von Vibrationseffekten ist die Simulation physischer Interaktionen. PRIMITIVE_THUD kann einen starken und nachhallenden Effekt erzeugen, der mit der Visualisierung eines Aufpralls kombiniert werden kann, z. B. in einem Video oder einer Animation, um die Gesamterfahrung zu verbessern.

Hier ist ein Beispiel für eine Ballwurf-Animation, die mit einem Knalleffekt ergänzt wird, der jedes Mal wiedergegeben wird, wenn der Ball unten am Bildschirm abprallt:

Animation einer fallengelassenen Kugel, die vom unteren Bildschirmrand abprallt.
Darstellung der Wellenform der Eingabevibration.

Abbildung 4: Diese Wellenform stellt die Ausgangsbeschleunigung der Vibration auf einem Gerät dar.

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;
            }
            }
        });
    }
}

Vibrationswellenform mit Hüllkurven

Beim Erstellen benutzerdefinierter Vibrationsmuster können Sie die Vibrationsamplitude steuern, um eine gleichmäßige Steigerung und Abnahme der Vibration zu erzielen. In diesem Abschnitt wird erläutert, wie Sie mithilfe von Wellenform-Hüllkurven dynamische haptische Effekte erstellen, mit denen sich die Vibrationsamplitude und -frequenz im Zeitverlauf präzise steuern lassen. So können Sie eine umfassendere und nuanciertere haptische Erfahrung schaffen.

Ab Android 16 (API-Ebene 36) bietet das System die folgenden APIs, um eine Vibrationswellenform durch Definition einer Sequenz von Steuerpunkten zu erstellen:

  • BasicEnvelopeBuilder: Ein zugänglicher Ansatz zum Erstellen von hardwareunabhängigen haptischen Effekten.
  • WaveformEnvelopeBuilder: Ein fortgeschrittenerer Ansatz zum Erstellen haptischer Effekte; erfordert Kenntnisse über haptische Hardware.

Android bietet keine Fallbacks für Envelope-Effekte. Wenn Sie diesen Support benötigen, führen Sie die folgenden Schritte aus:

  1. Prüfen Sie mit Vibrator.areEnvelopeEffectsSupported(), ob ein bestimmtes Gerät Envelope-Effekte unterstützt.
  2. Deaktivieren Sie die nicht unterstützten Funktionen oder verwenden Sie benutzerdefinierte Vibrationsmuster oder Kompositionen als Fallback-Alternativen.

Für einfachere Hüllkurveneffekte verwende die BasicEnvelopeBuilder mit den folgenden Parametern:

  • Ein Wert für die Intensität im Bereich \( [0, 1] \), der die wahrgenommene Stärke der Vibration angibt. Ein Wert von \( 0.5 \)wird beispielsweise als die Hälfte der globalen maximalen Intensität wahrgenommen, die mit dem Gerät erreicht werden kann.
  • Ein Schärfe-Wert im Bereich \( [0, 1] \), der die Schärfe der Vibration angibt. Niedrigere Werte sorgen für gleichmäßigere Vibrationen, während höhere Werte ein schärferes Gefühl erzeugen.

  • Ein Dauer-Wert, der die Zeit in Millisekunden angibt, die für den Übergang vom letzten Kontrollpunkt – also einem Paar aus Intensität und Schärfe – zum neuen benötigt wird.

Hier ist eine Beispielwellenform, bei der die Intensität über 500 ms von einer niedrigen Tonhöhe zu einer hohen Tonhöhe mit maximaler Stärke ansteigt und dann über 100 ms wieder auf\( 0 \) (Aus) abfällt.

vibrator.vibrate(VibrationEffect.BasicEnvelopeBuilder()
    .setInitialSharpness(0.0f)
    .addControlPoint(1.0f, 1.0f, 500)
    .addControlPoint(0.0f, 1.0f, 100)
    .build()
)

Wenn Sie sich mit Haptik auskennen, können Sie mit WaveformEnvelopeBuilder Envelope-Effekte definieren. Wenn Sie dieses Objekt verwenden, können Sie über VibratorFrequencyProfile auf die Zuordnung der Auslieferungsfrequenz zur Beschleunigung zugreifen.

  • Ein Amplitudenwert im Bereich \( [0, 1] \), der die erreichbare Vibrationsstärke bei einer bestimmten Frequenz darstellt, wie vom Geräte-FOAM bestimmt. Bei einem Wert von \( 0.5 \) wird beispielsweise die Hälfte der maximalen Ausgangsbeschleunigung generiert, die bei der angegebenen Frequenz erreicht werden kann.
  • Ein Frequenzwert, angegeben in Hertz.

  • Ein Dauer-Wert, der die Zeit in Millisekunden angibt, die für den Übergang vom letzten zum neuen Kontrollpunkt benötigt wird.

Der folgende Code zeigt eine Beispielwellenform, die einen 400 ms langen Vibrationseffekt definiert. Sie beginnt mit einer 50 ms langen Amplitudenrampe von „Aus“ auf „Volle Leistung“ bei konstanten 60 Hz. Anschließend steigt die Frequenz in den nächsten 100 ms auf 120 Hz an und bleibt 200 ms lang auf diesem Niveau. Schließlich fällt die Amplitude auf \( 0 \)ab und die Frequenz kehrt in den letzten 50 ms zu 60 Hz zurück:

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()
)

In den folgenden Abschnitten finden Sie mehrere Beispiele für Schwingungswellenformen mit Hüllkurven.

Federung

In einem vorherigen Beispiel wird PRIMITIVE_THUD verwendet, um Interaktionen mit einem „echten“ Ausstottern zu simulieren. Die Basic Envelope API bietet eine deutlich genauere Steuerung, mit der Sie die Vibrationsintensität und -schärfe präzise anpassen können. Das führt zu einem haptischen Feedback, das animierten Ereignissen genauer folgt.

Hier ein Beispiel für eine kostenlos fallende Feder, bei der die Animation durch einen einfachen Envelope-Effekt ergänzt wird, der jedes Mal abgespielt wird, wenn die Feder vom unteren Bildschirmrand abprallt:

Eine Grafik der Ausgabebeschleunigungswellenform für eine Vibration, die eine federnde Feder simuliert.

@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")
    }
  }
}

Raketenstart

In einem vorherigen Beispiel wurde gezeigt, wie mit der einfachen Envelope API eine federnde Sprungreaktion simuliert wird. Mit der WaveformEnvelopeBuilder können Sie den gesamten Frequenzbereich des Geräts präzise steuern und so individuell angepasste haptische Effekte erzielen. In Kombination mit FOAM-Daten können Sie die Vibrationen an bestimmte Frequenzfunktionen anpassen.

Hier ist ein Beispiel für eine Simulation eines Raketenstarts mit einem dynamischen Vibrationsmuster. Der Effekt reicht von der minimal unterstützten Frequenz der Beschleunigungsausgabe, 0,1 G, bis zur Resonanzfrequenz, wobei immer eine Amplitudeneingabe von 10 % beibehalten wird. So kann der Effekt mit einer relativ starken Ausgabe beginnen und die wahrgenommene Intensität und Schärfe erhöhen, auch wenn die antreibende Amplitude gleich bleibt. Nach Erreichen der Resonanz sinkt die Effektfrequenz wieder auf das Minimum ab, was als abnehmende Intensität und Schärfe wahrgenommen wird. Das erzeugt ein Gefühl von anfänglichem Widerstand, gefolgt von einer Entspannung, die einen Start ins All simuliert.

Mit der einfachen Envelope API ist dieser Effekt nicht möglich, da die gerätespezifischen Informationen zur Resonanzfrequenz und zur Beschleunigungskurve der Ausgabe abstrahiert werden. Eine höhere Schärfe kann die äquivalente Frequenz über die Resonanz hinaus steigern und möglicherweise zu einem unbeabsichtigten Beschleunigungsabfall führen.

Eine Ausgabegrafik für die Beschleunigungswellenform einer Vibration, die einen Raketenstart simuliert.

@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()
  )
}