十五 android自定义View详解

概述

android开发中,系统控件有时候不适用于 具体的业务场景,此时就需要我们创造出自己的自定义View。 自定义View的方式大体上分为两种:

    1. 继承系统成熟控件(LinearLayout,RelativeLayout,ImageView等),然后进行魔改
    1. 继承View/ViewGroup 并自己绘制内容

方式1-半自定义(魔改)

如下图:继承RelativeLayout,然后在构造函数中动态添加一些view

以下是我们需要的效果:

自定义属性

比如上面的例子,我们需要对 左边的图,右边的图,以及中间的文案进行静态设置(布局xml中设定)。

必须先编写属性: 在 res/values/attr.xml中,声明如下内容

<declare-stylable name="CustomToolBar">

然后在 java部分的源代码中,获取自定义属性的值:

方式2:完全自定义

这种方式下,我们需要考虑几个问题:

  1. 如何将UI元素绘制到界面上
  2. 如何控制控件宽高
  3. 如果是自定义ViewGroup,如何确定子view的摆放

这3个问题分别对应View/ViewGroup的以下几个方法:

  1. onDraw
  2. onMeasure
  3. onLayout

onDraw 绘制

onDraw方法会接收一个 Canvas类型的参数,而Canvas中,存在以下这些基础的绘制函数 在安卓底层,绘制引擎其实是和Flutter一样的 skia,我们在java代码中看到的 canvas对象,其实是c++层 canvas对象的代理。当我们操作java层Canvas的时候,其实也就是在间接操作 c++层的canvas画布。

上图中可以看到在draw的时候有一个Painter(画笔)类型的入参,它决定了画笔的属性:

举个draw的例子: 当我们要画出一个圆形进度条,首先在外围画出一个圆,然后在内层画出一个弧形。

布局中的使用很简单:

同时可以在java代码中动态设定 进度值:

显示效果如下:

onMeasure 测量

上面的例子中,我们在使用 自定义组件时,是直接设定了 宽高为300dp,但是很多时候,我们不知道具体的宽高值,而是要根据父容器的宽高来综合决定。

Android中提供了 wrap_content(自适应大小) 和 match_parent(填满父容器) 两种属性来规范控件的范围,这两种情况其实是没有指定具体数值的。所以,我们在自定义View的时候,需要考虑这两种情况下,控件的宽高应该如何表现。

以下是view的测量方法: 这里可以看到 两个入参都是int类型的,可能以为这两个入参是 宽高的具体数值,其实并不是。 每一个int类型,为4个字节,每个字节为8位,一共32位,高2位,为测量模式 ,低30位为 具体数值(最大值为2^30-1)。

测量模式

一共有3种:

  • EXACT 具体数值 也就是我们在xml中直接指定了300dp这种情况 以及 XML中指定了 match_parent
  • AT_MOST 对应了 XML中设定的 WRAP_CONTENT
  • UNSPECIFIED 未指定模式,也就是说不加以限制,你甚至可以超出父容器大小

在int中获取测量模式和具体数值的方法如下: MeasureSpec.getMode()

决定宽高的方法

下图是最终决定控件宽高的方法:setMeasuredDimension()

也就是说,setMeasuredDimension(200,300) 之后,也就意味着,宽高被定死为 宽200像素,高300像素。

上图中为View源码中的默认大小,默认为 父视图的可用空间。

正确的onMeasure写法

上面的圆形进度条控件中,如果我们在xml中不去设定它的具体宽高300dp,而是指定为 wrap_content,那么它的测量模式就是 AT_MOST。那么它就默认使用了父容器的剩余可用空间,于是 就显示为:

绘制的内容超出了边界,这不是我们想要的结果。 回到它源代码中,重写它的onMeasure方法如下:

代码的含义为:如果测量模式为 AT_MOST(对应XML中的wrap_content),我们手动指定宽高中的较小值为自定义View的绘制边界。

ViewGroup的onMeasure

如果我们自定义的是一个ViewGroup容器,那么onMeasure方法会更复杂。因为 作为一个容器,很多情况下必须先测量出内部子view的大小,然后决定自身大小。

比如如下代码:一个LinearLayout中放了3个TextView,而且长短不一。

显示结果如下:

LinearLayout的宽度,由子View的最大宽度决定,高度由所有子View的总宽度决定。

所以,当我们自定义一个ViewGroup的时候,要综合考虑子View的宽高情况。 比如做一个流式布局的自定义ViewGroup容器。 以下是伪代码:

java 复制代码
@override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
            super.onMeasure(widthMeasureSpec,heightMeasureSpec);
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getMode(heightMeasureSpec);

            int childCount = getChildCount();
            int totalHeight = 0;// 记录累计的高度
            int totalWidth = 0; // 自身的最大宽度
            for(int i=0;i<= childCount-1;i++){
                View child = getChildAt(i);

                measureChild(child,widthMeasureSpec,heightMeasureSpec); // 测量子view

                // 测量完毕之后,子view就有了自己的宽高
                int childWidth = childView.getMeasuredWidth();
                int childHeight = childView.getMeasuredHeight();

                // ... 接下开就计算出每个子View的高度
                totalWidth  = Math.max(childWidth,totalWidth);
                totalHeight+=childHeight;
            }

            setMeasuredDimension(totalWidth,totalHeight);
}

上面的代码的目的为:

  • 调用 measureChild方法递归测量子view
  • 通过叠加每一行的高度,决定最后ViewGrop的高度
  • 通过每一次的max计算,得出ViewGroup的宽度

onLayout 放置

上面的onDraw和onMeasure决定了绘制的内容以及 控件的宽高,而还剩下最后一个问题,控件放在哪个坐标上。 下图为onLayout的源码:

我们的自定义ViewGroup需要重写onLayout方法来决定每一个子view的排布。

伪代码如下:

java 复制代码
        @override
        protected void onLayout(boolean changed,int l,int t,int l,int b){
            
            int childCount = getChildCount();
            for(int i = 0 ;i <= childCount -1;i++){
                View childView = getChildAt(i);
                // 布局逻辑,计算外间距等。。。
                childView.layout(,,,); //  指定具体的放置坐标
            }
            
        }

上面的代码中,遍历了每一个子view,并且进行布局逻辑的计算,然后分别指定每一个子view的排布坐标。

总结

自定义View离不开3个方法。

  • onDraw:负责绘制UI元素
  • onMeasure:负责测量控件宽高
  • onLayout:在自定义ViewGroup时用于决定子view的坐标。
相关推荐
森叶5 分钟前
Electron 安装包 asar 解压定位问题实战
前端·javascript·electron
drebander8 分钟前
ubuntu 安装 chrome 及 版本匹配的 chromedriver
前端·chrome
软件技术NINI17 分钟前
html知识点框架
前端·html
深情废杨杨21 分钟前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS21 分钟前
【vue3】vue3.3新特性真香
前端·javascript·vue.js
众生回避27 分钟前
鸿蒙ms参考
前端·javascript·vue.js
洛千陨28 分钟前
Vue + element-ui实现动态表单项以及动态校验规则
前端·vue.js
GHUIJS1 小时前
【vue3】vue3.5
前端·javascript·vue.js
&白帝&2 小时前
uniapp中使用picker-view选择时间
前端·uni-app
魔术师卡颂2 小时前
如何让“学源码”变得轻松、有意义
前端·面试·源码