Cómo creamos nuestra biblioteca de la Galería de Android para ver contenido multimedia



Hola Habr! No hace mucho tiempo, en busca de aventuras, nuevos proyectos y tecnologías, yo se convirtió en un robot Se instaló en Redmadrobot. Tengo una silla, un monitor y un macbook, y un pequeño proyecto interno para el calentamiento. Era necesario terminar y publicar una biblioteca autoescrita para ver el contenido multimedia que usamos en nuestros proyectos. En el artículo, le diré cómo comprender los eventos táctiles en una semana, convertirse en un código abierto, encontrar un error en el SDK de Android y publicar una biblioteca.


Inicio


Una de las características importantes de las aplicaciones de nuestra tienda es la capacidad de ver videos y fotos de productos y servicios cerca y desde todos lados. No queríamos reinventar la rueda y fuimos a buscar una biblioteca terminada que se adaptara a nosotros.


Planeamos encontrar una solución para que el usuario pudiera:


  • ver fotos;
  • Escala fotos usando pellizcar para hacer zoom y doble toque;
  • Ver un video
  • voltear contenido multimedia;
  • cierre la tarjeta fotográfica con un deslizamiento vertical (deslice para descartar).

Esto es lo que encontramos:


  • FrescoImageViewer : admite la visualización y el desplazamiento a través de fotos y gestos básicos, pero no admite la visualización de videos y está destinado a la biblioteca de Fresco .
  • PhotoView : admite la visualización de fotos, la mayoría de los principales gestos de control, excepto el desplazamiento, deslizar para descartar, no admite la visualización de videos.
  • PhotoDraweeView : similar en funcionalidad a PhotoView, pero destinado a Fresco.

Como ninguna de las bibliotecas encontró que cumplía con los requisitos, tuvimos que escribir la nuestra.


Nos damos cuenta de la biblioteca


Para obtener la funcionalidad necesaria, finalizamos las soluciones existentes de otras bibliotecas. Decidieron dar el modesto nombre de Android Gallery a lo que sucedió.


Implementamos funcionalidad


Ver y ampliar fotos
Para ver fotos, tomamos la biblioteca PhotoView, que admite el escalado de fábrica.


Ver video
Para ver el video, tomaron ExoPlayer , que se reutiliza en MediaPagerAdapter . Cuando un usuario abre un video por primera vez, se crea ExoPlayer. Al cambiar a otro elemento, se pone en cola, por lo que la próxima vez que inicie el video, se utilizará la instancia de ExoPlayer ya creada. Esto hace que la transición entre elementos sea más suave.


Desplazamiento de contenido multimedia
Aquí utilizamos MultiTouchViewPager de FrescoImageViewer, que no intercepta eventos multitáctiles, por lo que pudimos agregarle gestos para escalar la imagen.


Desliza para descartar
PhotoView no admitía deslizar para descartar y eliminar el rebote (restaurar el tamaño de la imagen original cuando la imagen se escala hacia arriba o hacia abajo).
Así es como logramos manejarlo.


Aprendizaje de eventos táctiles para implementar deslizar para descartar


Antes de pasar a admitir deslizar para descartar, debe comprender cómo funcionan los eventos táctiles. Cuando el usuario toca la pantalla, se llama al método dispatchTouchEvent(motionEvent: MotionEvent) en la Actividad actual, donde llega MotionEvent.ACTION_DOWN . Este método decide el destino del evento. Puede pasar motionEvent a onTouchEvent(motionEvent: MotionEvent) para el procesamiento táctil, o dejarlo ir más allá, de arriba a abajo, en la jerarquía Ver. Ver, que está interesado en un evento y / o eventos posteriores antes de ACTION_UP , devuelve verdadero.


Después de eso, todos los eventos del gesto actual caerán en esta Vista hasta que el gesto termine con el evento ACTION_UP o el ViewGroup padre tome el control (entonces el evento ACTION_CANCELED pasará a la Vista). Si el evento recorrió toda la jerarquía de Vista y nadie estaba interesado, volverá a la Actividad en onTouchEvent(motionEvent: MotionEvent) .



