这是我参与「第四届青训营」笔记创作活动的第35天
初识Android自定义View
一、什么是 View
View 是一种界面层的控件的一种抽象,它代表了一个控件,是 Android 中所有控件的基类。
如图所示:
从上图看,有很多布局类等为什么没有在上图看到,在这里要说明一下这里仅是 android.widget 包的,还有其他视图的虽然也继承 View 但是他们不属于 android.widget 包例如下面两个组件: RecyclerView 继承 ViewGroup ,但是属于 androidx.recyclerview.widget 包的。 ConstraintLayout 继承 ViewGroup,但是属于 androidx.constraintlayout.widget 包的。
还有很多其他包、或自定义控件,这里我们就不做过多描述了。
二、View 的种类
Android 中的视图类可分为3种:布局 (Layout) 类、视图容器 (View Container) 类和视图类(例如TextView) ,这3种类都是 android.view.View 的子类。ViewGroup 是一个容器类,该类也是 View 的重要子类,所有的布局类和视图容器类都是 ViewGroup 的子类,而视图类直接继承自 View 类。\
下图描述了 View、ViewGroup、视图容器类及视图类的继承关系:
从上图所示的继承关系我们可以看出:
-
Button、TextView、EditText 都是视图类,TextView 是 Button 和 EditText 的父类,TextView 直接继承自 View 类。
-
GridView 和 ListView 是 ViewGroup 的子类,但并不是直接子类,GridView、ListView 继承自 AbsListView继承自 AdapterView 继承自 ViewGroup,从而形成了视图容器类的层次结构。
-
布局视图虽然也属于容器视图,但由于布局视图具有排版功能,所以将这类视图置为布局类。
对于一个 Android 应用的图形用户界面来说,ViewGroup 作为容器来装其他组件, 而 ViewGroup 里除了可以包含普通 View 组件之外,还可以再次包含 ViewGroup 组件。
现在我们大致了解了 View 和 View 的子类 ViewGroup,接下来咱们探索自定义 View ,如何自定义 View
三、自定义 View
- 先来看一下 View 的额构造方法:
// 如果View在Java代码中是new出来的,就会调用第一个构造函数
public View(Context context) {
throw new RuntimeException("Stub!");
}
// 如果View是在.xml里面声明的就会调用第二个构造函数
public View(Context context, @Nullable AttributeSet attrs) {
throw new RuntimeException("Stub!");
}
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
throw new RuntimeException("Stub!");
}
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
throw new RuntimeException("Stub!");
}
AttributeSet 与自定义属性:系统自带的 View 可以在xml中配置属性,对于已经写好的自定义的 View 同样可以在 xml 中配置属性,为了使自定义 View 的属性可以在 xml 中配置,需要一下四个步骤:
- 通过
<declare-styleable>
为自定义 View 添加属性 - 在 xml 中为相应的属性生命属性值
- 在运行时获取属性值
- 将获取的属性值应用到 View
- View 的绘制流程图
- 自定义 View
自定义 View 的最基本的方法是:
- onMeasure():测量,决定 View 的大小。
- onLayout():布局,决定 View 在 ViewGroup 中的位置。
- onDraw():绘制,决定绘制这个 View。
onMeasure() 方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
}
首先看一下 onMeasure()
函数的两个参数,widthMeasureSpec
和 heightMeasureSpec
。这两个 int 型的数据包含了测量模式和尺寸。
// 获取测量模式
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
// 获取尺寸
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
什么是测量模式呢?测量模式有三种,分别是 UNSPECIFIED
/ EXCATLY
/ AT_MOST
。
测量模式 | 表示意思 |
---|---|
UNSPECIFIED | 父容器没有对当前 View 有任何限制,当前 View 可以任意取尺寸 |
EXACTLY | 当前的尺寸就是当前 View 应该取的尺寸 |
AT_MOST | 当前尺寸是当前 View 能取的最大尺寸 |
现在我们重写一个 onMeasure()
函数,实现一个自定义的正方形 View 。
// 继承View
public class SquareView extends View {
// 1.只有 Context 的构造函数
public SquareView(Context context) {
super(context);
}
// 2.含有 Context 和 AttributeSet 的构造函数
public SquareView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
// 3.重写onMesure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMySize(100, widthMeasureSpec);
int height = getMySize(100, heightMeasureSpec);
if (width < height) {
height = width;
}else {
width = height;
}
setMeasuredDimension(width, height);
}
// 根据测量模式
private int getMySize(int defaultSize, int measureSpec) {
int mSize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
// 父容器没有对当前 View 有任何限制,当前 View 可以任意取尺寸
case MeasureSpec.UNSPECIFIED:
mSize = defaultSize;
break;
// View 能取得的最大尺寸
case MeasureSpec.AT_MOST:
mSize = size;
break;
// 当前的尺寸就是当前 View 应该取的尺寸
case MeasureSpec.EXACTLY:
mSize = size;
break;
}
return mSize;
}
}
对应 xml 布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="15sp"
android:text="自定义View_SquareView"
android:textSize="30sp" />
<com.example.appb.SquareView
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@color/purple_200" />
<com.example.appb.SquareView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:background="@color/teal_200" />
</LinearLayout>
运行效果:
Tips:在自定义 View 的时候,需要两个构造函数。否则在编译的时候会报异常:Binary XML file line Error inflating class
. 原因是:Android 在根据 xml 文件夹创建 View 对象的时候会调用 View 的双参构造方法,即public SquareView(Context,AttributeSetattrs) {}
,所以如果没有写全的话就会报错。
onDraw() 方法
在 onMeasure()
方法中实现了自定义尺寸大小,在 onDraw 方法中实现了自定义的绘制 View 。接下来做一个自定义的圆形 View 。
@Override
protected void onDraw(Canvas canvas) {
// 调用父类的 onDraw 函数,因为 View 这个类实现了一些基本的绘制功能,比如绘制背景颜色和背景图片
super.onDraw(canvas);
// 半径
int r = getMeasuredWidth() / 2;
// 以圆心的横坐标为当前 View 的左起始位置 + 半径
int centerX = getLeft() + r;
// 以圆心的横坐标为当前 View 的顶部起始位置 + 半径
int centerY = getTop() + r;
Paint paint = new Paint();
paint.setColor(Color.YELLOW);
canvas.drawCircle(centerX, centerY, r, paint);
}
对应 xml 布局:
<com.example.appb.CycloView
android:layout_width="100dp"
android:layout_height="wrap_content"/>
运行效果:
四、总结
这里我们自定义 View 属性就先不说了。通过上边我们认识了自定义 View ,还有 View 对应的构造方法,还了解了一下 ViewGroup 。我们实现了 View 测量,还有简单的 View 绘制,其实呢还可以自定义 View 的触摸事件呢~
总之,自定义 View 花式多样,更多自定义 View 还得慢慢去学!