Gesture Handler und Reanimated kombinieren: komplexe Interaktionen bauen
AI generated
RN
native
React Native · Gesture Handler · Reanimated
Gesture Handler und Reanimated kombinieren
Komplexe Interaktionen ganz ohne Bridge-Overhead bauen

Gesten allein liefern nur Rohdaten, echte Interaktionen entstehen erst, wenn diese Daten in Echtzeit in Animationen fließen. Dieser Artikel zeigt, wie react-native-gesture-handler und Reanimated über Shared Values so zusammenarbeiten, dass Pan- und Pinch-Gesten direkt und synchron auf dem UI-Thread in flüssige Animationen umgesetzt werden, am Beispiel einer Swipeable Card mit Rückfeder-Effekt.

13 Min. Lesezeit Gesture.Pan Shared Values withSpring

1. Warum Gesten und Animationen zusammen auf dem UI-Thread gehören

Eine Geste ohne direkte Rückkopplung fühlt sich für Nutzer sofort falsch an. Wenn zwischen dem Finger auf dem Bildschirm und der visuellen Reaktion auch nur wenige Millisekunden Verzögerung liegen, etwa weil der Wert erst über die Bridge zum JS-Thread und wieder zurück muss, wirkt eine Karte beim Ziehen zäh statt direkt. Gesture Handler und Reanimated lösen dieses Problem gemeinsam, weil beide Bibliotheken auf demselben JSI-Fundament aufbauen.

Ohne diese Kombination müsste jede Fingerbewegung als Event über die Bridge zum JS-Thread geschickt, dort verarbeitet und als neuer Style-Wert zurückgeschickt werden, was bei 60 oder mehr Events pro Sekunde spürbar hinterherhinkt. Mit Gesture Handler und Reanimated bleibt die gesamte Kette von der Fingerbewegung bis zum gezeichneten Frame auf dem UI-Thread, ohne einen einzigen Bridge-Roundtrip pro Frame.

2. Die Gesture-API von react-native-gesture-handler im Überblick

Seit Version 2 bietet react-native-gesture-handler eine deklarative Gesture-API, bei der Gesten wie Gesture.Pan(), Gesture.Pinch() oder Gesture.Tap() über Callback-Methoden wie onStart, onUpdate und onEnd konfiguriert werden. Diese Callbacks werden automatisch als Worklets behandelt, sofern sie mit einem GestureDetector verbunden sind, wodurch sie direkt auf dem UI-Thread ausgeführt werden können.

Mehrere Gesten lassen sich über Gesture.Simultaneous(), Gesture.Race() oder Gesture.Exclusive() zu komplexeren Erkennungsregeln kombinieren, etwa um Pan- und Pinch-Gesten gleichzeitig zuzulassen, aber einen Tap nur dann zu erkennen, wenn keine der beiden anderen Gesten aktiv geworden ist.


const pan = Gesture.Pan()
  .onStart(() => {
    scale.value = withTiming(1.05);
  })
  .onUpdate((event) => {
    translateX.value = event.translationX;
    translateY.value = event.translationY;
  })
  .onEnd(() => {
    scale.value = withTiming(1);
  });

3. Shared Values als Brücke zwischen Geste und Animation

Der Kern der Kombination ist, dass eine Geste ihre Rohdaten, etwa translationX oder scale, direkt in einen Shared Value schreibt, ohne einen Umweg über React-State. Jede davon abhängige useAnimatedStyle-Berechnung reagiert dann im selben Frame, in dem sich der Shared Value ändert, weil beide Seiten auf demselben UI-Thread laufen.

Diese direkte Kopplung bedeutet auch, dass keine zusätzliche Synchronisationslogik nötig ist. Anders als bei einer klassischen Lösung mit PanResponder und Animated.Value muss hier kein useNativeDriver-Flag gesetzt und keine begrenzte Property-Liste beachtet werden, jede Eigenschaft, die über useAnimatedStyle gesteuert wird, profitiert automatisch von derselben Performance.

4. Praxisbeispiel: eine Karte per Pan-Geste ziehen

