评论

收藏

[Android] Android View绘制流程

移动开发 移动开发 发布于:2021-11-16 20:44 | 阅读数:269 | 评论:0

Android View绘制流程
DSC0000.jpg
如上图,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:表示子布局想要多大就多大,很少使用

关注下面的标签,发现更多相似文章