1)View绘制流程
2)计算一个view的嵌套层级
3)View刷新机制。invalidate和 postInvalidate、requestLayout的区别及使用
4)自定义控件原理,如何优化自定义View
(1. 降低刷新频率,减少不必要的invalidate 2. 不要在OnDraw当中创建绘制对象 3.硬件加速)
5)自定义View的事件 (onTouchEvent / 新建接口在onTouch中触发)
6)自定义View如何提供获取View属性的接口?(TypedArray)
7)为什么不能在子线程更新UI?(若可以的话,那么在多线程中并发访问可能会导致UI控件处于不可预期的状态。)
一. View绘制流程
每一个View的绘制过程都必须经历三个最主要的过程,也就是measure、layout和draw。
整个View树的绘图流程是在ViewRootImpl类的performTraversals()方法开始的,该函数做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小(measure)、是否重新放置视图的位置(layout)、以及是否重绘 (draw),其核心也就是通过判断来选择顺序执行这三个方法中的哪个,如下:
private void performTraversals() {
......
//最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
//lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
1. Measure
/**
* <p>
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* </p>
*
* <p>
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* </p>
*
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
*
* @see #onMeasure(int, int)
*/
//final方法,子类不可重写
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
//回调onMeasure()方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
为整个View树计算实际的大小,然后设置实际的高和宽,每个View控件的实际宽高都是由父视图和自身决定的。实际的测量是在onMeasure方法进行,所以在View的子类需要重写onMeasure方法,这是因为measrue方法是final的,不允许重载,所以子view只能通过重载onMeasure来实现自己的测量逻辑。
这个方法的两个参数都是从父view传递过来的,也就是代表了父view的规格。它由两部分组成,高2位表示MODE,定义在MeasureSpec类中(View的内部类),三种类型MeasureSpec.EXACTLY表示确定大小,MeasureSpec.AT_MOST表示最大大小,MeasureSpec.UNSPECIFIED表示不确定。低30位表示size,也就是View的大小。对于系统Window类的DecorView对象Mode一般都为MeasureSpec.EXACTLY,而size分别对应屏幕宽高。对于子view来说大小由父view和子view共同决定。
看出measure方法最终回调了View的onMeasure方法,我们来看下View的onMeasure源码,如下:
/**
* <p>
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overriden by subclasses to provide accurate and efficient
* measurement of their contents.
* </p>
*
* <p>
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* </p>
*
* <p>
* The base class implementation of measure defaults to the background size,
* unless a larger size is allowed by the MeasureSpec. Subclasses should
* override {@link #onMeasure(int, int)} to provide better measurements of
* their content.
* </p>
*
* <p>
* If this method is overridden, it is the subclass's responsibility to make
* sure the measured height and width are at least the view's minimum height
* and width ({@link #getSuggestedMinimumHeight()} and
* {@link #getSuggestedMinimumWidth()}).
* </p>
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
*
* @see #getMeasuredWidth()
* @see #getMeasuredHeight()
* @see #setMeasuredDimension(int, int)
* @see #getSuggestedMinimumHeight()
* @see #getSuggestedMinimumWidth()
* @see android.view.View.MeasureSpec#getMode(int)
* @see android.view.View.MeasureSpec#getSize(int)
*/
//View的onMeasure默认实现方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension传入的参数都是通过getDefaultSize返回的,所以再来看下getDefaultSize方法源码,如下:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//通过MeasureSpec解析获取mode与size
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
如果specMode等于AT_MOST或EXACTLY就返回specSize,这就是系统默认的规格。
回过头继续看上面onMeasure方法,其中getDefaultSize参数的widthMeasureSpec和heightMeasureSpec都是由父View传递进来的。getSuggestedMinimumWidth与getSuggestedMinimumHeight都是View的方法,具体如下:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
建议的最小宽度和高度都是由View的Background尺寸与通过设置View的miniXXX属性共同决定的。
到此一次最基础的元素View的measure过程就完成了。上面说了View实际是嵌套的,而且measure是递归传递的,所以每个View都需要measure。实际能够嵌套的View一般都是ViewGroup的子类,所以在ViewGroup中定义了measureChildren, measureChild, measureChildWithMargins方法来对子视图进行测量,measureChildren内部实质只是循环调用measureChild,measureChild和measureChildWithMargins的区别就是是否把margin和padding也作为子视图的大小。如下我们以ViewGroup中稍微复杂的measureChildWithMargins方法来分析:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子视图的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//调整MeasureSpec
//通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格
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);
//调运子View的measure方法,子View的measure中会回调子View的onMeasure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
该方法就是对父视图提供的 measureSpec 参数结合自身的 LayoutParams 参数进行了调整,然后再来调用child.measure()方法,具体通过方法 getChildMeasureSpec 来进行参数调整。所以我们继续看下 getChildMeasureSpec 方法代码,如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 获取当前Parent View的Mode和Size
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 获取Parent size与padding差值(也就是Parent剩余大小),若差值小于0直接返回0
int size = Math.max(0, specSize - padding);
// 定义返回值存储变量
int resultSize = 0;
int resultMode = 0;
// 依据当前Parent的Mode进行switch分支逻辑
switch (specMode) {
// Parent has imposed an exact size on us
// 默认Root View的Mode就是EXACTLY
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
// 如果child的layout_wOrh属性在xml或者java中给予具体大于等于0的数值
// 设置child的size为真实layout_wOrh属性值,mode为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 如果child的layout_wOrh属性在xml或者java中给予MATCH_PARENT
// Child wants to be our size. So be it.
// 设置child的size为size,mode为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 如果child的layout_wOrh属性在xml或者java中给予WRAP_CONTENT
// 设置child的size为size,mode为AT_MOST
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
......
// 其他Mode分支类似
}
// 将mode与size通过MeasureSpec方法整合为32位整数返回
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
getChildMeasureSpec的逻辑是通过其父View提供的MeasureSpec参数得到specMode和specSize,然后根据计算出来的specMode以及子View的childDimension(layout_width或layout_height)来计算自身的measureSpec,如果其本身包含子视图,则计算出来的measureSpec将作为调用其子视图measure函数的参数,同时也作为自身调用setMeasuredDimension的参数,如果其不包含子视图则默认情况下最终会调用onMeasure的默认实现,并最终调用到setMeasuredDimension。
所以可以看见onMeasure的参数其实就是这么计算出来的。同时从上面的分析可以看出来,最终决定View的measure大小是View的setMeasuredDimension方法,所以我们可以通过setMeasuredDimension设定死值来设置View的mMeasuredWidth和mMeasuredHeight的大小,但是一个好的自定义View应该会根据子视图的measureSpec来设置mMeasuredWidth和mMeasuredHeight的大小,这样的灵活性更大,所以这也就是上面分析onMeasure时说View的onMeasure最好不要重写死值的原因。
2. layout
public void layout(int l, int t, int r, int b) {
......
//实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量
//判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//需要重新layout
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//回调onLayout
onLayout(changed, l, t, r, b);
......
}
......
}
- View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。
- measure操作完成后得到的是对每个View经测量过的 measuredWidth 和 measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的 mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。
- 凡是layout_XXX的布局属性基本都针对的是包含子 View 的 ViewGroup 的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的。
- 使用View的 getWidth() 和 getHeight() 方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。
3.draw
- 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。
- View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。
- View的绘制是借助onDraw方法传入的Canvas类来进行的。
- 区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机显示等不同动画效果)。
- 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。
- 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。
二. 计算一个view的嵌套层级
int i = 0;
private void getParents(ViewParent view){
if (view.getParent() == null) {
Log.v("tag", "最终==="+i);
return;
}
i++;
ViewParent parent = view.getParent();
Log.v("tag", "i===="+i);
Log.v("tag", "parent===="+parent.toString());
getParents(parent);
}
因为 public abstract class ViewGroup extends View implements ViewParent
三. View的invalidate和postInvalidate方法源码分析
invalidate追源码最后发现均回调用到 invalidateInternal 方法。
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
......
// Propagate the damage rectangle to the parent 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);
//传递调运Parent ViewGroup的invalidateChild方法
p.invalidateChild(this, damage);
}
......
}
View的invalidate(invalidateInternal)方法实质是将要刷新区域直接传递给了父ViewGroup的invalidateChild方法,在invalidate中,调用父View的invalidateChild,这是一个从当前向上级父View回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集 。所以我们看下ViewGroup的invalidateChild方法,源码如下:
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
......
do {
......
//循环层层上级调运,直到ViewRootImpl会返回null
parent = parent.invalidateChildInParent(location, dirty);
......
} while (parent != null);
}
这个过程最后传递到ViewRootImpl的invalidateChildInParent方法结束,所以我们看下ViewRootImpl的invalidateChildInParent方法,如下:
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
......
//View调运invalidate最终层层上传到ViewRootImpl后最终触发了该方法
scheduleTraversals();
......
return null;
}
2. postInvalidate 方法源码分析
public void postInvalidate() {
postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
//核心,实质就是调运了 ViewRootImpl.dispatchInvalidateDelayed方法
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
继续看他调运的ViewRootImpl类的dispatchInvalidateDelayed方法,如下源码:
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
通过ViewRootImpl类的Handler发送了一条MSG_INVALIDATE消息,继续追踪这条消息的处理可以发现:
public void handleMessage(Message msg) {
......
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
......
}
......
}
实质就是又在UI Thread中调运了View的invalidate();方法,那接下来View的invalidate();
3. requestLayout方法源码分析
public void requestLayout() {
......
if (mParent != null && !mParent.isLayoutRequested()) {
//由此向ViewParent请求布局
//从这个View开始向上一直requestLayout,最终到达ViewRootImpl的requestLayout
mParent.requestLayout();
}
......
}
当我们触发View的requestLayout时其实质就是层层向上传递,直到ViewRootImpl为止,然后触发ViewRootImpl的requestLayout方法,如下就是ViewRootImpl的requestLayout方法:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
//View调运requestLayout最终层层上传到ViewRootImpl后最终触发了该方法
scheduleTraversals();
}
}
类似于上面分析的invalidate过程,只是设置的标记不同,导致对于View的绘制流程中触发的方法不同而已。
requestLayout()方法会调用measure过程和layout过程,不会调用draw过程,也不会重新绘制任何View包括该调用者本身。