知识点摘要:只需要会简单的自定义 View、ViewGroup,不必了解 onMeasure、onLayout 和 onDraw 的过程。最后还提供了一个比较复杂的小米时针 View。

自定义 View

自定义 View 其实很简单,只需要继承 View,然后重写构造函数、 onMeasure 和 onDraw 方法即可,下面我们就来学习学习他们的用法。

重写构造函数

在继承 View 之后,编译器提醒我们必须实现构造函数,我们一般实现如下两种即可

public CustomView(Context context) {
    super(context);
}

public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}

复制代码

重写 onMeasure()

onMeasure 顾名思义就是测量当前 View 的大小,你可能会有疑惑,我们不是在布局 xml 中已经指定 View 的 layout_width 和 layout_height,这两个属性不就是 View 的高宽吗?没错,这两个属性就是设置 View 的大小,不过如果你应该使用过 wrap_content 和 match_parent 这样的值。我们知道它们分别代表「包裹内容」和「填充父容器」,我们还知道所有代码最后通过编译器都会编译成机器码,但是 cpu 肯定不可能明白「包裹内容」和「填充父类」是什么意思,所以我们应该将它们转化成具体的数值,如 100 px(100 个像素点,最后在屏幕根据像素点显示)。

啰嗦了半天,我们还是来看代码更为直观,我们如果想画一个正方形,并且这个正方形的宽度需要填满整个父容器,这个时候就需要重写 onMeasure 来设置 View 的具体值。

这是重写 onMeasure 的基础代码,有两个参数 widthMeasureSpec 和 heightMeasureSpec,它们保存了 view 的长宽和「测量模式」信息。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
复制代码

长宽我们懂,这个「测量模式」又是什么东西?简单来说「测量模式」包含三种 UNSPECIFIED、EXACTLY 和 AT_MOST。

UNSPECIFIED : 父容器对当前 view 没有任何限制,可以设置任意的尺寸。
EXACTLY : 当前读到的尺寸就是 view 的尺寸。
AT_MOST : 当前读到的尺寸是 view 能够设置的最大尺寸。

三种测量模式与 match_parent 、wrap_content 、固定尺寸之间的关系,可以看到 UNSPECIFIED 模式我们基本上不会触发。

match_parent --> EXACTLY。match_parent 就是要利用父 View 给我们提供的所有剩余空间,而父 View 剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。

wrap_content --> AT_MOST。wrap_content 就是我们想要将大小设置为包裹我们的 view 内容,那么尺寸大小就是父 View 给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。

固定尺寸(如 100dp)--> EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。

我们弄懂了 onMeasure 方法的作用以及参数,接下来直接实现正方形 view 的取值

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = getMySize(100, widthMeasureSpec);   //从 widthMeasureSpec 得到宽度
    int height = getMySize(100, heightMeasureSpec);  //从 heightMeasureSpec 得到高度
    if (width < height) {   // 取最小的那个值
        height = width;
    } else {
        width = height;
    }
    setMeasuredDimension(width, height);    //设置 view 具体的尺寸
}

private int getMySize(int defaultSize, int measureSpec) {
int mySize = defaultSize;

int mode = MeasureSpec.getMode(measureSpec);    //得到测量模式
int size = MeasureSpec.getSize(measureSpec);    //得到建议尺寸

switch (mode) {
    <span class="hljs-keyword">case</span> MeasureSpec.UNSPECIFIED: {  //如果没有指定大小,就设置为默认值
        mySize = defaultSize;
        <span class="hljs-built_in">break</span>;
    }
    <span class="hljs-keyword">case</span> MeasureSpec.AT_MOST: {  //如果测量模式是最大值,就设置为 size
        //我们将大小取最大值,你也可以取其他值
        mySize = size;
        <span class="hljs-built_in">break</span>;
    }
    <span class="hljs-keyword">case</span> MeasureSpec.EXACTLY: {  //如果是固定的大小,那就不要去改变它
        mySize = size;
        <span class="hljs-built_in">break</span>;
    }
    default:
        <span class="hljs-built_in">break</span>;
}
<span class="hljs-built_in">return</span> mySize;

}

复制代码

重写 onDraw()

我们已经设置好了 view 的尺寸,也就是将画板准备好。接下来需要在画板上绘制图形,我们只需要重写 onDraw 方法。参数 Canvas 是官方为我们提供的画图工具箱,我们可以利用它绘制各种各样的图形。

@Override
protected void onDraw(Canvas canvas) {
    //调用父 View 的 onDraw 函数,因为 View 这个类帮我们实现了一些
    // 基本的而绘制功能,比如绘制背景颜色、背景图片等
    super.onDraw(canvas);
    int r = getMeasuredHeight() / 2;
    //圆心的从横坐标
    int centerX = r;
    //圆心的从纵坐标
    int centerY = r;
Paint p = new Paint();  //画笔
p.setColor(Color.GREEN);    //设置画笔的颜色
//开始绘制
canvas.drawCircle(centerX, centerY, r, p);

}

复制代码

