前言

说起Android自定义View,很多人的想法可能就是好难,既要量测宽高、获取属性、如果使用到贝塞尔曲线的话可能还牵扯到几何图形计算、更高级的还要用到高等数学;很多人一听到这些,心里估计就在打退堂鼓了!其实不然.

自定义View的学习,也是需要一个过程。需要大量的积累、学习,量变引起质变,首先第一步就是不要怕,再难的问题拆分成很多块,一块一块的啃,一个问题一个问题的搞清楚,终究你会啃下这块骨头;

今天这篇开始我们就进入了自定义View的正式学习,之前的文章只是学习自定义View的铺垫基础

注重点:自定义View流程

注:时间有限,我就没有自己去画一遍了,就直接百度上截取的图片;

我们将重要的几个方法打印出来看看:

CustomView代码

public CustomView(Context context, @androidx.annotation.Nullable AttributeSet attrs) {
       super(context, attrs);
       Log.e(TAG, "CustomView: 两个参数的构造方法");
   }

   public CustomView(Context context, @androidx.annotation.Nullable AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
   }

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
       Log.e(TAG, "onMeasure: ======");
   }

   @Override
   protected void onSizeChanged(int w, int h, int oldw, int oldh) {
       super.onSizeChanged(w, h, oldw, oldh);
       Log.e(TAG, "onSizeChanged: =======");
   }

   @Override
   protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);
       Log.e(TAG, "onDraw: =======");
   }
复制代码

MainActivity的xml中添加CustomView:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <com.example.lt.CustomView
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

复制代码

打印结果:

2018-10-22 22:27:40.104 24881-24881/com.example.lt E/CustomView: CustomView: 两个参数的构造方法
2018-10-22 22:27:40.128 24881-24881/com.example.lt E/CustomView: onMeasure: ======
2018-10-22 22:27:40.147 24881-24881/com.example.lt E/CustomView: onSizeChanged: =======
2018-10-22 22:27:40.167 24881-24881/com.example.lt E/CustomView: onDraw: =======
复制代码

不少的人会很疑惑,为什么这个很重要?这里我嘴皮子说破了估计你也不知道他的重要性,但是我先把话放在这,当你对自定义View有一定的认识了之后,你就会知道这张图的重要性。

自定义View的分类

经常我们将Android中的自定义分为两大体系:自定义View自定义ViewGroup

1、自定义ViewGroup

自定义ViewGroup,简单解释就是说:根据已有的组件通过特定的布局的方式来组成新的组件,大多数是继承自ViewGroup或者是各种Layout(LinearLayout、Relativelayout、Framelayout等等),自定义ViewGroup中包含的有子View;

2、自定义View

自定义View:没有满足需求的View,需要我们自定义View,一般继承自View或SurfaceView或其他的View,是不包含子View;

重点:个人的观点,大多数的自定义View的图形都是可以使用图片来代替,动画效果可以使用gif、Lottie加载json文件、SVG矢量文件等;那么为什么我们要使用自定义View呢?我觉得有三点:

1、直接使用图片无法满足需求;

2、复杂的动画效果

3、追求性能、稳定(gif内存占用太大、Lottie存在坑、svg很多情况下不能满足)

几个重要的方法

1、构造方法说明

public class LTCustomView extends View {
   public LTCustomView(Context context) {
       super(context);
   }

   public LTCustomView(Context context, @Nullable AttributeSet attrs) {
       super(context, attrs);
   }

   public LTCustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
   }

   public LTCustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
       super(context, attrs, defStyleAttr, defStyleRes);
   }


复制代码

可以看到总共有四种构造方法:

一个参数的构造方法:

@Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       
       LTCustomView customView = new LTCustomView(this);
   }

复制代码

二个参数的构造方法:

<com.example.lt.LTCustomView
       android:layout_width="0dp"
       android:layout_height="0dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
复制代码

三个参数的构造方法:第三个参数是默认的style,三个参数的默认style是在Application或者Activity中指定的Theme中的style,一般我们很少使用到,所以就不详细的说明,之后有使用到的时候将详细说明;

四个参数的构造方法:

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
   public LTCustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
       super(context, attrs, defStyleAttr, defStyleRes);
   }
