前言
复习、复习、复习
主要解决的问题
View的事件分发
滑动冲突
多次测量
绘制流程
环境:API 29
目录
一、窗口事件传递
事件从哪里来?又是怎么传递的?
首先,你触摸屏幕时,顶层PhoneWindow会捕获到该事件(事件从驱动传递到IMS,IMS与WMS通信,WMS通过ViewRootImpl传递给了目标窗口),然后调用Activity的dispatchTouchEvent,然后会调用DecorView的dispatchTouchEvent (DecorView继承FrameLayout,FrameLayout继承ViewGroup),最终事件到ViewGroup的dispatchTouchEvent方法,如果返回false,都没有将事件进行消耗,那么就会调用Activity的onTouchEvent方法。
事件传到ViewGroup其实也符合 自下而上 的流程。一层层传递到View,然后处理事件返回到Activity。
二、View事件传递
View事件传递,主要看ViewGroup的dispatchTouchEvent方法,对这个方法抽象出来的伪代码如下
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
如果子View是ViewGroup,那么会继续调这个方法进行分发,然后层层往上返回。
这是一种自下而上的流程,使用了递归思想、责任链模式。
为什么如此设计?
-
View的排版规则,嵌套越深,显示层级越高,层级越高就越容易倍用户看见 -
所见即所得,用户看到了什么,触摸到的也应该是什么,符合用户直觉
三、View事件分发
我们先大概了解一下事件分发(简易版)的流程
通过之前View事件的传递,我们可以了解到分发内部的核心点:
-
当前
View是否需要拦截touch事件 -
是否需要将
touch事件继续分发给子View -
如何将
touch事件分发给子View
我们按照核心点分析,代码都是处于 dispatchTouchEvent中
1、拦截事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
当按下事件为ACTION_DOWN时,mFirstTouchTarget = null (当有子View捕获到了事件不为空) ,并且 mGroupFlags = FLAG_DISALLOW_INTERCEPT , 然后会先调用 自身的 onInterceptTouchEvent 方法来确定是否需要拦截。
2、分发事件
如果没有拦截事件,即 intercepted = false
if (!canceled && !intercepted) {
....
//注释1
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
if (newTouchTarget == null && childrenCount != 0) {
.....
//注释 2
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);
.....
//注释 3
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
....
//注释 4
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
.....
//注释 5
if (mFirstTouchTarget == null) {
//注释 5.1
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//注释 5.2
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
.......
}
}
首次touch屏幕,触发的ACTION_DOWN事件,所以注释1处为true。
注释 2 处是遍历所有的子View
注释 3 处是判断事件是否在子View坐标范围内,并且子View没有处于动画状态
注释 4 处是将事件分发给子View,如果子View捕获事件成功,会利用 mFirstTouchTarget 存储所有需要分发的子View,结构为链表结构,场景之一就是多点触控。
注释 5 处是如果mFirstTouchTarget为null,说明在处理ACTION_DOWN事件时拦截了子View或者说是你这个触摸没有触摸到任何一个子View,然后会触发到 注释 5.1,注释 5.1 内实际上会调用自身的 onTouchEvent 来处理事件。
注释 5.2 处会判断,如果此时拦截了子View分发,dispatchTransformedTouchEvent中会给子View传递ACTION_CANCEL事件
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
.....
}
那么注释 5.2 处的 intercepted不是为false么?毕竟刚开始ACTION_DOWN事件时并没有拦截。那有没有这种可能,在处理其它事件时 intercepted 改成了 true
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//注释 1
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
我们可以看到第一次没有拦截的情况下,会走到上述代码 注释 1 处,然后只要 disallowIntercept 为 fasle,onInterceptToucheEvent 为true,那么 intercepted 就会为 true。即场景是当父视图 onInterceptTouchEvent 先返回false,子View dispatchTouchEvent 返回 true,此时父视图 onInterceptTouchEvent 返回true导致 intercepted重置为 true ,子View就会收到ACTION_CANCEL的touch事件。
流程大概就是先检查当前ViewGroup是否需要拦截事件,然后再将事件分发给子View,然后再根据mFirstTouchTarget再将事件分发给子View,已经分发的就不会再次分发。
如果 mFirstTouchTarget 不为 null,即子View对touch事件进行了捕获,则直接将当前及以后的事件(ACTION_MOVE、ACTION_UP等)交给mFirstTouchTarget 存放的View进行处理。
四、View滑动冲突
滑动冲突场景简单来说可以分为三个
-
外部滑动方向和内部滑动方向不一致
ViewPager+Fragment -
外部滑动方向和内部滑动方向一致
ViewPager+ 横向RecyclerView -
上面两种情况的嵌套
ViewPager+Fragment+RecyclerView
解决方式模板
-
外部拦截法
//重写父容器HorizontalScrollViewEx的拦截方法 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://对于ACTION_DOWN事件必须返回false,一旦拦截后续事件将不能传递给子View intercepted = false; break; case MotionEvent.ACTION_MOVE://对于ACTION_MOVE事件根据需要决定是否拦截 if (父容器需要当前事件) { intercepted = true; } else { intercepted = flase; } break; } case MotionEvent.ACTION_UP://onClick事件在UP事件中调用,一旦拦截子View的onClick事件将不会触发 intercepted = false; break; default : break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; } -
内部拦截法
//重写父类HorizontalScrollViewEx的onInterceptTouchEvent方法 public boolean onInterceptTouchEvent (MotionEvent event) { int action = event.getAction(); if(action == MotionEvent.ACTION_DOWN) { return false; } else { return true; } } //重写子类ListView的dispatchTouchEvent方法 public boolean dispatchTouchEvent ( MotionEvent event ) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction) { case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true);//为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); }
对于内部拦截法中子View所调用的 parent.requestDisallowInterceptTouchEvent(true)
@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;
}
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
我们可以看到如果传递 true 并且 mGroupFlags & FLAG_DISALLOW_INTERCEPT = 0,然后mGroupFlags 值就变成了 FLAG_DISALLOW_INTERCEPT的值了。
我们再看第三节事件分发拦截那块的代码
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
可以看到这里 disallowIntercept 会被赋值为 true ,然后 intercepted就为 false了,以此达到禁止父视图拦截的目的。
总结
递归调用child.dispatchTouchEvent 然后再依次 return ,true代表消耗了事件,整个流程就是递归、回溯过程。
每次执行dispatchTouchEvent时都可以判断是否需要拦截当前事件或者传递给下一级
最后一个child 的dispatchTouchEvent内 判断是否view enabled 并实现了 onTouchListener,如果设置的listener内部return true,则消耗事件,不执行onTouchEvent 。否则执行onTouchEvent,内部会根据action来执行onClick或者是onLongClick
需要注意的是,在dispatchTouchEvent 内,当ACTION_DOWN被拦截后,往后的ACTION_MOVE和ACTION_UP都不会拦截,对应于onInterceptTouchEvent 只调用一次,如果拦截 ,mFirstTouchTarget标记为null,后续根据这个标记,都会被拦截。捕获到触摸的View都会放到mFirstTouchTarget。
在父视图的onInterceptTouchEvent 返回false,如果在MOVE的过程中 onInterceptToucheEvent 返回了 true, 此时会resetTouchState 会传递给子控件ACTION_CANCEL事件
处理滑动冲突的话有内部拦截和外部拦截
外部拦截法主要是重写父View的onInterceptTouche 方法,这里需要注意不能屏蔽ACTION_DOWN事件。
内部拦截发主要是重写父view的onInterceptTouchEvent,然后子View利用 requestDisallowInterceptTouchEvent 方法来改变 mGroupFlags 的值,如果包含 FLAG_DISALLOW_INTERCEPT则表示不允许拦截,进而调用父view的onInterceptTouchEvent方法
五、View的绘制流程
Activity的启动过程,我们从Context的startActivity说明,其实现是ContextImpl的startActivity,然后内部会通过Instrumentation来尝试启动Activity,这是一个跨进程过程,它会调用AMS的startActivity方法,当AMS校验Activity的合法性后,会通过ApplicationThread回调到我们的进程,这也是一次跨进程通信,而ApplicationThread就是一个Binder。回调逻辑是在Binder线程池中完成的,所以需要通过Handler H将其切回UI线程,第一个消息是LAUNCH_ACTIVITY,它对应这handleLaunchActivity。这个方法里面完成了Activity的创建和启动。接着,在Activity的onResume中,Activity的内容将开始渲染到Window上面,然后开始绘制直到我们可以看见。(四大组件启动源码流程可以自行搜索、笔者以前写的排版不好,已经删除、后续不再分析)
首先我们需要明白,Window是在handleLaunchActivity方法中,在创建Activity后调用attach方法时创建的PhoneWindow。
在activity中的onCreate方法调用setContentView就是将PhoneWindwo和DecorView关联起来,然后handleResumeActivity阶段会通过远程调用,将View添加到Window中,这个阶段同时会创建ViewRootImpl,并且调用requestLayout方法。
对于View的刷新机制和Surface相关知识 可以参考 屏幕刷新机制小结 Android屏幕刷新机制 VSync、Choreographer
requestLayout内部其实也是调用的performTraversals 方法,内部调用流程如下,至于 performTraversals 如何调用的,可以看刚才给的链接。
performMeasure 进行测量onMeasure工作
performLayout 进行onLayout操作
performDraw 进行 onDraw 操作
六、View的测量过程
MeasureSpec 高 2 位代表SpecMode(测量模式)、低30位代表SpecSize(测量模式下的规格大小)。其中SpecMode共有三类,如下
-
UNSPECIFIED父容器不对View限制,要多大有多大 -
EXACTLY精确大小,为SpecSize值,一般对应于match_parent和具体数据两种模式 -
AT_MOST父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值
其中关键代码
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
我们通过Window尺寸和getRootMeasureSpec确定DecorView的MeasureSpec
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
然后通过performMeasure 方法调用 DecorView 的 onMeasure 方法 (DecorView是继承FrameLayout的,onMeasure也在FrameLayout中),onMeasure 关键代码如下
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
我们分析一下 measureChildWithMargins 方法
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
这里的意思主要是 根根据父视图的 MeasureSpec以及View本身的LayoutParams来确定子元素的MeasureSpec,关键方法是 getChildMeasureSpec
//padding参数为父视图已经占用的空间
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
主要作用就是根据父视图的MeasureSpec以及View本身的LayoutParams来确定子元素的MeasureSpec。
大概规则如下
总结一下,就是当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式,大小都是LayoutParams中的大小;当View的宽高是match_parent时,如果父容器是精准模式,那么View也是精准模式并且大小是父容器的剩余空间,如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。当View的宽高是wrap_content时,不管父容器是精准模式还是最大化模式,View总是最大化并且大小不超过父容器的剩余空间。
当你自定义View时,是不是需要重写onMeasure方法,如果不重写,你对View设置wrap_content或者是match_parent都是parentSize,撑满屏幕大小?通过上面介绍,你应该心里有底了。
这块还有个常见的问题,为什么onMeasure会调用两次?
这个可能根据不同控件、不同API有不同,例如api 16-19 可能会执行2次 onMeasure、2次onLayout、1次onDraw,而api 21-23执行 3次 onMeasure、2次onLayout、1次onDraw,api 24 -25 执行2次onMeasure、2次onLayout、1次onDraw。
有的网友说是performTravels被调用了两次,其实个人认为确实是这样的。在performTravesls 代码的最后
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
第一次走到这的时候,cancelDraw 是为 false 的,因为 isViewVisible 是 true。因为在Activity启动过程中,是先有requestLayout方法(这里主要是通过发送异步屏障消息),然后立马在调用了DecorView的setVisibility方法。所以android 10中其实是一次就完事了,没有多次onMeasure。
我们看下android 9 的api
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
此时newSurface 为false,只有等Surface准备好了之后,onDraw才能拿到Canvas,往Surface的buffer中写数据。
if (!hadSurface) {
if (mSurface.isValid()) {
// If we are creating a new surface, then we need to
// completely redraw it. Also, when we get to the
// point of drawing it we will hold off and schedule
// a new traversal instead. This is so we can tell the
// window manager about all of the windows being displayed
// before actually drawing them, so it can display then
// all at once.
newSurface = true;
...
}
public boolean isValid() {
synchronized (mLock) {
if (mNativeObject == 0) return false;
return nativeIsValid(mNativeObject);
}
}
**那你还有可能会问?**我问的不是启动时候onMeasure执行次数,我是问的例如FrameLayout中这个方法的执行次数?
首先我们要明白整个流程,我们是先经过测量,然后父布局才能将子View放置到合适的地方,然后再绘制。那测量这个过程,是自上向下的一个过程,子View测量完后将结果汇总返回给父布局,父布局再设置自身的宽高,也就是先算孩子再算自己。可以理解为第一次用预置宽高测量View Tree,因为存在需要宽度比预置的大的子布局,所以对结果不满意,又会重新测量,直到满意为止。
七、View的布局过程
performLayout实际会调用 FrameLayout的layoutChildren方法就行布局,这里没有太多需要分析的点。
我们通过常说 invalidate 必须在主线程执行, postInvalidate 可以在子线程执行,并且这两个方法不一定会触发View的measure和layout,多数情况下只会执行draw方法,这是为什么呢?
在View的measure中,如果要触发onMeasure方法,需要设置View的 PFLAG_FORCE_LAYOUT 标志位
public final void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
.....
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
}
通过上述代码,可以发现如果forceLayout为true,就会执行 onMeasure 方法,在requestLayout中执行了 mPrivateFlags |= PFLAG_FORCE_LAYOUT 而invalidate 并没有设置这个标志。
onLayout也是同理
public void onLayout(int l, int t, int r, int b)
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
......
}
可以看出,当View的位置改成,或者是添加PFLAG_LAYOUT_REQUIRED 标志位后,onLayout才会被执行,当调用invalidate 时,如果view没有改变,或者没有设置这个标识位,也不会调用onLayout。
你可能会问 invalidate 是如何更新view的,其实触发流程最后还是会走到scheduleTraversals -> performTravels的。
而postInvalidate为什么能在主线程执行,普遍做法,基本都是内部通过Handler切换到主线程,然后调用invalidate,很多开源库都是这样做的。
八、View的绘制过程
绘制过程主要做了什么?
draw:
canvas = mSuface.lockCanvas();
drawBackground(canvas);
.......
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
surface.unlockCanvasAndPost(canvas);
主要是绘制View的背景、绘制View的自身内容、对draw事件进行分发,递归调用子View的draw事件。
通过lockCanvas获取一块buffer, 接下来java层会通过View绘制,最后通过unlockCanvasAndPost提交buffer。
这里lockCanvas获取的是backCanvas,视图刷新使用了双缓冲机制。
每次CPU提交的数据被缓存到Back Buffer中,然后GPU对Back Buffer中的数据做栅格化操作,完成之后将其交换(swap)到 Frame Buffer 中,最后屏幕从 Frame Buffer中取数据显示。GPU会负责定期交换Back Buffer和Frame Buffer中的数据,以保证屏幕上显示最新的内容。当CPU正在向Back Buffer写入数据时,GPU会将Back Buffer锁定,如果此时正好到了交换两个Buffer的时间点,那么这次swap就会被忽略,直接导致的就是Frame Buffer还是保存的之前一帧的数据,即之前的内容,即对应Android的丢帧。
刷新数据:
首先应用从系统服务内申请buffer,系统返回buffer,应用拿到buffer后,绘制,然后提交buffer给系统服务,系统服务把这块buffer写到屏幕缓冲区域,屏幕以一定帧率去刷新,每次刷新的时候会去缓冲区域里把图像刷数据读出来,显示出来,如果缓冲区域里没有新的数据,屏幕就一直用老的数据,看起来屏幕就没有变一样。(缓冲不只是只有一个,一个缓冲写,一个缓冲读)。