Flexible VerticalSwipeBehavior schreiben

Hallo habr Mein Name ist Ilya Osintsev, ich bin ein Android-Entwickler bei Apiqa. Unter der Katze finden Sie ein Beispiel für die Verwendung von ViewDragHelper zum Erstellen einer Benutzeroberflächenkomponente, die SwipeDismissBehavior ähnelt, jedoch vertikal arbeitet.


Mit dem Aufkommen von Material Design sind Anwendungen zu interaktiveren Elementen geworden, die auf Benutzeraktionen reagieren. Sie sparen nicht nur Platz, sondern ermöglichen auch unterhaltsame Mikrointeraktionen. In einigen unserer Projekte haben wir uns dazu entschlossen, vertikal bewegte Banner für die Mechanik zum Abweisen von Streichen zu verwenden. Um der Oberfläche Lebendigkeit zu verleihen, müssen Banner die Geschwindigkeit des Zeigers berücksichtigen und die Transparenz in Abhängigkeit von der Richtung des Versatzes ändern.


Bewerten Sie die Aufgabe


Persönliches BüroKabinetttechniker
Persönliches BüroKabinetttechniker

In unserer Anwendung "Mein Konto" dient der Banner als schnelle Möglichkeit, eine Anfrage für einen Mietersuchdienst für Ihre Wohnung zu hinterlassen. In der Anwendung „Appliance Technique“ können Sie mit dem Banner den Kontext der Arbeit des Benutzers mit der Aufgabe speichern, wenn Sie von einer Informationskarte zu Kommentaren wechseln. Im ersten Fall betonen wir die Optionalität des PIK-Rent-Dienstes und lassen den Kunden sich in der Anwendung wie zu Hause fühlen. In einem anderen Fall implementieren wir Swipe an der Eingabeaufforderung, sodass die Nachrichtenzeile zwischen dem Dispatcher und den Ausführenden nicht überlappt.


Zu Beginn habe ich eine einfache Demo zusammengestellt, die auf SwipeDismissBehavior basiert, um zu lernen, wie es funktioniert, und um den Umfang der Änderungen abzuschätzen. Der Versuch, es im XML-Markup anzugeben, löst bei der Ausführung eine Ausnahme aus:


E/AndroidRuntime: FATAL EXCEPTION: main Process: io.apiqa.android.example, PID: 1024 android.view.InflateException: Binary XML file line #115: Could not inflate Behavior subclass com.google.android.material.behavior.SwipeDismissBehavior 

Die Entwickler haben gegen den Behaviour-Vertrag verstoßen und vergessen, den Klassenkonstruktor aus dem Kontext und dem AttributeSet zu überschreiben. Sie können dieses Verhalten jetzt nur verwenden, indem Sie eine Instanz davon in Ihrem Code erstellen. Das Ansichtsverhalten entspricht jedoch auch dann grundsätzlich nicht unseren Anforderungen, auch wenn die horizontale Richtung nicht berücksichtigt wird.


SwipeDismissBehavior sofort einsatzbereit


In den Protokollen der Demo-Anwendung wurden Meldungen angezeigt, die besagten, dass einige der Berührungsereignisse nicht in den Handler fielen.


E / ViewDragHelper: PointerId = -1 wird ignoriert, da ACTION_DOWN für diesen Zeiger nicht vor ACTION_MOVE empfangen wurde. Dies ist wahrscheinlich darauf zurückzuführen, dass ViewDragHelper nicht alle Ereignisse im Ereignisstrom empfangen hat.

Als Alternative gab es eine auf OnTouchListener basierende OnTouchListener , die uns die Möglichkeit nahm, OnClickListener zu verwenden, was bedeutet, dass wir das Bewegungsgesetz des Banners in der Aktivität beschreiben müssen. Wir möchten die Bewegungsparameter (z. B. Empfindlichkeit) während der Bewegung nicht ändern, und die Verwendung von OnTouchListener scheint hier überflüssig. Außerdem wurden in beiden Projekten Banner in CoordinatorLayout platziert.


Wenn Sie die Getters und Setter der optionalen Parameter nicht berücksichtigen, ist das SwipeDismissBehavior selbst ziemlich kurz und verwendet ViewDragHelper . Ich habe mehrere Veröffentlichungen über ihn im Netzwerk gefunden und beschlossen, meine eigene Implementierung der erforderlichen Komponente zu schreiben.


