ViewRoot 与 DecorView
ViewRoot 是连接 WindowManager 和 DecorView 的纽带,其实现类是 ViewRootImpl 类。View 的三大流程均由ViewRoot 完成。Activity 被创建完成后,会将 DecorView 添加到 Window 中,同时创建 ViewRootImpl 对象,并与DecorView 建立关联。
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams, panelParentView);
View 的绘制流程从 ViewRoot 的 performTraversals 方法开始:
performMeasure、performLayout、performDraw 三个方法分别完成顶级 View 的measure、layout、draw 这三大流程。其中,performMeasure 中调用 measure 方法,measure 中又调用 onMeasure 方法,onMeasure中会对所有子元素进行 measure 过程。measure 流程就是这样从父元素传递到子元素中,子元素会重复这个过程,从而完成整个 View树的遍历。performLayout、performDraw 的传递流程与此类似(draw方法中通过 dispatchDraw 来实现传递过程)。
- measure 过程决定 View 的宽高,完成后可通过
getMeasuredWidth
和getMeasuredHeight
方法获取测量后的宽高,一般此宽高就是 View 的最终宽高(特殊情况下例外)。 - layout 过程决定 View 的四个顶点和实际宽高,完成后可通过
getTop
、getBotton
、getLeft
和getRight
拿到 View 的四个顶点坐标,且可以通过getWidth
、getHeight
方法拿到最终宽高。
*draw 过程决定了 View 的显示。
DecorView作为顶级View,其实是一个 FrameLayout ,它包含一个竖直方向的 LinearLayout ,这个LinearLayout 分为标题栏和内容栏两个部分。
Activity通过setContextView所设置的布局文件其实就是被加载到内容栏之中的。这个内容栏的id是
R.android.id.content
,通过 ViewGroup content = findViewById(R.android.id.content);
可以得到这个contentView。content.getChildAt(0)
可以获得我们设置的 View。
MeasureSpec
MeasureSpec 参与 View 的 measure 的过程,类似于一个测量规格的概念。其创建过程受父容器影响。测量过程中,系统根据 View 的 LayoutParams 根据父容器所施加规则转换成对应MeasureSpec,根据这个 measureSpec 得到View 的测量宽高。
MeasureSpec 代表一个 32 位 int 值,高 2 位代表 SpecMode,低 30 位 代表 SpecSize。
MeasureSpec 通过 makeMeasureSpec(int size , int mode)
方法将模式和尺寸打包成一个 int 值,且可以通过getMode
和 getSize
方法解包原始值(此处 MeasureSpec 指其代表 int 值,而非对象本身)。
SpecMode有三类:
- UNSPECIFIED 父容器不对View有限制,要多大给多大,系统内部用。
- EXACTLY 父容器已检测出 View 所需精确大小。View 最终大小就是 SpecSize 指定值,对应
match_parent 和具体数值。 - AT_MOST 父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体值要看 View 的具体实现。对应 wrap_content。
DecorView 的 MeasureSpec 由窗口尺寸及自身参数共同决定。
普通 View 的 MeasureSpec 则由父容器与自身参数共同决定。
这里的parentSize 实际上是父容器剩余的可用空间,要减去父容器 padding、自身 margin、和父容器已经使用的部分。
View 的工作流程
measure
View 的 measure 过程
getSuggestedMinimumSize()
方法中,若有设置背景,会获取背景(Drawable)的原始宽高作为备选的返回值。Drawable若无原始宽高会默认为0。ShapeDrawable 无原始宽高,BitmapDrawable 有原始宽高(图片的尺寸)。直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则
wrap_content 效果等同 matct_parent。原因通过阅读源码可知,getDefaultSize 方法中只判断了是否为 UNSPECIFIED 模式,对EXACTLY 和 AT_MOST 处理是一样的,即返回 MeasureSpec 的 size 作为 View 的测量宽高。通过View 的 MeasureSpec 的创建规则表可知,size 为父容器剩余的可用空间。
//设置 wrap_content 时的自身大小
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//mWidth, mHeight 为自定义的默认内部宽高
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
ViewGroup 的 measure 过程
ViewGroup 除了完成自身的 measure 过程外,应该去遍历所有子 View 的measure 方法,各个子元素再去递归执行这个过程。但是 ViewGroup 是一个抽象类,并没有重写 onMeasure 方法。所以自定义 ViewGroup 时应该根据所需重写 onMeasure 方法。
VIewGroup 虽然没有重写 onMeasure 方法,但是相应提供了一些自定义时可以直接使用的方法:
measureChild()
:取出子 View 的LayoutParams 和本身的MeasureSpec 来创建子 View 的 MeasureSpec(通过getChildMeasureSpec
方法),直接传递给子 View 的measure 方法进行测量。measureChildren()
:作用是遍历所有子 View,对所有可见不为 GONE 的 View 使用measureChild()
方法。直接放到ViewGroup 的onMeasure 中可以实现类似无 wrap_content 功能的 FrameLayout 的效果。resolveSizeAndState
:主要是根据期望值(根据业务逻辑在重写的 onMeasure 中计算的尺寸)和ViewGroup 的 MeasureSpec 的模式和尺寸,判断最终返回的测量尺寸,一般用于重写 onMeasure 方法最后 setMeasureDimension 的参数处理。防止出现超过父容器剩余空间等情况。
一般重写 ViewGroup 的onMeasure 方法的思路是,遍历子元素进行 measure,按需求保存某方向尺寸的累加值或者最大值,加上padding、子View 的 margin等。最终使用这个值设定 VIewGroup 的测量尺寸。
具体实现可以看看这篇文章,自定义ViewGroup。
获取 View 的宽高
measure 过程完成后,理论上可以通过 getMeasureWidth/Height 方法获取 View 的测量宽高,但是极端情况下,系统可能需要多次 measure 才能确定最终测量宽高,这种情况下获取数值可能出错。
应该在 onLayout 方法中去获取 VIew 的测量宽高或者最终宽高。
由于 View 的 measure 过程和 Activity 的生命周期并不是同步执行,无法保证在Activity的 onCreate、onStart、onResume 时某个View就已经测量完毕。想要在 Activity 启动的时候就获取一个View的宽高的方法有以下 4 种:
- Activity / View #
onWindowFocusChanged
:这个方法的含义是:View 已经初始化完毕了,宽高已经准备好了,需要注意:它会被调用多次,当 Activity 的窗口得到焦点和失去焦点均会被调用。 -
view.post(runnable)
:通过 post 将一个 runnable 投递到消息队列的尾部,当 Looper 调用此 runnable 的时候,View 也初始化好了。 - ViewTreeObserver : 使用 ViewTreeObserver 的众多回调可以完成这个功能,比如 OnGlobalLayoutListener 这个接口,当 View 树的状态发送改变或 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法会被回调,这是获取 View 宽高的好时机。需要注意的是,伴随着 View 树状态的改变, onGlobalLayout 会被回调多次。
-
view.measure(int widthMeasureSpec,int heightMeasureSpec)
:手动对view进行measure。需要根据View的layoutParams分情况处理。
为 match_parent 时,无法 measure 出具体的宽高,因为不知道父容器的剩余空间,无法测量出 View 的大小。
为具体的数值时( dp/px):
//假设宽高为 100
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
为 wrap_content 时:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
// View的尺寸使用30位二进制表示,最大值30个1,在AT_MOST模式下,我们用View理论上能支持的最大
//值去构造MeasureSpec是合理的
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
layout
layout 过程比较简单,layout 的方法大致流程是:setFrame 方法设定 4 个顶点位置(初始化 mLeft、mRight、mTop、mBottom 4 个值),然后调用 onLayout 方法(View 中为空实现、ViewGroup 为抽象方法),基本实现思路是:遍历所有子 View,获取其测量宽高,和根据需求计算其中两个顶点,根据这 4 个值对子 View 使用 layout 方法。
getWidth/Height 与getMeasureWidth/Height 方法的区别
public final int getHeight() {
return mBottom - mTop;
}
public final int getWidth() {
return mRight - mLeft;
}
在View 的默认实现中,VIew 的测量宽高等于最终宽高,但是测量宽高的形成时间先于最终宽高。在日常开发中,一般来说两者相等。但是存在特殊情况导致两者不等。如在 layout 方法中操作 setFrame 方法参数。另外一些情况下,View 需要多次 measure 才能确定自己的测量宽高,这会导致在前几次测量之后得出的测量宽高与最终宽高不等。
draw
比较简单大致流程如下:
- 绘制背景 background.draw(canvas)
- 绘制自己(onDraw)
- 绘制 children (dispatchDraw)
- 绘制装饰 (onDrawScrollBars)
需要注意两个方法 :
dispatchDraw
方法会遍历调用所有子元素的 draw 方法。
setWillNotDraw
操作一个标记位,在 View 默认为 false,ViewGroup 中默认为 true。作用是View本身不需要绘制任何内容时(绘制自己,即 onDraw 方法没干啥事),系统会进行相应优化。所以,当一个 ViewGroup 需要通过 onDraw 来绘制内容时(重写了 onDraw 方法时),需要显示的关闭该标记位。
自定义View
自定义 View 的分类与须知
自定义 VIew 的 4 类:
- 继承View 重写 onDraw 方法:主要用于实现不规则效果,往往需要静态或动态绘制一些不规则图形。需要自己实现支持 wrap_content 与 padding。
- 继承 ViewGroup 派生特殊的 Layout:用于实现自定义布局,实现比较复杂,需要处理自身和子元素的 measure、layout 两个过程。
- 继承特定 View:较常见做法,一般用于扩展已有 View 的功能。不需要自己实现支持 wrap_content 与 padding。
- 继承特定 ViewGroup:较常见做法,用于实现 多种 View 组合的效果。不需自己处理自身和子元素的 measure、layout 。
自定义 View 须知:
- 直接继承 View 或者 ViewGroup 时要自己处理支持 wrap_content 效果。
- 直接继承 View 的控件,需要在 draw 方法中处理 padding。直接继承 ViewGroup 的控件需要在 onMeasure 和 onLayout 中考虑 padding、子元素 margin 的影响。
- 尽量不要在 View 中使用 Handler,因为 View 本身提供 post 系列方法。
- View 中如果有线程或者动画需要停止,考虑在 onDetachedFromWindow 中处理。该方法当包含此 View 的 Activity 退出或者 View 被 remove 时会被调用。
- View 带有嵌套滑动情形时,需要处理好滑动冲突。
自定义 View 示例
直接继承 View 的示例,主要 3 个点:
- 支持 padding:onDraw 里面获取 padding,并进行处理。
- 支持 wrap_content:onMeasure 里面判断 AT_MOST 模式,使用需要的大小
- 支持自定义属性:写一个自定义属性集合的 xml 文件,在构造方法中加载、解析即可。
以下是实现上述 3 点,作用为画一个圆形的自定义 View 代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--自定义属性结合名-->
<declare-styleable name="CircleView">
<!--属性id 和值的格式 还可以是 string/integer/boolean 等-->
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
public class CircleView extends View {
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
//使用自定义属性时 应该为调用另一个构造方法
this(context, attrs, 0);
}
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//加载自定义属性集合 CircleView
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//解析 CircleView 属性集合 中 id 为 circleView_circle_color 的属性
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
a.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
//支持padding
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int pL = getPaddingLeft();
int pR = getPaddingRight();
int pT = getPaddingTop();
int pB = getPaddingBottom();
int w = getWidth() - pL - pR;
int h = getHeight() - pT - pB;
int radius = Math.min(w, h) / 2;
//把圆心定在左和上方除去padding后的位置加上设定宽高的一半的位置
canvas.drawCircle(w / 2 + pL, h / 2 + pT, radius, mPaint);
}
//支持 wrap_content 即当宽高设为 wrap_content 时默认为 200 pt
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (wSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, hSpecSize);
} else if (hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(wSpecSize, 200);
}
}
}
直接继承 ViewGroup 示例:
这种方法实现比较复杂,以下是一个带有水平滑动效果的类似 LinearLayout(水平方向上)的自定义View,基本思路是 onMeasure 里,当判断模式为 wrap_content 时,获取子 View 的宽高,进行相应处理,onLayout 中,使用一个变量保存宽度累加值,实现水平排列。另外此例实现了内容滑动效果,且解决了滑动冲突。
public class HorizontalScrollViewEx extends ViewGroup {
private int mChildrenSize; //子View数量
private int mChildWidth; //子 View 宽度 本例默认子View大小是一样的
private int mChildIndex; //处于屏幕可见最左边的子 View 序号
private int mLastX;
private int mLastY;
private int mLastXInterceptTouchEvent;
private int mLastYInterceptTouchEvent;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalScrollViewEx(Context context) {
super(context);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//实例化 Scroller 和 VelocityTracker(速度追踪)
private void init() {
if (mScroller == null) {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
}
//包含此 View 的 Activity 退出或者 View 被 remove 时会被调用 回收资源
@Override protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
@Override public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
if (!mScroller.isFinished()) {
//滑动动画效果未完成时 再次传来事件序列 则中止滑动动画 且拦截事件
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXInterceptTouchEvent;
int deltaY = y - mLastYInterceptTouchEvent;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
//如果 Y 轴滑动距离大于 X 轴,不拦截事件
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastX = x;
mLastY = y;
mLastXInterceptTouchEvent = x;
mLastYInterceptTouchEvent = y;
return intercepted;
}
//自身的滑动事件
@Override public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y = mLastY;
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
//获取当前View边缘与View内容左边缘的距离
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
//mChildIndex代表在屏幕最左边的子View的序号 0开始
if (Math.abs(xVelocity) > 50) {
//如果是快速滑动 判断滑动方向 使最左边一个子View的序号加减1
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
//当前View边缘与View内容左边缘的距离加上半个子 View 的宽度 除去子View宽,使滑动超过半个子View宽情况下
// 滑动到下个子View 否则只滑动整数个子View宽度
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
//使mChildIndex不会超过实际子View数,以及防止负数出现
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
//在smoothScrollBy调用前 实际状态是滑动距离是 正scrollX 所以减去
// 使得手指离开后一直处于滑动整数个子View的状态
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
//缓慢滑动 需要 computeScroll 配合
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
@Override public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
//遍历子View 进行measure
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
//若无子元素,设置宽高为0
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
//若宽高都使用了 wrap content
final View childView = getChildAt(0);
//使用第一个子View的宽乘以数量 其他需求可以考虑改写 measureChildren 等方法
measuredWidth = childView.getMeasuredWidth() * childCount;
//使用第一个子View的高
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
//若高度使用了 wrap content
final View childView = getChildAt(0);
//使用第一个子View的高度
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpecSize, measuredHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
//若宽度使用了 wrap content
final View childView = getChildAt(0);
//使用第一个子View的宽乘以数量 其他需求可以考虑改写 measureChildren 等方法
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, heightSpecSize);
}
}
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
//累加所有子View的宽度
childLeft += childWidth;
}
}
}
}
以上代码存在两个不规范之处,一是,没有子元素时不应该直接设宽高为 0。应该根据 LayoutParams 进行处理。二是,在measure 和 layout 中都没有根据自身的 padding 和子 View 的 margin 进行处理。