我们只需要在布局 xml 中加入 CustomView 控件,就能看到效果

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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">
&lt;com.wendraw.customviewexample.CustomView
    android:layout_width=<span class="hljs-string">"match_parent"</span>
    android:layout_height=<span class="hljs-string">"match_parent"</span>
    android:background=<span class="hljs-string">"#f00"</span> /&gt;

</LinearLayout>

复制代码

自定义 view Custom View

自定义布局属性

不知道你在写布局文件的时候,有没有思考过设置 layout_width 属性,相应的 View 对象就会改变,这是怎么实现的呢?我们的 CustomView 可不可以自己定义一个这样的布局属性呢?

我们在重写构造函数时,其实埋下了一个伏笔,为什么我们要实现 public CustomView(Context context, AttributeSet attrs) 方法呢?AttributeSet 参数又有什么作用呢?

我们在使用 view 时会发现,defaultSize 值被我们写死了,如果有别的开发者想使用我们的 CustomView,但是默认大小想设置为 200,就需要去修改源码,这就破坏了代码的封装特性,有的人会说我们可以增加 getDefaultSize、setDefaultSize 方法,这个方法没有问题,但是还不够优雅,其实 Google 已经帮我们优雅的实现了,就本节要讲到的 AttributeSet。

首先我们需要新建一个 res/values/attr.xml 文件,用来存放各种自定义的布局属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- name 为声明的"属性集合"名,可以随便取,但是最好是设置为跟我们的 View 一样的名称-->
    <declare-styleable name="CustomView">
        <!-- 声明我们的属性,名称为 default_size,取值类型为尺寸类型(dp,px等)-->
        <attr name="default_size" format="dimension" />
    </declare-styleable>
</resources>
复制代码

接下来就能在布局文件中使用这个属性了

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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">
&lt;com.wendraw.customviewexample.CustomView
    android:layout_width=<span class="hljs-string">"match_parent"</span>
    android:layout_height=<span class="hljs-string">"match_parent"</span>
    android:background=<span class="hljs-string">"#f00"</span>
    app:default_size=<span class="hljs-string">"100dp"</span> /&gt;

</LinearLayout>

复制代码

注意:需要在根标签(LinearLayout)里面设定命名空间,命名空间名称可以随便取,比如 app,命名空间后面取得值是固定的:"schemas.android.com/apk/res-aut…"

我们在布局文件中使用当然还不会产生效果,因为我们没有将它解析到 CustomView 类中,解析的过程也很简单,使用我们前面介绍过带 AttributeSet 参数的构造函数即可:

public CustomView(Context context, AttributeSet attrs) {
    super(context, attrs);
    //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
    //即属性集合的标签,在R文件中名称为R.styleable+name
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
//第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
//第二个参数为,如果没有设置这个属性,则设置的默认的值
mDefaultSize = typedArray.getDimensionPixelSize(R.styleable.CustomView_default_size, 100);

//最后将 TypedArray 回收
typedArray.recycle();

}

复制代码

全局变量 mDefaultSize 就等于布局文件中的 default_size 属性中解析来的值。

至此一个简单的自定义 view 就创建成功了,跟我们平时使用的 Buttom 控件是一样的,我们还可以在 activity_main.xml 的 Design 界面的左上角看到我们刚刚创建的控件

Project Custom View 自定义控件

自定义 ViewGroup

我们写一个布局文件用到的就是两个元素,控件、布局。控件在上一节已经讲了,这一节我们一起来学习布局 ViewGroup。布局就是一个 View 容器,其作用就是决定控件的摆放位置。

其实官方给我们提供的六个布局已经够用了,我们学习自定义 view 主要是为了在使用布局的时候更好的理解其原理。既然是布局就要满足几个条件:

  1. 要知道子 view 的大小,根据子 View 才能设置 ViewGroup 的大小。
  2. 要知道布局功能,也就是子 View 需要怎么摆放,知道了子 View 的尺寸和摆放方式才能确定 ViewGroup 的大小。
  3. 最后就是将子 View 填到相应的位置。

接下来我们通过一个简单的案例学习一下,自定义一个将子 View 按垂直方向一次摆放的布局。我们先创建一个 CustomViewLayout 类并继承 ViewGroup。

实现 onMeasure,测量子 View 的大小,设置 ViewGroup 的大小

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //将所有的子View进行测量,这会触发每个子View的onMeasure函数
    //注意要与measureChild区分,measureChild是对单个view进行测量
    measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int childCount = getChildCount();

