View绘制及事件机制原理

2022-03-31  本文已影响0人  玉圣

一、View绘制流程机制

1、View绘制起点

2、View绘制流程

.1、measure(测量)

顶级View(即DecorView)的测量在ViewRootImpl的源码里(getRootMeasureSpec方法):

//desire的这2个参数就代表屏幕的宽高,
  childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
  childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

  //decorView的measureSpec就是在这里确定的,其实比普通view的measurespec要简单的多
  private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
}

注意:

  • ViewGroup中没有onMeasure方法
  • View会进行多次的测量,第一次测量和最终测量的实际宽高不一定相等,在layout流程中可确定View的实际宽高
  • Activity#onWindowFocusChange()中获取
  • view.post(Runnable)将获取的代码投递到消息队列的尾部
  • ViewTreeObservable方法
.2、layout(布局)
.3、draw(绘制)
  • 1、Draw the background:绘制背景 —— drawBackground(canvas)
    1. If necessary, save the canvas' layers to prepare for fading:保存图层
    1. Draw view's content:绘制view自身的内容 —— onDraw(canvas)
    1. Draw children:绘制子View(分发) —— dispatchDraw(canvas)
    1. If necessary, draw the fading edges and restore layers:绘制一些图层
    1. Draw decorations (scrollbars for instance):绘制装饰(如滚动条) —— onDrawForeground(canvas)

注:View中的dispatchDraw(canvas) 是空方法,ViewGroup中的dispatchDraw(canvas)调用了其drawChild方法

  • setWillNotDraw:用于设置绘制的标志位,view是否需要draw绘制。
    若自定义的View 不需要draw,则可以设置这个方法为true。系统会根据此标记来优化执行速度。
    ViewGroup 一般都默认设置这个为true,因为ViewGroup多数都是只负责布局,不负责draw的。
    View 的这个标志位默认一般都是关闭的。
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
  • 一般执行动画,会多次调用onDraw方法,通过监听动画的参数值变化,不断 invalidate,不断重绘。
    invalidate 是在 主线程 中进行调用,会引发onDraw进行重绘
    postInvalidate 是在 子线程 中调用,最终调用的仍是invalidate

参考链接:
Android View绘制13问13答
要点提炼|开发艺术之View

二、View及ViewGroup的事件机制

1、相关概念:

  • 事件类型:
    ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
    ACTION_MOVE:手指在屏幕上移动时候产生该事件
    ACTION_UP:手指从屏幕上松开的瞬间产生该事件
  • 事件序列:
    从ACTION_DOWN开始到ACTION_UP结束我们称为一个事件序列
   ViewConfiguration.get(getContext()).getScaledTouchSlop()

2、事件分发的要点

   // ViewGroup
   public boolean dispatchTouchEvent(MotionEvent ev) {
       // 事件是否被消费
       boolean consume = false;
       // 调用onInterceptTouchEvent判断是否拦截事件
       if (onInterceptTouchEvent(ev)) {
           // 如果拦截则调用自身的onTouchEvent方法
           consume = onTouchEvent(ev);
       } else {
           if (targetChild == null) {
              // 没有找到目标child,则调用父容器的分发方法
              consume = super.dispatchTouchEvent(ev);
           } else {
              // 不拦截调用子View的dispatchTouchEvent方法
              consume = child.dispatchTouchEvent(ev);
           }
       }
       // 返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
       return consume;
   }

   // View
   public boolean dispatchTouchEvent(MotionEvent ev) {
       boolean consume = false;
       // 是否实现了TouchListener#onTouch方法
       if (onTouchListener != null) {
           // 调用实现的onTouchListener#onTouch方法
           consume = onTouchListener.onTouch(ev);
       } else {
           // onTouchEvent()中调用了perform*Click()等方法
           consume = onTouchEvent(ev);
       }
       //返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
       return consume;
   }

3、事件分发的机制&流程

流程详述:
    // Activity源码:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 事件分发并返回结果
        if (getWindow().superDispatchTouchEvent(ev)) {
            //事件被消费
            return true;
        }
        // 无View 消费事件,则调用Activity#onTouchEvent方法
        return onTouchEvent(ev);
    }

