Android开发艺术探索读书笔记系列
ViewRoot、DecorView和MeasureSpec
ViewRoot、DecorView
ViewRoot是连接WindowManager和DecorView的纽带,View的measure、layout和draw三大流程都是通过ViewRoot来完成的。
View的绘制流程是从ViewRoot的performTraversals方法开始的:
MeasureSpec
MeasureSpec代表一个32位int值,高两位代表SpecMode,低30位代表SpecSize,SpecMode代表测量模式,SpecSize指在某种测量模式下的规格大小!
SpecMode有三类:
- UNSPECIFIED 父容器不对view有任何限制,要多大给多大
- EXACTLY 对应match_parent和具体数值
- AT_MOST 对应wrap_content
MeasureSpec的创建规则:
View的工作流程
View的工作流程主要指measure、layout和draw,其中measure确定View的测量宽高,layout确定View的最终宽高和四个定点的位置,draw将View绘制到屏幕上!
measure流程
1、view的messure过程
measure方法是final类型,不能被子类重写,自定义view的时候只能重写measure里面的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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;
}
UNSPECIFIED模式主要用于系统内部,我们一般不需要,所以对我们来说getDefaultSize返回的大小就是measureSpec的specSize(View测量后的大小),一般这个测量大小和最终大小相等,但是最终大小是在layout阶段确定的!View的宽高取决于sepcSize,直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时自身大小,否则布局中使用了wrap_content就相当于使用了match_parent!解决方案如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(myWidhh,myHeight);//指定的默认宽高
}else if (widthMode == MeasureSpec.AT_MOST){
setMeasuredDimension(myWidth, heightSize);
}else if (heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize, myHeight);
}
}
查看TextView、ImageView等直接继承View的组件都针对wrap_content做了特殊处理如TextView
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
// Check against our minimum width
width = Math.max(width, getSuggestedMinimumWidth());
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
...
2、ViewGroup的measure过程
ViewGroup是抽象类,没有重写View的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];
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);
}
可以看到ViewGroup没有定义其测量的具体过程,因为它是一个抽象类,其测量过程的onMeasure需要各个子类去具体实现,如LinearLayout、RelativeLayout等,去读一下他们的具体实现就会知道,大体上依然是去判断其mode,来决定其宽高,或是具体数值、或是计算子类的宽高来确定等。
View的measure过程是三大流程中最复杂的一个,通过getMeasureWidh/Height方法可以获取View的测量宽高,由于某些情况下,系统可能多次measure才能确定宽高,所以在onLayout里面获取view的宽高是一个比较好的习惯!
由于测量宽高和生命周期不是同步的,在Activity获取View的宽高最好使用下面的方法:
- onWindowFocusChanged 这个方法是用来判断窗口的焦点变化的,Activity得到失去焦点,该方法都会被调用!该方法调用时View已经初始化完毕,可以获取宽高:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus){
int widthSize = textView.getMeasuredWidth();
int heightSize = textView.getMeasuredHeight();
}
}
- view.post(runnable) 通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用的时候,View已经初始化好了:
@Override
protected void onStart() {
super.onStart();
new TextView(this).post(new Runnable() {
@Override
public void run() {
int widthSize = textView.getMeasuredWidth();
int heightSize = textView.getMeasuredHeight();
}
});
}
- ViewTreeObserver 使用ViewTreeObserver的众多回调也可以,如当View树的状态发生改变或则View树内部的View的可见性发生改变时,会调用OnGlobalLayoutListener的onGlobalLayout方法,在这里也可以!
- view.measure(int widthMeasureSpec, int heightMeasureSpec)
layout过程
和measure过程相比,layout过程就比较简单了,作用是用来确定子元素的位置!当ViewGroup的位置被确定后,它会在onLayout中会遍历所有子元素并调用其layout方法,layout又会调用onLayout!layout确定View本身的位置,onLayout会确定子元素的位置!
下面解释一下view的测量宽高和最终宽高的区别,这个问题可以具体为:View的getMeasuredWidth和getWidth的区别,在View的默认实现中两者是相等的,但是测量宽高和最终宽高的形成时机不同,如果在layout中对最终宽高赋值作出修改,如:
public void layout(int l, int t, int r, int b){
super.layout(l, t, r+100, b+100);
}
这时测量宽高和最终宽高就不一样了!
draw过程
draw过程比较简单,它的作用是将View绘制到屏幕上!其绘制过程 如下:
- 绘制背景background.draw(canvas)
- 绘制自己(onDraw)
- 绘制children(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
这点从其源码中可以很清晰的看到:
@CallSuper
public void draw(Canvas canvas) {
...
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 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)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
//step 4, draw the children
dispatchDraw(canvas);
...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
}
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法!
自定义View
自定义View的分类
1、继承View重写onDraw方法
这种方法主要用于实现不规则或特殊的视觉效果,采用这种方式需要自己支持wrap_content,并且需要处理padding!
2、继承ViewGroup派生的Layout
这种方法主要用于自定义布局,一般需要处理ViewGroup及其子View的测量、布局这两个过程!
3、继承特定的View
这种方法一般是扩展某种已有的View的功能!
4、继承特定的ViewGroup,如LinearLayout
区别于方式2直接继承ViewGroup,这种方法不需要重写测量和布局的过程!
自定义View注意事项
- 让View支持wrap_content
直接继承View或ViewGroup,需要在onMeasure中对wrap_content做特殊处理! - 如果有必要,让View支持padding
- 尽量不要在View中使用Handler
View本身有post系列方法,可以替代Handler的作用,除非你明确要使用Handler来发送消息! - View中如果有线程或者动画需要及时停止
如果有线程或动画需要停止时,可以在onDetachedFromWindow方法处理,该方法会在包含该View的Activity退出或者当前View被remove时被调用!另外当View变得不可见时我们也需要停止线程和动画,否则有可能造成内存泄露! - View带有滑动嵌套情形时,需要处理好滑动冲突
示例
1、直接继承View, 无自定义属性
public class MyCustomCircle extends View {
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public MyCustomCircle(Context context) {
super(context);
init();
}
public MyCustomCircle(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public MyCustomCircle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//下面是处理wrap_content的清空 否则直接继承View其作用就相当于match_parent
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200,200);//指定的默认宽高
}else if (widthMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, heightSize);
}else if (heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize, 200);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//处理padding 如果不处理 padding将无效
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radio = Math.min(width,height)/2;
canvas.drawCircle(width/2 + paddingLeft, height/2 + paddingTop, radio, mPaint);
}
}
然后直接在布局文件中使用即可
<com.example.scy.myapplication.custom.MyCustomCircle
android:id="@+id/myCricle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:background="#671165"
android:padding="10dp" />
2、下面是一个带有自定义属性的示例
自定义属性大致分三个步骤
第一, 在res/values/下面创建attrs.xml文件, 在里面声明自定义属性集合"MyCustomCircle", 内容如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyCustomCircle">
<attr name="mColor" format="color"/>
<attr name="mText" format="string"/>
<attr name="mTextSize" format="dimension"/>
</declare-styleable>
</resources>
自定义属性声明非常简单,一个name是属性名,一个format是格式,常用的格式如下:
1) string:字符串类型;
2) integer:整数类型;
3) float:浮点型;
4) dimension:尺寸,后面必须跟dp、dip、px、sp等单位;
5) Boolean:布尔值;
6) reference:引用类型,传入的是某一资源的ID,必须以“@”符号开头;
7) color:颜色,必须是“#”符号开头;
8) fraction:百分比,必须是“%”符号结尾;
9) enum:枚举类型
第二,在View的构造方法中解析自定义属性:
public MyCustomCircle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//加载自定义属性集合
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyCustomCircle);
//有的在这里使用for循环加条件遍历也可以,但是那样的话,通过下面的方式设置的默认值就无效了
mColor = typedArray.getColor(R.styleable.MyCustomCircle_mColor, Color.RED);//默认红色
mTextSize = typedArray.getDimensionPixelSize(R.styleable.MyCustomCircle_mTextSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));//默认14sp
mText = typedArray.getString(R.styleable.MyCustomCircle_mText);
typedArray.recycle();
}
第三, 布局文件中引用
<com.example.scy.myapplication.custom.MyCustomCircle
android:id="@+id/myCricle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:background="#671199"
app:mText="自定义"
app:mTextSize="16sp"
android:padding="10dp" />
下面是MyCustomCircle的完整代码:
public class MyCustomCircle extends View {
private int mColor;
private int mTextSize;
private String mText;
/**
* 绘制时控制文本绘制的范围
*/
private Rect mBound;
private Paint mPaint;
public MyCustomCircle(Context context) {
this(context, null);
}
public MyCustomCircle(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyCustomCircle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyCustomCircle);
mColor = typedArray.getColor(R.styleable.MyCustomCircle_mColor, Color.RED);
mTextSize = typedArray.getDimensionPixelSize(R.styleable.MyCustomCircle_mTextSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));
mText = typedArray.getString(R.styleable.MyCustomCircle_mText);
typedArray.recycle();
mPaint = new Paint();
mPaint.setColor(mColor);
mPaint.setTextSize(mTextSize);
//下面两句 是获取text的绘制范围
mBound = new Rect();
mPaint.getTextBounds(mText, 0, mText.length(), mBound);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//下面是处理wrap_content的清空 否则直接继承View其作用就相当于match_parent
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);//指定的默认宽高
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, 200);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//处理padding 如果不处理 padding将无效
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
//width / 2 + paddingLeft 这句和前面画圆一样 找中心并考虑内边距
//... -mBound.width() / 2 没有这句 文字的起点就是上面说的中心点 而我们想要的是上面的中心点也是文字的中心点
canvas.drawText(mText, width / 2 + paddingLeft - mBound.width() / 2, height / 2 + paddingTop + mBound.height() / 2, mPaint);
}
3、继承ViewGroup派生特殊的Layout
public class HorizontalLayout extends ViewGroup {
public HorizontalLayout(Context context) {
this(context, null);
}
public HorizontalLayout(Context context, AttributeSet attrs) {
this(context,attrs, 0);
}
public HorizontalLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//将所有子View进行测量
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childCount = getChildCount();
if (childCount == 0){
setMeasuredDimension(0,0);
}else {
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
int width = getTotleWidth(childCount);
int height = getMaxChildHeight(childCount);
setMeasuredDimension(width, height);
}else if (widthMode == MeasureSpec.AT_MOST){
setMeasuredDimension(getTotleWidth(childCount), heightSize);
}else if (heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize, getMaxChildHeight(childCount));
}
}
}
/**
* 将所有子View的宽度相加 事实上这里还应该考虑 子view的外边距
* @param childCount
* @return
*/
private int getTotleWidth(int childCount) {
int width = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
width += child.getMeasuredWidth();
}
return width;
}
/**
* 以最大的子View的高度作为 父View的高度
* @param childCount
* @return
*/
private int getMaxChildHeight(int childCount) {
int maxHeight = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getMeasuredHeight() > maxHeight){
maxHeight = child.getMeasuredHeight();
}
}
return maxHeight;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int mWidth = l;//记录宽度
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE){
int height = child.getMeasuredHeight();
int width = child.getMeasuredWidth();
child.layout(mWidth, 0, mWidth + width, 0 + height);
mWidth += width;
}
}
}
}
上述代码有两点不太规范的地方,第一是没有子元素时,不应该直接将自己的宽高都设置为0,而应该根据LayoutParams中的宽高来处理,第二测量宽高时应该考虑子View的margin和自己的padding!