Benutzerdefinierte haptische Effekte erstellen

Auf dieser Seite finden Sie Beispiele für die Verwendung verschiedener Haptik-APIs, um in einer Android-App benutzerdefinierte Effekte zu erstellen, die über die standardmäßigen Vibrations-Waveforms hinausgehen.

Auf dieser Seite finden Sie die folgenden Beispiele:

Weitere Beispiele findest du unter Haptisches Feedback zu Ereignissen hinzufügen. Beachte dabei immer die Designprinzipien für Haptik.

Fallbacks zur Behandlung der Gerätekompatibilität verwenden

Beachten Sie beim Implementieren benutzerdefinierter Effekte Folgendes:

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

Die Android Haptics API-Referenz enthält Details dazu, wie Sie die Unterstützung für Komponenten prüfen, die an der Haptik beteiligt sind, damit Ihre App ein einheitliches Gesamterlebnis bieten kann.

Je nach Anwendungsfall möchten Sie möglicherweise benutzerdefinierte Effekte deaktivieren oder alternative benutzerdefinierte Effekte basierend auf verschiedenen potenziellen Funktionen bereitstellen.

Planen Sie die folgenden allgemeinen Klassen von Gerätefunktionen:

  • Wenn Sie haptische Primitiven verwenden, sind Geräte erforderlich, die die für die benutzerdefinierten Effekte benötigten Primitiven unterstützen. Weitere Informationen zu Primitiven finden Sie im nächsten Abschnitt.

  • Geräte mit Amplitudensteuerung.

  • Geräte mit einfacher Vibrationsunterstützung (Ein/Aus), d. h. ohne Amplitudensteuerung.

Wenn die Auswahl der haptischen Effekte in Ihrer App diese Kategorien berücksichtigt, sollte die haptische Nutzererfahrung auf jedem Gerät vorhersehbar bleiben.

Verwendung von haptischen Primitiven

Android enthält mehrere Haptik-Primitive, die sich in Amplitude und Frequenz unterscheiden. Sie können ein einzelnes Primitiv oder mehrere Primitive in Kombination verwenden, um komplexe haptische Effekte zu erzielen.

  • Verwenden Sie Verzögerungen von mindestens 50 ms, um erkennbare Lücken zwischen zwei Primitiven zu schaffen. Berücksichtigen Sie dabei nach Möglichkeit auch die Dauer des Primitivs.
  • 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 Skalierungen 0,5, 0,7 und 1,0, um eine Version mit niedriger, mittlerer und hoher Intensität eines Primitivs zu erstellen.

Benutzerdefinierte Vibrationsmuster erstellen

Vibrationsmuster werden häufig für haptische Benachrichtigungen wie Benachrichtigungen und Klingeltöne verwendet. Der Dienst Vibrator kann lange Vibrationsmuster abspielen, bei denen sich die Vibrationsamplitude im Laufe der Zeit ändert. Solche Effekte werden als Wellenformen bezeichnet.

Wellenformeffekte sind in der Regel wahrnehmbar, aber plötzliche lange Vibrationen können den Nutzer in einer ruhigen Umgebung erschrecken. Wenn die Zielamplitude zu schnell erreicht wird, kann das auch zu hörbaren Summtönen führen. Sie können Wellenformmuster entwerfen, um die Amplitudenübergänge zu glätten und so Ein- und Ausblendeffekte zu erzielen.

Beispiele für Vibrationsmuster

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

Muster für die Erhöhung des Prozentsatzes

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 Ganzzahlwert zwischen 0 und 255. Dabei steht 0 für den Vibrator im „Aus“-Zustand und 255 für die maximale Amplitude des Geräts.
  3. Wiederholungsindex:Der Index im Array, der im ersten Argument angegeben ist, ab dem die Wellenform wiederholt werden soll, oder -1, wenn das Muster nur einmal wiedergegeben werden soll.

Hier ist ein Beispiel für eine Wellenform, die zweimal pulsiert, mit einer Pause von 350 ms zwischen den Pulsen. Der erste Impuls ist ein sanfter Anstieg zur maximalen Amplitude und der zweite ein schneller Anstieg zur maximalen Amplitude. Das Ende wird durch den negativen Wiederholungsindexwert 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

Wellenformen 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, vibriert das Gerät so lange, bis die Vibration im Dienst explizit beendet 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 intermittierende Ereignisse, die eine Nutzeraktion erfordern, um sie zu bestätigen. Beispiele für solche Ereignisse sind eingehende Anrufe und ausgelöste Alarme.

Muster mit Fallback

