Android View 绘制流程之四:绘制流程触发机制
系列文章:
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(@FocusRealD