Como criamos nossa biblioteca da Galeria do Android para exibir conteúdo de mídia



Olá Habr! Não faz muito tempo, em busca de aventura, novos projetos e tecnologias, eu tornou-se um robô estabeleceu-se em Redmadrobot. Eu tenho uma cadeira, um monitor e um macbook e um pequeno projeto interno para aquecer. Foi necessário finalizar e publicar uma biblioteca auto-escrita para visualizar o conteúdo de mídia que usamos em nossos projetos. No artigo, mostrarei como entender os eventos de toque em uma semana, tornar-se um código aberto, encontrar um bug no Android sdk e publicar uma biblioteca.


Iniciar


Uma das características importantes de nossos aplicativos da loja é a capacidade de visualizar vídeos e fotos de produtos e serviços perto e de todos os lados. Não queríamos reinventar a roda e fomos à procura de uma biblioteca pronta que nos convivesse.


Planejamos encontrar essa solução para que o usuário pudesse:


  • ver fotos;
  • Dimensione fotos usando pitada de zoom e toque duas vezes;
  • Assista a um vídeo
  • lançando conteúdo de mídia;
  • feche o cartão fotográfico com um deslize vertical (deslize para dispensar).

Aqui está o que encontramos:


  • FrescoImageViewer - suporta a visualização e a rolagem de fotos e gestos básicos, mas não suporta a visualização de vídeos e é destinado à biblioteca Fresco .
  • PhotoView - suporta a visualização de fotos, a maioria dos principais gestos de controle, exceto rolagem, deslize para dispensar, não suporta a visualização de vídeos.
  • PhotoDraweeView - semelhante em funcionalidade ao PhotoView, mas destinado ao Fresco.

Como nenhuma das bibliotecas encontradas atendeu totalmente aos requisitos, tivemos que escrever nossas próprias.


Percebemos a biblioteca


Para obter a funcionalidade necessária, finalizamos as soluções existentes de outras bibliotecas. Eles decidiram dar o nome modesto Android Gallery ao que aconteceu.


Implementamos funcionalidade


Ver e ampliar fotos
Para visualizar as fotos, tiramos a biblioteca PhotoView, que suporta o dimensionamento imediato.


Assista ao vídeo
Para assistir ao vídeo, eles usaram o ExoPlayer , que é reutilizado no MediaPagerAdapter . Quando um usuário abre um vídeo pela primeira vez, o ExoPlayer é criado. Ao alternar para outro elemento, ele é colocado na fila; portanto, da próxima vez que você iniciar o vídeo, a instância do ExoPlayer já criada será usada. Isso facilita a transição entre os elementos.


Rolagem de conteúdo de mídia
Aqui, usamos o MultiTouchViewPager do FrescoImageViewer, que não intercepta eventos multi-touch, portanto, pudemos adicionar gestos para dimensionar a imagem.


Deslize para dispensar
O PhotoView não suporta furto para dispensar e rejeitar (restaurando o tamanho da imagem original quando a imagem é ampliada ou reduzida).
Veja como conseguimos lidar com isso.


Aprender eventos de toque para implementar furto para dispensar


Antes de passar para dar suporte ao deslize para dispensar, você precisa entender como os eventos de toque funcionam. Quando o usuário toca na tela, o método dispatchTouchEvent(motionEvent: MotionEvent) é chamado na Activity atual, onde MotionEvent.ACTION_DOWN chega. Este método decide o destino do evento. Você pode passar motionEvent para onTouchEvent(motionEvent: MotionEvent) para processamento por toque ou deixá-lo ir além, de cima para baixo, na hierarquia Exibir. A exibição, interessada em um evento e / ou eventos subsequentes anteriores a ACTION_UP , retorna true.


Depois disso, todos os eventos do gesto atual cairão nessa Visualização até que o gesto termine com o evento ACTION_UP ou o ViewGroup pai assuma o controle (o evento ACTION_CANCELED chegará à View). Se o evento percorreu toda a hierarquia da Visualização e ninguém estava interessado, ele retornará à Atividade em onTouchEvent(motionEvent: MotionEvent) .