Die Steuerung der Amplitude einer Vibration ist eine hardwareabhängige Funktion. Wenn eine Wellenform auf einem Low-End-Gerät ohne diese Funktion wiedergegeben wird, vibriert das Gerät bei jedem positiven Eintrag im Amplituden-Array mit der maximalen Amplitude. Wenn Ihre App solche Geräte unterstützen muss, verwenden Sie entweder ein Muster, das unter diesen Bedingungen keinen summenden Effekt 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 werden Möglichkeiten vorgestellt, Vibrationen zu längeren und komplexeren benutzerdefinierten Effekten zusammenzufügen. Außerdem wird erläutert, wie Sie mit erweiterten Hardwarefunktionen umfangreiche Haptik erstellen können. Sie können Kombinationen von Effekten verwenden, bei denen Amplitude und Frequenz variieren, um komplexere haptische Effekte auf Geräten mit haptischen Aktuatoren mit einer größeren Frequenzbandbreite zu erzeugen.

Im Prozess zum Erstellen benutzerdefinierter Vibrationsmuster, der zuvor auf dieser Seite beschrieben wurde, wird erläutert, wie Sie die Vibrationsamplitude steuern, um sanfte Effekte des Hoch- und Herunterfahrens zu erzielen. Rich Haptics verbessert dieses Konzept, indem der breitere Frequenzbereich des Gerätevibrators genutzt wird, um den Effekt noch sanfter zu gestalten. Diese Wellenformen eignen sich besonders gut, um einen Crescendo- oder Diminuendo-Effekt zu erzeugen.

Die Kompositions-Primitive, die weiter oben auf dieser Seite beschrieben werden, werden vom Gerätehersteller implementiert. Sie erzeugen eine kurze, angenehme Vibration, die den Haptik-Grundsätzen für eine klare Haptik entspricht. Weitere Informationen zu diesen Funktionen und ihrer Funktionsweise finden Sie unter Vibrationsaktoren – Grundlagen.

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, sollten Sie prüfen, ob ein bestimmtes Gerät alle von Ihnen verwendeten Primitiven unterstützt.

  2. Deaktivieren Sie die konsistente Gruppe von Funktionen, die nicht unterstützt werden, und nicht nur die Effekte, für die ein Primitiv fehlt.

Weitere Informationen dazu, wie Sie die Unterstützung des Geräts prüfen können, finden Sie in den folgenden Abschnitten.

Zusammengesetzte Vibrationseffekte erstellen

Mit VibrationEffect.Composition können Sie zusammengesetzte Vibrationseffekte erstellen. Hier ist 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 Primitives hinzugefügt werden, die nacheinander abgespielt werden sollen. Jedes Primitive ist auch skalierbar, sodass Sie die Amplitude der Vibration steuern können, die von jedem Primitive erzeugt wird. Die Skala ist als Wert zwischen 0 und 1 definiert, wobei 0 einer minimalen Amplitude entspricht, bei der dieses Primitive vom Nutzer (kaum) wahrgenommen werden kann.

Varianten in Vibrationsprimitiven erstellen

Wenn Sie eine schwache und eine starke Version desselben Primitivs erstellen möchten, sollten Sie Stärkeverhältnisse von mindestens 1,4 verwenden, damit der Unterschied in der Intensität deutlich wahrgenommen werden kann. Versuchen Sie nicht, mehr als drei Intensitätsstufen desselben Primitivs zu erstellen, da sie nicht wahrnehmbar unterschiedlich sind. Verwenden Sie beispielsweise die Skalierungen 0,5, 0,7 und 1,0, um Versionen eines Primitivs mit niedriger, mittlerer und hoher Intensität 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 Primitivs 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 eine erkennbare Lücke zwischen zwei Primitives erstellen 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 Primitives

Mit den folgenden APIs lässt sich die Geräteunterstützung für bestimmte Primitives 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 Primitives zu prüfen und dann basierend auf der Geräteunterstützungsebene zu entscheiden, welche zusammengesetzt 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 Vibrationsmuster

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

Widerstand (mit wenigen Ticks)

Sie können die Amplitude der primitiven Vibration steuern, um nützliches Feedback zu einer laufenden Aktion zu geben. Mit eng beieinander liegenden Skalierungswerten lässt sich ein sanfter Crescendo-Effekt eines Primitivs erzeugen. Die Verzögerung zwischen aufeinanderfolgenden Primitiven kann auch dynamisch auf Grundlage der Nutzerinteraktion festgelegt werden. Das wird im folgenden Beispiel einer Ansichtsanimation veranschaulicht, die durch eine Ziehbewegung gesteuert und mit Haptik erweitert wird.

Animation eines Kreises, der nach unten gezogen wird.
Diagramm der Eingabevibrationswellenform.

Abbildung 1: Diese Wellenform stellt die Ausgabebeschleunigung 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);
    }
}

Erweitern (mit Anstieg und Abfall)