Das folgende Beispiel zeigt eine Karte, die per Pan-Geste horizontal verschoben werden kann und sich dabei leicht dreht, proportional zur horizontalen Verschiebung. Die Rotation wird über interpolate() direkt aus demselben Shared Value abgeleitet, der auch für die Verschiebung genutzt wird, sodass keine zweite Geste oder ein zweiter Listener nötig ist.

Wichtig ist, dass context-Werte, also der Zustand zu Beginn der Geste, über useSharedValue statt über normale Variablen gehalten werden, weil Worklets bei jedem Frame neu ausgewertet werden und lokale Variablen zwischen zwei Frames nicht automatisch erhalten bleiben.


const translateX = useSharedValue(0);
const startX = useSharedValue(0);

const pan = Gesture.Pan()
  .onStart(() => {
    startX.value = translateX.value;
  })
  .onUpdate((event) => {
    translateX.value = startX.value + event.translationX;
  })
  .onEnd(() => {
    translateX.value = withSpring(0);
  });

const cardStyle = useAnimatedStyle(() => ({
  transform: [
    { translateX: translateX.value },
    { rotateZ: `${interpolate(translateX.value, [-200, 200], [-15, 15])}deg` },
  ],
}));

5. Der Rückfeder-Effekt mit withSpring und Schwellenwerten

Ein Rückfeder-Effekt entsteht, wenn beim Loslassen der Geste über withSpring statt withTiming animiert wird, weil Spring-Animationen ein natürliches Überschwingen und Ausschwingen mitbringen, das sich physikalisch korrekt anfühlt. Über die Config-Parameter damping, stiffness und mass lässt sich das Verhalten von straff und schnell bis weich und langsam ausschwingend fein justieren.

In der Praxis wird zusätzlich meist ein Schwellenwert geprüft: Überschreitet die Verschiebung beim Loslassen einen bestimmten Wert, etwa ein Drittel der Kartenbreite, wird die Karte komplett aus dem Bildschirm hinaus animiert und ein runOnJS-Aufruf informiert die JS-Seite über das Entfernen der Karte. Bleibt die Verschiebung darunter, federt die Karte per withSpring auf ihre Ausgangsposition zurück.


.onEnd((event) => {
  const shouldDismiss = Math.abs(translateX.value) > CARD_WIDTH / 3;
  if (shouldDismiss) {
    translateX.value = withSpring(
      Math.sign(translateX.value) * CARD_WIDTH * 1.5,
      { velocity: event.velocityX },
      () => runOnJS(onCardDismissed)(),
    );
  } else {
    translateX.value = withSpring(0, { damping: 15, stiffness: 150 });
  }
});

6. Pinch-Gesten für Zoom-Interaktionen kombinieren

Eine Pinch-Geste liefert über event.scale einen relativen Skalierungsfaktor zum Startzeitpunkt der Geste. Wie bei der Pan-Geste wird auch hier der Startwert in einem eigenen Shared Value gesichert, damit mehrere aufeinanderfolgende Pinch-Gesten korrekt aufeinander aufbauen, statt bei jeder neuen Geste wieder bei eins zu beginnen.

Kombiniert man Pinch mit Pan über Gesture.Simultaneous(), lassen sich Bildbetrachter oder Kartenansichten bauen, bei denen gleichzeitig gezoomt und verschoben werden kann, ein Muster, das ohne die enge Kopplung von Gesture Handler und Reanimated kaum ruckelfrei umsetzbar wäre.


const scale = useSharedValue(1);
const startScale = useSharedValue(1);

const pinch = Gesture.Pinch()
  .onStart(() => {
    startScale.value = scale.value;
  })
  .onUpdate((event) => {
    scale.value = startScale.value * event.scale;
  })
  .onEnd(() => {
    scale.value = withSpring(Math.max(1, Math.min(scale.value, 4)));
  });

const combined = Gesture.Simultaneous(pan, pinch);

7. Häufige Fallstricke bei der Kombination

Ein verbreiteter Fehler ist, Zustand für den Gestenstart in einer normalen JavaScript-Variablen statt in einem Shared Value zu halten. Da Gesture-Callbacks als Worklets auf dem UI-Thread laufen, verhält sich eine solche Variable bei jedem Callback wie eine neue, unabhängige Kopie, wodurch der Startwert bei aufeinanderfolgenden Gesten verloren geht.

