Kotlin 第一弹:自定义 ViewGroup 实现流式标签控件

此文来自我的 CSDN 技术博客,博文写作于 2017 年 5 月。现在迁移到公众号,有疑问的地方欢迎大家一起研讨。

上周 Google I/O 大会的召开,宣布了 Kotlin 语言正式成为了官方开发语言。一时间 Android 开发者的圈子炸开了锅,各种关于 Kotlin 的资料介绍也如雨后春笋不断的冒出。

大家都对这比较关心,我觉得最大的原因是,当初宣布 Android Studio 成为官方 IDE 后,很多开发者都还在坚守 Eclipse,但是现在来看,大部分都转为 Android Studio 开发了。所以,开发者肯定担心,Kotlin 会不会也最后完美取代 Java 呢?

我是在官网看了下资料,简单入门的。

我确实感受到了 Kotlin 与 Java 的不同,但我不觉得 Java 已经老态龙钟了,相反我对 Java 有感情,未来的几年我将会更深入地学习和研究它的语言特性和虚拟机底层细节。

我认为编程思想是最重要的,语言是其次。所以,我可以用 Kotlin 来替代平时通过 Java 实现的代码。

光说不练,假把式。语法大家都看得懂,关键是在于对于陌生事物,只有反复刻意的练习,你才能进入自己的舒适区。

好了,下面进入我们的主题,通过 Kotlin 来实现一个自定义 ViewGroup。这篇博文的目的也算作是个人针对 Kotlin 学习的编程练习吧。

当然,首先我已经默认大家知道怎么通过 Android Studio 创建 Kotlin 工程了。如果还不熟悉的话,请自行查阅相关资料。

然后,这篇文章目的也不是为了讲解 kotlin 的基础语法的,也希望不熟悉 kotlin 的同学先去官网通读一遍基础语法。

不过,我还是会在博文中适当地介绍一下 kotlin 一些语法特性。

自定义 ViewGroup 之流式标签控件

对于软件开发者而言,流式标签控件想必大家一定见过,如下图:

至于为什么叫做流式标签呢?我想可能因为是在 Html 开发时,网页的布局有个流式布局的概念的,模块都是自动向左贴紧,如果屏幕不能在一行显示内容,就会进行适当的换行。上面的这个控件的场景比较像,所以叫流式标签控件。也许讲得不对,但便于自己的理解,如有错误希望热心网友批评指出。

显然这个流式标签控件是一个 ViewGroup,所以我们就需要自定义这样一个 ViewGroup,取名字叫做 TagView,后方中所有的 TagView 都是指代要实现的这个流式标签控件。

测量尺寸

我们大多都知道,自定义一个 View 需要测量、布局、绘制三个流程。而我个人觉得这三个流程中,测量是最让初学者头痛的问题。因此我特地写了一篇博文《长谈:关于 View Measure 测量机制,让我一次把话说完 》 为的就是想一次性把测量细节说清楚,有兴趣的同学可以去看看。好了,回到主题,接下来我们就需要来思考怎么样测量 TagView 的尺寸。

自定义 View 需要考虑到两种测量模式:MeasureSpec.EXACTLY 和 MeasureSpec.AT_MOST。

MeasureSpec.EXACTLY

对于这种模式,我们知道 layout_width 或者 layout_height 的取值为 match_parent 或者是具体的尺寸如 30dp。针对这种情况,其实我们用不着处理,因为 parent 在子 View 的 onMeasure() 中传递的尺寸规格里面就包含了建议尺寸,而这个尺寸是精确的,所以我们只需要在 onMeasure() 方法的最后调用 setMeasureDimension() 并传入相应的值便是。

MeasureSpec.AT_MOST

对于这种测量模式,开发者面对的处境难一些。对于自定义 View 而言要根据业务需求,确定好自身的内容显示范围。而对于自定义 ViewGroup 而言,它的难度更加提高了。因为它的尺寸是要根据子 view 来确定的,所以测量子 View 的尺寸也就成了它的第一部。好在系统自带相应的 API,measureChildren() 和 measureChild() 方法,减少了开发者的负担。