Es gibt zwei Grundelemente, um die wahrgenommene Vibrationsintensität zu steigern: PRIMITIVE_QUICK_RISE und PRIMITIVE_SLOW_RISE. Beide erreichen dasselbe Ziel, aber mit unterschiedlichen Laufzeiten. Es gibt nur ein Primitiv für das Herunterfahren, PRIMITIVE_QUICK_FALL. Diese Primitives funktionieren besser zusammen, um ein Wellenformsegment zu erstellen, das an Intensität zunimmt und dann abklingt. Sie können skalierte Primitives ausrichten, um plötzliche Amplitudensprünge zwischen ihnen zu vermeiden. Das ist auch eine gute Möglichkeit, die Gesamtdauer des Effekts zu verlängern. Menschen nehmen den ansteigenden Teil immer stärker wahr als den abfallenden. Wenn Sie den ansteigenden Teil kürzer als den abfallenden gestalten, können Sie den Schwerpunkt auf den abfallenden Teil verlagern.

Hier sehen Sie ein Beispiel für die Anwendung dieser Komposition zum Ein- und Ausblenden eines Kreises. Der Rise-Effekt kann das Gefühl der Expansion während der Animation verstärken. Die Kombination aus Ein- und Ausblendeffekten trägt dazu bei, das Einblenden am Ende der Animation zu betonen.

Animation eines sich ausdehnenden Kreises.
Diagramm der Eingabevibrationswellenform.

Abbildung 2: Diese Wellenform stellt die Ausgabebeschleunigung 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;
    }
}

Schaukeln (mit Drehungen)

Eines der wichtigsten Haptikprinzipien ist es, Nutzer zu begeistern. Eine unterhaltsame Möglichkeit, einen angenehmen, unerwarteten Vibrationseffekt einzuführen, ist die Verwendung von PRIMITIVE_SPIN. Dieses Primitive ist am effektivsten, wenn es mehrmals aufgerufen wird. Durch das Verketten mehrerer Drehungen kann ein wackeliger und instabiler Effekt erzeugt werden, der durch das Anwenden einer zufälligen Skalierung auf jedes Primitive noch verstärkt werden kann. Sie können auch mit dem Abstand zwischen aufeinanderfolgenden Spin-Primitiven experimentieren. Zwei Drehungen ohne Lücke (0 ms dazwischen) erzeugen ein Gefühl der schnellen Drehung. Wenn Sie die Lücke zwischen den Drehungen von 10 auf 50 ms erhöhen, wird das Drehen weniger ruckartig. So können Sie die Dauer an die eines Videos oder einer Animation anpassen.

Verwenden Sie keine Lücke, die länger als 100 ms ist, da die aufeinanderfolgenden Drehungen nicht mehr gut ineinander übergehen und sich wie einzelne Effekte anfühlen.

Hier sehen Sie ein Beispiel für eine elastische Form, die zurückfedert, nachdem sie nach unten gezogen und dann losgelassen wurde. Die Animation wird durch zwei Dreheffekte verstärkt, die mit unterschiedlichen Intensitäten abgespielt werden, die proportional zur Sprungverschiebung sind.

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

Abbildung 3: Diese Wellenform stellt die Ausgabebeschleunigung 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)
    }
}

Aufprallen (mit dumpfen Geräuschen)

Eine weitere anspruchsvolle 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 das Gesamterlebnis zu verbessern.

Hier ist ein Beispiel für eine Animation, bei der ein Ball herunterfällt und bei jedem Aufprall auf dem unteren Bildschirmrand ein dumpfer Aufprall zu hören ist:

Animation eines fallenden Balls, der vom unteren Bildschirmrand abprallt.
Diagramm der Eingabevibrationswellenform.

Abbildung 4: Diese Wellenform stellt die Ausgabebeschleunigung 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 sanfte Effekte zu erzielen, bei denen die Vibrationen langsam stärker und wieder schwächer werden. In diesem Abschnitt wird beschrieben, wie Sie dynamische haptische Effekte mit Wellenformhüllkurven erstellen, die eine präzise Steuerung der Vibrationsamplitude und -frequenz im Zeitverlauf ermöglichen. So können Sie komplexere und differenziertere haptische Erlebnisse schaffen.

Ab Android 16 (API-Level 36) bietet das System die folgenden APIs zum Erstellen einer Vibrationswellenform, indem eine Sequenz von Kontrollpunkten definiert wird:

  • BasicEnvelopeBuilder:Ein barrierefreier Ansatz zum Erstellen von hardwareunabhängigen haptischen Effekten.
  • WaveformEnvelopeBuilder:Ein anspruchsvollerer Ansatz zum Erstellen haptischer Effekte, der Kenntnisse der Haptik-Hardware erfordert.

