Comment nous avons créé notre bibliothèque Android Gallery pour afficher le contenu multimédia



Bonjour, Habr! Il n'y a pas si longtemps, à la recherche d'aventures, de nouveaux projets et technologies, je est devenu un robot installés à Redmadrobot. J'ai eu une chaise, un moniteur et un macbook, et un petit projet interne pour me réchauffer. Il était nécessaire de terminer et de publier une bibliothèque auto-écrite pour visualiser le contenu multimédia que nous utilisons dans nos projets. Dans l'article, je vais vous expliquer comment comprendre les événements tactiles en une semaine, devenir un open source, trouver un bug dans Android sdk et publier une bibliothèque.


Commencer


L'une des caractéristiques importantes de nos applications de magasin est la possibilité de visualiser des vidéos et des photos de produits et services à proximité et de tous côtés. Nous ne voulions pas réinventer la roue et sommes allés à la recherche d'une bibliothèque finie qui nous conviendrait.


Nous avions prévu de trouver une telle solution pour que l'utilisateur puisse:


  • voir des photos;
  • Redimensionnez les photos à l'aide d'une pincée pour zoomer et appuyez deux fois;
  • Regardez une vidéo
  • retourner le contenu multimédia;
  • fermez la carte photo avec un glissement vertical (glissez pour ignorer).

Voici ce que nous avons trouvé:


  • FrescoImageViewer - prend en charge l'affichage et le défilement des photos et des gestes de base, mais ne prend pas en charge l'affichage des vidéos et est destiné à la bibliothèque Fresco .
  • PhotoView - prend en charge l'affichage des photos, la plupart des principaux gestes de contrôle, à l'exception du défilement, du balayage pour ignorer, ne prend pas en charge l'affichage des vidéos.
  • PhotoDraweeView - fonctionnalités similaires à PhotoView, mais destinées à Fresco.

Puisqu'aucune des bibliothèques trouvées ne répondait pleinement aux exigences, nous avons dû écrire la nôtre.


Nous réalisons la bibliothèque


Pour obtenir les fonctionnalités nécessaires, nous avons finalisé les solutions existantes d'autres bibliothèques. Ils ont décidé de donner le modeste nom Android Gallery à ce qui s'est passé.


Nous implémentons des fonctionnalités


Afficher et agrandir des photos
Pour voir les photos, nous avons pris la bibliothèque PhotoView, qui prend en charge la mise à l'échelle hors de la boîte.


Regardez la vidéo
Pour regarder la vidéo, ils ont pris ExoPlayer , qui est réutilisé dans le MediaPagerAdapter . Lorsqu'un utilisateur ouvre une vidéo pour la première fois, ExoPlayer est créé. Lorsque vous passez à un autre élément, il est mis en file d'attente, donc au prochain démarrage de la vidéo, l'instance ExoPlayer déjà créée sera utilisée. Cela facilite la transition entre les éléments.


Défilement du contenu multimédia
Ici, nous avons utilisé le MultiTouchViewPager de FrescoImageViewer, qui n'intercepte pas les événements multi-touch, nous avons donc pu lui ajouter des gestes pour mettre l'image à l'échelle.