Ein zweiter Fallstrick ist die Nutzung von runOnJS innerhalb von onUpdate, also bei jedem einzelnen Frame einer laufenden Geste, statt nur in onStart oder onEnd. Das flutet den JS-Thread mit Nachrichten und führt paradoxerweise genau zu dem Ruckeln, das die Kombination von Gesture Handler und Reanimated eigentlich vermeiden soll.

8. Ein wiederverwendbarer Hook: useSwipeableCard()

Sobald mehrere Kartentypen dieselbe Swipe-und-Rückfeder-Logik brauchen, lohnt es sich, Geste, Shared Values und Style in einen eigenen Hook auszulagern, der Gesture-Objekt und animierten Style als fertiges Paar zurückgibt. Die aufrufende Komponente muss dann nur noch den Hook einbinden und den zurückgegebenen Style an eine Animated.View hängen, ohne die Gesten-Details selbst zu kennen.

Diese Kapselung erleichtert auch das Testen: Reine Berechnungslogik, etwa die Umrechnung von Verschiebung in Rotationswinkel oder die Schwellenwert-Prüfung fürs Verwerfen, lässt sich als eigene, von Worklets unabhängige Funktion extrahieren und mit gewöhnlichen Jest-Tests abdecken, während der Hook selbst nur noch die Verdrahtung mit Gesture Handler und Reanimated übernimmt.


function useSwipeableCard(cardWidth: number, onDismissed: () => void) {
  const translateX = useSharedValue(0);
  const startX = useSharedValue(0);

  const gesture = Gesture.Pan()
    .onStart(() => {
      startX.value = translateX.value;
    })
    .onUpdate((event) => {
      translateX.value = startX.value + event.translationX;
    })
    .onEnd((event) => {
      const dismiss = Math.abs(translateX.value) > cardWidth / 3;
      translateX.value = dismiss
        ? withSpring(Math.sign(translateX.value) * cardWidth * 1.5, {
            velocity: event.velocityX,
          }, () => runOnJS(onDismissed)())
        : withSpring(0, { damping: 15, stiffness: 150 });
    });

  const style = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { rotateZ: `${interpolate(translateX.value, [-cardWidth, cardWidth], [-15, 15])}deg` },
    ],
  }));

  return { gesture, style };
}

9. Der Unterschied zur klassischen Animated-API mit useNativeDriver

Vor Reanimated und der neuen Gesture-API war die übliche Lösung eine Kombination aus PanResponder und Animated.Value mit useNativeDriver: true. Das funktionierte für einfache Transform- und Opacity-Animationen brauchbar, war aber auf genau diese Property-Liste beschränkt und ließ sich nicht mit beliebiger JavaScript-Logik innerhalb der Animation kombinieren.

Gesture Handler und Reanimated heben diese Einschränkung vollständig auf, weil Worklets beliebige Berechnungen direkt auf dem UI-Thread ausführen können, nicht nur eine feste Liste unterstützter Properties. Das macht komplexe, bedingte Interaktionen wie die Swipeable Card mit Rückfeder-Effekt überhaupt erst praktikabel umsetzbar.

Interaktion Geste Animations-API Wichtiger Parameter
Karte horizontal ziehen Gesture.Pan() useAnimatedStyle translationX
Rückfeder beim Loslassen onEnd withSpring damping, stiffness
Karte verwerfen onEnd mit Schwellenwert withSpring + runOnJS velocity
Bild zoomen Gesture.Pinch() useAnimatedStyle event.scale
Zoom und Verschieben gleichzeitig Gesture.Simultaneous() useAnimatedStyle getrennte Shared Values pro Geste
Rotation proportional zur Verschiebung Gesture.Pan() interpolate() Eingabe- und Ausgabebereich

Mironsoft

React-Native-App-Entwicklung und Magento-Anbindung

Eine mobile App zum Magento-Shop, die wirklich rund läuft?

Wir entwickeln React-Native-Apps, die sauber an die Magento REST- oder GraphQL-API angebunden sind, von der ersten Codezeile bis zur Veröffentlichung im App Store und bei Google Play.

App-Konzeption

Architektur und Feature-Umfang einer Magento-angebundenen App gemeinsam planen.

