第四章 view的工作原理
- 目标:
- 掌握view的底层工作原理,如measure,layout,draw。
- 通过继承view, viewGroup或现有的系统控件来实现自定义view。
- 再次需要注意的是viewGroup继承自view, 所以view中的方法viewGroup中都有.
ViewRoot与DecorView
- DecorView是根view。在ActivityTread中, 当Activity被创建后,会将DecorView添加到window中,
View decor = r.window.getDecorView();
wm.addView(decor, l);
- 同时创建
ViewRootImpl
对象(是ViewRoot的实现)并与DecorView
建立联系。 ViewRootImpl对象在WindowManagerGlobal
中被创建.
root = new ViewRootImpl(view.getContext(), display);//这里的view就是decorview
root.setView(view, wparams, panelParentView);
- View的三大流程均是通过ViewRootImpl来完成。
- performTraversals的工作流程图
MeasureSpec
- 是一个32位的
int
值,高2位代表SpecMode
, 低30位代表SpecSize
.- 是一种类似于
测量规格
的东西, 对view进行测量时需要用到.- 子view的测量需要父view的
MeasureSpec
(或窗口大小)和自身的LayoutParams
来共同确定.SpecMode
有三种类型UNSPECIFIED
,EXACTLY
,AT_MOST
view的工作过程
从DecorView开始分析
- DecorView是窗口大小和自身LayoutParams两者共同确定自己的MeasureSpec. 此处是通过getRootMeasureSpec来获取返回值.
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
- 调用performMeasure方法执行measure函数.
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);//在view中measure方法为final,不可更改
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
measure
方法中会调用onMeasure
方法- 所以DecorView(继承于FrameLayout)中重写了
onMeasure
. 同时FrameLayout(继承于viewGroup, viewGroup继承于view)自己也重写了onMeasure
方法. 之后DecorView将处理结果(自身宽高的精确值)通过super.onMeasure
传递到FrameLayout中处理.- FrameLayout中的
onMeasure
方法会对子view进行遍历同时调用measureChildWithMargins
. measureChildWithMargins又会调用view.measure
, measure又会调用onMeasure. 直到最后只剩下view(不是viewGroup)时, 在onMeasure中调用setMeasuredDimension
设置单独view的宽高.- 当每个字view宽高都计算完时, 加上子view各自的margin, 父view的padding后, viewGroup就能得出自身的大小.
- 在第5条中的measureChildWithMargins, 调用了getChildMeasureSpec方法. 这个方法是普通view利用父MeasureSpec和自身lp来创建自己MeasureSpec的规则.
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);//父容器对padding做了处理
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
- 以上总结的表格
- 根据规则可知, 当自定义view的属性为wrap时, SpecSize都是parentSize(即父view的剩余空间大小).所以此时在自定义view中, 要重写onMeasure方法, 在SpecMode模式为AT_MOST时设定一个默认的内部宽高.
view工作过程的总结
- ViewRootImpl中调用
performTraversals -> performMeasure
performMeasure -> view.measure -> onMeasure
(decorView重写了view的onMeasure)-> super.OnMeasure
(FrameLayout中). 因为decorView继承自FrameLayout.- 到了FrameLayout后, 遍历子view并调用measureChildWithMargins,
measureChildWithMargins -> view.measure
.- decorView的工作结束(其MeasureSpec是由窗口和自身lp决定), 此时
view.measure -> onMeasure
- 系统对后面的处理是分为单独的view还是viewGroup来考虑
- 如果是
view
,onMeasure -> setMeasuredDimension
从而确定好这个view的宽高.- 如果是
viewGroup
, 它没有提供一致的onMeasure方法. 因为不同的viewGroup其特性不同导致工作的逻辑不同.所以像在上面提到的FrameLayout或是LinearLayout等继承了viewGroup都是根据自己的特性重写了OnMeasure(可以查看相关的源码得到验证).- 在viewGroup中提供了便捷的方法
measureChildren
(参数是父MeasureSpec)-> measureChild
(参数是父MeasureSpec, 此方法中通过getChildMeasureSpec
得到子MeasureSpec)-> child.measure
(参数是子MeasureSpec), 这样形成循环, 一直到只有单个view的时候. 此时是6 中的情况.- 所以当自定义viewGroup时可以在重写的onMeasure中分析好逻辑, 然后灵活调用measureChildren方法. (在LinearLayout中的
measureChildrenBeforeLayout
方法实际上就是调用了measureChildren
)- 不懂view测量过程看这里
获取view宽高
- 获取view的测量宽高: getMeasuredWidth, getMeasuredHeight
- 获取view的最终宽高: getWidth, getHeight
- 测量宽高和最终宽高基本上相同, 但在极端情况下系统需要多次测量才能确定最终宽高. 所以比较好的做法是在onLayout方法中去获取view的测量宽高或最终宽高
- 如果想要在Activity已启动的时候获取某个view的宽高, 但由于Activity的生命周期与view的measure过程不是同步执行的, 所以不能直接调用1,2 中的方法.
解决方法:
1.在onWindowFocusChanged中使用
2.在view.post(runnable)的run方法中使用
3.手动使用view.measure.(P182)
4.使用ViewTreeObserver
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addGlobalLayoutListener(new OnGlobalLayoutListener(){
public void onGlobalLayout(){
view.getViewTreeObserver().removeGlobalLayoutListener(this);
//这里使用
}
})
layout过程
- performTraversals 中 经过了performMeasure 到了 performLayout
- performLayout -> layout -> setFrame(设置自身位置) -> onLayout(确定所有子view的位置)
- view和viewGroup都没有实现onLayout方法
- 所以自定义的时候需要自己实现onLayout方法
- onLayout -> child.layout
draw过程
- draw 在源码中做了如下几步
- 绘制背景background.draw(canvas)
- 绘制自己(onDraw)
- 绘制children(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
- setWillNotDraw(Boolean )
- 如果一个view不需要绘制任何内容,那么设置这个标志位为true, 系统会进行相应的优化
- 默认情况view没有启用, viewGroup启用了
- 在继承viewGroup的自定义控件需要通过onDraw来绘制内容时, 我们需要显示的关闭
自定义view
- 自定义view
- 继承
view
重写onDraw方法
1.重写onDraw方法,并且需要自己支持wrap_content(设置默认宽高setMeasuredDimension
), padding.- 继承
ViewGroup
派生自己的layout
1.需要合理处理ViewGroup的 测量, 布局过程. 并同时处理子元素的测量和布局过程.- 继承
特定的View
(TextView之类)
1.较为容易实现, 不需要自己支持wrap_content, padding.- 继承
特定的ViewGroup
(LinearLayout之类)
1.比2中的方法方便一些, 一般2中能实现的效果, 4都能实现. 但2更接近底层. 此方法不需要自己处理ViewGroup的测量和布局过程
- 注意事项
- view中如果有线程或动画, 需要及时停止, 否则会造成内存泄漏
- Activity中的
onAttachedToWindow
和onDetachedFromWindow
分别是开启和关闭的好地方.- 处理好滑动嵌套中的滑动冲突问题
- 提供自定义属性
- values目录下创建自定义属性的xml文件, 命名最好以
attrs_
开头
<resource>
<declare-styleable name="xxx"> <--!自定义属性集合名称-->
<attr name="circle_color" format="color"/> <--!格式(format)是相应的类型,可以查看相应的文档-->
</declare-styleable>
</resource>
- 在自定义的view构造方法中(有三种, 是其中attributeSet那种)
TypeArray a = context.obtainStyledAttributes(自定义xml名, R.styleable.xxx)
my = a.getColor(styleable.xxx_circle_color, 默认值)
my.recycle();//实现资源
- 添加schemas声明(
xmlns:app=http://schemas.android.com/apk/res-auto
), app是前缀, 可以修改
- 继承ViewGroup实现自己的Layout
主要看onMeasure
和onLayout
方法, 不规范的地方有
- 没有处理自己的padding和子元素的margin(在
onMeasure
和onLayout
中都要去处理)- 没有子元素时不应该直接把宽高设置为0, 而应该根据LayoutParams中的宽高来做相应的处理
手机上看不全下面代码HorizontalScrollViewEx
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
public class HorizontalScrollViewEx extends ViewGroup {
private static final String TAG = "HorizontalScrollViewEx";
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
// 分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
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 defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
if (mScroller == null) {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
Log.d(TAG, "intercepted=" + intercepted);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = 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: {
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, heightSpaceSize);
}
}
@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());
childLeft += childWidth;
}
}
}
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 onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}