Na nossa biblioteca da Galeria do Android, o primeiro evento ACTION_DOWN chega a dispatchTouchEvent() no PhotoView, onde motionEvent passado para a implementação onTouch() , que retorna true. Além disso, todos os eventos passam pela mesma cadeia até um dos seguintes:


  • ACTION_UP ;
  • O ViewPager tentará interceptar o evento para rolagem;
  • VerticalDragLayout tentará interceptar o evento para que o furto seja descartado.

Somente o ViewGroup no onInterceptTouchEvent(motionEvent: MotionEvent) pode interceptar eventos. Mesmo que a View esteja interessada em qualquer MotionEvent, o evento em si passará pelo dispatchTouchEvent(motionEvent: MotionEvent) toda a cadeia anterior do ViewGroup. Consequentemente, os pais sempre “vigiam” seus filhos. Qualquer ViewGroup pai pode interceptar o evento e retornar true no onInterceptTouchEvent(motionEvent: MotionEvent) ; todas as MotionEvent.ACTION_CANCEL filho receberão MotionEvent.ACTION_CANCEL em onTouchEvent(motionEvent: MotionEvent) .


Exemplo: o usuário segura um dedo em um elemento em um RecyclerView e os eventos são processados ​​no mesmo elemento. Porém, assim que ele começar a mover o dedo para cima / para baixo, o RecyclerView interceptará os eventos, a rolagem começará e a exibição receberá o evento ACTION_CANCEL .



Na Galeria do Android, o VerticalDragLayout pode interceptar eventos para deslizar para dispensar ou o ViewPager para rolar. Mas o View pode impedir que o pai requestDisallowInterceptTouchEvent(true) eventos chamando o método requestDisallowInterceptTouchEvent(true) . Isso pode ser necessário se o View precisar executar ações cuja interceptação pelos pais não é desejável para nós.


Por exemplo, quando um usuário em um jogador pula uma faixa para um horário específico. Se o ViewPager pai interceptasse uma rolagem horizontal, haveria uma transição para a próxima faixa.


Para manipular o deslize para dispensar, escrevemos VerticalDragLayout, mas ele não recebeu eventos de toque do PhotoView. Para entender por que isso acontece, eu tive que descobrir como os eventos de toque são processados ​​no PhotoView.