PhoneWindow#superDispatchTouchEvent(MotionEvent)
getWindow().superDispatchTouchEvent(ev)Window的抽象方法,具体由PhoneWindow实现
其内部是调用的顶级View(DecorView)的superDispatchTouchEvent方法

    // PhoneWindow源码:
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

DecorView.superDispatchTouchEvent(MotionEvent)
顶级View(DecorView)一般为ViewGroup,其方法中调用了ViewGroup#dispatchTouchEvent方法

    public boolean superDispatchTouchEvent(MotionEvent event) {
        // 此处调用的是ViewGroup的dispatchTouchEvent方法
        return super.dispatchTouchEvent(event);
    }
    // ViewGroup源码:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        // Handle an initial down.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Throw away all previous state when starting a new touch gesture.
            // The framework may have dropped the up or cancel event for the previous gesture
            // due to an app switch, ANR, or some other state change.
            cancelAndClearTouchTargets(ev);
            //清除FLAG_DISALLOW_INTERCEPT设置并且mFirstTouchTarget 设置为null
            resetTouchState();
        }
        // Check for interception.
        final boolean intercepted;//是否拦截事件
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //FLAG_DISALLOW_INTERCEPT是子View通过
            //requestDisallowInterceptTouchEvent方法进行设置的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //调用onInterceptTouchEvent方法判断是否需要拦截
                intercepted = onInterceptTouchEvent(ev);
                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;
        }
        ...
    }
    // ViewGroup源码:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final View[] children = mChildren;
        //对子View进行遍历
        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;
            }

            //判断1,View可见并且没有播放动画。2,点击事件的坐标落在View的范围内
            //如果上述两个条件有一项不满足则continue继续循环下一个View
            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }

            newTouchTarget = getTouchTarget(child);
            //如果有子View处理即newTouchTarget 不为null则跳出循环。
            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);
            //dispatchTransformedTouchEvent第三个参数child这里不为null
            //实际调用的是child的dispatchTouchEvent方法
            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();
                //当child处理了点击事件,那么会设置mFirstTouchTarget 在addTouchTarget被赋值
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                //子View处理了事件,然后就跳出了for循环
                break;
            }
        }
    }

dispatchTransformedTouchEvent方法:

    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }

mFirstTouchTarget的赋值:

    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
   // Dispatch to touch targets.
   if (mFirstTouchTarget == null) {
       // No touch targets so treat this as an ordinary view.
       handled = dispatchTransformedTouchEvent(ev, canceled, null,
               TouchTarget.ALL_POINTER_IDS);
   }

dispatchTouchEvent(MotionEvent)

     // View源码:
    //如果窗口没有被遮盖
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        //当前监听事件
        ListenerInfo li = mListenerInfo;
        //需要特别注意这个判断当中的li.mOnTouchListener.onTouch(this, event)条件
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        //result为false调用自己的onTouchEvent方法处理
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

onTouchEvent(MotionEvent)

// View源码:
public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        // 判断是否不可用,但仍会消费事件
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            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;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                                if (!post(mPerformClick)) {
                                    // 其内部调用的是performClick方法
                                    performClickInternal();
                                }
                    ...
                    break;
                    ...
            }

            return true;
        }

        return false;
    }

performClick()

   // View源码:
    /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener that was called, false
     *         otherwise is returned.
     */
    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

说明: setClickable 失效的原因:
View的setOnClickListener会默认将View的clickable设置成true。
View的setOnLongClickListener同样会将View的longClickable设置成true。

    // View源码:
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

    public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

4、事件分发的相关问题:

-- 问题:如果一个事件序列的 ACTION_DOWN 事件被 ViewGroup 拦截,此时子 View 调用 requestDisallowInterceptTouchEvent 方法有没有用?

子View可以通过 requestDisallowInterceptTouchEvent方法干预父View的事件分发过程(ACTION_DOWN事件除外),而这就是我们处理滑动冲突常用的关键方法。

