【轮子】自定义控件,范围选择器

自定义控件的时候,就想把公司项目里,我自己写的自定义控件挑一个发出来,然而我懒。

写在开头

使用自定义控件的目的一般有两个

  1. 实现特别的界面效果,若要实现漂亮自由的界面,靠系统的控件通常是很难做到的。
  2. 或者是简化重复的无意义劳动。页面中一些相似但不完全相同的控件,例如一个页面的头部通常包含一个返回键,一个title,有时候还有一个右侧的按钮。虽然这种情况通过include引入一个布局文件也可以解决问题,但是每次都需要在activity中设置一次title,并且这个方法很不直观。

自定义View的分类

  1. 继承View
  2. 继承ViewGroup,
  3. 继承特定的View,比如TextView
  4. 继承特定的ViewGroup,比如LinearLayout

继承View或者ViewGroup,好处是更加灵活,但是有比较多的东西需要自定义,比如padding的处理。
继承特定的View,比如TextView,或者LinearLayout,好处是实现起来比较快,很多地方不需要管,但是灵活性差一点。

自己实现自定义控件

本文通过一个自定义的范围选择器,来展示自定义控件的实现思路。
大概就是下面这个样子。


一个或者两个滑块的选择器.png

很简单的控件,支持一个滑块或者两个滑块。

是不是真的需要自定义

在写自定义控件的时候,首先要确定的是,这个控件是否真的需要自己实现。
毕竟android自带控件是经过时间与性能的考验的,TextView动辄一万来行,性能与功能都可以得到保障。
一般的动画需求,通过属性动画加上系统控件都可以得到实现。
但是难保碰到一个脑洞大开的产品非要让你根据手机壳动态切换主题。那就没办法,自己写喽。

三个构造函数

自定义控件时,系统会为我们生成3个构造函数,对应本文中的

 public RangeSelectBar(Context context){}
 public RangeSelectBar(Context context, @Nullable AttributeSet attrs){}
 public RangeSelectBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr){}

区别是
如果在代码中,通过new来实例化一个控件,就是调用第一个构造函数
如果在布局文件中,添加一个控件,就会调用第二个构造函数
而第三个构造函数不常用,它可以由我们自己调用,并且传入一个style

我比较喜欢通过this函数,在第一个构造中调用第二个,在第二个构造中调用第三个,或者看情况忽视第三个。

自定义属性

通过自定义属性可以让我们的自定义控件更具有灵活性与实用性,而非一次性的产品。

声明

首先是声明自定义属性。
res-values文件夹下新建一个Value resource file,命名为attrs.xml
接下来,按照以下格式往文件里写入自定义属性规则

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="自定义属性组名">
        <attr name="属性名,会展示在XML文件中" format="该属性允许接接收的值的类型" /><!--注释-->
    </declare-styleable>
</resources>

在当前项目中,使用了以下几种格式

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RangeSelectBar">
        <attr name="RSBSelectColor" format="color" /><!--选中的范围颜色。-->
        <attr name="RSBHeight" format="dimension" /><!--进度条高度-->
        <attr name="RSBSliderRes" format="reference|color" /><!--滑块资源id,左右两边相同。-->
        <attr name="RSBLeftSliderPosition" format="integer" /><!--左方滑块位置-->
        <attr name="RSBHideLeftSlide" format="boolean" /><!-- 隐藏左侧滑块-->
    </declare-styleable>
</resources>

其中
color代表可以使用颜色id或者色值
dimension可以填入长度数字,dp,px之类
reference代表图片或者shape资源文件
integer代表整形数值
boolean代表布尔类型的数值
此外,比较常用的还有string字符型,以及float浮点型
并且接受的属性类型,允许同时接收两种或者两种以上的类型,比如可接受资源文件的属性一般会兼容颜色类型,就是代码中的reference|color
源码中,各个view的background属性以及imageview的src属性便是这样。

使用