复制代码

可以看到至少得API 21上才能使用,所以就暂时不细讲.

2、OnMeasure()方法说明:

onMeasure()方法:量测自身View的大小.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   int widthsize = MeasureSpec.getSize(widthMeasureSpec);      //取出宽度数值
   int widthmode = MeasureSpec.getMode(widthMeasureSpec);      //取出宽度的测量模式
   
   int heightsize = MeasureSpec.getSize(heightMeasureSpec);    //取出高度数值
   int heightmode = MeasureSpec.getMode(heightMeasureSpec);    //取出高度的测量模式
}
复制代码

onMeasure()方法中的两个参数,其实不是一个Int值,而是由Size和Mode组合起来的值,要想理解这个值,那么需要知道这Mode中的三个属性值:

  • UNSPECIFIED 00 默认值,父控件没有给子view任何限制,子View可以设置为任意大小。
  • EXACTLY 01 表示父控件已经确切的指定了子View的大小。
  • AT_MOST 10 表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。

AT_MOST对应我们xml文件中的:

android:layout_width="wrap_content"
android:layout_height="wrap_content"

复制代码

EXACTLY对应我们xml中的:

android:layout_width="10dp"
android:layout_height="10dp"
复制代码

其实我们一般是不用去特意的去量测,除非你有什么特殊的需求;

下面是View源码中的resolveSize()的处理方式:

public static int resolveSize(int size, int measureSpec) {
       return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
   }

   /**
    * Utility to reconcile a desired size and state, with constraints imposed
    * by a MeasureSpec. Will take the desired size, unless a different size
    * is imposed by the constraints. The returned value is a compound integer,
    * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
    * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
    * resulting size is smaller than the size the view wants to be.
    *
    * @param size How big the view wants to be.
    * @param measureSpec Constraints imposed by the parent.
    * @param childMeasuredState Size information bit mask for the view's
    *                           children.
    * @return Size information bit mask as defined by
    *         {@link #MEASURED_SIZE_MASK} and
    *         {@link #MEASURED_STATE_TOO_SMALL}.
    */
   public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
       final int specMode = MeasureSpec.getMode(measureSpec);
       final int specSize = MeasureSpec.getSize(measureSpec);
       final int result;
       switch (specMode) {
           case MeasureSpec.AT_MOST:
               if (specSize < size) {
                   result = specSize | MEASURED_STATE_TOO_SMALL;
               } else {
                   result = size;
               }
               break;
           case MeasureSpec.EXACTLY:
               result = specSize;
               break;
           case MeasureSpec.UNSPECIFIED:
           default:
               result = size;
       }
       return result | (childMeasuredState & MEASURED_STATE_MASK);
   }
复制代码

注意:我们如果对width or height改变了,这个时候我们就不要执行 super.onMeasure( widthMeasureSpec, heightMeasureSpec),否则你设置的宽高的值就无效了,此时我们应该调用setMeasuredDimension( widthsize, heightsize)

3、onSizeChange()

该方法在我们的自定义View的视图大小发生改变时会调用此方法。

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
   super.onSizeChanged(w, h, oldw, oldh);
}
复制代码

四个参数:

  • w:宽度
  • h:高度
  • oldw:上一次宽度
  • oldh:上一次高度

实际使用中只需要关注w、h即可;

4、自定义onLayout()

该方法是为了是确定子View布局的位置;

@Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
       
   }
复制代码

这个方法是一个抽象方法,那就意味着自定义ViewGroup必须要实现的方法。一般使用方法是在onLayout()中循环的去给子child.layout(l,t,r,b)

  • l:左侧距离父View的距离 getLeft()
  • t:顶部距离父View的距离 getTop()
  • r:右边距离父View的距离 getRight()
  • b:下边距离父View的距离 getBottom()

5、onDraw()方法

@Override
   protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);
       Log.e(TAG, "onDraw: =======");
   }
复制代码

onDraw()我们自定义View中最重要的方法,我们所有的绘制工作就是在onDraw()中完成,因此是我们之后讲解的重点内容,后面会详细介绍,这里就不过多的介绍了。