
Hallo Habr! Vor nicht allzu langer Zeit war ich auf der Suche nach Abenteuern, neuen Projekten und Technologien wurde ein Roboter in Redmadrobot angesiedelt. Ich bekam einen Stuhl, einen Monitor und ein Macbook und ein kleines internes Projekt zum Aufwärmen. Es war notwendig, eine selbst geschriebene Bibliothek fertigzustellen und zu veröffentlichen, um die Medieninhalte anzuzeigen, die wir in unseren Projekten verwenden. In diesem Artikel werde ich Ihnen erklären, wie Sie Berührungsereignisse in einer Woche verstehen, Open Source werden, einen Fehler in Android SDK finden und eine Bibliothek veröffentlichen.
Starten Sie
Eine der wichtigen Funktionen unserer Store-Apps ist die Möglichkeit, Videos und Fotos von Waren und Dienstleistungen in der Nähe und von allen Seiten anzuzeigen. Wir wollten das Rad nicht neu erfinden und machten uns auf die Suche nach einer fertigen Bibliothek, die zu uns passen würde.
Wir wollten eine solche Lösung finden, damit der Benutzer:
- Fotos anzeigen;
- Skalieren Sie Fotos mit einer Prise zum Zoomen und zweimaligen Tippen.
- Sehen Sie sich ein Video an
- Medieninhalte umdrehen;
- Schließen Sie die Fotokarte mit einem vertikalen Wisch (Wischen zum Entlassen).
Folgendes haben wir gefunden:
- FrescoImageViewer - unterstützt das Anzeigen und Scrollen durch Fotos und grundlegende Gesten, unterstützt jedoch nicht das Anzeigen von Videos und ist für die Fresco- Bibliothek vorgesehen.
- PhotoView - unterstützt das Anzeigen von Fotos. Die meisten Hauptsteuerungsgesten, außer Scrollen, Wischen zum Schließen, unterstützen das Anzeigen von Videos nicht.
- PhotoDraweeView - ähnlich wie PhotoView, jedoch für Fresco gedacht.
Da keine der gefundenen Bibliotheken die Anforderungen vollständig erfüllte, mussten wir unsere eigenen schreiben.
Wir realisieren die Bibliothek
Um die erforderliche Funktionalität zu erhalten, haben wir vorhandene Lösungen aus anderen Bibliotheken fertiggestellt. Sie beschlossen, dem Geschehenen den bescheidenen Namen Android Gallery zu geben .
Wir implementieren Funktionalität
Fotos anzeigen und zoomen
Zum Anzeigen von Fotos haben wir die PhotoView-Bibliothek verwendet, die das sofortige Skalieren unterstützt.
Video ansehen
Um das Video anzusehen, haben sie ExoPlayer verwendet , der im MediaPagerAdapter wiederverwendet wird . Wenn ein Benutzer zum ersten Mal ein Video öffnet, wird ExoPlayer erstellt. Wenn Sie zu einem anderen Element wechseln, wird es in die Warteschlange gestellt. Wenn Sie das Video das nächste Mal starten, wird die bereits erstellte ExoPlayer-Instanz verwendet. Dies macht den Übergang zwischen Elementen reibungsloser.
Scrollen von Medieninhalten
Hier haben wir den MultiTouchViewPager von FrescoImageViewer verwendet, der keine Multi-Touch-Ereignisse abfängt, sodass wir ihm Gesten hinzufügen konnten, um das Bild zu skalieren.
Wischen Sie, um zu entlassen
PhotoView unterstützte das Streichen und Entprellen nicht (Wiederherstellen der ursprünglichen Bildgröße, wenn das Bild vergrößert oder verkleinert wird).
So haben wir es geschafft.
Lernen von Touch-Ereignissen zum Implementieren von Swipe zum Entlassen
Bevor Sie Swipe zum Schließen unterstützen, müssen Sie wissen, wie Berührungsereignisse funktionieren. Wenn der Benutzer den Bildschirm berührt, wird die Methode dispatchTouchEvent(motionEvent: MotionEvent)
in der aktuellen Aktivität aufgerufen, zu der MotionEvent.ACTION_DOWN
. Diese Methode entscheidet über das Schicksal des Ereignisses. Sie können motionEvent
zur motionEvent
an onTouchEvent(motionEvent: MotionEvent)
übergeben motionEvent
in der onTouchEvent(motionEvent: MotionEvent)
von oben nach unten weiter onTouchEvent(motionEvent: MotionEvent)
. View, das an einem Ereignis und / oder nachfolgenden Ereignissen vor ACTION_UP
, gibt true zurück.
Danach fallen alle Ereignisse der aktuellen Geste in diese Ansicht, bis die Geste mit dem Ereignis ACTION_UP
endet oder die übergeordnete ViewGroup die Kontrolle übernimmt (dann wird das Ereignis ACTION_CANCELED
). Wenn sich das Ereignis um die gesamte onTouchEvent(motionEvent: MotionEvent)
und niemand interessiert war, kehrt es zur Aktivität in onTouchEvent(motionEvent: MotionEvent)
.

