Android View绘制流程
Android View绘制流程
如上图,Activity的window组成,Activity内部有个Window成员,它的实例为PhoneWindow,PhoneWindow有个内部类是DecorView,这个DecorView就是存放布局文件的,里面有TitleActionBar和我们setContentView传入进去的layout布局文件
Window类时一个抽象类,提供绘制窗口的API
PhoneWindow是继承Window的一个具体的类,该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View
DecorView继承FrameLayout,里面id=content的就是我们传入的布局视图
依据面向对象从抽象到具体我们可以类比上面关系就像如下:
Window是一块电子屏,PhoneWindow是一块手机电子屏,DecorView就是电子屏要显示的内容,Activity就是手机电子屏安装位置
View 绘制中主要流程分为measure,layout, draw 三个阶段。
measure :根据父 view 传递的 MeasureSpec 进行计算大小。
layout :根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。
draw :把 View 对象绘制到屏幕上。
一、Measure
1、MeasureSpec
MeasureSpec 封装了从父级传递到子级的布局要求。每个 MeasureSpec 代表对宽度或高度的要求。MeasureSpec 由大小和模式组成。
MeasureSpec 一个32位二进制的整数型,前面2位代表的是mode,后面30位代表的是size。mode 主要分为3类,分别是
EXACTLY:父容器已经测量出子View的大小。对应是 View 的LayoutParams的match_parent 或者精确数值。
AT_MOST:父容器已经限制子view的大小,View 最终大小不可超过这个值。对应是 View 的LayoutParams的wrap_content
UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。(这种不怎么常用)
View 的 MeasureSpec 并不是父 View 独自决定,它是根据父 view 的MeasureSpec加上子 View 的自己的 LayoutParams,通过相应的规则转化。
2、流程
在measure 方法,核心就是调用onMeasure( ) 进行View的测量。在onMeasure( )里面,获取到最小建议值,如果父类传递过来的模式是MeasureSpec.UNSPECIFIED,也就是父View大小未定的情况下,使用最小建议值,如果是AT_MOST或者EXACTLY模式,则设置父类传递过来的大小。
然后调用setMeasuredDimension 方法进行存储大小。
二、Layout
确定 View 在父 View 的位置进行排版布局
三、draw
draw 过程中一共分成7步,其中两步我们直接直接跳过不分析了。
第一步:drawBackground(canvas): 作用就是绘制 View 的背景。
第三步:onDraw(canvas) :绘制 View 的内容。View 的内容是根据自己需求自己绘制的,所以方法是一个空方法,View的继承类自己复写实现绘制内容。
第三步:dispatchDraw(canvas):遍历子View进行绘制内容。在 View 里面是一个空实现,ViewGroup 里面才会有实现。在自定义 ViewGroup 一般不用复写这个方法,因为它在里面的实现帮我们实现了子 View 的绘制过程,基本满足需求。
第四步:onDrawForeground(canvas):对前景色跟滚动条进行绘制。
第五步:drawDefaultFocusHighlight(canvas):绘制默认焦点高亮
四、自定义视图类
1、步骤
1、自定义View的属性
2、在View的构造方法中获得我们自定义的属性
3、重写onMeasure
4、重写onDraw
2、定义自定义属性
自定义View的属性,首先在res/values/ 下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。
如需在自定义视图中启用此行为,您必须:
- 在
<declare-styleable>
资源元素中定义视图的自定义属性 - 在 XML 布局中指定属性值
- 在运行时检索属性值
- 将检索到的属性值应用到视图
<resources>
<declare-styleable name="CustomView">
<attr name="titleText" format="string"/>
<attr name="titleTextColor" format="color"/>
<attr name="titleTextSize" format="dimension"/>
</declare-styleable>
</resources>
定义了自定义属性后,便可像内置属性一样在布局 XML 文件中使用它们。唯一的区别是自定义属性属于另一个命名空间。它们不属于 http://schemas.android.com/apk/res/android
命名空间,而是属于 http://schemas.android.com/apk/res/[your package name]
。
当然在我的Android Studio中我改用xmlns:custom="http://schemas.android.com/apk/res-auto"
,因为当前工程是做为lib使用,那么你如上所写 ,会出现找不到自定义属性的错误 。这时候你就可以写成xmlns:custom="http://schemas.android.com/apk/res-auto"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.uidesign.CustomView
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_centerInParent="true"
custom:titleText="liming"
custom:titleTextColor="#ff0000"
custom:titleTextSize="40sp"/>
</RelativeLayout>
3、应用自定义属性
通过 XML 布局创建视图时,XML 标记中的所有属性都会从资源包读取,并作为 AttributeSet
传递到视图的构造函数中。虽然可以直接从 AttributeSet
读取值,但这样做有一些弊端:
- 不解析属性值中的资源引用
- 不应用样式
请改为将 AttributeSet
传递给 obtainStyledAttributes()
。此方法会传回一个 TypedArray
数组,其中包含已解除引用并设置了样式的值。
Android 资源编译器做了大量工作,以便您更轻松地调用 obtainStyledAttributes()
。对于 res 目录中的各个 <declare-styleable>
资源,生成的 R.java 定义一个由属性 ID 组成的数组,同时定义一组常量,用于定义该数组中各属性的索引。您可以使用预定义的常量从 TypedArray
读取属性。
public class CustomView extends View {
private String mTitleText;
private int mTitleTextColor;
private int mTitleTextSize;
private Rect rect;
private Paint paint;
public CustomView(Context context, AttributeSet attrs) {
super(context,attrs);
//为自定义View添加事件
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mTitleText=randomText();
postInvalidate();
}
});
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.CustomView,
0, 0);
try {
mTitleText = a.getString(R.styleable.CustomView_titleText);
mTitleTextColor=a.getColor(R.styleable.CustomView_titleTextColor,Color.BLACK);
mTitleTextSize=a.getDimensionPixelSize(R.styleable.CustomView_titleTextSize,(int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
} finally {
a.recycle();
}
}
//重写onMeasure
@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);
int width;
int height ;
if (widthMode == MeasureSpec.EXACTLY)
{
width = widthSize;
} else
{
paint.setTextSize(mTitleTextSize);
paint.getTextBounds(mTitleText, 0, mTitleText.length(), rect);
float textWidth = rect.width();
int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
width = desired;
}
if (heightMode == MeasureSpec.EXACTLY)
{
height = heightSize;
} else
{
paint.setTextSize(mTitleTextSize);
paint.getTextBounds(mTitleText, 0, mTitleText.length(), rect);
float textHeight = rect.height();
int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
height = desired;
}
setMeasuredDimension(width, height);
}
//重写onDraw
@Override
protected void onDraw(Canvas canvas) {
paint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);
paint.setColor(mTitleTextColor);
canvas.drawText(mTitleText, getWidth() / 2 - rect.width() / 2, getHeight() / 2 + rect.height() / 2, paint);
}
}
TypedArray
对象是共享资源,必须在使用后回收。
由于系统帮我们测量的高度和宽度都是MATCH_PARNET,当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。
所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法。重写之前先了解MeasureSpec的specMode,一共三种类型:
EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用