声明完成之后便是使用了,一般声明完成的属性,便可以在xml中直接敲出来,但是需要添加命名空间“app”,自定义属性使用时,也是需要使用app这个命名空间而非系统控件中的android命名空间。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    ...
    >

    <com.zx.rangselectbar.RangeSelectBar
        ...
        app:RSBBackgroundColor="@color/gray_word_light"
        ...
         />
</LinearLayout>

不过这个命名空间不需要手写,在xml中写入自定义属性,系统便会提示添加命名空间了。

读取

最后也是最重要的,就是读取自定义属性值了
系统读取到布局文件中的属性以及属性值以后,会通过第二个构造函数中的第二个属性“attrs”将该集合传入。

        TypedArray t = context.obtainStyledAttributes(attrs, R.styleable.RangeSelectBar);
        barHeight = t.getDimension(R.styleable.RangeSelectBar_RSBHeight, 4);
        barSelectColor = t.getColor(R.styleable.RangeSelectBar_RSBSelectColor, getResources().getColor(R.color.colorPrimary));
        sliderDrawable = t.getDrawable(R.styleable.RangeSelectBar_RSBSliderRes);
        leftSliderPosition = t.getInt(R.styleable.RangeSelectBar_RSBLeftSliderPosition, 0);
        hideLeftSlider = t.getBoolean(R.styleable.RangeSelectBar_RSBHideLeftSlide, false);
        // 最后记得回收
        t.recycle();

系统通过集合的形式来整合布局文件中的属性以及属性的值,其中,如果需要同时接收reference|color的值,一般使用Drawable来接受该属性。
这个过程一般会在构造阶段完成。

View生命周期的三个函数

自定义控件一文中详细写过这三个函数的作用,这里在总结一下。
measure测量当前控件需要的空间大小
layout对当前控件进行布局
draw将布局完成的控件呈现出来

onMeasure

我们知道,当不进行特殊处理时,布局文件中wrap与match属性所呈现的效果都是一致的,都会填充父布局,为了不让我们的控件像是一个不知道自己饭量的傻子,简单的处理是必须的。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);

        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        if (heightSpecMode == MeasureSpec.AT_MOST) {
            heightSpecSize = (int) (lagerBetweenTheTwo + barBottomPadding + textSize + getPaddingTop() + getPaddingBottom());
            setMeasuredDimension(widthSpecSize, heightSpecSize);
        }
    }

继承View的自定义控件,自身最小需要多少空间非常好算
本文中,范围选择器所需的最小高度就是
轴体/滑块比较宽的那一个+文字大小+两者间隙+上下间距。

而手机的显示器的宽比较短,宽度填充父容器才能得到比较好的显示效果,所以不进行处理。

onLayout

一般来说,自定义控件是不需要处理onLayout的,onlayout方法多用于父控件对子控件的布局控制。
但是本文中的自定义控件,虽说是继承于View,并且没有机会对其添加子控件,但是它自身却是由多个部分组成的,所以接下来需要处理布局。

此时,控件被测量完毕,开始计算各个部分的放置位置。控件总共由三部分组成,从上到下依次是

  • 控件主体,是长条加滑块的一个组合,取其中高度最高者
  • 控件主体与下方文字间隙的高度
  • 下方文字高度

然后按照事先想好的规则,就像拼积木一样将每个部分放上去,计算四角的坐标就可以了。
这段代码对讲解意义不大就不贴出来了。

onDraw

onLayout 部分已经计算好了每个部分的位置,最后一步只要将其画出来即可。
当然本例中,会在onDraw中计算滑块的四角坐标,因为滑块式随着手指移动的,而每次手指触摸滑块时,会实时计算滑块的坐标,并且通过invalidate方法对图形进行刷新,这样的话,在onDraw方法中,通过滑块中心点统一计算滑块的坐标反而是比较节省计算时间的一个方式。代码也更加简洁。

onTouch

当然对于一部分同学来说,如何在自定义控件中处理触摸事件也是一件比较头疼的事情,但这个不是自定义控件的难点所在,希望事件分发机制会对你有帮助。

以及最重要的

最重要的源码地址,欢迎下载,觉得有用请给我个star。


个人理解,难免有错误纰漏,欢迎指正。转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容