Wir koordinieren mit ViewDragHelper


ViewDragHelper ist eine Utility-Klasse, die die Drag & Drop-Unterstützung auf der View-Ebene erleichtert. Es verfolgt die Position von Widgets und enthält mehrere nützliche Funktionen zum Animieren entlang einer oder zweier Achsen innerhalb der übergeordneten ViewGroup. Für die Arbeit benötigt er einen Handler, der ViewDragHelper.Callback implementiert. Der Handler hat eine obligatorische Methode, und damit sich das Banner zu bewegen beginnt, genügt es, ein paar weitere neu zu definieren. Im Allgemeinen ist dieser Helfer einfach zu bedienen und in jedem Projekt verfügbar, da er mit appcompat geliefert wird. Um einen Helfer zu erstellen, ist ein Verweis auf das übergeordnete CoordinatorLayout erforderlich, sodass eine verzögerte Initialisierung organisiert wird. In onInterceptTouchEvent und onTouchEvent wir die entsprechenden onTouchEvent aufrufen, der Rest der Logik befindet sich im Handler.


 class VerticalSwipeBehavior<V: View>: CoordinatorLayout.Behavior<V> { companion object { @Suppress("UNCHECKED_CAST") fun <V: View> from(v: V): VerticalSwipeBehavior<V> { val lp = v.layoutParams require(lp is CoordinatorLayout.LayoutParams) val behavior = lp.behavior requireNotNull(behavior) require(behavior is VerticalSwipeBehavior) return behavior as VerticalSwipeBehavior<V> } } @Suppress("unused") constructor() : super() @Suppress("unused") constructor(context: Context, attrs: AttributeSet) : super(context, attrs) private var dragHelper: ViewDragHelper? = null private var interceptingEvents = false override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: V, ev: MotionEvent): Boolean { var isIntercept = interceptingEvents when (ev.actionMasked) { MotionEvent.ACTION_DOWN -> { isIntercept = parent.isPointInChildBounds(child, ev.x.toInt(), ev.y.toInt()) interceptingEvents = isIntercept } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { interceptingEvents = false } } return if (isIntercept) { helper(parent).shouldInterceptTouchEvent(ev) } else false } override fun onTouchEvent(parent: CoordinatorLayout, child: V, ev: MotionEvent): Boolean { val helper = helper(parent) val isViewUnder = helper.isViewUnder(child, ev.x.toInt(), ev.y.toInt()) if (helper.capturedView == child || isViewUnder ) { helper.processTouchEvent(ev) return true } else { return false } } private fun helper(parent: ViewGroup): ViewDragHelper { var h = dragHelper if (h == null) { h = ViewDragHelper.create(parent, callback) dragHelper = h return h } return h } } 

In der erforderlichen tryCaptureView müssen wir entscheiden, ob eine bestimmte Ansicht verschoben werden soll. Damit der Helfer die notwendigen Ereignisse nicht verpasst, geben wir den zuvor empfangenen Zeiger zu. Um die flexibelste Lösung zu erhalten, werden drei zusätzliche Schnittstellen eingeführt, über die Sie das Banner-Design detailliert steuern können:


  • SideEffect spiegelt den SideEffect in den Ansichtseigenschaften wider
  • VerticalClamp entwickelt, um die vertikale Bewegung der Ansicht zu begrenzen
  • PostAction wird aufgerufen, nachdem der Benutzer das Wischen beendet hat. Hier können wir die Ansichtsbewegung fortsetzen.

