Google verbessert unser Leben weiter, indem es neue praktische Bibliotheken und APIs veröffentlicht. Darunter war auch das neue MotionLayout. Angesichts der Fülle an Animationen in unseren Anwendungen implementierte mein Kollege Cedric Holtz sofort die wichtigste Animation unserer Anwendung - Voting in Dating - mithilfe der neuen API und sparte dabei eine große Menge Code. Ich teile die Übersetzung seines Artikels.Die Google I / O 2019-Konferenz wurde kürzlich beendet, auf der Updates und die neuesten Verbesserungen für unser geliebtes SDK angekündigt wurden. Persönlich war ich besonders an der Präsentation von Nicholas Road und John Hoford über die zukünftige Funktionalität von ConstraintLayout interessiert. Genauer gesagt über seine Erweiterung in Form von MotionLayout.
Nach der Beta-Version wollte ich eine Dating-Animation basierend auf dieser Bibliothek implementieren.
Definieren wir zunächst die Begriffe:
"MotionLayout ist ein ConstraintLayout, mit dem Sie Layouts zwischen verschiedenen Status animieren können." - Dokumentation
Wenn Sie keine
Artikelserie von Nicholas Road gelesen haben, in der die wichtigsten Ideen von MotionLayout erläutert werden, empfehle ich dringend, sie zu lesen.
Nachdem die Einführung abgeschlossen ist, wollen wir nun sehen, was wir bekommen möchten:

Kartenstapel
Wir zeigen die verschobene Karte
Fügen Sie zunächst MotionLayout zum Layoutverzeichnis hinzu, das bisher nur eine oberste Karte enthält:
<androidx.constraintlayout.motion.widget.MotionLayout android:id="@+id/motionLayout" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutDescription="@xml/scene_swipe" app:motionDebug="SHOW_ALL"> <FrameLayout android:id="@+id/topCard" android:layout_width="0dp" android:layout_height="0dp" /> </androidx.constraintlayout.motion.widget.MotionLayout>
Beachten Sie diese Zeile: app: motionDebug = "SHOW_ALL". Es ermöglicht uns, Debugging-Informationen, die Flugbahn von Objekten, die Zustände mit dem Beginn und dem Ende der Animation sowie den aktuellen Fortschritt anzuzeigen. Die Zeile hilft beim Debuggen sehr, aber vergessen Sie nicht, sie zu löschen, bevor Sie sie an das Produkt senden: Es gibt keine Erinnerung dafür.
Wie Sie sehen, haben wir hier keine Einschränkungen für die Ansicht festgelegt. Sie werden aus der Szene (MotionScene) übernommen, die wir jetzt definieren.
Beginnen wir mit der Definition des Ausgangszustands: Eine Karte liegt in der Mitte des Bildschirms mit Einkerbungen.
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <ConstraintSet android:id="@+id/rest"> <Constraint android:id="@id/topCard" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="50dp" android:layout_marginEnd="50dp" android:layout_marginStart="50dp" android:layout_marginTop="50dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> </ConstraintSet> </MotionScene>
Hinzufügen von Constraint-Sets (ConstraintSet) pass und like. Sie geben den Status der obersten Karte wieder, wenn sie vollständig nach links oder rechts verschoben ist. Wir möchten, dass die Karte angehalten wird, bevor sie vom Bildschirm verschwindet, um eine schöne Animation anzuzeigen, die unsere Entscheidung bestätigt.
<ConstraintSet android:id="@+id/pass" app:deriveConstraintsFrom="@+id/rest"> <Constraint android:id="@id/topCard" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginBottom="80dp" android:layout_marginEnd="200dp" android:layout_marginStart="50dp" android:layout_marginTop="20dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintWidth_percent="0.7" /> </ConstraintSet> <ConstraintSet android:id="@+id/like" app:deriveConstraintsFrom="@id/rest"> <Constraint android:id="@id/topCard" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginBottom="80dp" android:layout_marginEnd="50dp" android:layout_marginStart="200dp" android:layout_marginTop="20dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintWidth_percent="0.7" /> </ConstraintSet>
Fügen Sie der vorherigen Szene beide Sätze von Einschränkungen hinzu. Sie sind fast identisch und werden nur auf beiden Seiten des Bildschirms gespiegelt.
Jetzt haben wir drei Arten von Einschränkungen - Start, Like und Pass. Definieren wir den Übergang zwischen diesen Zuständen.
Fügen Sie dazu einen Übergang für den Wisch nach links und den anderen für den Wisch nach rechts hinzu.
<Transition app:constraintSetEnd="@+id/pass" app:constraintSetStart="@+id/rest" app:duration="300"> <OnSwipe app:dragDirection="dragLeft" app:onTouchUp="autoComplete" app:touchAnchorId="@id/topCard" app:touchAnchorSide="left" app:touchRegionId="@id/topCard" /> </Transition> <Transition app:constraintSetEnd="@+id/like" app:constraintSetStart="@+id/rest" app:duration="300"> <OnSwipe app:dragDirection="dragRight" app:onTouchUp="autoComplete" app:touchAnchorId="@+id/topCard" app:touchAnchorSide="right" app:touchRegionId="@id/topCard" /> </Transition>
Für die oberste Karte setzen wir die Wischanimation nach links und den gleichen Spiegel für den Wisch nach rechts.
Diese Eigenschaften werden dazu beitragen, die Interaktion mit unserer Szene zu verbessern:
- touchRegionId: Da wir die Karte aufgefüllt haben, müssen wir sicherstellen, dass die Berührung nur im Bereich der Karte selbst und nicht im gesamten MotionLayout erkannt wird. Dies kann mit touchRegionId erfolgen.
- onTouchUp: Was passiert mit der Animation, nachdem wir die Karte freigegeben haben? Es sollte entweder weitergehen oder in seinen Ausgangszustand zurückkehren, damit die automatische Vervollständigung anwendbar ist.
Mal sehen, was passiert ist:

Die Karte wird automatisch vom Bildschirm entfernt
Jetzt arbeiten wir an der Animation, die startet, wenn die Karte vom Bildschirm verschwindet.
Wir fügen zwei weitere ConstraintSet-Sets für jeden Endzustand unserer Animationen hinzu: Die Karte verlässt den Bildschirm links und rechts.
In den folgenden Beispielen werde ich zeigen, wie der Like-Status erstellt wird, und der Pass-Status wiederholt ihn gespiegelt. Ein funktionierendes Beispiel ist vollständig im
Repository zu sehen .
<ConstraintSet android:id="@+id/offScreenLike"> <Constraint android:id="@id/topCard" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginBottom="80dp" android:layout_marginEnd="50dp" android:layout_marginTop="20dp" app:layout_constraintStart_toEndOf="parent" app:layout_constraintWidth_percent="0.7" /> </ConstraintSet>
Jetzt müssen Sie wie im vorherigen Beispiel den Übergang vom Wischzustand zum Endzustand bestimmen. Der Übergang sollte unmittelbar nach der Animation des Wischens automatisch funktionieren. Dies kann mit autoTransition erfolgen:
<Transition app:autoTransition="animateToEnd" app:constraintSetEnd="@+id/offScreenLike" app:constraintSetStart="@+id/like" app:duration="150" />
Jetzt haben wir eine Wischkarte, die vom Bildschirm gewischt werden kann!

Animation der unteren Karte
Lassen Sie uns nun die unterste Karte erstellen, um die Illusion der Unendlichkeit des Decks zu erzeugen.
Fügen Sie dem Layout eine weitere Karte hinzu, ähnlich der ersten:
<FrameLayout android:id="@+id/bottomCard" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/colorAccent" />
Ändern Sie das XML, um die Einschränkungen festzulegen, die für diese Karte in jeder Phase der Animation gelten:
<ConstraintSet android:id="@id/rest"> <!-- ... --> <Constraint android:id="@id/bottomCard"> <Layout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="50dp" android:layout_marginEnd="50dp" android:layout_marginStart="50dp" android:layout_marginTop="50dp" /> <Transform android:scaleX="0.90" android:scaleY="0.90" /> </Constraint> </ConstraintSet> <ConstraintSet android:id="@+id/offScreenLike" app:deriveConstraintsFrom="@id/like"> <!-- ... --> <Constraint android:id="@id/bottomCard"> <Transform android:scaleX="1" android:scaleY="1" /> </Constraint> </ConstraintSet>
Dazu können wir die bequeme ConstraintSet-Eigenschaft verwenden.
Standardmäßig übernimmt jeder neue Satz Attribute aus dem übergeordneten MotionLayout. Mit dem Flag deriveConstraintsFrom können Sie jedoch ein anderes übergeordnetes Element für unser Set festlegen. Es sollte berücksichtigt werden, dass wir, wenn wir Einschränkungen mithilfe des Einschränkungstags festlegen, alle Einschränkungen aus dem übergeordneten Satz neu definieren. Um dies zu vermeiden, können Sie bestimmte Attribute in den
Tags festlegen, sodass nur diese ersetzt werden.

