Android自定义View | 青训营笔记

61 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的的第7天

自定义View

rBsADV2-lQOALmxoAACaadO1-uc637-16610949012032.png

1、自定义属性步骤

1.在attrs.xml里进行声明,其实都行,都是resourse,举例:

    <declare-styleable name="RoundImage_Style">
        <attr name="radius" format="dimension" />
        <attr name="showBorder" format="boolean" />
        <attr name="borderWidth" format="dimension" />
    </declare-styleable>
​

2.使用属性

    <com.sunofbeaches.calendarproviderdemo.RoundImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        sob:borderWidth="5dp"
        sob:radius="4dp"
        sob:showBorder="true" />
​

添加命名空间

    xmlns:sob="https://schemas.android.com/apk/res-auto"

3.获取属性值

public class RoundImageView extends AppCompatImageView {
​
    private static final String TAG = "RoundImageView";
    private float mRadius;
    private float mBorderWidth;
    private boolean mShowBorder;
​
    public RoundImageView(Context context) {
        this(context,null);
    }
​
    public RoundImageView(Context context,AttributeSet attrs) {
        this(context,attrs,0);
    }
​
    public RoundImageView(Context context,AttributeSet attrs,int defStyleAttr) {
        super(context,attrs,defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.RoundImage_Style);
        //一般默认值设置成常量
        mRadius = typedArray.getDimension(R.styleable.RoundImage_Style_radius,0);
        mBorderWidth = typedArray.getDimension(R.styleable.RoundImage_Style_borderWidth,0);
        mShowBorder = typedArray.getBoolean(R.styleable.RoundImage_Style_showBorder,false);
        Log.d(TAG,"mRadius -- > " + mRadius);
        Log.d(TAG,"mBorderWidth -- > " + mBorderWidth);
        Log.d(TAG,"mShowBorder -- > " + mShowBorder);
        typedArray.recycle();
    }
}
​
​

运行结果:

 D/RoundImageView: mRadius -- > 12.0
 D/RoundImageView: mBorderWidth -- > 15.0
 D/RoundImageView: mShowBorder -- > true

2、View绘制的流程框架

image-20220821232003155.png

View的绘制是从上往下一层层迭代下来的。DecorView-->ViewGroup(--- >ViewGroup)-->View ,按照这个流程从上往下,依次measure(测量),layout(布局),draw(绘制)。

3、measure(测量)

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
​

由上述流程来看Measure流程很简单,关键点是在于widthMeasureSpec, heightMeasureSpec这两个参数信息怎么获得? 如果有了widthMeasureSpec, heightMeasureSpec,通过一定的处理(可以重写,自 定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定 View的宽高,完成测量工作。

MeasureSpec的确定

1.什么是MeasureSpec?

MeasureSpec由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。 其中,Mode模式共分为三类:

UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部

EXACTLY: 对应LayoutParams中的match_parent和具体数值这两种模式。检测到 View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值

AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器 的大小。

2.那么MeasureSpec又是如何确定的?

对于DecorView,其确定是通过屏幕的大小,和自身的布局参数LayoutParams。 根据LayoutParams的布局格式(match_parent,wrap_content或 指定大小),将自身大小和屏幕大小相比,设置一个不超过屏幕大小的宽高,以及对应模式。

对于其他View(包括ViewGroup),其确定是通过父布局的MeasureSpec和自身的 布局参数LayoutParams。

3、layout(布 局)

测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上 下左右四个点来确定的。

其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然 后在onLayout()方法中再调用子View的layout()方法,让子View布局。

在Measure 过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。

public void layout(int l, int t, int r, int b) {
      // 当前视图的四个顶点
      int oldL = mLeft;
      int oldT = mTop;
      int oldB = mBottom;
      int oldR = mRight;
      // setFrame() / setOpticalFrame():确定View自身的位置
      // 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回
      boolean changed = isLayoutModeOptical(mParent) ?
      setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
      //如果视图的大小和位置发生变化,会调用onLayout()
      if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PF
      LAG_LAYOUT_REQUIRED) {
      // onLayout():确定该View所有的子View在父容器的位置
      onLayout(changed, l, t, r, b);
      ...
}
​

上面看出通过 setFrame() / setOpticalFrame():确定View自身的位置,通过 onLayout()确定子View的布局。 setOpticalFrame()内部也是调用了 setFrame(),所以具体看setFrame()怎么确定自身的位置布局。

protected boolean setFrame(int left, int top, int right, int bottom) {
      ...
      // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
      // 即确定了视图的位置
      mLeft = left;
      mTop = top;
      mRight = right;
      mBottom = bottom;
      mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}
​

确定了自身的位置后,就要通过onLayout()确定子View的布局。onLayout()是一个 可继承的空方法。

protected void onLayout(boolean changed, int left, int top, intright, int bottom) {
}

如果当前View就是一个单一的View,那么没有子View,就不需要实现该方法。 如果当前View是一个ViewGroup,就需要实现onLayout方法,该方法的实现个自 定义ViewGroup时其特性有关,必须自己实现。 由此便完成了一层层的的布局工作。

4、draw(绘制)

View的绘制过程遵循如下几步:

  1. 绘制背景 background.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制Children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)
public void draw(Canvas canvas) {
      // 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
      // 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。
      // 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。
          ...
      int saveCount;
      if (!dirtyOpaque) {
        // 步骤1: 绘制本身View背景
        drawBackground(canvas);
      }
      // 如果有必要,就保存图层(还有一个复原图层)
      // 优化技巧:
      // 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
      // 因此在绘制的时候,节省 layer 可以提高绘制效率
      final int viewFlags = mViewFlags;
      if (!verticalEdges && !horizontalEdges) {
        if (!dirtyOpaque)
        // 步骤2:绘制本身View内容 默认为空实现, 自定义View时需要进行复写
        onDraw(canvas);
        ......
        // 步骤3:绘制子View 默认为空实现 单一View中不需要实现,Viewgroup中已经实现该方法
        dispatchDraw(canvas);
        ........
        // 步骤4:绘制滑动条和前景色等等
        onDrawScrollBars(canvas);
        ..........
        return;
      }
  ...
}      

5、总结

从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种 类不同,可能分别要自定义实现不同的方法。

但是这些方法不外乎:onMeasure() 方法,onLayout()方法,onDraw()方法。

  • onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定 View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会 执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。
  • onLayout()方法:单一View,不需要实现该方法。ViewGroup必须实现,该方法是 个抽象方法,实现该方法,来对子View进行布局。
  • onDraw()方法:无论单一View,或者ViewGroup都需要实现该方法,因其是个空 方法