In jedem von ihnen ist die Methode onViewCaptured(View) , hier können Client-Implementierungen die Anfangswerte der View-Eigenschaften extrahieren. Die Reihenfolge der Aufrufe dieser Methode ist nicht garantiert.


 var sideEffect: SideEffect = AlphaElevationSideEffect() var clamp: VerticalClamp = FractionConstraintWithTopMargin(1f, 1f) var settle: PostAction = OriginSettleAction() private val callback = object: ViewDragHelper.Callback() { private val INVALID_POINTER_ID = -1 private var currentPointer = INVALID_POINTER_ID private var originTop: Int = 0 override fun tryCaptureView(child: View, pointerId: Int): Boolean { return currentPointer == INVALID_POINTER_ID || pointerId == currentPointer } override fun onViewCaptured(child: View, activePointerId: Int) { originTop = child.top currentPointer = activePointerId sideEffect.onViewCaptured(child) settle.onViewCaptured(child) clamp.onViewCaptured(child.top) } override fun onViewReleased(child: View, xvel: Float, yvel: Float) { // TODO currentPointer = INVALID_POINTER_ID } override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int) = child.left // TODO } 

Obwohl sich unsere Ansicht nicht vertikal bewegt, müssen wir daran denken, clampViewPositionHorizontal im Handler zu implementieren, um visuelle Fehler zu vermeiden. Eine einfache Implementierung gibt die linke Koordinate zurück, was bedeutet, dass sich das Widget nicht horizontal bewegt.


In unserem Fall wird der Aufruf des clampViewPositionVertical an die VerticalClamp- Schnittstelle delegiert. Die constraint sollte eine Höhenkoordinate zurückgeben, die durch die maximale und / oder minimale Ansichtsposition begrenzt ist. Wenn es erreicht ist, begrenzt ViewDragHelper die Bewegung. Die upCast(distance, top, height, dy) und downCast haben dieselbe Signatur und geben einen Bruchteil der zurückgelegten Strecke zurück, wobei die ursprüngliche Position der Ansicht berücksichtigt wird. In der onViewPositionChanged wir den Fortschritt des Verschiebens und übergeben ihn an SideEffect#apply(View, Float) , in dem Sie die Transparenz oder andere Ansichtseigenschaften abhängig vom Fortschritt der Geste ändern können. Ist die aktuelle Position der Ansicht höher als die ursprüngliche, wird der Fortschritt mit einem Minuszeichen übertragen.


 override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int { return clamp.constraint(child.height, top, dy) } override fun onViewPositionChanged(child: View, left: Int, top: Int, dx: Int, dy: Int) { val factor = if (top < originTop) { val diff = originTop - top -clamp.bottomCast(diff, top, child.height, dy) } else { val diff = top - originTop clamp.topCast(diff, top, child.height, dy) } sideEffect.apply(child, factor) } 

Standardmäßig wird FractionClamp verwendet, wodurch die Bewegung der Ansicht um eine Höhe nach oben und unten begrenzt wird (die Koeffizienten werden im Konstruktor festgelegt). AlphaElevationSideEffect ändert die Transparenz und Höhe des Banners. Um die Aufgabe als erledigt zu betrachten, müssen Sie die Fähigkeit zum animierten Verschieben des Banners hinzufügen, nachdem der Benutzer es freigegeben hat.


Wenn der Benutzer die Ansicht freigibt, merkt sich der Helfer die Geschwindigkeit des Zeigers und ruft onViewReleased auf dem Handler auf. Darin können wir die Bewegungsanimation mit settleCapturedViewAt oder smoothSlideViewTo . Gemäß dem Vertrag sollte continueSettling nach einem erfolgreichen Aufruf in jedem nächsten Frame aufgerufen werden, damit sich die Ansicht weiter bewegt. In diesem Fall kann settleCapturedViewAt nur von der onViewReleased-Methode aufgerufen werden, wenn das interne Flag des mReleaseInProgress- Helfers auf true festgelegt ist. Ein weiterer Unterschied besteht darin, dass smoothSlideViewTo die Geschwindigkeit des Zeigers nicht berücksichtigt.


 override fun onViewReleased(child: View, xvel: Float, yvel: Float) { val diff = child.top - originTop if (abs(yvel) > 0) { val settled = dragHelper?.let { if (diff > 0) { settle.releasedBelow(it, diff, child) } else { settle.releasedAbove(it, diff, child) } } ?: false if (settled) { listener?.onPreSettled(diff) child.postOnAnimation(RecursiveSettle(child, diff)) } } currentPointer = INVALID_POINTER_ID } 

Diese Logik ist in der PostAction Schnittstelle gekapselt. Die Methoden releasedAbove und releasedBelow können so implementiert werden, dass sich das Banner beim Hochfahren mit der gleichen Geschwindigkeit weiter bewegt, den Bildschirm verlässt und beim Herunterfahren an seine ursprüngliche Position zurückkehrt. Wenn eine der Methoden true zurückgibt, wurde die Animation ausgelöst und der Ansichtsereigniswarteschlange wird ein RecursiveSettle hinzugefügt, der in der Warteschlange verbleibt, bis die Animation beendet ist. Standardmäßig wird OriginSettleAction verwendet, wenn die Ansicht an einem beliebigen Versatz zum Startpunkt zurückkehrt. Eine weitere Option - SettleOnTopAction beim Verschieben der Ansicht nach unten zum Startpunkt und beim Verschieben nach oben vom Bildschirm weg.


 class SettleOnTopAction: PostAction { private var originTop: Int = -1 override fun onViewCaptured(child: View) { originTop = child.top } override fun releasedAbove(helper: ViewDragHelper, child: View): Boolean { return helper.settleCapturedViewAt(child.left, originTop) } override fun releasedBelow(helper: ViewDragHelper, child: View): Boolean { return helper.settleCapturedViewAt(child.left, -child.height) } } 

Bei Bedarf können Sie Ereignisse abonnieren, indem Sie die VerticalSwipeBehavior.SwipeListener Schnittstelle implementieren. Es gibt zwei symmetrische Methoden, eine vor dem Start der Verschiebungsanimation und die andere nach dem Ende. Das Argument gibt die Richtung und Entfernung an, in der der Benutzer das Banner freigegeben hat. Das resultierende Ergebnis erfüllt unsere Anforderungen.


Das resultierende Ergebnis


Um das Ergebnis zu erhalten, müssen Sie die Eigenschaften wie folgt definieren:


 val drag = findViewById<View>(R.id.drag) VerticalSwipeBehavior.from(drag).apply { settle = SettleOnTopAction() sideEffect = NegativeFactorFilterSideEffect(AlphaElevationSideEffect()) clamp = BelowFractionalClamp() } 

Übrigens bietet der Helfer auch andere Bewegungssteuerungsfunktionen. Mit der Methode setMinVelocity(Float) können Sie beispielsweise die Mindestgeschwindigkeit für das Verschieben der Ansicht begrenzen. Der Helfer unterstützt auch die Erkennung von Wischbewegungen an den Rändern des Bildschirms. Dazu müssen Sie sie in der setEdgeTrackingEnabled(Int) -Methode angeben. Es sollte beachtet werden, dass eine Instanz von ViewDragHelper die Bewegung von nur einer Ansicht steuern kann und nur einen Zeiger berücksichtigt.


Schlüsse ziehen


Ich habe die Erfahrung gemacht, dass ViewDragHelper das Erstellen von Drag & Drop oder das Verschieben von Bedienfeldern in einer Anwendung vereinfacht. Helper ist einfach in Behavior zu verwenden oder ViewGroup zu überschreiben. Es verfügt über mehrere nützliche Methoden zum Animieren der Ansicht und zum Steuern ihrer Bewegung. Das Studium der internen Implementierung von Material Design-Komponenten ist eine gute Erfahrung in der Karriere eines Android-Entwicklers. Solche Aufgaben von Designern motivieren mich, neue Ansätze zum Aufbau einer Anwendungsschnittstelle zu untersuchen und Wissen mit Kollegen und der Community zu teilen.


Sie können die resultierende Bibliothek in Ihren Projekten verwenden. build.gradle zum build.gradle die Abhängigkeit in der Datei build.gradle


 dependencies { implementation 'io.apiqa.android:verticalswipebehavior:1.0.0' } 

app:layout_behavior="io.apiqa.android.verticalswipe.VerticalSwipeBehavior" für eine geeignete Ansicht im CoordinatorLayout die app:layout_behavior="io.apiqa.android.verticalswipe.VerticalSwipeBehavior" Eigenschaft an app:layout_behavior="io.apiqa.android.verticalswipe.VerticalSwipeBehavior" im Markup. Sie können das Banner mithilfe von Einzügen innerhalb des übergeordneten Elements positionieren. Durch die Koordination der Implementierungen von SideEffect , VerticalClamp und PostAction können Sie das gewünschte Banner-Verhalten erzielen. Im Repository sind jeweils Arbeitsversionen verfügbar.


Frohes neues Jahr!

Source: https://habr.com/ru/post/de481910/


All Articles