<span class="hljs-keyword">if</span> (childCount == 0) {
    //如果没有子View,当前ViewGroup没有存在的意义,不用占用空间
    <span class="hljs-built_in">set</span>MeasuredDimension(0, 0);
} <span class="hljs-keyword">else</span> {
    //如果高宽都是包裹内容
    <span class="hljs-keyword">if</span> (widthMode == MeasureSpec.AT_MOST &amp;&amp; heightMode == MeasureSpec.AT_MOST) {
        //我们就将高度设为所有子 View 的高度相加,宽度设为子 View 最大的。
        int width = getMaxChildWidth();
        int height = getTotalHeight();
        <span class="hljs-built_in">set</span>MeasuredDimension(width, height);

    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (widthMode == MeasureSpec.AT_MOST) {    //只有宽度是包裹内容
        //高度设置为 ViewGroup 的测量值,宽度为子 View 的最大宽度
        <span class="hljs-built_in">set</span>MeasuredDimension(getMaxChildWidth(), heightSize);

    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (heightMode == MeasureSpec.AT_MOST) {    //只有高度是包裹内容
        //高度设置为 ViewGroup 的测量值,宽度为子 View 的最大宽度
        <span class="hljs-built_in">set</span>MeasuredDimension(widthSize, getTotalHeight());
    }
}

}

/**
* 获取子 View 中宽度最大的值
*
* @return 子 View 中宽度最大的值
*/
private int getMaxChildWidth() {
int childCount = getChildCount();
int maxWidth = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getMeasuredWidth() > maxWidth) {
maxWidth = childView.getMeasuredWidth();
}
}
return maxWidth;
}

/**
* 将所有子 View 的高度相加
*
* @return 所有子 View 的高度的总和
*/
private int getTotalHeight() {
int childCount = getChildCount();
int height = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
height += childView.getMeasuredHeight();
}
return height;
}

复制代码

代码已经注释的比较详细了,我就不赘述了。我们解决了 ViewGroup 的大小问题,接下来就是解决子 View 的摆放问题。

实现 onLayout 摆放子 View

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int count = getChildCount();
    //记录当前的高度位置
    int curHeight = t;
    //将子 View 逐个拜访
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        int width = child.getMeasuredWidth();
        int height = child.getMeasuredHeight();
        //摆放子 View,参数分别是子 View 矩形区域的左、上、右、下边
        child.layout(l, curHeight, l + width, curHeight + height);
        curHeight += height;
    }
}
复制代码

代码很简单,用一个循环将子 View 按照顺序一次执行 layout,设置子 View 的摆放位置。

至此一个简单的自定义布局我们也完成了,我们来测试一下:

<?xml version="1.0" encoding="utf-8"?>
<com.wendraw.customviewexample.CustomViewLayout 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">
&lt;com.wendraw.customviewexample.CustomViewLayout
    android:layout_width=<span class="hljs-string">"wrap_content"</span>
    android:layout_height=<span class="hljs-string">"wrap_content"</span>
    android:background=<span class="hljs-string">"#0f0"</span>&gt;

    &lt;com.wendraw.customviewexample.CustomView
        android:layout_width=<span class="hljs-string">"300dp"</span>
        android:layout_height=<span class="hljs-string">"100dp"</span>
        android:background=<span class="hljs-string">"#f00"</span>
        app:default_size=<span class="hljs-string">"200dp"</span> /&gt;

    &lt;Button
        android:layout_width=<span class="hljs-string">"300dp"</span>
        android:layout_height=<span class="hljs-string">"wrap_content"</span>
        android:text=<span class="hljs-string">"xxxxxxxxxx"</span> /&gt;

    &lt;com.wendraw.customviewexample.CustomView
        android:layout_width=<span class="hljs-string">"match_parent"</span>
        android:layout_height=<span class="hljs-string">"50dp"</span>
        android:background=<span class="hljs-string">"#f00"</span>
        app:default_size=<span class="hljs-string">"200dp"</span> /&gt;
&lt;/com.wendraw.customviewexample.CustomViewLayout&gt;

&lt;View
    android:layout_width=<span class="hljs-string">"match_parent"</span>
    android:layout_height=<span class="hljs-string">"100dp"</span> /&gt;

&lt;com.wendraw.customviewexample.CustomView
    android:layout_width=<span class="hljs-string">"wrap_content"</span>
    android:layout_height=<span class="hljs-string">"wrap_content"</span>
    android:background=<span class="hljs-string">"#f00"</span>
    app:default_size=<span class="hljs-string">"100dp"</span> /&gt;

</com.wendraw.customviewexample.CustomViewLayout>

复制代码

可以看到我们创建的自定义的 View 和 ViewGroup 跟使用平常的控件、布局的方式一样,我们组合起来其效果如下:

demo 自定义 View 和 ViewGroup

深入学习自定义 View

通过上面的学习你应该对自定义 View 和 ViewGroup 有一定的认识,甚至觉得还有一点点简单,接下来你就可以学习一下更复杂的 View。比如小米时钟,你可以先尝试自己实现,不会的再参考我的代码。

MiClock Demo MiClock Demo

结束

在入门阶段我们不需要去详细 onMeasure、onLayout 和 onDraw 的过程,只需要会简单的自定义 View、ViewGroup 即可。最后比较复杂还提供了一个小米时针 View。所有的代码都上传到了 GayHub CustomViewExample 欢迎拍砖。

参考

自定义 View,有这一篇就够了

Github-MiClockView

  • Android

    开放手机联盟(一个由 30 多家科技公司和手机公司组成的团体)已开发出 Android,Android 是第一个完整、开放、免费的手机平台。

    293 引用
感谢    赞同    分享    收藏    关注    反对    举报    ...