In unserem Fall bedeutet dies, dass wir im Pass-Set das Layout-Tag nicht definieren, sondern vom übergeordneten Tag kopieren. Wir überschreiben jedoch Transform. Daher ersetzen wir alle im Transform-Tag angegebenen Attribute durch unsere eigenen, in diesem Fall eine Änderung der Skalierung.
So einfach ist es, mit MotionLayout ein neues Element hinzuzufügen und es nahtlos in die Animationen unserer Szene zu integrieren.

Die Animation endlos machen
Nachdem die Animation abgeschlossen ist, kann die oberste Karte nicht mehr weggewischt werden, da sie jetzt zu einer untersten Karte geworden ist. Um endlose Animationen zu erhalten, müssen Sie die Karten austauschen.
Zuerst wollte ich dies mit einem neuen Übergang tun:
<Transition app:autoTransition="jumpToEnd" app:constraintSetEnd="@+id/rest" app:constraintSetStart="@+id/offScreenLike" app:duration="0" />

Die gesamte Animation wird so abgespielt, wie sie sollte. Jetzt haben wir einen Kartenstapel, den Sie endlos durchziehen können!
Mit einem kleinen Schlag bemerkte ich etwas. Der Übergang zum Ende der Deck-Animation stoppt, wenn Sie die Karte berühren. Obwohl die Animationsdauer Null ist, stoppt sie immer noch, was schlecht ist.

Ich konnte nur auf eine Weise gewinnen - indem ich den aktiven Übergang in MotionLayout programmgesteuert änderte.
Dazu setzen wir nach Abschluss der Animation einen Rückruf. Sobald offScreenLike und offScreenPass abgeschlossen sind, setzen wir den Übergang einfach wieder in den Ruhezustand zurück und setzen den Fortschritt auf Null.
motionLayout.setTransitionListener(object : TransitionAdapter() { override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) { when (currentId) { R.id.offScreenPass, R.id.offScreenLike -> { motionLayout.progress = 0f motionLayout.setTransition(R.id.rest, R.id.like) } } } })
Es spielt keine Rolle, welchen Übergang wir setzen, bestehen oder mögen, wenn wir wischen, wechseln wir zum gewünschten.

Es sieht genauso aus, aber die Animation hört nicht auf! Lass uns weitermachen!
Datenbindung
Erstellen Sie Testdaten, die auf den Karten angezeigt werden sollen. Im Moment beschränken wir uns darauf, die Hintergrundfarbe jeder Karte zu ändern.
Wir erstellen ein ViewModel mit einer svayp-Methode, die nur neue Daten ersetzt. Binden Sie es auf folgende Weise an Aktivität:
val viewModel = ViewModelProviders .of(this) .get(SwipeRightViewModel::class.java) viewModel .modelStream .observe(this, Observer { bindCard(it) }) motionLayout.setTransitionListener(object : TransitionAdapter() { override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) { when (currentId) { R.id.offScreenPass, R.id.offScreenLike -> { motionLayout.progress = 0f motionLayout.setTransition(R.id.rest, R.id.like) viewModel.swipe() } } } })
Es bleibt, um ViewModel über den Abschluss der Wischanimation zu informieren, und es aktualisiert die aktuell angezeigten Daten.

Popup-Symbole
Fügen Sie zwei Ansichten hinzu, die beim Streichen auf einer der Seiten des Bildschirms angezeigt werden (nur eine wird unten angezeigt, die zweite wird gespiegelt).
<ImageView android:id="@+id/likeIndicator" android:layout_width="0dp" android:layout_height="0dp" />
Für die Karten müssen Sie nun die Animationszustände mit diesen Ansichten festlegen.
<ConstraintSet android:id="@id/rest"> <!-- ... --> <Constraint android:id="@+id/like"> <Layout android:layout_width="40dp" android:layout_height="40dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Transform android:scaleX="0.5" android:scaleY="0.5" /> <PropertySet android:alpha="0" /> </Constraint> </ConstraintSet> <ConstraintSet android:id="@+id/like" app:deriveConstraintsFrom="@id/rest"> <!-- ... --> <Constraint android:id="@+id/like"> <Layout android:layout_width="100dp" android:layout_height="100dp" app:layout_constraintBottom_toBottomOf="@id/topCard" app:layout_constraintEnd_toEndOf="@id/topCard" app:layout_constraintStart_toStartOf="@id/topCard" app:layout_constraintTop_toTopOf="@id/topCard" /> <Transform android:scaleX="1" android:scaleY="1" /> <PropertySet android:alpha="1" /> </Constraint> </ConstraintSet>
Es ist nicht erforderlich, Einschränkungen für Animationen festzulegen, die über den Bildschirm hinausgehen, da sie von den Eltern geerbt werden. Und in unserem Fall ist dies ein Schlagzustand.
Das ist alles was wir tun müssen. Jetzt können Sie Animationsketten ganz einfach Komponenten hinzufügen.

