前言
复习、复习、复习
主要解决的问题
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
写到屏幕缓冲区域,屏幕以一定帧率去刷新,每次刷新的时候会去缓冲区域里把图像刷数据读出来,显示出来,如果缓冲区域里没有新的数据,屏幕就一直用老的数据,看起来屏幕就没有变一样。(缓冲不只是只有一个,一个缓冲写,一个缓冲读)。