Ordem de processamento:


  1. Quando MotionEvent.ACTION_DOWN em VerticalDragLayout, interceptTouchEvent() acionado, que retorna falso, porque este ViewGroup está interessado apenas na vertical ACTION_MOVE. A direção ACTION_MOVE é definida em dispatchTouchEvent() , após o qual o evento é passado para o método super.dispatchTouchEvent() no ViewGroup, onde o evento é passado para a implementação de interceptTouchEvent() em VerticalDragLayout.



  2. Quando o evento ACTION_DOWN atinge o método onTouch() no PhotoView, a exibição retira a capacidade de interceptar o gerenciamento de eventos. Todos os eventos de gestos subsequentes não se enquadram no método interceptTouchEvent() . A capacidade de interceptar o controle é concedida aos pais apenas se o gesto for concluído ou se ACTION_MOVE horizontal ACTION_MOVE na borda direita / esquerda da imagem.
     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); } } 



    Como o PhotoView permite que o pai intercepte o controle apenas no caso de ACTION_MOVE horizontal e deslize para descartar é vertical ACTION_MOVE , o VerticalDragLayout não pode interceptar o controle de eventos para implementar um gesto. Para corrigir isso, você precisa adicionar a capacidade de interceptar o controle no 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); } } 

    Agora, no caso da primeira vertical ACTION_MOVE PhotoView, você terá a oportunidade de interceptar o pai:



    O seguinte ACTION_MOVE será interceptado em VerticalDragLyout e o evento ACTION_CANCEL voará para a exibição filho:



    Todos os outros ACTION_MOVE voam para VerticalDragLayout ao longo da cadeia padrão. É importante que, depois que o ViewGroup assuma o controle dos eventos da Visualização filho, a Visualização filho não possa recuperar o controle.



    Por isso, implementamos o furto para dispensar o suporte à biblioteca PhotoView. Em nossa biblioteca, usamos as fontes modificadas do PhotoView tiradas em um módulo separado e criamos uma solicitação de mesclagem no repositório original do PhotoView.

    Implementamos debinds no PhotoView


    Lembre-se de que uma rejeição é uma restauração de animação de uma escala aceitável quando a imagem é dimensionada além de seus limites.



    Não havia essa possibilidade no PhotoView. Mas desde que começamos a procurar o código aberto de outra pessoa, por que parar por aí? No PhotoView, você pode definir um limite de zoom. Inicialmente, esse é o mínimo - x1 e o máximo - x3. A imagem não pode ir além desses 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(); } } 

    Para começar, decidimos remover a condição "proibição de dimensionar para atingir o mínimo": simplesmente descartamos a condição getScale() > mMinScale || scaleFactor > 1f getScale() > mMinScale || scaleFactor > 1f . E então de repente ...



    Debuns ganhos! Aparentemente, isso aconteceu devido ao fato de que os criadores da biblioteca decidiram jogar pelo seguro duas vezes, fazendo tanto um rebounce quanto uma restrição no dimensionamento. Na implementação do evento onTouch, principalmente no caso de MotionEvent.ACTION_UP , se o usuário tiver escalado mais / menos que o máximo / mínimo, o AnimatedZoomRunnable será iniciado, retornando a imagem ao tamanho 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; } } 

    Assim como com o toque para dispensar, finalizamos o PhotoView nas fontes de nossa biblioteca e criamos uma solicitação de recebimento com a "adição" de uma devolução ao PhotoView original.


    Corrigir um erro repentino no PhotoView


    PhotoView tem um bug muito desagradável. Quando o usuário deseja ampliar a imagem, toque duas vezes e ele tem um ataque de epilepsia Se a imagem começar a aumentar, ela poderá girar 180 graus na vertical. Esse bug pode ser encontrado até em aplicativos populares do Google Play, por exemplo, em ciano.



    Após uma longa pesquisa, ainda localizamos esse bug: algumas vezes, um scaleFactor negativo é alimentado na matriz de entrada para dimensionamento de imagem para dimensionamento, o que faz com que a imagem seja invertida.


    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 dimensionar a partir do Android ScaleGestureDetector, obtemos scaleFactor, que é calculado da seguinte forma:


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

    Se você substituir esse método pelos logs de depuração, poderá rastrear em quais valores específicos das variáveis ​​um scaleFactor negativo é obtido:


     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 uma suspeita de que eles tentaram resolver esse problema multiplicando spanDiff por SCALE_FACTOR == 0,5. Mas essa solução não ajudará se a diferença entre mCurrSpan e mPrevSpan for mais de três vezes. Um ticket já foi iniciado para esse bug, mas ainda não foi corrigido.
    Muleta A solução mais fácil para esse problema é simplesmente pular valores de scaleFactor negativos. Na prática, o usuário não notará que a imagem às vezes é ampliada com um pouco menos de suavidade do que o normal.


    Em vez de uma conclusão


    O destino das solicitações pull


    Fizemos uma correção local e criamos a última solicitação de recebimento no PhotoView. Apesar do fato de alguns PRs estarem suspensos por um ano, nossos PRs foram adicionados à filial principal e até mesmo uma nova versão do PhotoView foi lançada. Depois disso, decidimos cortar o módulo local da Galeria do Android e acessar as fontes oficiais do PhotoView. Para fazer isso, tive que adicionar suporte ao AndroidX, que foi adicionado ao PhotoView na versão 2.1.3 .


    Onde encontrar a biblioteca


    Procure o código fonte da biblioteca da Galeria do Android aqui - https://github.com/redmadrobot-spb/android-gallery , junto com as instruções de uso. E para dar suporte a projetos que ainda usam a Biblioteca de suporte, criamos uma versão separada do Android-Gallery-Reprovated . Mas tenha cuidado, porque em um ano a Biblioteca de Suporte se tornará uma abóbora!


    O que vem a seguir


    Agora a biblioteca nos convém completamente, mas no processo de desenvolvimento surgiram novas idéias. Aqui estão alguns deles:


    • a capacidade de usar a biblioteca em qualquer layout, e não apenas um FragmentDialog separado;
    • a capacidade de personalizar a interface do usuário;
    • a capacidade de substituir Gilde e ExoPlayer;
    • a capacidade de usar algo em vez do ViewPager.

    Referências



    UPD


    Enquanto escrevia um artigo, saiu uma biblioteca semelhante dos desenvolvedores do FrescoImageViewer . Eles adicionaram suporte para animação de transição , mas até agora só temos suporte para vídeo. :)

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


All Articles