[Android] Mecanismo de distribuição de eventos do View (análise do código-fonte)

Índice

1. Objeto de distribuição - MotionEvent

2. Como passar eventos

1. Processo de entrega

2. Análise do código-fonte da distribuição de eventos

3. Método principal:

4. Ouvinte na entrega do evento

5. Como lidar com conflitos deslizantes com distribuição de eventos


1. Objeto de distribuição - MotionEvent


Os tipos de evento são:

1. ACTION_DOWN-----O dedo acabou de tocar na tela

2. ACTION_MOVE------movimentos de dedo na tela

3. ACTION_UP------O momento em que o dedo é liberado da tela

4. ACTION_CANCEL-----acionado quando o evento é interceptado pela camada superior

O método principal de MotionEvent:

getX() Obtenha a coordenada do eixo x do evento (em relação à exibição atual)
getY() Obtenha a coordenada do eixo y do evento (em relação à exibição atual)
getRawX() Obtenha a coordenada do eixo x do evento (em relação ao vértice esquerdo da tela)
getRawY() Obtenha a coordenada do eixo y do evento (em relação ao vértice esquerdo da tela)

2. Como passar eventos


1. Processo de entrega

Acesse IMS->ViewRootImpl->activity->viewgroup->view

2. Análise do código-fonte da distribuição de eventos

1. Processo de distribuição da atividade para eventos de clique

  • Activity#dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
//事件交给Activity所附属的Window进行分发,如果返回true,循环结束,返回false,没人处理
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
//所有View的onTouchEvent都返回false,那么Activity的onTouchEvent就会被调用
        return onTouchEvent(ev);
}
  • Janela#superDispatchTouchEvent
 public abstract boolean superDispatchTouchEvent(MotionEvent event);
  • PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
  • DecorView#superDispatchTouchEvent()
 public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
}
  • ViewGroup#dispatchTouchEvent()
   public boolean dispatchTouchEvent(MotionEvent ev) {
   
   

2. O processo de distribuição da View de nível superior para o evento click

Explique o código no método dispatchTouchEvent() de ViewGroup nas seções

primeiro parágrafo:

Descreve a lógica de se o View intercepta eventos de clique

 // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {//事件类型为down或者mFirstTouchTarget有值
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);//询问是否拦截,方法返回true就拦截
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;//直接拦截了
            }

Quando o tipo de evento estiver inativo ou mFirstTouchTarget tiver um valor, o evento atual não será interceptado, caso contrário, o evento será interceptado diretamente. Então, quando mFirstTouchTarget tem um valor? Quando o ViewGroup não intercepta o evento e passa o evento para o elemento filho para processamento, mFirstTouchTarget tem um valor e aponta para o elemento filho. Portanto, quando o tipo de evento está inativo e o evento é interceptado, mFirstTouchTarget está vazio, o que fará com que os seguintes eventos se movam e não atendam à condição de que mFirstTouchTarget tenha um valor e o método onInterceptTouchEvent não possa ser chamado diretamente.

Caso especial: Defina o bit sinalizador FLAG_DISALLOW_INTERCEPT por meio do método requestDisallowInterceptTouchEvent e o ViewGroup não poderá interceptar eventos de clique diferentes de ACTION_DOWN. Esse bit sinalizador não pode afetar o evento ACTION_DOWN, porque quando o evento for ACTION_DOWN, o bit sinalizador será redefinido, o que causará o bit da configuração de visualização filho deste sinalizador é inválido.

Resumir:

1. Quando ViewGroup decidir interceptar o evento, os eventos de clique subsequentes serão entregues a ele para processamento por padrão e seu método onInterceptTouchEvent não será mais chamado. 

2. Quando o ViewGroup não intercepta o evento ACTION_DOWN, o bit sinalizador FLAG_DISALLOW_INTERCEPT faz com que o ViewGroup não intercepte mais o evento.

Segundo parágrafo:

Quando o ViewGroup não interceptar o evento, distribua o evento para a sub-View para ver qual sub-View lida com o evento

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                       //对子元素进行排序
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
//
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

Percorra todos os subelementos do ViewGroup para determinar se o subelemento está reproduzindo animação e se as coordenadas do evento de clique estão dentro da área do subelemento. Nesse caso, o evento de clique pode ser recebido e o evento será passado a ele para processamento.

Vamos dar uma olhada no método dispatchTransformedTouchEvent

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            ...
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            ...
}

dispatchTransformedTouchEvent realmente chama o método dispatchTouchEvent do elemento filho.

Se o método dispatchTouchEvent do elemento filho retornar true, então mFirstTouchTarget será atribuído e sairá do loop for

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

Essas linhas de código completam a atribuição de mFirstTouchTarget e terminam a travessia dos elementos filhos.Se o método dispatchTouchEvent do elemento filho retornar false, o ViewGroup distribuirá o evento para o próximo elemento filho.

Na verdade, a atribuição real de mFirstTouchTarget está no método addTouchTarget e mFirstTouchTarget é uma única estrutura de lista encadeada.

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

Terceiro parágrafo:

evento de execução

//当前View的事件处理代码
if (mFirstTouchTarget == null) {
          // No touch targets so treat this as an ordinary view.
           handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
//子View的事件处理代码
...

O terceiro parâmetro do método dispatchTransformedTouchEvent é null, e será chamado o método super.dispatchTouchEvent, que é o método dispatchTouchEvent do View, então o evento click é processado pelo View.

Visualização do processamento de eventos de clique

View (não incluindo ViewGroup) é um elemento separado, não possui elementos filhos e só pode lidar com eventos por si só.

public boolean dispatchTouchEvent(MotionEvent event) {
   ...
   boolean result = false;
   ...
   if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
     }
    ...
    return result;
}

Primeiro, determine se OnTouchListener está definido. Se o método onTouch de OnTouchListener retornar true, o método onTouchEvent não será chamado, caso contrário, o método onTouchEvent será chamado.

public boolean onTouchEvent(MotionEvent event) {
           ...
           if ((viewFlags & ENABLED_MASK) == DISABLED
                && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                     setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                 return clickable;
          }
               ...
}

A exibição no estado indisponível ainda consumirá eventos de clique

switch (action) {
    case MotionEvent.ACTION_UP:
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
         if ((viewFlags & TOOLTIP) == TOOLTIP) {
                  handleTooltipUp();
         }
         ...
         if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
         ...
              if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                           removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                }     
         } 
         ...
         mIgnoreNextUpEvent = false;
         break;

Quando ocorrer o evento ACTION_UP, o método performClick será acionado.Se a View estiver configurada com OnClickListener, seu método onClick será chamado dentro do método performClick.

 private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

        return performClick();
    }
public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        //关键代码,判断是否设置了onClickListener
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        ...
         
        return result;//最终返回执行结果
}

A implementação do código-fonte do mecanismo de distribuição do evento click foi analisada.

3. Método principal:


1. dispatchTouchEvent: Usado para distribuir eventos. Se o evento puder ser entregue à View atual, então este método será chamado. O resultado retornado é afetado pelo onTouchEvent da View atual e pelo método dispatchTouchEvent da View subordinada, indicando se para consumir o evento atual.

2. onInterceptTouchEvent: usado para determinar se deve interceptar um evento, se o View atual interceptar um evento, então na mesma sequência de eventos, este método não será chamado novamente, e o resultado retornado indica se deve interceptar o evento atual.

3. onTouchEvent: usado para processar eventos click, e o resultado retornado indica se deve consumir o evento atual, caso contrário, na mesma sequência de eventos, a View atual não pode receber o evento novamente.

4. requestDisallowInterceptTouchEvent: Geralmente usado em Views filhas, exigindo que a View pai não intercepte eventos.

