Android(安卓)View 绘制流程之四:绘制流程触发机制
Android View 绘制流程之四:绘制流程触发机制
- 一.View状态的改变(包括Background的drawable状态改变)
- 1.setEnable()
- 2.setPressed()
- 3.setSelected()
- 4.refreshDrawableState()
- 二.View焦点的改变
- 1.findFoucs()
- 2.getFocusedChild()
- 3.hasFocus()/hasFocusable()
- 4.isFocusable()/isFocusableInTouchMode()
- 5.requestFocus()请求获取焦点
- 三.View可见性的改变
- 四.请求重新布局requestLayout
- 五.请求重绘invalidate
- 六.performTraversal()方法
系列文章:
Android View 绘制流程之一:measure测量
Android View 绘制流程之二:layout布局
Android View 绘制流程之三:draw绘制
Android View 绘制流程之四:绘制流程触发机制
View系统的核心部分就是测量、布局、绘制三大过程,我们平时调用的setVisibility、requestLayout、invalidate等方法看似都会去导致View系统的重新遍历,本文就是讲述这些方法如何发起了一个View重新遍历的请求,以及该请求如何被处理的、在哪里被处理的。
-
首先需要知道,ViewRootImpl类作为View的最顶层的一个ViewParent,管理着整个View系统,当然也就包括了测绘流程
-
ViewRootImpl类中的scheduleTraversals方法是发起一次View系统的遍历的调用方法,它内部会使用handleR向主线程MessageQueue中发起一个消息,且是一个同步消息,即不进行完整个application不会运作(具体如何发的还不是很理解,后续会学习),最终执行该消息时,会调用其Callback(一个Runnable对象)的run方法,执行doTraversal方法,该方法内部调用performTraversals方法,即是开启整个View遍历的起点
-
我们平时的一些改变view的状态、可见性、增加删除包括请求重绘、请求布局等操作,都会直接或间接的调用到scheduleTraversals方法发起遍历请求,最终调用performTraversal方法进行遍历
-
整个测绘流程实质是:在一次消息处理中,可以任意改变任意view的各种状态,各种参数,然后统一发起一次遍历请求,在这个请求执行时,根据这些已经更新的状态、参数重新测绘view系统
下面就分几个常见的方面来说明下平时的一些操作如何发起了请求,并在最后说明performTraversal的运行流程。
一.View状态的改变(包括Background的drawable状态改变)
View系统给View提供了许多状态,常见的比如enable、focused、pressed、selected,我们可以在drawable里定义,最终系统将drawable文件转换为一个StateListDrawable对象,目的是为了改变view在不同状态下的背景效果,在使用时,有的状态需要我们自己手动改变,有的是系统在一些时机自己改变,当改变后,会根据新的state选择相应的drawable进行重绘背景,先介绍一下这些常见状态:
-
enable:是否可用,我们可以通过setEnable()方法改变其状态,而且其默认的效果是使view变为50%透明
-
focused:是否具有焦点,一个窗口只能有一个View获得焦点,具体焦点部分介绍在下面会说
-
pressed:是否按下,我们一般使用的view如果定义了点击事件,按下时都会有背景的改变,因为系统层面以及调用了setPressed方法替我们改变了其状态,当然我们也可以自己进行控制
-
selected:是否选中,这是一个完全交由用户定义控制的状态,系统不会处理,加入我们需要自定义的一种状态,就可以通过setSelected来控制该状态,不过要注意的是,如果setSelected(false)的话,会将pressed状态也置为false
下面来介绍一下改变这几个状态(focus相关下一小节单说)的方法及其如何刷新View的:
1.setEnable()
public void setEnabled(boolean enabled) { if (enabled == isEnabled()) return;//如果没有改变则不做处理 setFlags(enabled ? ENABLED : DISABLED, ENABLED_MASK);//改变了view的flag,这里用不到 refreshDrawableState();//根据新state刷新drawable,该方法最后会说 invalidate(true);//由于默认的操作是将view变为50%透明度,所以仍然需要刷新重绘...}
流程很简单,就是改变flag,刷新drawable,再重绘变为50%透明度即可,refreshDrawableState方法就是刷新drawable的方法,下面会说到。
2.setPressed()
public void setPressed(boolean pressed) { final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);//pressed状态是用标志位的方式记录的,所以用位运算判断是否改变了状态//更新标志位 if (pressed) { mPrivateFlags |= PFLAG_PRESSED; } else { mPrivateFlags &= ~PFLAG_PRESSED; }//如果改变了状态则需要刷新drawable if (needsRefresh) { refreshDrawableState(); } dispatchSetPressed(pressed);//通知子view状态改变}//ViewGroup的dispatchSetPressed方法protected void dispatchSetPressed(boolean pressed) { final View[] children = mChildren; final int count = mChildrenCount; for (int i = 0; i < count; i++) { final View child = children[i]; if (!pressed || (!child.isClickable() && !child.isLongClickable())) {//规则就是:如果父view已经点击了,那么子view如果可以点击(有点击或者长按的功能)的话,就不能响应点击事件了 child.setPressed(pressed); } }}
和enable差不多,流程也是改变状态刷新drawable,只不过多了一个通知子view的步骤
3.setSelected()
public void setSelected(boolean selected) { //noinspection DoubleNegation if (((mPrivateFlags & PFLAG_SELECTED) != 0) != selected) {//也是用标志位来记录状态 mPrivateFlags = (mPrivateFlags & ~PFLAG_SELECTED) | (selected ? PFLAG_SELECTED : 0);//更新状态 if (!selected) resetPressedState();//如果selected置为false,那么需要重置pressed状态 invalidate(true);//重绘 refreshDrawableState();//刷新drawable dispatchSetSelected(selected);//通知子view,这里就是使每个子view都设置成该selected状态即可(即父view选中则子view都选中,父view不选中则都不选中) ... }}
4.refreshDrawableState()
上述几个方法都是主要都是调用refreshDrawableState方法来刷新drawable,下面来看看这个方法
public void refreshDrawableState() { mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;标识drawable的state需要更新 drawableStateChanged();//更新drawable的state ViewParent parent = mParent; if (parent != null) {//此步是通知父view要刷新drawable的state,原理:如果父view设置了一个叫addStatesFromChildren的属性,那么所有其子view拥有的状态都包含在其父view中,也就是说父view和其子view背景同步,做法就是直接调用父view的refreshDrawableState方法 parent.childDrawableStateChanged(this); }} protected void drawableStateChanged() { final int[] state = getDrawableState();//根据view的flag判断其拥有的状态位,并将其打成数组返回 final Drawable bg = mBackground; if (bg != null && bg.isStateful()) { bg.setState(state);//调用drawable的setState方法更新states } ...} public boolean setState(final int[] stateSet) { if (!Arrays.equals(mStateSet, stateSet)) {//状态改变了则需要更新 mStateSet = stateSet; return onStateChange(stateSet);//由Drawable子类重写该方法处理更新回调 } return false;} protected boolean onStateChange(int[] stateSet) { final boolean changed = super.onStateChange(stateSet); int idx = mStateListState.indexOfStateSet(stateSet);//找到新的状态对于的在该drawable的状态列表里的index if (idx < 0) { idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);//默认的状态index } return selectDrawable(idx) || changed;//选择drawable进行更新} public boolean selectDrawable(int idx) { if (idx == mCurIndex) {//没有改变则不做处理 return false; } ... if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) { final Drawable d = mDrawableContainerState.getChild(idx);//找到对应的drawable mCurrDrawable = d; mCurIndex = idx; ... } else { mCurrDrawable = null; mCurIndex = -1; } ... invalidateSelf();//核心方法,刷新drawable自己的部分,最基本的View会调用到invalidateDrawable方法 return true;} //View的invalidateDrawable方法public void invalidateDrawable(@NonNull Drawable drawable) { if (verifyDrawable(drawable)) { final Rect dirty = drawable.getDirtyBounds(); final int scrollX = mScrollX; final int scrollY = mScrollY; invalidate(dirty.left + scrollX, dirty.top + scrollY, dirty.right + scrollX, dirty.bottom + scrollY);//实质就是调用invalidate方法刷新drawable的区域 rebuildOutline(); }}
由代码可知,更新drawable流程就是根据view当前状态列表,找到对应的新的drawable,然后最终调用invalidate刷新drawable的这部分区域即可,本质也是调用invalidate
这里需要注意的是那个setAddStatesFromChildren方法,它将设置父view与子view的背景联动,实质就是在构建ViewGroup的drawableState时,会将子view的所有drawableState合并在一起交给父view,并在子view刷新drawable时通知父view即可
还有一个是setDuplicateParentStateEnabled方法,该方法是指该view的状态变化与父view一样,与上面这个相反
二.View焦点的改变
首先需要知道的是一个窗口最多只能有一个view处于拥有焦点的状态,而我们一般通过requestFocus来请求获取焦点,以及setFocusable和setFocusableInTouch来设置是否可获取焦点、是否可在触摸状态(窗口有触摸状态还有键盘输入状态)时获取焦点,还有一些辅助方法比如findFoucs、getFocusChild和hasFoucs,下面先来介绍一下这些辅助方法,再去看如何获取焦点的:
1.findFoucs()
//判断view是否拥有焦点public boolean isFocused() {//通过标志位来记录focus状态 return (mPrivateFlags & PFLAG_FOCUSED) != 0;}//ViewGroup的findFoucs方法public View findFocus() { if (isFocused()) {//如果是自身拥有焦点则返回自身 return this; } if (mFocused != null) {//mFocused是该ViewGroup保存的拥有焦点的直接子view(真正焦点view可能在更深层的view) return mFocused.findFocus();//调用mFocused的该方法递归寻找 } return null;//没有焦点view}//View的findFoucs方法public View findFocus() { return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;//直接判断view有没有该标志即可}
该方法可知:
-
返回的是具体的拥有焦点的一个view
-
可能没有找到拥有焦点的view(该view不是或者该ViewGroup没有焦点view)
2.getFocusedChild()
该方法是ViewGroup的方法,直接返回上面说过的mFocused对象,即该view的包含焦点view的直接子view,有可能为null,下面会说该变量的赋值更新
3.hasFocus()/hasFocusable()
hasFocus()方法是判断view是否有焦点,有两种可能,一种是其自身有焦点,一种是其包含焦点view(mFocused != null)
hasFocusable()方法是判断view是否有focusable属性,或者ViewGroup是否包含有focusable属性的子view
4.isFocusable()/isFocusableInTouchMode()
这两个方法就是判断view是否有focusable和focusableInTouchMode属性,属性可以通过xml或者代码设置
这几个常用的焦点相关的方法介绍完后,来看看View系统的寻找焦点流程:
5.requestFocus()请求获取焦点
requestFocus方法是请求焦点的方法,其返回值是boolean,代表是否该view或者该view的子view获取到了焦点;对于ViewGroup,会重载该方法,进而判断是自己获取焦点还是子view获取焦点;而对于View来说会直接调用requestFocusNoSearch来尝试使自己获取焦点;
下面来看看相关方法
//View的requestFocusNoSearch方法private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) { // 前提条件//需要focusable属性为true,且必须为VISIBLE状态才行 if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE || (mViewFlags & VISIBILITY_MASK) != VISIBLE) { return false; }//如果在touch模式下,focusableInTouch属性必须为true才行 if (isInTouchMode() && (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) { return false; }//循环遍历parent,如果有parent阻止子类获取焦点则不行(setDescendantFocusability方法设置ViewGroup的拦截焦点属性,下面会说) if (hasAncestorThatBlocksDescendantFocus()) { return false; }//前提条件满足,该view可以获取焦点,则返回true代表获取成功,当然还要调用handleFocusGainInternal去更新focus状态以及全局的focus相关数据 handleFocusGainInternal(direction, previouslyFocusedRect); return true;}//View的handleFocusGainInternal方法void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) { if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {//已经获取了焦点则不做处理 mPrivateFlags |= PFLAG_FOCUSED;//加入FOCUSED标志 View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;//找到根view,调用其findFocus方法找到整个view树中之前获取焦点的view if (mParent != null) { mParent.requestChildFocus(this, this);//通知父view-其子view请求了焦点,进而更新全局的焦点信息,是一个递归方法 } if (mAttachInfo != null) {//通知视图树的全局焦点变化事件 mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this); } onFocusChanged(true, direction, previouslyFocusedRect);//view的焦点改变回调 refreshDrawableState();//刷新drawable }}//ViewGroup的handleFocusGainInternal方法void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) { if (mFocused != null) {//相比view的该方法主要多了一步就是将自身的mFocused先取消焦点,再置为null,这也就说明,ViewGroup拿到焦点时其mFocused不是指向自己而是null mFocused.unFocus(this); mFocused = null; } super.handleFocusGainInternal(direction, previouslyFocusedRect);} public void requestChildFocus(View child, View focused) {//调用view的unFocus方法,也就是说使该view取消焦点(有的话) super.unFocus(focused); //更新ViewGroup的mFocused为child,并将原来的mFocused调用unFocus取消掉焦点 if (mFocused != child) { if (mFocused != null) { mFocused.unFocus(focused); } mFocused = child; } if (mParent != null) {//递归向上通知parent,第一个参数为this,这也说明mFocused指向的是包含焦点view的直接子view mParent.requestChildFocus(this, focused); }}//View的unFocus()方法,实际调用的是clearFocusInternal方法void clearFocusInternal(View focused, boolean propagate, boolean refocus) { if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {//无焦点不做处理 mPrivateFlags &= ~PFLAG_FOCUSED;//取消焦点标志 if (propagate && mParent != null) { mParent.clearChildFocus(this);//通知parent要清除焦点信息 } onFocusChanged(false, 0, null);//回调该view的方法告知已经取消了焦点 refreshDrawableState();//刷新drawable if (propagate && (!refocus || !rootViewRequestFocus())) { notifyGlobalFocusCleared(this);//通知全局视图树的焦点改变监听 } }}//ViewGroup的unFocus()方法void unFocus(View focused) { if (mFocused == null) { super.unFocus(focused);//mFocused为null,说明该view可能为焦点view,清除自身焦点(有的话) } else { mFocused.unFocus(focused);//否则递归调用mFocused的unFocus方法 mFocused = null;//并更新当前view的mFocused为null }} public void clearChildFocus(View child) { mFocused = null;//ViewGroup的mFocused信息清除 if (mParent != null) { mParent.clearChildFocus(this);//继续递归调用parent的该方法清楚mFocused }} //下面是ViewGroup的请求焦点方法public boolean requestFocus(int direction, Rect previouslyFocusedRect) { int descendantFocusability = getDescendantFocusability();//该属性就是上面说的父view对子view焦点获取的控制属性 switch (descendantFocusability) { case FOCUS_BLOCK_DESCENDANTS://组织子view获取焦点 return super.requestFocus(direction, previouslyFocusedRect);//那么直接调用view的请求焦点方法,ViewGroup尝试自己获取焦点 case FOCUS_BEFORE_DESCENDANTS: {//在子view前获取焦点 final boolean took = super.requestFocus(direction, previouslyFocusedRect);//先去自己请求焦点 return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);//自己请求成功则直接返回true,否则要去请求子view的焦点 } case FOCUS_AFTER_DESCENDANTS: {//在子view后获取焦点 final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);//先去请求子view的焦点 return took ? took : super.requestFocus(direction, previouslyFocusedRect);//子view拿到焦点则返回true,否则自己去请求 } }}protected boolean onRequestFocusInDescendants(int direction,Rect previouslyFocusedRect) { int index; int increment; int end; int count = mChildrenCount; if ((direction & FOCUS_FORWARD) != 0) { index = 0; increment = 1; end = count; } else {//默认时FOCUS_DOWN index = count - 1; increment = -1; end = -1; } final View[] children = mChildren; for (int i = index; i != end; i += increment) {//FOCUS_DOWN相当于从后往前找,即最后往前遍历child View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {//如果child可见,且获取到了焦点则返回true if (child.requestFocus(direction, previouslyFocusedRect)) { return true; } } } return false;}
由代码可知,请求焦点的流程大致是:
满足焦点属性且不被父view拦截时,可以获得焦点,改变标志位,递归通知parent更新焦点信息:包括自身取消焦点(有的话)、取消原有mFocused并更新mFocused信息,还有对于变更了焦点状态的view通知全局视图树焦点监听以及自身的回调,并刷新drawable;ViewGroup稍有不同的是获取焦点时回先根据descendantFocusability状态选择是自己获取焦点还是子view获取焦点
当view焦点状态变化时会调用onFocusChanged回调:
protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,@Nullable Rect previouslyFocusedRect) { InputMethodManager imm = InputMethodManager.peekInstance(); if (!gainFocus) {//失去焦点,重置press状态,隐藏输入法 if (isPressed()) { setPressed(false); } if (imm != null && mAttachInfo != null && mAttachInfo.mHasWindowFocus) { imm.focusOut(this); } onFocusLost(); } else if (imm != null && mAttachInfo != null && mAttachInfo.mHasWindowFocus) {//获得焦点,尝试打开输入法 imm.focusIn(this); } invalidate(true);//刷新整个view ListenerInfo li = mListenerInfo; if (li != null && li.mOnFocusChangeListener != null) {//回调自己添加的onFocusChangeListener li.mOnFocusChangeListener.onFocusChange(this, gainFocus); }...}
这个方法主要就是根据焦点的变化,控制输入法的状态,刷新view以及调用一些回调等事件,有几个注意点:
-
mAttachInfo.mHasWindowFocus全局的这个属性是指当前窗口是否有焦点,即当前窗口是否在前台与用户交互
-
imm.focusIn内部会判断参数view是否是可编辑,是的话才会尝试弹出输入法
另外我们注意到,通知parent请求焦点和取消焦点时调用的parent.requestChildFocus、parent.clearChildFocus是ViewParent的方法,递归最后会调用到ViewRootImpl的相应方法:
public void requestChildFocus(View child, View focused) { checkThread(); scheduleTraversals();}public void clearChildFocus(View child) { checkThread(); scheduleTraversals();}
我们看到,最终其实都是调用了scheduleTraversals方法发起了遍历请求。
三.View可见性的改变
首先需要知道的是view的大部分状态都是用flag的方式表示在int数据mViewFlags(状态相关状态),mPrivateFlags(逻辑相关状态)里的,即用指定的某一位代表一个状态,如果是一组状态还会有个MASK代表这一组状态的标志,然后添加、删除、获取和判断该状态时使用位运算即可,就拿VISIBILITY可见性这个状态举例:
-
VISIBILITY_MASK = 0x0000000C:可见性的掩码,换成2进制即0000 0000 0000 0000 0000 0000 0000 1100,意为这几个可见性状态占用的是第29、30位
-
VISIBLE = 0x00000000:可见状态的标志位,全为0,不占用某一位
-
INVISIBLE = 0x00000004:不可见状态的标志位,2进制为0000 0000 0000 0000 0000 0000 0000 0100,即占用的是第30位
-
GONE = 0x00000008:隐藏状态的标志位,2进制为0000 0000 0000 0000 0000 0000 0000 1000,即占用的是第29位
-
在调用view.getVisibility()获取可见性状态时,直接return mViewFlags & VISIBILITY_MASK;即当前标志位运用位运算,与可见性掩码按位与,比如当前状态为INVISIBLE,那么运算后得到的值就是INVISIBLE的标志位,即第30位为1
-
添加该状态时,只要按位或运算加上flag即可,原理是一样的
setVisibility()方法以及其他一些设置flag的操作实际都是调用的setFlags(flags,mask)方法,传入新的flag和其对于的mask即可,下面看看这个方法:
void setFlags(int flags, int mask) {... int old = mViewFlags; mViewFlags = (mViewFlags & ~mask) | (flags & mask);//根据mask清空原标志,按位或加上新的flag int changed = mViewFlags ^ old;//按位异或得到改变的量 if (changed == 0) {//如果没有改变则不做处理 return; } int privateFlags = mPrivateFlags; if (((changed & FOCUSABLE_MASK) != 0) && ((privateFlags & PFLAG_HAS_BOUNDS) !=0)) {//如果focusable属性改变了且view有大小,则需要处理焦点相关 if (((old & FOCUSABLE_MASK) == FOCUSABLE) && ((privateFlags & PFLAG_FOCUSED) != 0)) {//原有focusable为true且已经获取了焦点 clearFocus();//则需要清除焦点 } else if (((old & FOCUSABLE_MASK) == NOT_FOCUSABLE) && ((privateFlags & PFLAG_FOCUSED) == 0)) {//现在focusable为true且还没获取焦点//会递归(最终到ViewRootImpl)通知parent该view可以获取焦点了:如果此时整个视图树没有焦点view或者该view的某一父view拥有着焦点且可以让其子view先获取焦点,那么就让该view请求焦点 if (mParent != null) mParent.focusableViewAvailable(this); } } final int newVisibility = flags & VISIBILITY_MASK; if (newVisibility == VISIBLE) { if ((changed & VISIBILITY_MASK) != 0) {//变成了VISIBLE状态 mPrivateFlags |= PFLAG_DRAWN; invalidate(true);//重新绘制 needGlobalAttributesUpdate(true); if ((mParent != null) && (mBottom > mTop) && (mRight > mLeft)) { mParent.focusableViewAvailable(this);//和上面一样,通知parent该view可用焦点 } } } if ((changed & GONE) != 0) {//可见性状态的GONE标志位发生变化(有可能变为GONE,有可能从GONE变到某一个) needGlobalAttributesUpdate(false); requestLayout();//需要请求重新布局,因为GONE后布局一定发生变化 if (((mViewFlags & VISIBILITY_MASK) == GONE)) {//变为GONE时要清除焦点,刷新区域等操作 if (hasFocus()) clearFocus(); clearAccessibilityFocus(); destroyDrawingCache(); if (mParent instanceof View) { ((View) mParent).invalidate(true); } mPrivateFlags |= PFLAG_DRAWN; } if (mAttachInfo != null) { mAttachInfo.mViewVisibilityChanged = true; } } if ((changed & INVISIBLE) != 0) {//INVISIBLE标志位发生变化 needGlobalAttributesUpdate(false);//可被刷新 mPrivateFlags |= PFLAG_DRAWN; if (((mViewFlags & VISIBILITY_MASK) == INVISIBLE)) {//INVISIBLE也不可拥有焦点 // root view becoming invisible shouldn't clear focus and accessibility focus if (getRootView() != this) { if (hasFocus()) clearFocus(); clearAccessibilityFocus(); } } if (mAttachInfo != null) { mAttachInfo.mViewVisibilityChanged = true; } } if ((changed & VISIBILITY_MASK) != 0) {//如果可见性发生了改变,统一做一些处理 if (newVisibility != VISIBLE && mAttachInfo != null) { cleanupDraw(); } if (mParent instanceof ViewGroup) { ((ViewGroup) mParent).onChildVisibilityChanged(this, (changed & VISIBILITY_MASK), newVisibility);//通知ViewGroup的child发生可见性变化 ((View) mParent).invalidate(true);//刷新 } else if (mParent != null) { mParent.invalidateChild(this, null);//刷新child } if (mAttachInfo != null) { dispatchVisibilityChanged(this, newVisibility);//回调该方法通知view自己可见性发生变化 notifySubtreeAccessibilityStateChangedIfNeeded(); } } ... if ((changed & DRAW_MASK) != 0) {//DRAW标志位发生变化 if ((mViewFlags & WILL_NOT_DRAW) != 0) {//不用绘制内容,该标志可以外部设置,指明该view是否需要绘制其自己的内容即onDraw,一般view默认都没有,一般ViewGroup会在一些情况下设置该属性 if (mBackground != null || (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) {//有需要绘制的背景前景 mPrivateFlags &= ~PFLAG_SKIP_DRAW;//不能跳过绘制 } else {//可以跳过绘制 mPrivateFlags |= PFLAG_SKIP_DRAW; } } else {//取消掉跳过绘制的标志 mPrivateFlags &= ~PFLAG_SKIP_DRAW; } requestLayout();//重新布局 invalidate(true);//刷新 } ...}
本质流程就是用掩码加标志,用位运算的方式添加标志、获取标志、查看标志位是否改变等信息,然后做相应的布局、重绘、焦点等操作即可
以上所有这些常用的改变View状态的方法其实最终都直接或间接的调用到了requestLayout和invalidate方法发起了一次重新遍历的请求,即scheduleTraversals,那么接下来我们来看看这两个方法是如何发起请求的以及干了些什么
四.请求重新布局requestLayout
//View的requestLayout方法public void requestLayout() { ...//加入要求重新布局的FORCE_LAYOUT标志和view失效的INVALIDATED标志 mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; if (mParent != null && !mParent.isLayoutRequested()) {//如果parent没有被要求重新布局 mParent.requestLayout();//则递归向上调用parent的该方法 }...}public boolean isLayoutRequested() { return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;//该方法就是看View是否有FORCE_LAYOUT的标志,有就说明已经被要求过布局} //ViewRootImpl的requestLayout的方法public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true;//将该变量置为true,重新遍历时会依据此变量来决定是否重新测量和布局 scheduleTraversals();//这就是发起请求的那个方法 }}
该方法还是比较简单的,就是一步步向上调用parent的requestLayout方法,给其加入FORCE_LAYOUT标志后续遍历时使用,直到调用到ViewRootImpl的该方法,将mLayoutRequested变量置为true后续会导致重新测量和布局,然后调用scheduleTraversals方法发起重新遍历的请求
这里有一个需要注意的就是:scheduleTraversals里会根据一个mTraversalScheduled变量判断本次消息处理过程中是否以及发起过遍历请求,如果第一次,则发送一个并将该变量置为false,本次消息处理中的后续调用该方法,都不会再发起一次请求了,这也就是说加入我们连续调用100次requestLayout,真正发出的请求也只有一次,这点很重要,也印证了开头说的第(4)点
五.请求重绘invalidate
该方法是使View刷新的方法,一般来说view系统的绘制不会每次都绘制整个屏幕的所有内容,而是通过指定view调用invalidate刷新部分区域,并且将所有调用过刷新的区域汇总,下次遍历时只重绘该部分区域即可,所以该方法就是指定刷新的区域,并发起遍历请求;知道这一点我们来看它源码
一般来讲我们invalidate都是刷新自己的整个区域,所以一般都调用invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache,false)方法刷新view整个区域
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,boolean fullInvalidate) { if (skipInvalidate()) {//不是VISIBLE且没有动画的view跳过刷新 return; } if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED || (fullInvalidate && isOpaque() != mLastIsOpaque)) {//主要是有DRAWN标志且大小不为0时可以刷新,有INVALIDATED标识已经无效的不能刷新... mPrivateFlags |= PFLAG_DIRTY;//该标志很重要,会决定在view调用draw时是否需要绘制背景和内容,下面会说另一种状态... // 这步最为重要,将向上递归计算需要重绘的区域(因为view的ltrb区域是相对于父view的,需要知道对于整个根view的区域才能进行绘制) final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; if (p != null && ai != null && l < r && t < b) { final Rect damage = ai.mTmpInvalRect; damage.set(l, t, r, b);//设置相对于父view的区域 p.invalidateChild(this, damage);//向上调用 } ... }} public final void invalidateChild(View child, final Rect dirty) { ViewParent parent = this; final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) == PFLAG_DRAW_ANIMATION; Matrix childMatrix = child.getMatrix(); final boolean isOpaque = child.isOpaque() && !drawAnimation && child.getAnimation() == null && childMatrix.isIdentity();//通过动画、变化矩阵、child自身属性判断要绘制的child是不是完全不透明的 int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;//如果是完全不透明的,就会设置DIRTY_OPAQUE标志,就是上面说的另一种状态,该状态会导致view重绘时不去绘制内容和背景了(因为这部分区域的child不透明,完全盖住了)... final int[] location = attachInfo.mInvalidateChildLocation; location[CHILD_LEFT_INDEX] = child.mLeft; location[CHILD_TOP_INDEX] = child.mTop; //...矩阵变换 do { View view = null; if (parent instanceof View) { view = (View) parent; } ... if (view != null) { if ((view.mViewFlags & FADING_EDGE_MASK) != 0 && view.getSolidColor() == 0) { opaqueFlag = PFLAG_DIRTY; } if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {//更新parent的opaqueFlag,子view的状态会影响到parent的该状态 view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag; } } parent = parent.invalidateChildInParent(location, dirty);//递归调用该方法,计算在parent中的相对区域并更新矩形,返回新的parent ... } while (parent != null); }} public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) { if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN || (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {//DRAWN表示已经绘制完毕了 if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) != FLAG_OPTIMIZE_INVALIDATE) {//没有动画或者动画以及执行完 dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX, location[CHILD_TOP_INDEX] - mScrollY);//偏移当前location的left,top位置(考虑滚动量),即算出在当前view中需要刷新的区域的坐标 if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) { dirty.union(0, 0, mRight - mLeft, mBottom - mTop); } final int left = mLeft; final int top = mTop; if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {//默认有该属性 if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {//与该view的区域做交集处理 dirty.setEmpty(); } } mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; location[CHILD_LEFT_INDEX] = left;//更新location的left,top为当前view的left,top,以供parent后续做相同的处理 location[CHILD_TOP_INDEX] = top; if (mLayerType != LAYER_TYPE_NONE) { mPrivateFlags |= PFLAG_INVALIDATED; } return mParent; } else {//有动画 mPrivateFlags &= ~PFLAG_DRAWN & ~PFLAG_DRAWING_CACHE_VALID; location[CHILD_LEFT_INDEX] = mLeft; location[CHILD_TOP_INDEX] = mTop; if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {//有动画只能刷新整个区域了 dirty.set(0, 0, mRight - mLeft, mBottom - mTop); } else { // in case the dirty rect extends outside the bounds of this container dirty.union(0, 0, mRight - mLeft, mBottom - mTop); } if (mLayerType != LAYER_TYPE_NONE) { mPrivateFlags |= PFLAG_INVALIDATED; } return mParent; } } return null;} //最终调用到ViewRootImpl的invalidateChildInParent方法private void invalidateRectOnScreen(Rect dirty) { final Rect localDirty = mDirty;...//将新的需要重绘的区域与原有的需要重绘的区域做并集即可 localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);... if (!mWillDrawSoon && (intersected || mIsAnimating)) { scheduleTraversals();//发起遍历请求 }}
主要思路就是根据view需要刷新的区域,递归向上调用,计算出在整个根view中该区域需要绘制的范围及坐标,发起遍历请求,下一次遍历时,在需要重绘的区域里绘制
经测试,opaqueFlag的状态并不会导致绘制是被遮挡的部分不被绘制,应该是由于判断是否绘制的条件里,有一个是系统是否是支持alpha通道的,如果支持则都会绘制;所以大部分手机都会绘制
六.performTraversal()方法
上面说了很多次scheduleTraversals方法,发起一个消息到MessageQueue,执行该消息时,会调用doTraversal方法,内部调用performTraversal方法进行真正的遍历,下面就来看看这个View系统遍历的核心方法
由于这个方法太过复杂,这里只看一些有关测绘流程调用的主要步骤:
private void performTraversals() { final View host = mView; ... Rect frame = mWinFrame; if (mFirst) {//mFirst变量记录是否第一次遍历view mFullRedrawNeeded = true;//需要全部重绘 mLayoutRequested = true;//需要重新布局//根据窗口属性得到窗口尺寸 if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) { // NOTE -- system code, won't try to do compat mode. Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else { DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; } ...//传递全局的mAttachInfo信息给每个view,所以每个view拿到的mAttachInfo是一样的//内部还会调用view的onAttachedToWindow,OnAttachStateChangeListener,onWindowVisibilityChanged和onVisibilityChanged这些通知attach和visibility状态改变的方法,我们都可以重写其方法进行自定义 host.dispatchAttachedToWindow(mAttachInfo, 0); mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);//调用视图树的OnWindowAttachedChange监听告知窗口已经创建... } else {//更新参数 desiredWindowWidth = frame.width(); desiredWindowHeight = frame.height(); if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {//如果尺寸发生改变,需要各种重绘布局。。。 mFullRedrawNeeded = true; mLayoutRequested = true; windowSizeMayChange = true; } } if (viewVisibilityChanged) { mAttachInfo.mWindowVisibility = viewVisibility; host.dispatchWindowVisibilityChanged(viewVisibility);//仍然通知所有view的onWindowVisibilityChanged方法 ... } ...//以上都是一些设置、更新、通知窗口属性变化的操作//下面是测量部分 boolean insetsChanged = false; boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);//mLayoutRequested就是上面说过的,requestLayout时会更新的变量,说明请求了重新布局 if (layoutRequested) { final Resources res = mView.getContext().getResources(); if (mFirst) { // make sure touch mode code executes by setting cached value // to opposite of the added touch mode. mAttachInfo.mInTouchMode = !mAddedTouchMode; ensureTouchModeLocally(mAddedTouchMode);//执行触摸模式设置 } else { //设置insetsChanged} // Ask host how big it wants to be windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight);//内部调用perforMeasure方法,就会调用到根view的measure方法进行一次测量,并返回窗口大小是否可能改变 } ...//第一次进入或者可见性发生变化时,需要设置键盘模式,如果xml中设置好了则不用设置,否则根据情况选择:有滚动的view时会设置成resize,否则设置为pan if (mFirst || mAttachInfo.mViewVisibilityChanged) { mAttachInfo.mViewVisibilityChanged = false; int resizeMode = mSoftInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; // If we are in auto resize mode, then we need to determine // what mode to use now. if (resizeMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED) { final int N = mAttachInfo.mScrollContainers.size(); for (int i=0; i<N; i++) { if (mAttachInfo.mScrollContainers.get(i).isShown()) { resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; } } if (resizeMode == 0) { resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; } if ((lp.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) != resizeMode) { lp.softInputMode = (lp.softInputMode & ~WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) | resizeMode; params = lp; } } } if (params != null) { if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) { if (!PixelFormat.formatHasAlpha(params.format)) { params.format = PixelFormat.TRANSLUCENT; } } mAttachInfo.mOverscanRequested = (params.flags & WindowManager.LayoutParams.FLAG_LAYOUT_IN_OVERSCAN) != 0; } ...//窗口大小是否确定要更改 boolean windowShouldResize = layoutRequested && windowSizeMayChange && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT && frame.width() < desiredWindowWidth && frame.width() != mWidth) || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT && frame.height() < desiredWindowHeight && frame.height() != mHeight)); ... int relayoutResult = 0;//确定是否需要重新布局 if (mFirst || windowShouldResize || insetsChanged || viewVisibilityChanged || params != null) {//首次进入或者有尺寸需要改变或者可见性改变了 ... try { ... relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);//重新计算布局,Wms根据一些参数计算出窗口实际大小,并给Surface分配内存使其可用 ... if (!hadSurface) {//上面初始化了Surface,这个变量意思是之前没有,即是指首次进来时候不可用的时候 if (mSurface.isValid()) {//当前已经有效,需要重绘、布局 newSurface = true; mFullRedrawNeeded = true; mPreviousTransparentRegion.setEmpty(); ... } }... } catch (RemoteException e) { } ... if (!mStopped || mReportNextDraw) { boolean focusChangedDueToTouchMode = ensureTouchModeLocally( (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0); if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {//需要重新测量,并考虑weight属性重新测量 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); int width = host.getMeasuredWidth(); int height = host.getMeasuredHeight(); boolean measureAgain = false; if (lp.horizontalWeight > 0.0f) { width += (int) ((mWidth - width) * lp.horizontalWeight); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); measureAgain = true; } if (lp.verticalWeight > 0.0f) { height += (int) ((mHeight - height) * lp.verticalWeight); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); measureAgain = true; } if (measureAgain) {//重新测量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } layoutRequested = true;//需要重新布局 } } } else { ... }//下面是布局部分 final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);//是否需要重新布局 boolean triggerGlobalLayoutListener = didLayout || mAttachInfo.mRecomputeGlobalAttributes;//是否需要触发GlobalLayoutListener if (didLayout) {//需要重新布局 performLayout(lp, desiredWindowWidth, desiredWindowHeight);//主要是调用根view的layout方法,ltrb就是窗口的大小,之前以及计算出来了 ... } if (triggerGlobalLayoutListener) { mAttachInfo.mRecomputeGlobalAttributes = false; mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();//触发OnGloabalLayoutListener,在layout后执行,所以就可以拿到view的实际宽高了 } ... boolean skipDraw = false; if (mFirst) {//首次进入尝试请求焦点 if (mView != null) { if (!mView.hasFocus()) { mView.requestFocus(View.FOCUS_FORWARD);//从开始向后遍历寻找焦点view } else { } } } else if (mWindowsAnimating) {... } mFirst = false;//此处就把mFirst置为false了 mWillDrawSoon = false; mNewSurfaceNeeded = false; mViewVisibility = viewVisibility;//尝试将键盘弹出(根据焦点view) if (mAttachInfo.mHasWindowFocus && !isInLocalFocusMode()) { final boolean imTarget = WindowManager.LayoutParams .mayUseInputMethod(mWindowAttributes.flags); if (imTarget != mLastWasImTarget) { mLastWasImTarget = imTarget; InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null && imTarget) { imm.onPreWindowFocus(mView, true /* hasWindowFocus */); imm.onPostWindowFocus(mView, mView.findFocus(), mWindowAttributes.softInputMode, !mHasHadWindowFocus, mWindowAttributes.flags); } } } // Remember if we must report the next draw. if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) { mReportNextDraw = true; }//下面开始进入绘制部分//调用视图树的OnPreDraw监听,其返回值代表是否要取消本次绘制,有一个监听返回false则cancelDraw就为true boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || viewVisibility != View.VISIBLE; if (!cancelDraw && !newSurface) { if (!skipDraw || mReportNextDraw) { ... performDraw();//核心就是调用根view的draw方法开始一次绘制 } } else { ... } mIsInTraversal = false;//遍历完成 }} //下面看下performDraw调用的ViewRootImpl的draw方法private void draw(boolean fullRedrawNeeded) { Surface surface = mSurface; ... final Rect dirty = mDirty; ... if (fullRedrawNeeded) {//如果上面说的该变量为true则要全部重绘,就要将dirty区域置为整个窗口大小,否则直接使用dirty---即我们调用invalidate时确定的需要重绘的区域 mAttachInfo.mIgnoreDirtyState = true; dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f)); } mAttachInfo.mTreeObserver.dispatchOnDraw();//回调视图树的OnDraw监听,通知正在绘制(还没绘制) ... if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { ... mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this); } else { ...//该步真正调用绘制,使用Surface拿到指定区域的画布,调用根view的draw开始绘制 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { return; } } } if (animating) {//如果有动画在进行,需要重新再绘制 mFullRedrawNeeded = true; scheduleTraversals(); }}
该方法的大体思路就是这样,简单来说就是,跳转窗口大小,状态等并通知view,然后再结合之前请求时改变的参数决定是否需要重新测量而调用根view的measure方法,再根据这些参数决定是否需要重新布局而调用根view的layout,最后调用根view的draw方法,重绘指定区域或全部重绘,可以看出几点:
-
requestLayout等请求重新布局后,要先经过测量,即layout前要measure
-
调用invalidate等刷新方法后,一般不会再调用measure和layout过程,除非影响了整体的大小和位置等
-
一次消息处理中多次调用这些申请遍历的方法也只会向MessageQueue发送一个消息
-
(4)mAttachInfo全局唯一,我们平时添加的视图树监听以及使用的view的post等消息处理都是从view的mAttachInfo中取得,所以也都是全局唯一的
-
整个遍历过程中的多个节点都会有一些监听器、回调方法的调用,供我们自定义view时做处理
更多相关文章
- rk3288 Android(安卓)5.1root方法
- android annotation配置及简单使用
- Android(安卓)TelephonyManager类的使用
- Android(安卓)Audio Debug相关方法
- Android(安卓)读取文件内容实现方法总结
- Android(安卓)Studio ADB响应失败解决方法
- JAVA String.format 方法使用介绍
- 【Android(安卓)开发教程】显示复杂对话框
- Android如何获取屏幕的分辨率