android radiobutton_时隔一年,用新知识重构一个Android控件老库
一年前,用 Java 寫了一個高可擴展選擇按鈕庫。單個控件實現單選、多選、菜單選,且選擇模式可動態擴展。
一年后,一個新的需求要用到這個庫,項目代碼已經全 Kotlin 化,強硬地插入一些 Java 代碼顯得格格不入,Java 冗余的語法也降低了代碼的可讀性,于是決定用 Kotlin 重構一番,在重構的時候也增加了一些新的功能。這一篇分享下重構的過程。
建議關注:不定時分享Android方面的技術、及大廠面試真題分析Android技術進階屋?zhuanlan.zhihu.com選擇按鈕的可擴展性主要體現在 4 個方面:
擴展布局
原生的單選按鈕通過RadioButton+ RadioGroup實現,他們在布局上必須是父子關系,而RadioGroup繼承自LinearLayout,遂單選按鈕只能是橫向或縱向鋪開,這限制的單選按鈕布局的多樣性,比如下面這種三角布局就難以用原生控件實現:
為了突破這個限制,單選按鈕不再隸屬于一個父控件,它們各自獨立,可以在布局文件中任意排列,圖中 Activity 的布局文件如下(偽碼):
<AgeSelector表示一個具體的按鈕,本例中它是一個“上面是圖片,下面是文字”的單選按鈕。它繼承自抽象的Selector。
擴展樣式
從業務上講,Selector長什么樣是一個頻繁的變化點,遂把“構建按鈕樣式”這個行為設計成Selector的抽象函數onCreateView(),供子類重寫以實現擴展。
publicSelector繼承自FrameLayout,實例化時會構建按鈕視圖,并把該視圖作為孩子添加到自己的布局中。子類通過重寫onCreateView()擴展按鈕樣式:
publicAgeSelector的樣式被定義在 xml 中。
按鈕被選中之后的樣式,也是一個業務上的變化點,用同樣的思路可以將Selector這樣設計:
// 抽象按鈕實現點擊事件將選中按鈕狀態變化的效果抽象成一個算法,延遲到子類實現:
publicAgeSelector在選中狀態變化時定義了一個背景色漸變動畫。
函數類型變量代替繼承
在抽象按鈕控件中,“按鈕樣式”和“按鈕選中狀態變換”被抽象成算法,算法的實現推遲到子類,用這樣的方式,擴展按鈕的樣式和行為。繼承的一個后果就是類數量的膨脹,有沒有什么辦法不用繼承就能擴展按鈕樣式和行為?
可以把構建按鈕樣式的成員方法onCreateView()設計成一個View類型的成員變量,通過設值函數就可以改變其值。但按鈕選中狀態變換是一種行為,在 Java 中行為的表達方式只有方法,所以只能通過繼承來改變行為。
Kotlin 中有一種類型叫函數類型,運用這種類型,可以將行為保存在變量中:
class選中樣式和行為都被抽象為一個成員變量,只需賦值就可以動態擴展,不再需要繼承:
// 構建按鈕實例在構建Selector實例的同時,指定了它的樣式和選中變換效果(其中運用到 DSL 簡化構建代碼,詳細介紹可以點擊這里)
擴展選中模式
單個Selector已經可以很好的工作,但要讓多個Selector形成一種單選或多選的模式,還需要一個管理器來同步它們之間的選中狀態,Java 版本的管理器如下:
publicSelectorGroup將選中模式抽象成接口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 動態構建選擇按鈕:
// 縱向布局其中的按鈕視圖、按鈕控制器、按鈕效果變換器定義如下:
// 與游戲屬性對應的鍵 與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的android radiobutton_时隔一年,用新知识重构一个Android控件老库的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 地面装饰材料(地面装修用什么材料好)
- 下一篇: java 设计模式_快速上手Java设计