In unserer Android Gallery-Bibliothek kommt das erste ACTION_DOWN
Ereignis zu dispatchTouchEvent()
in PhotoView, wo motionEvent
an die onTouch()
motionEvent
, die true zurückgibt. Darüber hinaus durchlaufen alle Ereignisse dieselbe Kette, bis eines der folgenden Ereignisse:
ACTION_UP
;- ViewPager versucht, das Ereignis zum Scrollen abzufangen.
- VerticalDragLayout versucht, das Ereignis abzufangen, damit Swipe geschlossen wird.
Nur ViewGroup in der Methode onInterceptTouchEvent(motionEvent: MotionEvent)
kann Ereignisse abfangen. Selbst wenn die Ansicht an einem MotionEvent interessiert ist, durchläuft das Ereignis selbst das dispatchTouchEvent(motionEvent: MotionEvent)
gesamten vorherigen ViewGroup-Kette. Dementsprechend „beobachten“ Eltern ihre Kinder immer. Jede übergeordnete ViewGroup kann das Ereignis abfangen und in der onInterceptTouchEvent(motionEvent: MotionEvent)
. Anschließend erhalten alle MotionEvent.ACTION_CANCEL
in onTouchEvent(motionEvent: MotionEvent)
.
Beispiel: Der Benutzer hält einen Finger auf ein Element in einer RecyclerView, dann werden Ereignisse im selben Element verarbeitet. Sobald er jedoch anfängt, seinen Finger nach oben / unten zu bewegen, fängt der RecyclerView die Ereignisse ab, und der ACTION_CANCEL
beginnt, und die Ansicht empfängt das Ereignis ACTION_CANCEL
.

In der Android-Galerie kann VerticalDragLayout Ereignisse abfangen, die durch Wischen geschlossen werden sollen, oder ViewPager zum Scrollen. View kann jedoch verhindern, dass das übergeordnete requestDisallowInterceptTouchEvent(true)
Ereignisse requestDisallowInterceptTouchEvent(true)
indem die Methode requestDisallowInterceptTouchEvent(true)
wird. Dies kann erforderlich sein, wenn die Ansicht Aktionen ausführen muss, deren Abfangen durch die Eltern für uns nicht wünschenswert ist.
Zum Beispiel, wenn ein Benutzer in einem Player einen Titel zu einer bestimmten Zeit überspringt. Wenn der übergeordnete ViewPager einen horizontalen Bildlauf abfängt, erfolgt ein Übergang zum nächsten Titel.
Wir haben VerticalDragLayout geschrieben, aber keine Touch-Ereignisse von PhotoView empfangen. Um zu verstehen, warum dies passiert, musste ich herausfinden, wie Berührungsereignisse in PhotoView verarbeitet werden.
Bearbeitungsauftrag:
- Wenn MotionEvent.ACTION_DOWN in VerticalDragLayout ausgeführt wird, wird
interceptTouchEvent()
ausgelöst, das false zurückgibt, weil Diese ViewGroup interessiert sich nur für vertikale ACTION_MOVE. Die Richtung ACTION_MOVE wird in dispatchTouchEvent()
definiert. Anschließend wird das Ereignis an die Methode super.dispatchTouchEvent()
in der ViewGroup übergeben, wo das Ereignis an die Implementierung interceptTouchEvent()
in VerticalDragLayout übergeben wird.