Magento-API-Integration

Produktkatalog, Warenkorb und Checkout sauber an die Shop-API anbinden.

Store-Veröffentlichung

App Store- und Google-Play-Freigabeprozess ohne Stolperfallen begleiten.

10. Zusammenfassung

Gesture Handler und Reanimated

Kernidee

Gesten schreiben ihre Rohdaten direkt in Shared Values, wodurch Animationen ohne Bridge-Roundtrip im selben Frame reagieren.

Rückfeder-Effekt

withSpring mit passenden damping- und stiffness-Werten erzeugt ein natürliches Ausschwingen beim Loslassen.

Größte Falle

Gestenstart-Werte in normalen Variablen statt Shared Values gehen zwischen aufeinanderfolgenden Gesten verloren.

Kombinierbarkeit

Gesture.Simultaneous() erlaubt Pan und Pinch gleichzeitig, jede Geste mit eigenem Shared-Value-Paar.

11. FAQ: Gesture Handler und Reanimated

1Warum sind Gesture Handler und Reanimated performanter als PanResponder mit Animated?
Weil beide Bibliotheken auf JSI aufbauen und ihre Callbacks als Worklets direkt auf dem UI-Thread ausführen, während PanResponder Events über die klassische Bridge zum JS-Thread schickt, was bei 60 Events pro Sekunde spürbare Verzögerung verursacht.
2Muss ich GestureDetector für jede einzelne Geste verwenden?
Ja, jede Gesture-Definition muss mit einem GestureDetector verbunden werden, damit react-native-gesture-handler die Touch-Erkennung für diesen Bereich übernimmt und die Callbacks als Worklets registriert.
3Wie halte ich den Startwert einer Geste korrekt fest?
Über einen eigenen Shared Value, der in onStart mit dem aktuellen Wert des Ziel-Shared-Values befüllt wird. Normale JavaScript-Variablen funktionieren dafür nicht zuverlässig, weil Worklet-Callbacks bei jedem Aufruf isoliert ausgewertet werden.
4Wie erzeuge ich einen natürlichen Rückfeder-Effekt?
Mit withSpring statt withTiming, kombiniert mit sinnvollen damping- und stiffness-Werten. Niedrigere damping-Werte erzeugen mehr sichtbares Überschwingen, höhere stiffness-Werte eine schnellere Bewegung.
5Wann sollte ich runOnJS innerhalb einer Geste aufrufen?
Nur an klar definierten Übergängen wie onStart oder onEnd, niemals in onUpdate, weil sonst bei jedem Frame eine Nachricht an den JS-Thread geschickt wird und die Performance leidet.
6Wie kombiniere ich Pan- und Pinch-Gesten gleichzeitig?
Über Gesture.Simultaneous(pan, pinch), wobei jede Geste ihre eigenen Shared Values für Start- und aktuellen Wert verwenden sollte, damit sich beide Gesten nicht gegenseitig überschreiben.
7Wie bestimme ich den Schwellenwert für das Verwerfen einer Karte?
Üblich ist ein Bruchteil der Kartenbreite, etwa ein Drittel, kombiniert mit der Endgeschwindigkeit der Geste aus event.velocityX, damit auch schnelle, kurze Swipes zuverlässig erkannt werden.
8Brauche ich noch useNativeDriver bei dieser Kombination?
Nein, useNativeDriver ist ein Konzept der klassischen Animated-API und wird bei Reanimated nicht benötigt, weil dort jede über useAnimatedStyle gesteuerte Eigenschaft automatisch auf dem UI-Thread läuft.
9Kann ich Rotation aus derselben Geste ableiten wie die Verschiebung?
Ja, über interpolate() lässt sich aus demselben Shared Value, der für die Verschiebung genutzt wird, direkt ein Rotationswinkel berechnen, ohne eine zweite Geste oder einen zweiten Shared Value zu benötigen.
10Funktioniert diese Kombination auch bei Listen mit vielen swipebaren Karten?
Ja, solange jede Karte ihre eigenen Shared Values besitzt und nicht global geteilt wird. Bei sehr langen Listen empfiehlt sich zusätzlich, Karten außerhalb des sichtbaren Bereichs über eine virtualisierte Liste zu entfernen.