android radiobutton_时隔一年,用新知识重构一个Android控件老库

06adfb7748258365bc09edc970babdbd.png

一年前,用 Java 写了一个高可扩展选择按钮库。单个控件实现单选、多选、菜单选,且选择模式可动态扩展。

一年后,一个新的需求要用到这个库,项目代码已经全 Kotlin 化,强硬地插入一些 Java 代码显得格格不入,Java 冗余的语法也降低了代码的可读性,于是决定用 Kotlin 重构一番,在重构的时候也增加了一些新的功能。这一篇分享下重构的过程。

建议关注:不定时分享Android方面的技术、及大厂面试真题分析
Android技术进阶屋​zhuanlan.zhihu.com
b7d611f0a6093ff7cd64690cc9c0161c.png

选择按钮的可扩展性主要体现在 4 个方面:

  1. 选项按钮布局可扩展
  2. 选项按钮样式可扩展
  3. 选中样式可扩展
  4. 选择模式可扩展

扩展布局

原生的单选按钮通过RadioButton+ RadioGroup实现,他们在布局上必须是父子关系,而RadioGroup继承自LinearLayout,遂单选按钮只能是横向或纵向铺开,这限制的单选按钮布局的多样性,比如下面这种三角布局就难以用原生控件实现:

228c91c8c990cdc8616417a2e13fe3be.gif

为了突破这个限制,单选按钮不再隶属于一个父控件,它们各自独立,可以在布局文件中任意排列,图中 Activity 的布局文件如下(伪码):

<

AgeSelector表示一个具体的按钮,本例中它是一个“上面是图片,下面是文字”的单选按钮。它继承自抽象的Selector。

扩展样式

从业务上讲,Selector长什么样是一个频繁的变化点,遂把“构建按钮样式”这个行为设计成Selector的抽象函数onCreateView(),供子类重写以实现扩展。

public 

Selector继承自FrameLayout,实例化时会构建按钮视图,并把该视图作为孩子添加到自己的布局中。子类通过重写onCreateView()扩展按钮样式:

public 

AgeSelector的样式被定义在 xml 中。

按钮被选中之后的样式,也是一个业务上的变化点,用同样的思路可以将Selector这样设计:

// 抽象按钮实现点击事件

将选中按钮状态变化的效果抽象成一个算法,延迟到子类实现:

public 

AgeSelector在选中状态变化时定义了一个背景色渐变动画。

函数类型变量代替继承

在抽象按钮控件中,“按钮样式”和“按钮选中状态变换”被抽象成算法,算法的实现推迟到子类,用这样的方式,扩展按钮的样式和行为。

继承的一个后果就是类数量的膨胀,有没有什么办法不用继承就能扩展按钮样式和行为?

可以把构建按钮样式的成员方法onCreateView()设计成一个View类型的成员变量,通过设值函数就可以改变其值。但按钮选中状态变换是一种行为,在 Java 中行为的表达方式只有方法,所以只能通过继承来改变行为。

Kotlin 中有一种类型叫函数类型,运用这种类型,可以将行为保存在变量中:

class 

选中样式和行为都被抽象为一个成员变量,只需赋值就可以动态扩展,不再需要继承:

// 构建按钮实例

在构建Selector实例的同时,指定了它的样式和选中变换效果(其中运用到 DSL 简化构建代码,详细介绍可以点击这里)

扩展选中模式

单个Selector已经可以很好的工作,但要让多个Selector形成一种单选或多选的模式,还需要一个管理器来同步它们之间的选中状态,Java 版本的管理器如下:

public 

SelectorGroup将选中模式抽象成接口ChoiceAction,以便通过setChoiceMode()动态地扩展。

SelectorGroup还预定了两种选中模式:单选和多选。

单选可以理解为:点击按钮时,选中当前的并取消选中之前的。
多选可以理解为:点击按钮时无条件地反转当前选中状态。

Selector会持有SelectorGroup实例,以便将按钮点击事件传递给它统一管理:

public 

然后就可以像这样实现单选:

SelectorGroup 

也可以像这样实现菜单选:

SelectorGroup 

将 Java 中的接口改成lambda,存储在函数类型的变量中,这样可省去注入函数,Kotlin 版本的SelectorGroup如下:

class 

然后就可以像这样使用SelectorGroup:

// 构建管理器

构建的两个按钮拥有相同的groupTag和SelectorGroup,所以他们属于同一组并且是单选模式。

动态绑定数据

项目中一个按钮通常对应于一个“数据”,比如下图这种场景:

4a98312ed7fd0060daed023f52a166c3.png

图中的分组数据和按钮数据都由服务器返回。点击创建组队时,希望在selectChangeListener中拿到每个选项的 ID。那如何为Selector绑定数据?

当然可以通过继承,在Selector子类中添加一个具体的业务数据类型来实现。但有没有更通用的方案?

ViewModel中设计了一种为其动态扩展属性的方法,将它应用在Selector中

class 

为Selector新增一个Map类型的成员用于存放业务数据,业务数据被声明为Closeable的子类型,目的是将各式各样清理资源的行为抽象为close()方法,Selector重写了onDetachedFromWindow()且会遍历每个业务数据并调用它们的close(),即当它生命周期结束时,释放业务数据资源。

Selector也重载了设值和取值这两个运算符,以简化业访问业务数据的代码:

// 游戏属性实体类

因为重载了运算符,所以绑定和获取游戏属性的代码都更加简短。

用泛型就一定要强转?

绑定给 Selector 的数据被设计为泛型,业务层只有强转成具体类型才能使用,有什么办法可以不要在业务层强转?

CoroutineContext的键就携带了类型信息:

public 

而且每一个CoroutineContext的具体子类型都对应一个静态的键实例:

public 

这样,不需要强转就能获得具体子类型:

coroutineContext

模仿CoroutineContext,业务Selector的键设计了一个带泛型的接口:

interface 

在为Selector绑定数据时需要先构建“键实例”

val 

传入的键带有类型信息,可以在取值方法中提前完成强转再返回给业务层使用:

// 值的具体类型被参数 key 指定,强转之后再返回给业务层
operator fun <T : Closeable> get(key: Key<T>): T? = (tags.getOrElse(key, { null })) as T

借助于 DSL 根据数据动态地构建选择按钮就变得很轻松,上一幅 Gif 展示的界面代码如下:

// 游戏属性集合实体类

这是两个 Demo 中用到的数据实体类,真实项目中他们应该是服务器返回的,简单起见,本地模拟一些数据:

val 

最后用 DSL 动态构建选择按钮:

// 纵向布局

其中的按钮视图、按钮控制器、按钮效果变换器定义如下:

// 与游戏属性对应的键
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值