En nuestra biblioteca de Android Gallery, el primer evento ACTION_DOWN llega a dispatchTouchEvent() en PhotoView, donde motionEvent pasa a la implementación onTouch() , que devuelve verdadero. Además, todos los eventos pasan por la misma cadena hasta uno de:


  • ACTION_UP ;
  • ViewPager intentará interceptar el evento para desplazarse;
  • VerticalDragLayout intentará interceptar el evento para deslizar y descartar.

Solo ViewGroup en el onInterceptTouchEvent(motionEvent: MotionEvent) puede interceptar eventos. Incluso si la Vista está interesada en cualquier MotionEvent, el evento en sí pasará por el dispatchTouchEvent(motionEvent: MotionEvent) toda la cadena ViewGroup anterior. En consecuencia, los padres siempre "vigilan" a sus hijos. Cualquier ViewGroup principal puede interceptar el evento y devolver verdadero en el onInterceptTouchEvent(motionEvent: MotionEvent) , luego todas las MotionEvent.ACTION_CANCEL secundarias recibirán MotionEvent.ACTION_CANCEL en onTouchEvent(motionEvent: MotionEvent) .


Ejemplo: el usuario mantiene un dedo sobre un elemento en un RecyclerView, luego los eventos se procesan en el mismo elemento. Pero tan pronto como comience a mover su dedo hacia arriba / abajo, RecyclerView interceptará los eventos, comenzará el desplazamiento y la Vista recibirá el evento ACTION_CANCEL .



En la Galería de Android, VerticalDragLayout puede interceptar eventos para deslizar para descartar o ViewPager para desplazarse. Pero View puede evitar que el padre requestDisallowInterceptTouchEvent(true) eventos llamando al método requestDisallowInterceptTouchEvent(true) . Esto puede ser necesario si la Vista necesita realizar acciones cuya intercepción por parte del padre no es deseable para nosotros.


Por ejemplo, cuando un usuario en un jugador salta una pista a un tiempo específico. Si el ViewPager padre interceptara un desplazamiento horizontal, habría una transición a la siguiente pista.


Para manejar el deslizamiento para descartar, escribimos VerticalDragLayout, pero no recibió eventos táctiles de PhotoView. Para entender por qué sucede esto, tuve que descubrir cómo se procesan los eventos táctiles en PhotoView.


