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

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

建议关注:不定时分享Android方面的技术、及大厂面试真题分析

Android技术进阶屋​zhuanlan.zhihu.com

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

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

扩展布局

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

为了突破这个限制,单选按钮不再隶属于一个父控件,它们各自独立,可以在布局文件中任意排列,图中 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,所以他们属于同一组并且是单选模式。

动态绑定数据

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

图中的分组数据和按钮数据都由服务器返回。点击创建组队时,希望在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 动态构建选择按钮:

// 纵向布局

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

// 与游戏属性对应的键

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

  1. android绘制view的撤销,DrawingView android 上的一个涂鸦控件。可以设置画笔的粗细,颜色,撤销上一笔涂鸦,提供保存图片的接口。 @codeKK Android开源站...

    DrawingView 的原型来自DrawingView-Android,是 android 的一个可涂鸦控件. 之所以做这个控件是因为前段时间写了一个截图应用需要用到涂鸦功能,现在把涂鸦的控件单独拿 ...

  2. Android 软键盘弹出时把布局顶上去,控件乱套解决方法

    Android 软键盘弹出时把布局顶上去,控件乱套解决方法 参考文章: (1)Android 软键盘弹出时把布局顶上去,控件乱套解决方法 (2)https://www.cnblogs.com/zhuj ...

  3. 让一个图片填满一个控件_如何在Android中实现一个全景图控件(二)

    一.背景 在 如何在Android中实现一个全景图控件(一)中,介绍了项目的一些基本情况(有 demo 演示),如果项目对你有帮助,希望文章赏个赞,项目 star 一下. 项目地址:https://g ...

  4. 新发布AlbumOnNet 、dotnetCharting控件注册资料

    新发布AlbumOnNet .dotnetCharting控件注册资料,见http://www.cnblogs.com/midea0978/category/13271.html

  5. Android 安卓实现Neumorphism(新拟物化)UI控件

    文章目录 效果图 第三方库支持 代码示例 引入第三方库 黑暗模式布局 明亮风格 文档说明(案例) 效果图 第三方库支持 Github:https://github.com/fornewid/neumo ...

  6. android+捕获google账户+cancel按钮,MVVM: 这是一个android MVVM 框架,基于谷歌dataBinding技术实现...

    MVVM 这是一个android MVVM 框架,基于谷歌dataBinding技术实现.dataBinding 实现的 V 和 VM的关联:使用IOC架构实现了 M 和 V的关联. 框架具有以下功能 ...

  7. Android 自定义控件-高仿猎豹清理大师自定义内存开口圆环控件

    技术:Android+java 概述 看见猎豹清理大师的内存开口圆环比例进度 挺有意思的,于是就是想自己实现下这样的效果,于是反编译了猎豹清理 大师的app看了下,原来是有两张图,于是脑子里就过了下思 ...

  8. [WPF]winfom中ShowWPF新窗口时TextBox等控件无法输入问题解决方法 .

    项目使用ElementHost方式加载WPF,刚才直接在WPF中使用Show,而不是ShowDialog新开窗口时,窗口中的控件无法输入,但可以直接粘贴. 原来是因为WinForm与WPF实现Inpu ...

  9. Android 文本实现跑马灯效果 用自带的TextView控件

    注意跑马灯需要文本已经确定的情况下设置 1.第一种方式在布局代码中 <TextViewandroid:id="@+id/music_name_tv"android:layou ...

最新文章

  1. ScrollView和ListView冲突解决
  2. Python02 标准输入输出、数据类型、变量、随记数的生成、turtle模块详解
  3. 共享单车数据集_共享单车数据可视化报告
  4. 【转载】创建型-工厂方法模式
  5. 前端学习(1174):repeat方法
  6. [转载]C#异步委托的用法 .
  7. 深度学习笔记(36) 边界框预测
  8. 关于Android屏幕适配
  9. 遍历数组的两种for循环方式以及一种Arrays方式
  10. mybatis-plus更新问题 全量更新、只更新部分属性
  11. java sigar 远程_Java运用第三方开源jar包sigar.jar获取服务器信息
  12. jieba分词的原理
  13. PropertyUtils嵌套属性的使用
  14. 简约竞聘个人简历自我介绍PPT模板
  15. 对偶传播神经网络(CPN)
  16. 模态框获取页面请求数据
  17. html图片自带闪光效果,CSS实现的一闪而过的图片闪光效果
  18. 摆线方程推导(向量法)
  19. 批量修改系统bios选项(dell适用)
  20. FileUpload使用教程

热门文章

  1. juniper srx解决内网不能telnet公网IP的方法
  2. 分享WCF聊天程序--WCFChat
  3. 学习 WCF (3)--开发WCF客户程序
  4. Seata 是什么?
  5. idea中如何创建接口
  6. 查看mysql主从配置的状态及修正 slave不启动问题
  7. 打开闪光灯_用手机拍照这么久,你居然还不知道闪光灯怎么用
  8. 计算机安全原理与实践_《计算机图形学原理及实践》学习笔记之第三章
  9. 系统架构设计师与系统分析师历年实体分析与解答下载_医疗知识图谱问答系统探究(一)...
  10. eclipse jsp没有提示_JSP+Struts2+JDBC+Mysql实现的校园宿舍管理系统