初识Android自定义View | 青训营笔记

55 阅读5分钟

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

初识Android自定义View

一、什么是 View

View 是一种界面层的控件的一种抽象,它代表了一个控件,是 Android 中所有控件的基类

如图所示: img1

从上图看,有很多布局类等为什么没有在上图看到,在这里要说明一下这里仅是 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、视图容器类及视图类的继承关系: img2

从上图所示的继承关系我们可以看出:

  • Button、TextView、EditText 都是视图类,TextView 是 Button 和 EditText 的父类,TextView 直接继承自 View 类。

  • GridView 和 ListView 是 ViewGroup 的子类,但并不是直接子类,GridView、ListView 继承自 AbsListView继承自 AdapterView 继承自 ViewGroup,从而形成了视图容器类的层次结构。

  • 布局视图虽然也属于容器视图,但由于布局视图具有排版功能,所以将这类视图置为布局类

      对于一个 Android 应用的图形用户界面来说,ViewGroup 作为容器来装其他组件,
      而 ViewGroup 里除了可以包含普通 View 组件之外,还可以再次包含 ViewGroup 组件。
    

img3

现在我们大致了解了 View 和 View 的子类 ViewGroup,接下来咱们探索自定义 View ,如何自定义 View

三、自定义 View

  1. 先来看一下 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
  1. View 的绘制流程图

img4

  1. 自定义 View

自定义 View 的最基本的方法是:

  • onMeasure():测量,决定 View 的大小。
  • onLayout():布局,决定 View 在 ViewGroup 中的位置。
  • onDraw():绘制,决定绘制这个 View。

onMeasure() 方法

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
    }

首先看一下 onMeasure() 函数的两个参数,widthMeasureSpecheightMeasureSpec 。这两个 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>

运行效果:

img5

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"/>

运行效果:

img6

四、总结

这里我们自定义 View 属性就先不说了。通过上边我们认识了自定义 View ,还有 View 对应的构造方法,还了解了一下 ViewGroup 。我们实现了 View 测量,还有简单的 View 绘制,其实呢还可以自定义 View 的触摸事件呢~
总之,自定义 View 花式多样,更多自定义 View 还得慢慢去学!