變化是永恒的施禾,產(chǎn)品需求穩(wěn)定不變是不可能的,和產(chǎn)品經(jīng)理互懟是沒有用的屎媳,但有一個(gè)方向是可以努力的:讓代碼更有彈性,以不變應(yīng)萬變论巍。
繼上一次發(fā)版前突然變更單選按鈕樣式之后烛谊,又新增了兩個(gè)和選項(xiàng)按鈕有關(guān)的需求。它們分別是多選和菜單選环壤。多選類似于原生CheckBox
,而菜單選是多選和單選的組合钞诡,類似于西餐點(diǎn)菜郑现,西餐菜單將食物分為前菜、主食荧降、湯接箫,每種只能選擇 1 個(gè)(即同組內(nèi)單選,多組間多選)朵诫。
上一篇中的自定義單選按鈕Selector + SelectorGroup
完美 hold 住按鈕樣式的變化辛友,這一次能否從容應(yīng)對(duì)新增需求?
自定義單選按鈕
回顧下Selector + SelectorGroup
的效果:
其中每一個(gè)選項(xiàng)就是Selector
剪返,它們的狀態(tài)被SelectorGroup
管理废累。
這組自定義控件突破了原生單選按鈕的布局限制,選項(xiàng)的相對(duì)位置可以用 xml 定義(原生控件只能是垂直或水平鋪開)脱盲,而且還可以方便地更換按鈕樣式以及定義選中效果(上圖中選中后有透明度動(dòng)畫)
實(shí)現(xiàn)關(guān)鍵邏輯如下:
- 單個(gè)按鈕是一個(gè)抽象容器控件邑滨,它可以被點(diǎn)擊并借助
View.setSelected()
記憶按鈕選中狀態(tài)。按鈕內(nèi)元素布局由其子類填充钱反。
public abstract class Selector extends FrameLayout implements View.OnClickListener {
//按鈕唯一標(biāo)示符
private String tag ;
private SelectorGroup selectorGroup;
public Selector(Context context) {
super(context);
initView(context, null);
}
private void initView(Context context, AttributeSet attrs) {
//構(gòu)建視圖(延遲到子類進(jìn)行)
View view = onCreateView();
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
this.addView(view, params);
this.setOnClickListener(this);
}
//構(gòu)建視圖(在子類中自定義視圖)
protected abstract View onCreateView();
//將按鈕添加到組
public Selector setGroup(SelectorGroup selectorGroup) {
this.selectorGroup = selectorGroup;
selectorGroup.addSelector(this);
return this;
}
@Override
public void setSelected(boolean selected) {
//設(shè)置按鈕選中狀態(tài)
boolean isPreSelected = isSelected();
super.setSelected(selected);
if (isPreSelected != selected) {
onSwitchSelected(selected);
}
}
//按鈕選中狀態(tài)變更(在子類中自定義變更效果)
protected abstract void onSwitchSelected(boolean isSelect);
@Override
public void onClick(View v) {
//通知選中組掖看,當(dāng)前按鈕被選中
if (selectorGroup != null) {
selectorGroup.onSelectorClick(this);
}
}
}
Selector
通過模版方法模式,將構(gòu)建按鈕視圖和按鈕選中效果延遲到子類構(gòu)建面哥。所以當(dāng)按鈕內(nèi)部元素布局發(fā)生改變時(shí)不需要修改Selector
哎壳,只需要新建它的子類。
- 單選組持有所有按鈕尚卫,當(dāng)按鈕被點(diǎn)擊時(shí)归榕,選中組遍歷其余按鈕并取消選中狀態(tài),以此來實(shí)現(xiàn)單選效果
public class SelectorGroup {
//持有所有按鈕
private Set<Selector> selectors = new HashSet<>();
public void addSelector(Selector selector) {
selectors.add(selector);
}
public void onSelectorClick(Selector selector) {
cancelPreSelector(selector);
}
//遍歷所有按鈕吱涉,將之前選中的按鈕設(shè)置為未選中
private void cancelPreSelector(Selector selector) {
for (Selector s : selectors) {
if (!s.equals(selector) && s.isSelected()) {
s.setSelected(false);
}
}
}
}
剝離行為
選中按鈕后的行為被寫死在SelectorGroup.onSelectorClick()
中蹲坷,這使得SelectorGroup
中的行為無法被替換驶乾。
每次行為擴(kuò)展都重新寫一個(gè)SelectorGroup
怎么樣?不行循签!因?yàn)?code>Selector是和SelectorGroup
耦合的级乐,這意味著Selector
的代碼也要跟著改動(dòng),這不符合開閉原則县匠。
SelectorGroup
中除了會(huì)變的“選中行為”之外风科,也有不會(huì)變的成分,比如“持有所有的按鈕”乞旦。是不是可以增加一層抽象將變化的行為封裝起來贼穆,使得SelectorGroup
與變化隔離?
接口是封裝行為的最佳選擇兰粉,可以運(yùn)用策略模式將選中行為封裝起來
策略模式的詳細(xì)介紹可以點(diǎn)擊這里故痊。
這樣就可以在外部構(gòu)建具體的選中行為,再將其注入到SelectorGroup
中玖姑,以實(shí)現(xiàn)動(dòng)態(tài)修改行為:
public class SelectorGroup {
private ChoiceAction choiceMode;
//注入具體選中行為
public void setChoiceMode(ChoiceAction choiceMode) {
this.choiceMode = choiceMode;
}
//當(dāng)按鈕被點(diǎn)擊時(shí)應(yīng)用選中行為
void onSelectorClick(Selector selector) {
if (choiceMode != null) {
choiceMode.onChoose(selectors, selector, onStateChangeListener);
}
}
//選中后的行為被抽象成接口
public interface ChoiceAction {
void onChoose(Set<Selector> selectors, Selector selector, StateListener stateListener);
}
}
將具體行為替換成接口后就好像是在原本嚴(yán)嚴(yán)實(shí)實(shí)的SelectorGroup
中挖了一個(gè)洞愕秫,只要符合這個(gè)洞形狀的東西都可以塞進(jìn)來。這樣就很靈活了焰络。
如果每次使用SelectorGroup
戴甩,都需要重新自定義選中行為也很費(fèi)力,所以在其中添加了最常用的單選和多選行為:
public class SelectorGroup {
public static final int MODE_SINGLE_CHOICE = 1;
public static final int MODE_MULTIPLE_CHOICE = 2;
private ChoiceAction choiceMode;
//通過這個(gè)方法設(shè)置自定義行為
public void setChoiceMode(ChoiceAction choiceMode) {
this.choiceMode = choiceMode;
}
//通過這個(gè)方法設(shè)置默認(rèn)行為
public void setChoiceMode(int mode) {
switch (mode) {
case MODE_MULTIPLE_CHOICE:
choiceMode = new MultipleAction();
break;
case MODE_SINGLE_CHOICE:
choiceMode = new SingleAction();
break;
}
}
//單選行為
private class SingleAction implements ChoiceAction {
@Override
public void onChoose(Set<Selector> selectors, Selector selector, StateListener stateListener) {
//將自己選中
selector.setSelected(true);
//將除了自己外的其他按鈕設(shè)置為未選中
cancelPreSelector(selector, selectors);
}
}
//多選行為
private class MultipleAction implements ChoiceAction {
@Override
public void onChoose(Set<Selector> selectors, Selector selector, StateListener stateListener) {
//反轉(zhuǎn)自己的選中狀態(tài)
boolean isSelected = selector.isSelected();
selector.setSelected(!isSelected);
}
}
將原本具體的行為都移到了接口中闪彼,而SelectorGroup
只和抽象的接口互動(dòng)甜孤,不和具體行為互動(dòng),這樣的代碼具有彈性畏腕。
現(xiàn)在只要像這樣就可以分別實(shí)現(xiàn)單選和多選:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//多選
SelectorGroup multipleGroup = new SelectorGroup();
multipleGroup.setChoiceMode(SelectorGroup.MODE_MULTIPLE_CHOICE);
((Selector) findViewById(R.id.selector_10)).setGroup(multipleGroup);
((Selector) findViewById(R.id.selector_20)).setGroup(multipleGroup);
((Selector) findViewById(R.id.selector_30)).setGroup(multipleGroup);
//單選
SelectorGroup singleGroup = new SelectorGroup();
singleGroup.setStateListener(new SingleChoiceListener());
((Selector) findViewById(R.id.single10)).setGroup(singleGroup);
((Selector) findViewById(R.id.single20)).setGroup(singleGroup);
((Selector) findViewById(R.id.single30)).setGroup(singleGroup);
}
}
在activity_main.xml
中布局了6個(gè)Selector
缴川,其中三個(gè)用于單選,三個(gè)用于多余描馅。
菜單選
這一次新需求是多選和單選的組合:菜單選二跋。這種模式將選項(xiàng)分成若干組,組內(nèi)單選流昏,組間多選扎即。看下使用策略模式重構(gòu)后的SelectorGroup
是如何輕松應(yīng)對(duì)的:
class OrderChoiceMode implements SelectorGroup.ChoiceAction {
@Override
public void onChoose(Set<Selector> selectors, Selector selector, SelectorGroup.StateListener stateListener) {
//同組互斥選中
String tagPrefix = getTagPrefix(selector.getSelectorTag());
cancelPreSelectorBySameTag(selectors, tagPrefix, stateListener);
selector.setSelected(true);
}
//在同一組中取消之前的選擇(要求同一組按鈕的tag具有相同的前綴)
private void cancelPreSelectorBySameTag(Set<Selector> selectors, String tagPrefix, SelectorGroup.StateListener stateListener) {
for (Selector selector : selectors) {
String prefix = getTagPrefix(selector.getSelectorTag());
if (prefix.equals(tagPrefix) && selector.isSelected()) {
selector.setSelected(false);
if (stateListener != null) {
stateListener.onStateChange(selector.getSelectorTag(), false);
}
}
}
}
//獲取標(biāo)簽前綴
private String getTagPrefix(String tag) {
//約定tag由兩個(gè)部分組成况凉,中間用下劃線分割:前綴_標(biāo)簽名
int index = tag.indexOf("_");
return tag.substring(0, index);
}
}
在SelectorGroup.ChoiceAction
中重新定義按鈕選中時(shí)的行為:同組互斥選中谚鄙,不同組可以多選。這就需要一種標(biāo)識(shí)組的方法刁绒,本文采用了給同組按鈕設(shè)置相同前綴的做法:
<resources>
<string name="tag_starters_pork">starters_pork</string>
<string name="tag_starters_duck">starters_duck</string>
<string name="tag_starters_springRoll">starters_springRoll</string>
<string name="tag_main_pizza">main_pizza</string>
<string name="tag_main_pasta">main_pasta</string>
<string name="tag_soup_mushroom">soup_mushroom</string>
<string name="tag_soup_scampi">soup_scampi</string>
</resources>
前菜闷营、主食、湯分別采用了starters、main傻盟、soup這樣的前綴速蕊。
然后就可以像這樣動(dòng)態(tài)的為SelectorGroup
擴(kuò)展菜單選行為了:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//order-choice
SelectorGroup orderGroup = new SelectorGroup();
orderGroup.setChoiceMode(new OrderChoiceMode());
((Selector) findViewById(R.id.selector_starters_duck)).setGroup(orderGroup);
((Selector) findViewById(R.id.selector_starters_pork)).setGroup(orderGroup);
((Selector) findViewById(R.id.selector_starters_springRoll)).setGroup(orderGroup);
((Selector) findViewById(R.id.selector_main_pizza)).setGroup(orderGroup);
((Selector) findViewById(R.id.selector_main_pasta)).setGroup(orderGroup);
((Selector) findViewById(R.id.selector_soup_mushroom)).setGroup(orderGroup);
((Selector) findViewById(R.id.selector_soup_scampi)).setGroup(orderGroup);
}
}
效果如下:
其中單選按鈕通過繼承Selector
重寫onSwitchSelected()
,定義了選中效果為愛心動(dòng)畫娘赴。
總結(jié)
至此规哲,選項(xiàng)按鈕這個(gè)repository已經(jīng)將兩種設(shè)計(jì)模式運(yùn)用于實(shí)戰(zhàn)。
運(yùn)用了模版方法模式將變化的按鈕布局和點(diǎn)擊效果和按鈕本身隔離诽表。
運(yùn)用了策略模式將變化的選中行為和選中組隔離唉锌。
在經(jīng)歷多次需求變更的突然襲擊后,遍體鱗傷的我們需要找出自救的方法:
實(shí)現(xiàn)需求前竿奏,通過分析需求識(shí)別出“會(huì)變的”和“不變的”邏輯袄简,增加一層抽象將“會(huì)變的”邏輯封裝起來,以實(shí)現(xiàn)隔離和分層泛啸,將“不變的”邏輯和抽象的互動(dòng)代碼在上層類中固定下來绿语。需求發(fā)生變化時(shí),通過在下層實(shí)現(xiàn)抽象以多態(tài)的方式來應(yīng)對(duì)候址。這樣的代碼具有彈性吕粹,就能以“不變的”上層邏輯應(yīng)對(duì)變化的需求。
talk is cheap, show me the code
實(shí)例代碼省略了一些非關(guān)鍵的細(xì)節(jié)宗雇,完整代碼在這里