- Wenn das Ereignis
onTouch()
Methode onTouch()
in PhotoView erreicht, wird durch die Ansicht die Möglichkeit zum Abfangen der Ereignisverwaltung beeinträchtigt. Alle nachfolgenden Gestenereignisse fallen nicht in die interceptTouchEvent()
-Methode. Die Möglichkeit, die Kontrolle abzufangen, wird dem übergeordneten ACTION_MOVE
nur gegeben, wenn die Geste abgeschlossen ist oder wenn horizontale ACTION_MOVE
am rechten / linken Rand des Bildes ACTION_MOVE
.
if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { if (mScrollEdge == EDGE_BOTH || (mScrollEdge == EDGE_LEFT && dx >= 1f) || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) { if (parent != null) { parent.requestDisallowInterceptTouchEvent(false); } } } else { if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } }

Da PhotoView es dem übergeordneten Element ermöglicht, die Steuerung nur bei horizontaler ACTION_MOVE
, und das Wischen zum ACTION_MOVE
vertikale ACTION_MOVE
, kann VerticalDragLayout die Ereignissteuerung nicht abfangen, um eine Geste zu implementieren. Um dies zu beheben, müssen Sie die Möglichkeit hinzufügen, die Steuerung bei vertikalem ACTION_MOVE
.
if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f) || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f) || mVerticalScrollEdge == VERTICAL_EDGE_BOTH || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f) || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) { if (parent != null) { parent.requestDisallowInterceptTouchEvent(false); } } } else { if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } }
Im Fall der ersten vertikalen ACTION_MOVE
bietet PhotoView nun die Möglichkeit, das übergeordnete Element abzufangen:

Die folgende ACTION_MOVE
wird in VerticalDragLyout abgefangen, und das Ereignis ACTION_CANCEL
wird in die ACTION_CANCEL
Ansicht übertragen:

Alle anderen ACTION_MOVE
fliegen entlang der Standardkette zu VerticalDragLayout. Es ist wichtig, dass die untergeordnete Ansicht die Kontrolle nicht wiedererlangen kann, nachdem die ViewGroup die Kontrolle über Ereignisse aus der untergeordneten Ansicht übernommen hat.

Deshalb haben wir Swipe implementiert, um die Unterstützung für die PhotoView-Bibliothek zu beenden. In unserer Bibliothek haben wir die geänderten Quellen von PhotoView verwendet, die in einem separaten Modul herausgenommen wurden, und eine Zusammenführungsanforderung im ursprünglichen PhotoView-Repository erstellt.
Wir implementieren Debinds in PhotoView
Denken Sie daran, dass eine Entprellung eine Animationswiederherstellung mit einem akzeptablen Maßstab ist, wenn das Bild über seine Grenzen hinaus skaliert wird.

In PhotoView gab es keine solche Möglichkeit. Aber seit wir angefangen haben, Open Source von jemand anderem zu graben, warum sollten wir dort aufhören? In PhotoView können Sie eine Zoomgrenze festlegen. Dies ist zunächst das Minimum - x1 und das Maximum - x3. Das Bild kann diese Grenzen nicht überschreiten.
@Override public void onScale(float scaleFactor, float focusX, float focusY) { if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) { if (mScaleChangeListener != null) { mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); } mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
Zunächst haben wir beschlossen, die Bedingung „Skalierungsverbot beim Erreichen eines Minimums“ getScale() > mMinScale || scaleFactor > 1f
: Wir haben einfach die Bedingung getScale() > mMinScale || scaleFactor > 1f
getScale() > mMinScale || scaleFactor > 1f
. Und dann plötzlich ...