Führen Sie die Animation programmgesteuert aus
Wir können zwei Schaltflächen auf den Karten erstellen, damit der Benutzer nicht nur wischen, sondern auch über die Schaltflächen steuern kann.
Jede Schaltfläche startet dieselbe Animation wie das Wischen.
Abonnieren Sie wie gewohnt Schaltflächenklicks und starten Sie die Animation direkt auf dem MotionLayout-Objekt:
likeButton.setOnClickListener { motionLayout.transitionToState(R.id.like) } passButton.setOnClickListener { motionLayout.transitionToState(R.id.pass) }
Wir müssen sowohl der oberen als auch der unteren Karte Schaltflächen hinzufügen, damit die Animation kontinuierlich abgespielt wird. Für die untere Karte ist jedoch kein Klickabonnement erforderlich, da es entweder nicht sichtbar ist oder die obere Karte animiert ist und wir sie nicht unterbrechen möchten.

Ein weiteres gutes Beispiel dafür, wie MotionLayout Statusänderungen für uns handhabt. Lassen Sie uns die Animation etwas verlangsamen:

Sehen Sie sich den Übergang an, den MotionLayout ausführt, wenn pass wie ersetzt. Die Magie!
Wischen Sie die Karte entlang der Kurve
Angenommen, es gefällt uns, wenn sich die Karte nicht in einer geraden Linie, sondern in einer Kurve bewegt (um ehrlich zu sein, wollte ich das nur versuchen).
Dann müssen Sie KeyPosition für die Bewegung in beide Richtungen definieren, damit der Bewegungspfad durch einen Bogen gekrümmt wird.
Fügen Sie dies der Bewegungsszene hinzu:
<Transition app:constraintSetEnd="@+id/like" app:constraintSetStart="@+id/rest" app:duration="300"> <!-- ... --> <KeyFrameSet> <KeyPosition app:drawPath="path" app:framePosition="50" app:keyPositionType="pathRelative" app:motionTarget="@id/topCard" app:percentX="0.5" app:percentY="-0.1" /> </KeyFrameSet> </Transition>

Jetzt bewegt sich die Karte entlang eines nicht banal gekrümmten Pfades. Magisch!
Fazit
Wenn Sie die Menge an Code, die ich beim Erstellen dieser Animationen erhalten habe, mit unserer aktuellen Implementierung ähnlicher Animationen in der Produktion vergleichen, ist das Ergebnis erstaunlich.
MotionLayout behandelt unmerklich das Abbrechen von Übergängen (z. B. beim Berühren), das Erstellen von Animationsketten, das Ändern von Eigenschaften während Übergängen und vieles mehr. Dieses Tool ändert grundlegend alles und vereinfacht die UI-Logik erheblich.
Es gibt noch einige andere Dinge, an denen es sich zu arbeiten lohnt (hauptsächlich das Deaktivieren von Animationen und das bidirektionale Scrollen in RecyclerView), aber ich bin sicher, dass dies lösbar ist.
Denken Sie daran, dass sich die Bibliothek noch im Beta-Status befindet, aber bereits viele aufregende Möglichkeiten für uns eröffnet. Wir freuen uns auf die Veröffentlichung von MotionLayout, das sich in Zukunft sicherlich mehr als einmal als nützlich erweisen wird. Sie können die voll funktionsfähige Anwendung aus diesem Artikel im
Repository sehen .
PS: Und da sie mir das Wort als Übersetzer gegeben haben, hat unser Android-Team einen Platz für einen Entwickler . Vielen Dank für Ihre Aufmerksamkeit.