前言
上篇文章复习总结了Android中常见的布局和布局参数,这篇文章就来复习总结下自定义View(当然只是简单的)。那么什么时候需要使用自定义View? 当现有的组件无法满足我们的需要的我们就可能得使用自定义View。
一、View的工作流程
view的工作流程指的是View的三大方法measure、layout、draw。其中measure用来测量View的宽和高,layout用来决定View的位置,draw用于绘制View,下面先从入口开始说起
入口
既然View显示在Activity内,那么先从Activity启动说起,这里省略前面的相关步骤直接从handleLaunchActivity开始
// ActivityThread
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
Activity a = performLaunchActivity(r, customIntent);
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
}
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
Context appContext = createBaseContextForActivity(r, activity);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window);
mInstrumentation.callActivityOnCreate(activity, r.state);
}
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
r = performResumeActivity(token, clearHide, reason);
// wm为WindowManagerImpl实例,decor为DecorView实例
wm.addView(decor, l);
}
// WindowManagerImpl
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
// mGlobal为WindowMangerGlobal实例
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
// WindowMangerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
}
// ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
requestLayout();
// 设置Activity的decorView的parent为ViewRootImpl实例
view.assignParent(this);
}
public void requestLayout() {
scheduleTraversals();
}
void scheduleTraversals() {
// 在屏幕刷新信号到来以后会调用mTraversalRunnable.run()
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
void doTraversal() {
// 该方法内部真正进行View的三大流程
performTraversals();
}
private void performTraversals() {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, mWidth, mHeight);
performDraw();
}
知道了入口了以后,我们首先来看看View的measure过程吧
Measure
View的测量从DecorView开始,一层层的进行递归直到调用了所有View的onMeasure方法,继续从performTraversals开始
private void performTraversals() {
// 对于DecorView来说其onMeasure的两个参数由窗口大小和WindowManger.LayoutParams决定
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
performMeasure需要两个参数,都是通过getRootMeasureSpec获取的
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
根据窗口的大小和布局参数决定,继续看看performMeasure
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
// 这里的mView就是DecorView
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
里面调用了onMeasure, 对于View只需要测量自身即可,但是对于ViewGroup需要测量所有的子View,首先看看View的onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 调用了该方法以后该View的大小就被测量完了
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
public static int getDefaultSize(int size, int measureSpec) {
int result = 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;
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
由此可见以下两点
setMeasureDimension就是用来设置mMeasuredWidth和mMeasuredHeight的,默认View的onMeasure实现测量模式为MeasureSpec.AT_MOST与MeasureSpec.EXACTLY时取的大小是一样的,也就是说在布局文件中设置为wrap_content与match_parent效果是一样的- 当测量模式为
MeasureSpec.UNSPECIFIED时View没有设置背景就返回自动最小宽/高,不然返回背景的最小宽/高和自身最小宽/高直接的最大值
下面再看看ViewGroup由于其需要测量所有子View,并根据自己的规则决定最后需要多少尺寸,而且每个ViewGroup的规则都不尽相同因此ViewGroup并没有重写onMeasure,但是定义了一个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];
// 如果View的Visibility不是Gone就measureChild
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
// 根据父View的measureSpec和子View的LayoutParams,以及对应方向的padding来决定子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
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);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
我们通过一张表格来归纳下getChildMeasureSpec的结果
| 父View的measureSpec | 子View的LayoutParams | 结果 |
|---|---|---|
| 测量模式:MeasureSpec.EXACTLY 尺寸:A | 固定值B | 测量模式:MeasureSpec.EXACTLY 尺寸:固定值B |
| 测量模式:MeasureSpec.EXACTLY 尺寸:A | MATCH_PARENT | 测量模式:MeasureSpec.EXACTLY 尺寸:A-padding |
| 测量模式:MeasureSpec.EXACTLY 尺寸:A | WRAP_CONTENT | 测量模式:MeasureSpec.AT_MOST 尺寸:A-padding |
| 测量模式:MeasureSpec.AT_MOST 尺寸:A | 固定值B | 测量模式:MeasureSpec.EXACTLY 尺寸:固定值B |
| 测量模式:MeasureSpec.AT_MOST 尺寸:A | MATCH_PARENT | 测量模式:MeasureSpec.AT_MOST 尺寸:A-padding |
| 测量模式:MeasureSpec.AT_MOST 尺寸:A | WRAP_CONTENT | 测量模式:MeasureSpec.AT_MOST 尺寸:A-padding |
| 测量模式:MeasureSpec.UNSPECIFIED 尺寸:A | 固定值B | 测量模式:MeasureSpec.EXACTLY 尺寸:固定值B |
| 测量模式:MeasureSpec.UNSPECIFIED 尺寸:A | MATCH_PARENT | 测量模式:MeasureSpec.UNSPECIFIED 尺寸:A-padding |
| 测量模式:MeasureSpec.UNSPECIFIED 尺寸:A | WRAP_CONTENT | 测量模式:MeasureSpec.UNSPECIFIED 尺寸:A-padding |
Layout
Layout方法的作用是为了确定元素的位置,接着看看performLayout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
// host就是decorView
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
该方法的作用就是拿到decorView测量完的长/宽然后给出DecorView在屏幕中的位置要求其为它的子View进行定位
public void layout(int l, int t, int r, int b) {
onLayout(changed, l, t, r, b);
}
而onLayout方法在View里面是一个空实现,因为每个ViewGroup都有其自己的布局方式
Draw
Draw方法的作用是用来绘制UI,接着看看performDraw
private void performDraw() {
draw(fullRedrawNeeded);
}
private void draw(boolean fullRedrawNeeded) {
drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
mView.draw(canvas);
}
这里又调用了View的draw方法
public void draw(Canvas canvas) {
/*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
drawBackground(canvas);
onDraw(canvas);
// 去调用子View的draw方法
dispatchDraw(canvas);
onDrawForeground(canvas);
}
这里主要就是绘制背景、调用onDraw、调用子View的draw、绘制前景色
二、自定义View基本流程
最基本的自定义View需要进行以下两个步骤
1. 继承
首先,自定义View的时候我们一般会选择继承自现有的View的子类或者直接继承View,在继承的时候得注意一定要有两个参数(Context、AttributeSet)的构造方法除非这个View不在xml里面使用,因为当LayoutInflate在解析xml的时候会通过反射调用两个参数的构造器来创建View,如果找不到该构造器将导致程序crash
2. 自定义属性
我们可以在values目录下面新建一个declare-styleable来定义属性,然后在布局文件中使用注意需要引用以下命名空间,然后在自定义View的构造器中通过obtainStyledAttributes获取属性值
xmlns:app="http://schemas.android.com/apk/res-auto"
3. 重写
其次,我们需要重写几个方法,一般我们自定义View时需要重写onMeasure()、onDraw(),自定义ViewGroup则是需要重写onMeasure、onLayout,三个方法的作用如下所示
- onMeasure 用来测量View的宽和高
- onLayout 用来确定View的位置
- onDraw 用来绘制View
三、实例
详见以前写的一个自定义ViewPager和TabLayout