前言
说起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()中完成,因此是我们之后讲解的重点内容,后面会详细介绍,这里就不过多的介绍了。