Glissez pour ignorer
PhotoView ne prend pas en charge le balayage pour ignorer et rebondir (restauration de la taille d'image d'origine lorsque l'image est agrandie ou réduite).
Voici comment nous avons réussi à le gérer.


Apprentissage des événements tactiles pour implémenter le balayage pour ignorer


Avant de passer à la prise en charge du balayage pour ignorer, vous devez comprendre le fonctionnement des événements tactiles. Lorsque l'utilisateur touche l'écran, la méthode dispatchTouchEvent(motionEvent: MotionEvent) est appelée dans l'activité en cours, où se trouve MotionEvent.ACTION_DOWN . Cette méthode décide du sort de l'événement. Vous pouvez passer motionEvent à onTouchEvent(motionEvent: MotionEvent) pour le traitement tactile, ou le laisser aller plus loin, de haut en bas, dans la hiérarchie des vues. La vue, qui s'intéresse à un événement et / ou aux événements ultérieurs avant ACTION_UP , renvoie true.


Après cela, tous les événements du geste actuel tomberont dans cette vue jusqu'à ce que le geste se termine avec l'événement ACTION_UP ou que le ViewGroup parent prenne le contrôle (puis l'événement ACTION_CANCELED viendra à View). Si l'événement a contourné la hiérarchie de View entière et que personne n'était intéressé, il revient à l'activité dans onTouchEvent(motionEvent: MotionEvent) .



Dans notre bibliothèque Android Gallery, le premier événement ACTION_DOWN vient à dispatchTouchEvent() dans PhotoView, où motionEvent passé à l' onTouch() , qui retourne true. De plus, tous les événements passent par la même chaîne jusqu'à l'un des événements suivants:


  • ACTION_UP ;
  • ViewPager tentera d'intercepter l'événement pour le défilement;
  • VerticalDragLayout essaiera d'intercepter l'événement pour que le balayage soit ignoré.

Seul ViewGroup de la onInterceptTouchEvent(motionEvent: MotionEvent) peut intercepter des événements. Même si la vue est intéressée par un MotionEvent, l'événement lui-même passera par le dispatchTouchEvent(motionEvent: MotionEvent) toute la chaîne ViewGroup précédente. Par conséquent, les parents «surveillent» toujours leurs enfants. Tout ViewGroup parent peut intercepter l'événement et renvoyer true dans la onInterceptTouchEvent(motionEvent: MotionEvent) , puis toutes les MotionEvent.ACTION_CANCEL enfants recevront MotionEvent.ACTION_CANCEL dans onTouchEvent(motionEvent: MotionEvent) .


Exemple: l'utilisateur tient un doigt sur un élément dans un RecyclerView, puis les événements sont traités dans le même élément. Mais dès qu'il commence à déplacer son doigt vers le haut / bas, le RecyclerView interceptera les événements, et le défilement commencera, et la vue recevra l'événement ACTION_CANCEL .



Dans la galerie Android, VerticalDragLayout peut intercepter des événements pour un balayage à ignorer ou ViewPager pour un défilement. Mais View peut empêcher le parent d' requestDisallowInterceptTouchEvent(true) événements en appelant la requestDisallowInterceptTouchEvent(true) . Cela peut être nécessaire si la vue doit effectuer des actions dont l'interception par le parent n'est pas souhaitable pour nous.


Par exemple, lorsqu'un utilisateur d'un lecteur saute une piste à une heure spécifique. Si le ViewPager parent interceptait un défilement horizontal, il y aurait une transition vers la piste suivante.


Pour gérer le balayage à ignorer, nous avons écrit VerticalDragLayout, mais il n'a pas reçu d'événements tactiles de PhotoView. Pour comprendre pourquoi cela se produit, j'ai dû comprendre comment les événements tactiles sont traités dans PhotoView.


Ordre de traitement:


  1. Lorsque MotionEvent.ACTION_DOWN dans VerticalDragLayout, interceptTouchEvent() déclenché, ce qui renvoie false, car ce ViewGroup n'est intéressé que par ACTION_MOVE vertical. La direction ACTION_MOVE est définie dans dispatchTouchEvent() , après quoi l'événement est transmis à la méthode super.dispatchTouchEvent() dans ViewGroup, où l'événement est transmis à l'implémentation interceptTouchEvent() dans VerticalDragLayout.



  2. Lorsque l'événement ACTION_DOWN atteint la méthode onTouch() dans PhotoView, la vue enlève la possibilité d'intercepter la gestion des événements. Tous les événements de mouvement suivants ne tombent pas dans la méthode interceptTouchEvent() . La capacité d'intercepter le contrôle n'est donnée au parent que si le geste est terminé ou si ACTION_MOVE horizontal ACTION_MOVE sur la bordure droite / gauche de l'image.
     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); } } 



    Étant donné que PhotoView permet au parent d'intercepter le contrôle uniquement en cas d' ACTION_MOVE horizontal et que le balayage pour ACTION_MOVE est ACTION_MOVE vertical, VerticalDragLayout ne peut pas intercepter le contrôle d'événements pour implémenter un geste. Pour résoudre ce problème, vous devez ajouter la possibilité d'intercepter le contrôle en cas de ACTION_MOVE vertical.

     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); } } 

    Maintenant, dans le cas de la première ACTION_MOVE verticale, ACTION_MOVE donnera la possibilité d'intercepter le parent:



    L' ACTION_MOVE suivant sera intercepté dans VerticalDragLyout et l'événement ACTION_CANCEL volera dans la vue enfant:



    Tous les autres ACTION_MOVE voleront vers VerticalDragLayout le long de la chaîne standard. Il est important qu'après que le ViewGroup ait pris le contrôle des événements de la vue enfant, la vue enfant ne puisse pas reprendre le contrôle.



    Nous avons donc implémenté le balayage pour désactiver la prise en charge de la bibliothèque PhotoView. Dans notre bibliothèque, nous avons utilisé les sources modifiées de PhotoView extraites dans un module distinct et créé une demande de fusion dans le référentiel PhotoView d'origine.

    Nous implémentons des debinds dans PhotoView


    Rappelons qu'un anti-rebond est une animation-restauration d'une échelle acceptable lorsque l'image est mise à l'échelle au-delà de ses limites.



    Il n'y avait pas une telle possibilité dans PhotoView. Mais depuis que nous avons commencé à creuser l'open source de quelqu'un d'autre, pourquoi s'arrêter là? Dans PhotoView, vous pouvez définir une limite de zoom. Au départ, c'est le minimum - x1 et le maximum - x3. L'image ne peut pas dépasser ces limites.



     @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); //      checkAndDisplayMatrix(); } } 

    Pour commencer, nous avons décidé de supprimer la condition «interdiction de mise à l'échelle pour atteindre un minimum»: nous avons simplement supprimé la condition getScale() > mMinScale || scaleFactor > 1f getScale() > mMinScale || scaleFactor > 1f . Et puis soudain ...



    Debuns gagné! Apparemment, cela s'est produit en raison du fait que les créateurs de la bibliothèque ont décidé de la jouer deux fois en toute sécurité, faisant à la fois un rebond et une restriction sur la mise à l'échelle. Dans l'implémentation de l'événement onTouch, à savoir dans le cas de MotionEvent.ACTION_UP , si l'utilisateur a évolué plus / moins que le maximum / minimum, AnimatedZoomRunnable est lancé, ce qui ramène l'image à sa taille d'origine.


     @Override public boolean onTouch(View v, MotionEvent ev) { boolean handled = false; switch (ev.getAction()) { case MotionEvent.ACTION_UP: // If the user has zoomed less than min scale, zoom back // to min scale if (getScale() < mMinScale) { RectF rect = getDisplayRect(); if (rect != null) { v.post(new AnimatedZoomRunnable(getScale(), mMinScale, rect.centerX(), rect.centerY())); handled = true; } } else if (getScale() > mMaxScale) { RectF rect = getDisplayRect(); if (rect != null) { v.post(new AnimatedZoomRunnable(getScale(), mMaxScale, rect.centerX(), rect.centerY())); handled = true; } } break; } } 

    En plus de glisser pour ignorer, nous avons finalisé PhotoView dans les sources de notre bibliothèque et créé une pull request avec «l'ajout» d'un anti-rebond au PhotoView d'origine.


    Correction d'un bug soudain dans PhotoView


    PhotoView a un bug très méchant. Lorsque l'utilisateur souhaite agrandir l'image avec un double tap et il a une crise d'épilepsie l'image commence à l'échelle, elle peut basculer de 180 degrés verticalement. Ce bogue peut être trouvé même dans les applications populaires de Google Play, par exemple, dans Cyan.



    Après une longue recherche, nous avons toujours localisé ce bogue: parfois un scaleFactor négatif est appliqué à la matrice d'entrée pour la mise à l'échelle de l'image pour la mise à l'échelle, ce qui provoque le retournement de l'image.


    CustomGestureDetector


     @Override public boolean onScale(ScaleGestureDetector detector) { //  -   scaleFactor < 0 //      float scaleFactor = detector.getScaleFactor(); if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) return false; //    scaleFactor   callback //      mListener.onScale(scaleFactor, detector.getFocusX(), detector.getFocusY()); return true; } 

    Pour évoluer à partir d'Android ScaleGestureDetector, nous obtenons scaleFactor, qui est calculé comme suit:


     public float getScaleFactor() { if (inAnchoredScaleMode()) { // Drag is moving up; the further away from the gesture // start, the smaller the span should be, the closer, // the larger the span, and therefore the larger the scale final boolean scaleUp = (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) || (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan)); final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR); return mPrevSpan <= 0 ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff); } return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1; } 

    Si vous remplacez cette méthode par des journaux de débogage, vous pouvez suivre à quelles valeurs particulières des variables un scaleFactor négatif est obtenu:


     mEventBeforeOrAboveStartingGestureEvent is true; SCALE_FACTOR is 0.5; mCurrSpan: 1075.4398; mPrevSpan 38.867798; scaleUp: false; spanDiff: 13.334586; eval result is -12.334586 

    On soupçonne qu'ils ont essayé de résoudre ce problème en multipliant spanDiff par SCALE_FACTOR == 0,5. Mais cette solution n'aidera pas si la différence entre mCurrSpan et mPrevSpan est plus de trois fois. Un ticket a déjà été démarré pour ce bug, mais il n'a pas encore été corrigé.
    Béquille La solution la plus simple à ce problème consiste simplement à ignorer les valeurs scaleFactor négatives. En pratique, l'utilisateur ne remarquera pas que l'image est parfois agrandie légèrement moins régulièrement que d'habitude.


    Au lieu d'une conclusion


    Le sort des demandes de pull


    Nous avons effectué un correctif local et créé la dernière demande d'extraction dans PhotoView. Malgré le fait que certains PR sont suspendus depuis un an, nos PR ont été ajoutés à la branche principale et même une nouvelle version de PhotoView a été publiée. Après quoi, nous avons décidé de couper le module local de la galerie Android et de récupérer les sources officielles de PhotoView. Pour ce faire, j'ai dû ajouter la prise en charge d'AndroidX, qui a été ajouté à PhotoView dans la version 2.1.3 .


    Où trouver la bibliothèque


    Recherchez le code source de la bibliothèque Android Gallery ici - https://github.com/redmadrobot-spb/android-gallery , ainsi que les instructions d'utilisation. Et pour prendre en charge les projets qui utilisent toujours la bibliothèque de support, nous avons créé une version distincte de android-gallery-deprecated . Mais attention, car dans un an, la bibliothèque de support se transformera en citrouille!


    Et ensuite


    Maintenant, la bibliothèque nous convient parfaitement, mais en cours de développement, de nouvelles idées sont apparues. En voici quelques uns:


    • la possibilité d'utiliser la bibliothèque dans n'importe quelle mise en page, et pas seulement un FragmentDialog séparé;
    • la possibilité de personnaliser l'interface utilisateur;
    • la possibilité de remplacer Gilde et ExoPlayer;
    • la possibilité d'utiliser quelque chose au lieu de ViewPager.

    Les références



    UPD


    Lors de la rédaction d'un article, une bibliothèque similaire des développeurs de FrescoImageViewer est sortie . Ils ont ajouté la prise en charge de l' animation de transition , mais jusqu'à présent, nous n'avons qu'une prise en charge vidéo. :)

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


All Articles