requestDisallowInterceptTouchEvent 中,设置了 FLAG_DISALLOW_INTERCEPT 标志位,表示子View不希望此父级及其祖先使用 ViewGroup.onInterceptTouchEvent(MotionEvent) 拦截触摸事件。

ACTION_DOWN 事件是清除了这个标志位的,所以,requestDisallowInterceptTouchEvent 的设置对ACTION_DOWN无效。

    // ViewGroup源码,实现的是ViewParent的抽象方法
    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

    // ViewGroup#dispatchTouchEvent
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            ....
            //清除FLAG_DISALLOW_INTERCEPT设置并且mFirstTouchTarget 设置为null
            resetTouchState();
        }
        //是否拦截事件
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            //FLAG_DISALLOW_INTERCEPT是子View通过
            //requestDisallowInterceptTouchEvent方法进行设置的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //调用onInterceptTouchEvent方法判断是否需要拦截
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
        ...
    }

-- 问题:ACTION_DOWN 事件被子 View 消费了,那 ViewGroup 能拦截剩下的事件吗?如果拦截了剩下事件,当前这个事件 ViewGroup 能消费吗?子 View 还会收到事件吗?

ACTION_DOWN 事件被子 View 消费后,mFirstTouchTarget 则不为null了,就会直接拦截其他事件,intercepted = true;,见下面的源码。
设置了拦截,就不会再遍历子View进行事件分发了。则会取消cancelChild,拦截子View的事件。

   // ViewGroup#dispatchTouchEvent
   if (actionMasked == MotionEvent.ACTION_DOWN
           || mFirstTouchTarget != null) {
       // onInterceptTouchEvent方法的判断
       ......
   } else {
       // 重点在这里
       // There are no touch targets and this action is not an initial down
       // so this view group continues to intercept touches.
       intercepted = true;    // <---------------------------------------重点
   }

   ....
   // Dispatch to touch targets.
   if (mFirstTouchTarget == null) {
       // 没有子 View 消费事件,则传入 null 去分发,最终调用的是自身的 onTouchEvent 方法,进行处理 touch 事件
       handled = dispatchTransformedTouchEvent(ev, canceled, null,
               TouchTarget.ALL_POINTER_IDS);
   } else {
       // Dispatch to touch targets, excluding the new touch target if we already
       // dispatched to it.  Cancel touch targets if necessary.
       TouchTarget predecessor = null;
       TouchTarget target = mFirstTouchTarget;
       while (target != null) {
           final TouchTarget next = target.next;
           if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
               handled = true;
           } else {
               // 如果 intercepted 就取消 cancelChild,这便是拦截子 View 事件的原理
               final boolean cancelChild = resetCancelNextUpFlag(target.child)
                       || intercepted;   // <---------------------------------------重点
               if (dispatchTransformedTouchEvent(ev, cancelChild,
                       target.child, target.pointerIdBits)) {
                   //内部会比较 pointerIdBits 和当前事件的 pointerIdBits,一致才会处理
                   //这便是 Down 事件处理后后续事件都交给该 View 处理的原理
                   handled = true;
               }
               if (cancelChild) {
                   if (predecessor == null) {
                       mFirstTouchTarget = next;
                   } else {
                       predecessor.next = next;
                   }
                   target.recycle();
                   // 没有next则为null,就结束了循环
                   target = next;
                   continue;
               }
           }
           predecessor = target;
           target = next;
       }
   }
-- 问题:当 View Disable 时,会消费事件吗?

会消费事件,只是设为了不可用,可以看到,在源码中的注释为:

A disabled view that is clickable still consumes the touch events, it just doesn't respond to them.
[一个可点击的禁用view,仍然可以消费事件,它只是没有响应它们(事件)而已。]

    // View#onTouchEvent
   // 判断是否不可用,但仍会消费事件
   if ((viewFlags & ENABLED_MASK) == DISABLED) {
       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;
   }

参考链接:
一文读懂Android View事件分发机制
必问的事件分发,你答得上来吗
Android事件分发机制详解:史上最全面、最易懂
要点提炼|开发艺术之View

上一篇下一篇

猜你喜欢

热点阅读