但是,测量了子 View 只是第一步,接下来的这一步麻烦的地方是要结合布局来确定一个 ViewGroup 它最终在某个维度上的尺寸。而每个 ViewGroup 要实现的业务需求不一样,所以也没有用一种规格来适用于所有的 ViewGroup,只能是具体情况具体分析了。下面我们就来具体分析下 TagView。

经观察,TagView 最重要的尺寸信息其实就是它的 width。因为所有的子 View 不能在一行排列,所有的子 View 按自左向右的顺序排列,如果当前子 View 的显示范围超过了图中红框部分,也就是 parent 本身的尺寸范围,那么子 View 就应该换行在新的一行重新自左向右顺序排列,由于每个子 View 的宽度不一样,所以会造成每一行需要的宽度也不一样。

在上面的线框图中,TagView 有 3 行,而行所需要的宽度也是不一样的,这就造成了一个问题,对于 TagView 整体而言,在 layout_width 取值为 wrap_content 的时候,究竟哪一些行的宽度作为 TagView 的宽度尺寸呢?答案是明显的,肯定是宽度值最大的那一数值。

而 layout_height 为 wrap_content 而言,TagView 的高度值自然是每一行的高度值之和,这里为了美观而言。假定每个子 View 的高度是一致的。

好了,我们整理下思路。

测量子 View 的尺寸。

根据布局的特点,测量最小的宽高尺寸,并且这个数值不能大于 parent 给出的建议 size。

对于宽度而言,由于 TagView 每一行宽度可能不同,所以需要找出最宽的那一行。

对于高度而言,TagView 整体高度就是各行之和。

当然在 MeasureSpec.AT_MOST 测量规格下,尺寸数值是要包含 TagView 自身的 padding 和子 View 的 margin 值的。

布局

根据 TagView 的业务需求,所有的子 View 按自左向右的顺序排列,如果当前子 View 的显示范围超过了图中红框部分,也就是 parent 本身的尺寸范围,那么子 View 就应该换行在新的一行重新自左向右顺序排列。所以编码的思路便是遍历所有的子 View,然后依次排列,并且每次对子 View 进行 layout 之前,要预算它的显示范围,如果超出了 parent 的宽度,那么它就需要换行。

绘制

自定义 View 中绘制相关的方法是 onDraw(),但在 TagView 中它并不需要绘制特殊的界面效果,所以我们可以不理它。

具体编码

上面分析了要实现这样一个 TagView 的思路,接下来就是具体编码的过程。

创建一个 Kotlin 类

Kotlin 同 Java 一样,用关键字 class 来定义一个类,不同的是 Java 用 extends 表示继承,而 Kotlin 用一个 :实现。

TagView 需要在 xml 布局文件中使用,所以仅仅定义一个 TagView(context:Context) 构造函数是不够的,我们还需要定义另外一个。在 Kotlin 中构造函数与 Java 的构造方法也有不同。大家可以仔细感受一下。

大家仔细观察一下,第二个构造函数,它委托调用了 this()。这是因为有一条规则:

如果类有一个主构造函数(无论有无参数),每个次构造函数需要直接或间接委托给主构造函数,用this关键字

大家看到我在构造函数中获取了 mBackgroundDrawable 的值,其实这一步是有意为之,我特地为了测试在 kotlin 中获取自定义属性弄了这么一处。

attrs.xml

另外注意的地方是,我们希望子 View 拥有 margin 属性。所以我们要复写一个方法。

编写 onMeasure() 逻辑代码

前面已经详细分析了思路,所以呢接下来的编程自然是水到渠成。

代码何其相似,简直和 Java 实现流程一模一样,不一样的只是变量和方法的定义形式。

kotlin 函数的定义