Orden de procesamiento:


  1. Cuando MotionEvent.ACTION_DOWN en VerticalDragLayout, interceptTouchEvent() activa interceptTouchEvent() , que devuelve falso, porque este ViewGroup solo está interesado en ACTION_MOVE vertical. La dirección ACTION_MOVE se define en dispatchTouchEvent() , después de lo cual el evento se pasa al método super.dispatchTouchEvent() en ViewGroup, donde el evento se pasa a la implementación interceptTouchEvent() en VerticalDragLayout.



  2. Cuando el evento ACTION_DOWN alcanza el método onTouch() en PhotoView, la vista elimina la capacidad de interceptar la gestión de eventos. Todos los eventos de gestos posteriores no entran en el método interceptTouchEvent() . La capacidad de interceptar el control se otorga al padre solo si se completa el gesto o si ACTION_MOVE horizontal ACTION_MOVE en el borde derecho / izquierdo de la imagen.
     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); } } 



    Dado que PhotoView permite al padre interceptar el control solo en el caso de ACTION_MOVE horizontal, y deslizar para descartar es ACTION_MOVE vertical, entonces VerticalDragLayout no puede interceptar el control de eventos para implementar un gesto. Para solucionar esto, debe agregar la capacidad de interceptar el control en caso 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); } } 

    Ahora, en el caso de la primera ACTION_MOVE PhotoView vertical dará la oportunidad de interceptar al padre:



    El siguiente ACTION_MOVE será interceptado en VerticalDragLyout, y el evento ACTION_CANCEL volará a la Vista secundaria:



    Todos los demás ACTION_MOVE volarán a VerticalDragLayout a lo largo de la cadena estándar. Es importante que después de que ViewGroup tome el control de los eventos de la Vista secundaria, la Vista secundaria no pueda recuperar el control.



    Así que implementamos deslizar para descartar el soporte para la biblioteca PhotoView. En nuestra biblioteca, utilizamos las fuentes modificadas de PhotoView extraídas en un módulo separado y creamos una solicitud de fusión en el repositorio original de PhotoView.

    Implementamos debinds en PhotoView


    Recuerde que un rebote es una restauración de animación de una escala aceptable cuando la imagen se escala más allá de sus límites.



    No había tal posibilidad en PhotoView. Pero desde que comenzamos a cavar el código abierto de otra persona, ¿por qué parar allí? En PhotoView, puede establecer un límite de zoom. Inicialmente, este es el mínimo - x1 y el máximo - x3. La imagen no puede ir más allá de estos límites.



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

    Para empezar, decidimos eliminar la condición de "prohibición de escalar al alcanzar un mínimo": simplemente eliminamos la condición getScale() > mMinScale || scaleFactor > 1f getScale() > mMinScale || scaleFactor > 1f . Y luego, de repente ...



    Debuns ganados! Aparentemente, esto sucedió debido al hecho de que los creadores de la biblioteca decidieron jugar a lo seguro dos veces, lo que provocó un rechazo y una restricción en la escala. En la implementación del evento onTouch, es decir, en el caso de MotionEvent.ACTION_UP , si el usuario ha escalado más / menos que el máximo / mínimo, se inicia AnimatedZoomRunnable , que devuelve la imagen a su tamaño original.


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

    Además de deslizar para descartar, finalizamos PhotoView en las fuentes de nuestra biblioteca y creamos una solicitud de extracción con la "adición" de un rebote al PhotoView original.


    Solucione un error repentino en PhotoView


    PhotoView tiene un error muy desagradable. Cuando el usuario desea ampliar la imagen con doble toque y tiene un ataque de epilepsia la imagen comienza a escalar, puede girar 180 grados verticalmente. Este error se puede encontrar incluso en aplicaciones populares de Google Play, por ejemplo, en Cyan.



    Después de una larga búsqueda, todavía localizamos este error: a veces un factor de escala negativo se alimenta a la matriz de entrada para escalar la imagen para escalar, lo que hace que la imagen se voltee.


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

    Para escalar desde Android ScaleGestureDetector, obtenemos scaleFactor, que se calcula de la siguiente manera:


     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 anula este método con registros de depuración, puede rastrear en qué valores particulares de las variables se obtiene un scaleFactor negativo:


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

    Existe la sospecha de que intentaron resolver este problema multiplicando spanDiff por SCALE_FACTOR == 0.5. Pero esta solución no ayudará si la diferencia entre mCurrSpan y mPrevSpan es más de tres veces. Ya se ha iniciado un ticket para este error, pero aún no se ha solucionado.
    Muleta La solución más fácil a este problema es simplemente omitir los valores negativos de scaleFactor. En la práctica, el usuario no notará que la imagen a veces se acerca un poco menos de lo normal.


    En lugar de una conclusión


    El destino de las solicitudes de extracción


    Hicimos una solución local y creamos la última solicitud de extracción en PhotoView. A pesar del hecho de que algunos RP han estado colgados allí durante un año, nuestros RP se han agregado a la rama maestra e incluso se ha lanzado una nueva versión de PhotoView. Después de lo cual decidimos cortar el módulo local de la Galería de Android y extraer las fuentes oficiales de PhotoView. Para hacer esto, tuve que agregar soporte para AndroidX, que se agregó a PhotoView en la versión 2.1.3 .


    Donde encontrar la biblioteca


    Busque el código fuente de la biblioteca de la Galería de Android aquí: https://github.com/redmadrobot-spb/android-gallery , junto con las instrucciones de uso. Y para apoyar proyectos que todavía usan la Biblioteca de soporte, creamos una versión separada de android-gallery-obsoleto . Pero tenga cuidado, porque en un año la biblioteca de soporte se convertirá en una calabaza.


    Que sigue


    Ahora la biblioteca nos satisface completamente, pero en el proceso de desarrollo surgieron nuevas ideas. Aquí hay algunos de ellos:


    • la capacidad de usar la biblioteca en cualquier diseño, y no solo en un FragmentDialog separado;
    • la capacidad de personalizar la interfaz de usuario;
    • la capacidad de reemplazar a Gilde y ExoPlayer;
    • la capacidad de usar algo en lugar de ViewPager.

    Referencias



    UPD


    Mientras escribía un artículo, salió una biblioteca similar de los desarrolladores de FrescoImageViewer . Agregaron soporte para la animación de transición , pero hasta ahora solo tenemos soporte de video. :)

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


All Articles