本文参考
在介绍View的三大流程前,需要介绍一些基本概念:
View视图结构
-
PhoneWindow:是Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口
-
DecorView:起始节点View,继承于View类,作为整个视图容器来使用,View数的头结点
-
ViewRootImp:这是一个管理类,负责管理、布局、渲染窗口UI
无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个View树中各个View,最终确定整个View树的相关属性。
安卓坐标系
颜色模式
ViewRootImpl:
在Activtiy启动时创建,负责管理、布局、渲染窗口UI等等,view的三大流程都是通过ViewRootImpl来实现的
DecorView:
DecorView是最父级的View,本质是一个FrameLayout,view层的所有事件都要先经过DecorView,
MeasureSpec:
代表一个32位的int,高2位代表SpecMode,低30位代表SpecSize,是View的一个内部类,SpecMode是测量模式有三种:
- UNSPECIFIED:父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般
用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大) - EXACTLY:父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。
- AT_MOST:你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。
SpecSize是在某种测量模式下的参考尺寸
MeasureSpec类的重要方法:
/**
* MeasureSpec类的具体使用
**/
// 1. 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)
// 2. 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)
// 3. 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
- 对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定
- 对于不同的父容器和view本身不同的LayoutParams,view就可以有多种MeasureSpec。
1. 当view采用固定宽高的时候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;
2. 当view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么view也是最大模式并且其大小不会超过父容器的剩余空间;
3. 当view的宽高是wrap_content时,不管父容器的模式是精准还是最大化,view的模式总是最大化并且大小不能超过父容器的剩余空间。
4. Unspecified模式,这个模式主要用于系统内部多次measure的情况下,一般来说,我们不需要关注此模式(这里注意自定义View放到ScrollView的情况 需要处理)。
LayoutParams
布局参数,子View通过LayoutParams告诉父容器(ViewGroup)应该如何放置自己。从这个定义中也可以看出来LayoutParams与ViewGroup是息息相关的,因此脱离ViewGroup谈LayoutParams是没有意义的。
addView()
/**
* 重载方法1:添加一个子View
* 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
*/
public void addView(View child) {
addView(child, -1);
}
/**
* 重载方法2:在指定位置添加一个子View
* 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
* @param index View将在ViewGroup中被添加的位置(-1代表添加到末尾)
*/
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();// 生成当前ViewGroup默认的LayoutParams
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
/**
* 重载方法3:添加一个子View
* 使用当前ViewGroup默认的LayoutParams,并以传入参数作为LayoutParams的width和height
*/
public void addView(View child, int width, int height) {
final LayoutParams params = generateDefaultLayoutParams(); // 生成当前ViewGroup默认的LayoutParams
params.width = width;
params.height = height;
addView(child, -1, params);
}
/**
* 重载方法4:添加一个子View,并使用传入的LayoutParams
*/
@Override
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
/**
* 重载方法4:在指定位置添加一个子View,并使用传入的LayoutParams
*/
public void addView(View child, int index, LayoutParams params) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
// 添加子view的核心方法
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
.....
if (mTransition != null) {
mTransition.addChild(this, child);
}
if (!checkLayoutParams(params)) { // ① 检查传入的LayoutParams是否合法
params = generateLayoutParams(params); // 如果传入的LayoutParams不合法,将进行转化操作
}
if (preventRequestLayout) { // ② 是否需要阻止重新执行布局流程
child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout->onDraw)
} else {
child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout->onDraw)
}
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
.....
}
-
addInArray()讲当前子view添加到viewGroup的view tree 的数组中
-
requestLayout方法会导致View的onMeasure、onLayout、onDraw方法被调用;
-
invalidate方法则只会导致View的onDraw方法被调用
Android自定义View之requestLayout方法和invalidate方法
measure过程
分类:
单一View的measure:
/**
* 源码分析:measure()
* 定义:Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法
* 作用:基本测量逻辑的判断
**/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:View的宽 / 高测量规格
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 计算视图大小 ->>分析1
} else {
...
}
/**
* 分析1:onMeasure()
* 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
* b. 存储测量后的View宽 / 高:setMeasuredDimension()
**/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:View的宽 / 高测量规格
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
// setMeasuredDimension() :获得View宽/高的测量值 ->>分析2
// 传入的参数通过getDefaultSize()获得 ->>分析3
}
/**
* 分析2:setMeasuredDimension()
* 作用:存储测量后的View宽 / 高
* 注:该方法即为我们重写onMeasure()所要实现的最终目的
**/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//参数说明:测量后子View的宽 / 高值
// 将测量后子View的宽 / 高值进行传递
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
// 由于setMeasuredDimension()的参数是从getDefaultSize()获得的
// 下面我们继续看getDefaultSize()的介绍
/**
* 分析3:getDefaultSize()
* 作用:根据View宽/高的测量规格计算View的宽/高值
**/
public static int getDefaultSize(int size, int measureSpec) {
// 参数说明:
// size:提供的默认大小
// measureSpec:宽/高的测量规格(含模式 & 测量大小)
// 设置默认大小
int result = size;
// 获取宽/高测量规格的模式 & 测量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
// 返回View的宽/高值
return result;
}
ViewGroup的measure
原理:
- 遍历测量所有子view的大小
- 综合所有子view的大小得出viewGroup的大小
ViewGroup是一个抽象类,它没有重写View的onMeasure方法,但是他提供了一个叫measureChildren方法;
measureChildren()
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChildren方法很简单for循环挨个调用子view的measure()
ViewGroup没有定义明确的measure过程,因为这是一个抽象类,具体的测量逻辑需要在实现类的onMeasure中实现,因为ViewGroup子类的布局特性无法不同,所以测量的细节也不同;
onMeasure()的套路
/**
* 根据自身的测量逻辑复写onMeasure(),分为3步
* 1. 遍历所有子View & 测量:measureChildren()
* 2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值(自身实现)
* 3. 存储测量后View宽/高的值:调用setMeasuredDimension()
**/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 定义存放测量后的View宽/高的变量
int widthMeasure ;
int heightMeasure ;
// 1. 遍历所有子View & 测量(measureChildren())
// ->> 分析1
measureChildren(widthMeasureSpec, heightMeasureSpec);
// 2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值
void measureSelf{
... // 自身实现
}
// 3. 存储测量后View宽/高的值:调用setMeasuredDimension()
// 类似单一View的过程,此处不作过多描述
setMeasuredDimension(widthMeasure, heightMeasure);
}
重点总结
measure过程有什么作用
ViewGroup如何向子view传递限制信息
ScrollView嵌套ListView问题?
为什么直接继承View的自定义控件使用wrap_content相当于使用match_parent?如何处理?
如果我们的自定义view(下面检查mView)设置为wrap_content,那么根据parent的measureSpec和wrap_content,我们可以知道mView的SpecMode是AT_MOST,SpecSize是parentSize
,根据getDefaultSize
的实现来看,SpecSize
就是最后的测量大小,不管是match_parent还是wrap_content,都是用的parentSize做的SpecSize;在Android其他的源码中,TextView,ImageView针对onMeasure方法都对wrap_content做了特殊处理;
如何处理wrap_content失效?
//控件默认的宽高
int defaultWidth = 300;
int defaultHeight = 200;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int withSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT
&& getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
setMeasuredDimension(defaultWidth,defaultHeight);
} else if(getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT){
setMeasuredDimension(defaultWidth,heightSize);
} else if(getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
setMeasuredDimension(withSize,defaultHeight);
}
}
解决方案是给View指定一个默认的内部宽高,并且在wrap_content的时候调用setMeasuredDimension传入指定宽高
Layout
layout的作用是确定view的坐标点,计算出left,right,top,bottom
坐标
View的layout过程:
layout()
/**
* 源码分析:layout()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
*/
public void layout(int l, int t, int r, int b) {
// 当前视图的四个顶点
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 1. 确定View的位置:setFrame() / setOpticalFrame()
// 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回
// ->>分析1、分析2
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 2. 若视图的大小 & 位置发生变化
// 会重新确定该View所有的子View在父容器的位置:onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现->>分析3
// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现(后面会详细说)
...
}
/**
* 分析1:setFrame()
* 作用:根据传入的4个位置值,设置View本身的四个顶点位置
* 即:最终确定View本身的位置
*/
protected boolean setFrame(int left, int top, int right, int bottom) {
...
// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
// 从而确定了视图的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}
/**
* 分析2:setOpticalFrame()
* 作用:根据传入的4个位置值,设置View本身的四个顶点位置
* 即:最终确定View本身的位置
*/
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
// 内部实际上是调用setFrame()
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
// 回到调用原处
/**
* 分析3:onLayout()
* 注:对于单一View的laytou过程
* a. 由于单一View是没有子View的,故onLayout()是一个空实现
* b. 由于在layout()中已经对自身View进行了位置计算,所以单一View的layout过程在layout()后就已完成了
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 参数说明
// changed 当前View的大小和位置改变了
// left 左部位置
// top 顶部位置
// right 右部位置
// bottom 底部位置
}
setFrame()和setOpticalFrame()的作用是设置view的四个顶点,setOpticalFrame()最终也是调用了setFrame()
ViewGroup的layout过程
onLayout()是用来layout子view的,所以单一View的onLayout()是一个空实现,layout()确定自身的坐标,onLayout()确定子View的坐标;
ViewGroup layout过程的注意点:
- 一开始计算ViewGroup位置时,调用的是
ViewGroup
的layout()
和onLayout()
; - 当开始遍历子View & 计算子View位置时,调用的是
子View
的layout()
和onLayout()
ViewGroup的layout()
/**
* 源码分析:layout()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
* 注:与单一View的layout()源码一致
*/
public void layout(int l, int t, int r, int b) {
// 当前视图的四个顶点
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 1. 确定View的位置:setFrame() / setOpticalFrame()
// 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回
// ->>分析1、分析2
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 2. 若视图的大小 & 位置发生变化
// 会重新确定该View所有的子View在父容器的位置:onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现(上面已分析完毕)
// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现 ->>分析3
...
}
/**
* 分析1:setFrame()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
*/
protected boolean setFrame(int left, int top, int right, int bottom) {
...
// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
// 从而确定了视图的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}
/**
* 分析2:setOpticalFrame()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
*/
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
// 内部实际上是调用setFrame()
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
// 回到调用原处
/**
* 分析3:onLayout()
* 作用:计算该ViewGroup包含所有的子View在父容器的位置()
* 注:
* a. 定义为抽象方法,需重写,因:子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现
* b. 在自定义ViewGroup时必须复写onLayout()!!!!!
* c. 复写原理:遍历子View 、计算当前子View的四个位置值 & 确定自身子View的位置(调用子View layout())
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 参数说明
// changed 当前View的大小和位置改变了
// left 左部位置
// top 顶部位置
// right 右部位置
// bottom 底部位置
// 1. 遍历子View:循环所有子View
for (int i=0; i
draw
单一View的绘制
draw()
draw()主要按照顺序调用了如下四个方法,draw()的代码就不贴了,直接看下面四个方法
drawBackground()
private void drawBackground(Canvas canvas) {
// 获取背景 drawable
final Drawable background = mBackground;
if (background == null) {
return;
}
// 根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界
setBackgroundBounds();
.....
// 获取 mScrollX 和 mScrollY值
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
// 若 mScrollX 和 mScrollY 有值,则对 canvas 的坐标进行偏移
canvas.translate(scrollX, scrollY);
// 调用 Drawable 的 draw 方法绘制背景
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
onDraw()和dispatchDraw()都是空实现,需要根据业务去重写
onDrawForeground()
public void onDrawForeground(Canvas canvas) {
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (foreground != null) {
if (mForegroundInfo.mBoundsChanged) {
mForegroundInfo.mBoundsChanged = false;
final Rect selfBounds = mForegroundInfo.mSelfBounds;
final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
if (mForegroundInfo.mInsidePadding) {
selfBounds.set(0, 0, getWidth(), getHeight());
} else {
selfBounds.set(getPaddingLeft(), getPaddingTop(),
getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
}
final int ld = getLayoutDirection();
Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
foreground.setBounds(overlayBounds);
}
foreground.draw(canvas);
}
}
绘制装饰,foreground, scrollbars等
ViewGroup的draw()
ViewGroup比View多了dispatchDraw()的实现
protected void dispatchDraw(Canvas canvas) {
......
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 循环调用drawChild方法,绘制child
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
......
}
在dispatchDraw()中,循环了children数组,然后依次调用了drawChild(),前面省略的操作和animation
相关,没有仔细看,大概是绘制因为动画而消失的view
drawChild()
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}