kotlin 用一个关键字 fun 定义函数,如果不指定返回值,它返回的是 Unit,Unit 跟 Java 中的 Void 类似,但 Unit 是真正的对象。典型的 kotlin 函数形式如下:

kotlin 中变量的定义都是 x : 类型 的形式,并且不同于 Java,函数的返回值也是在方法名最后用 :类型如上面示例的右括号后面的 :Int。

kotlin 变量的定义

kotlin 的变量分为 val( 不可变) 和 var( 可变 )。val 同 Java 中的 final 关键字

kotlin 建议定义变量的时候尽量用 val,当然在确定变量会多次赋值时用 var。

kotlin 中的条件循环

上面的代码我们看到了一个 for 循环,但是跟 Java 中的也不一样。

通常的 for 循环如下形式

collection 是一个集合,in 是关键字,表示遍历 collection 中每一个 item。

当然 for 循环还有以 index 形式,这是广大 Java 开发者乐于接受的。上面的代码,遍历子 View 时就是这种方式。

好的,上面简单回顾了一下 kotlin 的基础语法。现在回到 TagView 代码本身。

在 onMeasure() 中我给代码进行了较为详细的注释,开发流程也是根据之前分析的思路。相信大家能看得比较明白。

核心就在于 MeasureSpec.AT_MOST 模式下,确定最宽的那一行的宽度值,然后根据行数确定 TagView 的高度。

编写 onLayout 的逻辑代码

onLayout 与布局有关,其实前面的 onMeasure() 方法中确定宽高尺寸的时候,就是根据布局方案来的。

主要逻辑就是当子 View 一行的宽度要超过 TagView 本身尺寸时就换行。代码非常简单,不再详细讲解。

编写测试代码

我们默认为 TagView 的子 View 为 TextView。所以,为了美观大方,我们先给它定义一个背景。我们可以用一个 shape 实现。

我们现在可以对 TagView 进行测试了,我们可以在布局文件 activity_main.xml 中添加代码。

然后,它的最终效果图如下:

自此,TagView 就算初步完成了。但是还是有很多地方需要优化。

TagView 优化之处

针对子 View visibility 为 gone 的处理

上面的例子中,我们默认所有的子 View 都是可见的,实际上呢?如果我们将测试代码稍微改一下,会怎么样?

我们将两个选项设置为 gone,实际效果怎么样呢?

可以发现其实没有多大影响,TagView 还是按照正确的方式显示。我猜应该是获取子元素的时候,属性为 gone 的子元素不能获取。

TagView 中子 View 的高度问题。

按照之前的设想,我们假定的是每个子 View 的高度是一致的,但是如果实际运行中不一致呢?会出现什么情况?

我们将第一个子 View 高度设置为 50 dp,显然它的高度比其它的 TextView 要高,这个时候 TagView 会发生什么呢?

这个结果肯定就不是我们想要的了。我们希望每个子 View 高度一致,如果不一致也行,尊重你,但是我们需要在 TagView 中进行处理,把每一行的行高变成那一行中最高的子 View 的高度值。所以 TagView 代码要做处理。

如上面代码所示,给每行确定好高度之后,TagView 显示就很完善了。

TagView 的子 View

其实到了这里的时候,这个初级的 TagView 就已经完成了。但是功能还是比较简单。它的子 View 都是 TextView 然后背景在 xml 中用统一的 shape 来代替,所以我们可以实现圆角矩形的式样。如果我们还想更自由一点,那么就需要自定义一个 View 了,那将是另外一话题了。

自定义一个 View,步骤无非也是测量、绘制。因为篇幅过长,接下来的内容我简单带过。

我给自定义的 View 取名叫做 Tag。它是一个封闭图形,左边一个半圆,中间一个矩形,右边是一个半圆。然后,内容区域主要是 title 部分,它可以自定义 textSize,还有距中间矩形的间距。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180905G1L2IG00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券