5. dispatchTransformedTouchEvent: Se o filho não estiver vazio, envie para o dispatchTouchEvent do filho, caso contrário, envie para você mesmo.

4. Ouvinte na entrega do evento


A ordem das chamadas onTouch , performClick e onClick e o efeito do valor de retorno de onTouch?

Quando uma View precisa processar um evento, no método dispatchTouchEvent da View, se OnTouchListener estiver definido, o método onTouch de OnTouchListener será chamado. Quando o método onTouch retornar true, onTouchEvent não será chamado. Quando o método onTouch retornar false, onTouchEvent O método é chamado, insira o método performClick no método onTouchEvent e julgue se deve definir o onClickListener no método performClick e, se o onClickListener estiver definido, o método onClick será chamado e o método performClick retornará verdadeiro. onClickListener não está definido, o método performClick retorna falso.

Em geral, a ordem das chamadas de método é

5. Como lidar com conflitos deslizantes com distribuição de eventos


Definição de conflito deslizante : Quando há duas camadas de Views dentro e fora que podem responder ao evento, quem decidirá o evento.

Tipos de conflitos de deslizamento : 1. Quando as direções de deslizamento das camadas interna e externa do View são inconsistentes

                         2. Quando a direção de deslizamento das camadas interna e externa é a mesma

                         3. Superposição de duas situações

Soluções:

Interceptação interna: dispatchTouchEvent+dispatchTransformedTouchEvent

Reescreva o método dispatchTouchEvent do elemento filho

O evento down é distribuído aos elementos filho e o evento move depende das condições. Se as condições não forem atendidas, o evento será entregue ao elemento filho para processamento. Se a condição for atendida, o evento de processamento do filho elemento será cancelado e, em seguida, o evento será entregue ao elemento pai

public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                //down事件,父容器不要拦截我
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要此类点击事件) {
                   //父容器拦截我
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

(Quando o evento move ocorrer, digite o primeiro bloco de código, chame interceptado = onInterceptTouchEvent(ev), definimos no método onInterceptTouchEvent para retornar true se não for um evento inativo, então interceptado é true, então o segundo bloco de código não será executado, digite o terceiro código de bloco, porque interceptado é verdadeiro, então cancelChild é verdadeiro, cancele a execução do evento do elemento filho, chame o método dispatchTransformedTouchEvent, cancel é verdadeiro->

event.setAction(MotionEvent.ACTION_CANCEL)->manipulado = filho.dispatchTouchEvent(evento)

Defina mFirstTouchTarget como vazio, de modo que, quando o próximo evento de movimento vier, mFirstTouchTarget estará vazio, interceptado será verdadeiro no primeiro trecho de código, o segundo trecho de código não será executado e o terceiro trecho de código será dispatchTransformedTouchEvent(ev, cancelado, nulo , TouchTarget.ALL_POINTER_IDS), ou seja, o código de processamento do evento (elemento pai) da View atual)

Substituir o método onInterceptTouchEvent do elemento pai

Quando for um evento inativo, retorne false, pois no método dispatchTouchEvent do ViewGroup, quando for um evento inativo, o método resetTouchState() será chamado, e o método resetTouchState() redefinirá o estado e redefinirá mGroupFlags, o que Como resultado, o parent.requestDisallowInterceptTouchEvent(true) anterior é inútil, então retornamos false ao configurar o evento down no método onInterceptTouchEvent, porque onInterceptTouchEvent definitivamente será executado durante o evento down.

   public boolean onInterceptTouchEvent(MotionEvent event) {

        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            super.onInterceptTouchEvent(event);
            return false;
        } else {
            return true;
        }
    }

Interceptação externa: onInterceptTouchEvent

O evento click é interceptado primeiro pelo contêiner pai. Se o contêiner pai precisar, ele será interceptado e, se não for necessário, não será interceptado.

public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (满足父容器的拦截要求) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }

Acho que você gosta

Origin blog.csdn.net/weixin_63357306/article/details/128629042
Recomendado
Clasificación