Debuns verdient! Anscheinend geschah dies aufgrund der Tatsache, dass die Schöpfer der Bibliothek beschlossen, zweimal auf Nummer sicher zu gehen, was sowohl eine Entprellung als auch eine Einschränkung der Skalierung zur Folge hatte. Wenn bei der Implementierung des onTouch-Ereignisses, nämlich im Fall von MotionEvent.ACTION_UP
, der Benutzer mehr / weniger als das Maximum / Minimum skaliert hat, wird AnimatedZoomRunnable gestartet, wodurch das Bild auf seine ursprüngliche Größe zurückgesetzt wird.
@Override public boolean onTouch(View v, MotionEvent ev) { boolean handled = false; switch (ev.getAction()) { case MotionEvent.ACTION_UP:
Neben dem Wischen zum Entlassen haben wir PhotoView in den Quellen unserer Bibliothek finalisiert und eine Pull-Anfrage mit dem „Hinzufügen“ einer Entprellung zum ursprünglichen PhotoView erstellt.
Beheben Sie einen plötzlichen Fehler in PhotoView
PhotoView hat einen sehr bösen Fehler. Wenn der Benutzer das Bild durch zweimaliges Tippen auf und vergrößern möchte Er hat einen Anfall von Epilepsie Das Bild beginnt zu skalieren und kann vertikal um 180 Grad gedreht werden. Dieser Fehler kann sogar in beliebten Anwendungen von Google Play gefunden werden, beispielsweise in Cyan.

Nach einer langen Suche haben wir diesen Fehler immer noch lokalisiert: Manchmal wird der Eingangsmatrix ein negativer scaleFactor zur Bildskalierung zur Skalierung zugeführt, wodurch das Bild umgedreht wird.
CustomGestureDetector
@Override public boolean onScale(ScaleGestureDetector detector) {
Um vom Android ScaleGestureDetector aus zu skalieren, erhalten wir scaleFactor, der wie folgt berechnet wird:
public float getScaleFactor() { if (inAnchoredScaleMode()) {
Wenn Sie diese Methode mit Debug-Protokollen überschreiben, können Sie verfolgen, bei welchen bestimmten Werten der Variablen ein negativer scaleFactor erhalten wird:
mEventBeforeOrAboveStartingGestureEvent is true; SCALE_FACTOR is 0.5; mCurrSpan: 1075.4398; mPrevSpan 38.867798; scaleUp: false; spanDiff: 13.334586; eval result is -12.334586
Es besteht der Verdacht, dass sie versucht haben, dieses Problem durch Multiplikation von spanDiff mit SCALE_FACTOR == 0.5 zu lösen. Diese Lösung hilft jedoch nicht, wenn der Unterschied zwischen mCurrSpan und mPrevSpan mehr als dreimal beträgt. Für diesen Fehler wurde bereits ein Ticket gestartet, das jedoch noch nicht behoben wurde.
Krücke Die einfachste Lösung für dieses Problem besteht darin, negative scaleFactor-Werte einfach zu überspringen. In der Praxis wird der Benutzer nicht bemerken, dass das Bild manchmal etwas weniger sanft als gewöhnlich vergrößert wird.
Anstelle einer Schlussfolgerung
Das Schicksal von Pull-Anfragen
Wir haben eine lokale Korrektur vorgenommen und die letzte Pull-Anforderung in PhotoView erstellt. Trotz der Tatsache, dass einige PRs seit einem Jahr dort hängen, wurden unsere PRs in die Hauptniederlassung aufgenommen und sogar eine neue Version von PhotoView wurde veröffentlicht. Danach haben wir beschlossen, das lokale Modul aus der Android-Galerie auszuschneiden und die offiziellen PhotoView-Quellen aufzurufen. Dazu musste ich Unterstützung für AndroidX hinzufügen, das in Version 2.1.3 zu PhotoView hinzugefügt wurde .
Wo finde ich die Bibliothek?
Suchen Sie hier nach dem Quellcode der Android Gallery-Bibliothek - https://github.com/redmadrobot-spb/android-gallery - sowie nach einer Gebrauchsanweisung. Und um Projekte zu unterstützen, die weiterhin die Support-Bibliothek verwenden, haben wir eine separate Version von android-gallery-veraltet erstellt . Aber seien Sie vorsichtig, denn in einem Jahr wird die Support Library zu einem Kürbis!
Was weiter
Jetzt passt die Bibliothek ganz zu uns, aber im Laufe der Entwicklung entstanden neue Ideen. Hier sind einige davon:
- die Möglichkeit, die Bibliothek in jedem Layout und nicht nur in einem separaten FragmentDialog zu verwenden;
- die Möglichkeit, die Benutzeroberfläche anzupassen;
- die Fähigkeit, Gilde und ExoPlayer zu ersetzen;
- die Möglichkeit, etwas anstelle von ViewPager zu verwenden.
Referenzen
UPD
Beim Schreiben eines Artikels kam eine ähnliche Bibliothek der Entwickler von FrescoImageViewer heraus . Sie haben Unterstützung für Übergangsanimationen hinzugefügt, aber bisher haben wir nur Video-Unterstützung. :) :)