Android bietet keine Fallbacks für Umschlag-Effekte. Wenn Sie diese Unterstützung benötigen, führen Sie die folgenden Schritte aus:

  1. Mit Vibrator.areEnvelopeEffectsSupported() können Sie prüfen, ob ein bestimmtes Gerät Envelope-Effekte unterstützt.
  2. Deaktivieren Sie die konsistente Gruppe von Funktionen, die nicht unterstützt werden, oder verwenden Sie benutzerdefinierte Vibrationsmuster oder Zusammensetzungen als Fallback-Alternativen.

Wenn Sie einfachere Umschlageffekte erstellen möchten, verwenden Sie BasicEnvelopeBuilder mit diesen Parametern:

  • Ein intensity-Wert im Bereich \( [0, 1] \), der die wahrgenommene Stärke der Vibration darstellt. Ein Wert von \( 0.5 \)entspricht beispielsweise der Hälfte der globalen maximalen Intensität, die mit dem Gerät erreicht werden kann.
  • Ein Schärfewert im Bereich \( [0, 1] \), der die Schärfe der Vibration darstellt. Niedrigere Werte führen zu sanfteren Vibrationen, während höhere Werte ein schärferes Gefühl erzeugen.

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

Hier ist ein Beispiel für eine Wellenform, bei der die Intensität über 500 ms von einer Vibration mit niedriger Tonhöhe zu einer Vibration mit hoher Tonhöhe und 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 über fortgeschrittene Kenntnisse im Bereich Haptik verfügen, können Sie mit WaveformEnvelopeBuilder Hüllkurveneffekte definieren. Wenn Sie dieses Objekt verwenden, können Sie über VibratorFrequencyProfile auf die Frequency-to-Output-Acceleration Mapping (FOAM) zugreifen.

  • Ein Amplitudenwert im Bereich \( [0, 1] \), der die erreichbare Vibrationsstärke bei einer bestimmten Frequenz darstellt, wie sie vom FOAM des Geräts bestimmt wird. Ein Wert von \( 0.5 \) erzeugt beispielsweise die Hälfte der maximalen Ausgabebeschleunigung, die bei der angegebenen Frequenz erreicht werden kann.
  • Ein frequency-Wert, angegeben in Hertz.

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

Der folgende Code zeigt ein Beispiel für eine Wellenform, die einen 400 ms langen Vibrationseffekt definiert. Sie beginnt mit einem 50 ms langen Amplitudenanstieg von aus auf voll bei einer konstanten Frequenz von 60 Hz. Dann steigt die Frequenz in den nächsten 100 ms auf 120 Hz an und bleibt 200 ms lang auf diesem Niveau. Schließlich sinkt die Amplitude in den letzten 50 ms auf \( 0 \)und die Frequenz kehrt 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 Vibrationswellenformen mit Hüllkurven.

Springende Feder

In einem früheren Beispiel wird PRIMITIVE_THUD verwendet, um Interaktionen mit physischen Sprungvorgängen zu simulieren. Die Basic Envelope API bietet eine deutlich feinere 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 Hüllkurveneffekt verstärkt wird, der jedes Mal abgespielt wird, wenn die Feder vom unteren Bildschirmrand abprallt:

Animation einer fallengelassenen Feder, die vom unteren Bildschirmrand abprallt.
Diagramm der Eingabevibrationswellenform.

Abbildung 5: Ein Beschleunigungs-Wellenformdiagramm für eine Schwingung, die eine hüpfende 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 früheren Beispiel wurde gezeigt, wie die Basic Envelope API verwendet wird, um eine federnde Reaktion zu simulieren. Die WaveformEnvelopeBuilder ermöglicht eine präzise Steuerung des gesamten Frequenzbereichs des Geräts und somit hochgradig angepasste haptische Effekte. Wenn Sie diese Daten mit FOAM-Daten kombinieren, können Sie Vibrationen an bestimmte Frequenzfunktionen anpassen.

Hier ist ein Beispiel für eine Raketenstartsimulation mit einem dynamischen Vibrationsmuster. Der Effekt reicht von der minimal unterstützten 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, obwohl die treibende Amplitude gleich ist. Bei Erreichen der Resonanz sinkt die Effektfrequenz wieder auf das Minimum, was als abnehmende Intensität und Schärfe wahrgenommen wird. Dadurch entsteht ein Gefühl des anfänglichen Widerstands, gefolgt von einer Freigabe, die an einen Start ins Weltall erinnert.

Dieser Effekt ist mit der Basic Envelope API nicht möglich, da sie gerätespezifische Informationen zur Resonanzfrequenz und zur Ausgabebeschleunigungskurve abstrahiert. Wenn Sie die Schärfe erhöhen, kann die entsprechende Frequenz über die Resonanz hinausgehen, was möglicherweise zu einem unbeabsichtigten Abfall der Beschleunigung führt.

Animation einer Rakete, die vom unteren Bildschirmrand abhebt.
Diagramm der Eingabevibrationswellenform.

Abbildung 6 Ein Diagramm der Beschleunigungsausgabe für eine 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()
  )
}