Android View绘制流程

Android View绘制流程

img

如上图,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步,其中两步我们直接直接跳过不分析了。

image-20211116131514724

第一步: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:表示子布局想要多大就多大,很少使用

posted @ 2021-11-16 13:39  RichardLM  阅读(1131)  评论(0编辑  收藏  举报