android radiobutton_时隔一年,用新知识重构一个Android控件老库
一年前,用 Java 寫(xiě)了一個(gè)高可擴(kuò)展選擇按鈕庫(kù)。單個(gè)控件實(shí)現(xiàn)單選、多選、菜單選,且選擇模式可動(dòng)態(tài)擴(kuò)展。
一年后,一個(gè)新的需求要用到這個(gè)庫(kù),項(xiàng)目代碼已經(jīng)全 Kotlin 化,強(qiáng)硬地插入一些 Java 代碼顯得格格不入,Java 冗余的語(yǔ)法也降低了代碼的可讀性,于是決定用 Kotlin 重構(gòu)一番,在重構(gòu)的時(shí)候也增加了一些新的功能。這一篇分享下重構(gòu)的過(guò)程。
建議關(guān)注:不定時(shí)分享Android方面的技術(shù)、及大廠(chǎng)面試真題分析Android技術(shù)進(jìn)階屋?zhuanlan.zhihu.com選擇按鈕的可擴(kuò)展性主要體現(xiàn)在 4 個(gè)方面:
擴(kuò)展布局
原生的單選按鈕通過(guò)RadioButton+ RadioGroup實(shí)現(xiàn),他們?cè)诓季稚媳仨毷歉缸雨P(guān)系,而RadioGroup繼承自L(fǎng)inearLayout,遂單選按鈕只能是橫向或縱向鋪開(kāi),這限制的單選按鈕布局的多樣性,比如下面這種三角布局就難以用原生控件實(shí)現(xiàn):
為了突破這個(gè)限制,單選按鈕不再隸屬于一個(gè)父控件,它們各自獨(dú)立,可以在布局文件中任意排列,圖中 Activity 的布局文件如下(偽碼):
<AgeSelector表示一個(gè)具體的按鈕,本例中它是一個(gè)“上面是圖片,下面是文字”的單選按鈕。它繼承自抽象的Selector。
擴(kuò)展樣式
從業(yè)務(wù)上講,Selector長(zhǎng)什么樣是一個(gè)頻繁的變化點(diǎn),遂把“構(gòu)建按鈕樣式”這個(gè)行為設(shè)計(jì)成Selector的抽象函數(shù)onCreateView(),供子類(lèi)重寫(xiě)以實(shí)現(xiàn)擴(kuò)展。
publicSelector繼承自FrameLayout,實(shí)例化時(shí)會(huì)構(gòu)建按鈕視圖,并把該視圖作為孩子添加到自己的布局中。子類(lèi)通過(guò)重寫(xiě)onCreateView()擴(kuò)展按鈕樣式:
publicAgeSelector的樣式被定義在 xml 中。
按鈕被選中之后的樣式,也是一個(gè)業(yè)務(wù)上的變化點(diǎn),用同樣的思路可以將Selector這樣設(shè)計(jì):
// 抽象按鈕實(shí)現(xiàn)點(diǎn)擊事件將選中按鈕狀態(tài)變化的效果抽象成一個(gè)算法,延遲到子類(lèi)實(shí)現(xiàn):
publicAgeSelector在選中狀態(tài)變化時(shí)定義了一個(gè)背景色漸變動(dòng)畫(huà)。
函數(shù)類(lèi)型變量代替繼承
在抽象按鈕控件中,“按鈕樣式”和“按鈕選中狀態(tài)變換”被抽象成算法,算法的實(shí)現(xiàn)推遲到子類(lèi),用這樣的方式,擴(kuò)展按鈕的樣式和行為。繼承的一個(gè)后果就是類(lèi)數(shù)量的膨脹,有沒(méi)有什么辦法不用繼承就能擴(kuò)展按鈕樣式和行為?
可以把構(gòu)建按鈕樣式的成員方法onCreateView()設(shè)計(jì)成一個(gè)View類(lèi)型的成員變量,通過(guò)設(shè)值函數(shù)就可以改變其值。但按鈕選中狀態(tài)變換是一種行為,在 Java 中行為的表達(dá)方式只有方法,所以只能通過(guò)繼承來(lái)改變行為。
Kotlin 中有一種類(lèi)型叫函數(shù)類(lèi)型,運(yùn)用這種類(lèi)型,可以將行為保存在變量中:
class選中樣式和行為都被抽象為一個(gè)成員變量,只需賦值就可以動(dòng)態(tài)擴(kuò)展,不再需要繼承:
// 構(gòu)建按鈕實(shí)例在構(gòu)建Selector實(shí)例的同時(shí),指定了它的樣式和選中變換效果(其中運(yùn)用到 DSL 簡(jiǎn)化構(gòu)建代碼,詳細(xì)介紹可以點(diǎn)擊這里)
擴(kuò)展選中模式
單個(gè)Selector已經(jīng)可以很好的工作,但要讓多個(gè)Selector形成一種單選或多選的模式,還需要一個(gè)管理器來(lái)同步它們之間的選中狀態(tài),Java 版本的管理器如下:
publicSelectorGroup將選中模式抽象成接口ChoiceAction,以便通過(guò)setChoiceMode()動(dòng)態(tài)地?cái)U(kuò)展。
SelectorGroup還預(yù)定了兩種選中模式:單選和多選。
單選可以理解為:點(diǎn)擊按鈕時(shí),選中當(dāng)前的并取消選中之前的。多選可以理解為:點(diǎn)擊按鈕時(shí)無(wú)條件地反轉(zhuǎn)當(dāng)前選中狀態(tài)。
Selector會(huì)持有SelectorGroup實(shí)例,以便將按鈕點(diǎn)擊事件傳遞給它統(tǒng)一管理:
public然后就可以像這樣實(shí)現(xiàn)單選:
SelectorGroup也可以像這樣實(shí)現(xiàn)菜單選:
SelectorGroup將 Java 中的接口改成lambda,存儲(chǔ)在函數(shù)類(lèi)型的變量中,這樣可省去注入函數(shù),Kotlin 版本的SelectorGroup如下:
class然后就可以像這樣使用SelectorGroup:
// 構(gòu)建管理器構(gòu)建的兩個(gè)按鈕擁有相同的groupTag和SelectorGroup,所以他們屬于同一組并且是單選模式。
動(dòng)態(tài)綁定數(shù)據(jù)
項(xiàng)目中一個(gè)按鈕通常對(duì)應(yīng)于一個(gè)“數(shù)據(jù)”,比如下圖這種場(chǎng)景:
圖中的分組數(shù)據(jù)和按鈕數(shù)據(jù)都由服務(wù)器返回。點(diǎn)擊創(chuàng)建組隊(duì)時(shí),希望在selectChangeListener中拿到每個(gè)選項(xiàng)的 ID。那如何為Selector綁定數(shù)據(jù)?
當(dāng)然可以通過(guò)繼承,在Selector子類(lèi)中添加一個(gè)具體的業(yè)務(wù)數(shù)據(jù)類(lèi)型來(lái)實(shí)現(xiàn)。但有沒(méi)有更通用的方案?
ViewModel中設(shè)計(jì)了一種為其動(dòng)態(tài)擴(kuò)展屬性的方法,將它應(yīng)用在Selector中
class為Selector新增一個(gè)Map類(lèi)型的成員用于存放業(yè)務(wù)數(shù)據(jù),業(yè)務(wù)數(shù)據(jù)被聲明為Closeable的子類(lèi)型,目的是將各式各樣清理資源的行為抽象為close()方法,Selector重寫(xiě)了onDetachedFromWindow()且會(huì)遍歷每個(gè)業(yè)務(wù)數(shù)據(jù)并調(diào)用它們的close(),即當(dāng)它生命周期結(jié)束時(shí),釋放業(yè)務(wù)數(shù)據(jù)資源。
Selector也重載了設(shè)值和取值這兩個(gè)運(yùn)算符,以簡(jiǎn)化業(yè)訪(fǎng)問(wèn)業(yè)務(wù)數(shù)據(jù)的代碼:
// 游戲?qū)傩詫?shí)體類(lèi)因?yàn)橹剌d了運(yùn)算符,所以綁定和獲取游戲?qū)傩缘拇a都更加簡(jiǎn)短。
用泛型就一定要強(qiáng)轉(zhuǎn)?
綁定給 Selector 的數(shù)據(jù)被設(shè)計(jì)為泛型,業(yè)務(wù)層只有強(qiáng)轉(zhuǎn)成具體類(lèi)型才能使用,有什么辦法可以不要在業(yè)務(wù)層強(qiáng)轉(zhuǎn)?
CoroutineContext的鍵就攜帶了類(lèi)型信息:
public而且每一個(gè)CoroutineContext的具體子類(lèi)型都對(duì)應(yīng)一個(gè)靜態(tài)的鍵實(shí)例:
public這樣,不需要強(qiáng)轉(zhuǎn)就能獲得具體子類(lèi)型:
coroutineContext模仿CoroutineContext,業(yè)務(wù)Selector的鍵設(shè)計(jì)了一個(gè)帶泛型的接口:
interface在為Selector綁定數(shù)據(jù)時(shí)需要先構(gòu)建“鍵實(shí)例”:
val傳入的鍵帶有類(lèi)型信息,可以在取值方法中提前完成強(qiáng)轉(zhuǎn)再返回給業(yè)務(wù)層使用:
// 值的具體類(lèi)型被參數(shù) key 指定,強(qiáng)轉(zhuǎn)之后再返回給業(yè)務(wù)層 operator fun <T : Closeable> get(key: Key<T>): T? = (tags.getOrElse(key, { null })) as T借助于 DSL 根據(jù)數(shù)據(jù)動(dòng)態(tài)地構(gòu)建選擇按鈕就變得很輕松,上一幅 Gif 展示的界面代碼如下:
// 游戲?qū)傩约蠈?shí)體類(lèi)這是兩個(gè) Demo 中用到的數(shù)據(jù)實(shí)體類(lèi),真實(shí)項(xiàng)目中他們應(yīng)該是服務(wù)器返回的,簡(jiǎn)單起見(jiàn),本地模擬一些數(shù)據(jù):
val最后用 DSL 動(dòng)態(tài)構(gòu)建選擇按鈕:
// 縱向布局其中的按鈕視圖、按鈕控制器、按鈕效果變換器定義如下:
// 與游戲?qū)傩詫?duì)應(yīng)的鍵 與50位技術(shù)專(zhuān)家面對(duì)面20年技術(shù)見(jiàn)證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的android radiobutton_时隔一年,用新知识重构一个Android控件老库的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 地面装饰材料(地面装修用什么材料好)
- 下一篇: android 带记忆功能的播放器源码,