變化是永恆的,產品需求穩定不變是不可能的,和產品經理互懟是沒有用的,但有一個方向是能夠努力的:讓代碼更有彈性,以不變應萬變。git
繼上一次發版前忽然變動單選按鈕樣式以後,又新增了兩個和選項按鈕有關的需求。它們分別是多選和菜單選。多選相似於原生CheckBox
,而菜單選是多選和單選的組合,相似於西餐點菜,西餐菜單將食物分爲前菜、主食、湯,每種只能選擇 1 個(即同組內單選,多組間多選)。github
上一篇中的自定義單選按鈕Selector + SelectorGroup
完美 hold 住按鈕樣式的變化,這一次可否從容應對新增需求?spring
回顧下Selector + SelectorGroup
的效果: 設計模式
其中每個選項就是Selector
,它們的狀態被SelectorGroup
管理。bash
這組自定義控件突破了原生單選按鈕的佈局限制,選項的相對位置能夠用 xml 定義(原生控件只能是垂直或水平鋪開),並且還能夠方便地更換按鈕樣式以及定義選中效果(上圖中選中後有透明度動畫)dom
實現關鍵邏輯以下:ide
View.setSelected()
記憶按鈕選中狀態。按鈕內元素佈局由其子類填充。public abstract class Selector extends FrameLayout implements View.OnClickListener {
//'按鈕惟一標示符'
private String tag;
//'按鈕所在組的標示符,單選的按鈕應該設置相同的groupTag'
private String groupTag;
private SelectorGroup selectorGroup;
public Selector(Context context) {
super(context);
initView(context, null);
}
private void initView(Context context, AttributeSet attrs) {
//'構建視圖(延遲到子類進行)'
View view = onCreateView();
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
this.addView(view, params);
this.setOnClickListener(this);
}
//'構建視圖(在子類中自定義視圖)'
protected abstract View onCreateView();
//'設置按鈕的按鈕組'
public Selector setGroup(String groupTag, SelectorGroup selectorGroup) {
this.selectorGroup = selectorGroup;
this.groupTag = groupTag;
return this;
}
@Override
public void setSelected(boolean selected) {
//'設置按鈕選中狀態'
boolean isPreSelected = isSelected();
super.setSelected(selected);
if (isPreSelected != selected) {
onSwitchSelected(selected);
}
}
//'按鈕選中狀態變動(在子類中自定義變動效果)'
protected abstract void onSwitchSelected(boolean isSelect);
@Override
public void onClick(View v) {
//'通知選中組,當前按鈕被選中'
if (selectorGroup != null) {
selectorGroup.onSelectorClick(this);
}
}
}
複製代碼
Selector
經過模版方法模式,將構建按鈕視圖和按鈕選中效果延遲到子類構建。因此當按鈕內部元素佈局發生改變時不須要修改Selector
,只須要新建它的子類。佈局
public class SelectorGroup {
//'持有上次選中的按鈕組'
private HashMap<String, Selector> selectorMap = new HashMap<>();
//'獲取上次選中按鈕'
public Selector getPreSelector(String groupTag) {
return selectorMap.get(groupTag);
}
//'取消上次選中按鈕'
private void cancelPreSelector(Selector selector) {
String groupTag = selector.getGroupTag();
Selector preSelector = getPreSelector(groupTag);
if (preSelector != null) {
preSelector.setSelected(false);
}
}
void onSelectorClick(Selector selector) {
//'選中當前按鈕'
selector.setSelected(true);
//'取消以前按鈕'
cancelPreSelector(selector);
//'將此次選中按鈕保存在map中'
selectorMap.put(selector.getGroupTag(), selector);
}
}
複製代碼
選中按鈕後的行爲被寫死在SelectorGroup.onSelectorClick()
中,這使得SelectorGroup
中的行爲沒法被替換。post
每次行爲擴展都從新寫一個SelectorGroup
怎麼樣?不行!由於Selector
是和SelectorGroup
耦合的,這意味着Selector
的代碼也要跟着改動,這不符合開閉原則。動畫
SelectorGroup
中除了會變的「選中行爲」以外,也有不會變的成分,好比「持有上次選中按鈕」。是否是能夠增長一層抽象將變化的行爲封裝起來,使得SelectorGroup
與變化隔離?
接口是封裝行爲的最佳選擇,能夠運用策略模式將選中行爲封裝起來
策略模式的詳細介紹能夠點擊這裏。
這樣就能夠在外部構建具體的選中行爲,再將其注入到SelectorGroup
中,以實現動態修改行爲:
public class SelectorGroup {
private ChoiceAction choiceMode;
//'注入具體選中行爲'
public void setChoiceMode(ChoiceAction choiceMode) {
this.choiceMode = choiceMode;
}
//'當按鈕被點擊時應用選中行爲'
void onSelectorClick(Selector selector) {
if (choiceMode != null) {
choiceMode.onChoose(selector, this, onStateChangeListener);
}
}
//'選中後的行爲被抽象成接口'
public interface ChoiceAction {
void onChoose(Selector selector, SelectorGroup selectorGroup, StateListener stateListener);
}
}
複製代碼
將具體行爲替換成接口後就好像是在本來嚴嚴實實的SelectorGroup
中挖了一個洞,只要符合這個洞形狀的東西均可以塞進來,這樣就很靈活了。
若是每次使用SelectorGroup
,都須要從新自定義選中行爲也很費力,因此在其中添加了最經常使用的單選和多選行爲:
public class SelectorGroup {
public static final int MODE_SINGLE_CHOICE = 1;
public static final int MODE_MULTIPLE_CHOICE = 2;
private ChoiceAction choiceMode;
//'經過這個方法設置默認行爲'
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(Selector selector, SelectorGroup selectorGroup, StateListener stateListener) {
selector.setSelected(true);
cancelPreSelector(selector);
if (stateListener != null) {
stateListener.onStateChange(selector.getSelectorTag(), true);
}
}
}
//'多選行爲'
private class MultipleAction implements ChoiceAction {
@Override
public void onChoose(Selector selector, SelectorGroup selectorGroup, StateListener stateListener) {
boolean isSelected = selector.isSelected();
selector.setSelected(!isSelected);
if (stateListener != null) {
stateListener.onStateChange(selector.getSelectorTag(), !isSelected);
}
}
}
}
複製代碼
將本來具體的行爲都移到了接口中,而SelectorGroup
只和抽象的接口互動,不和具體行爲互動,這樣的SelectorGroup
具備彈性。
如今只要像這樣就能夠分別實現單選和多選:
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);
multipleGroup.setStateListener(new MultipleChoiceListener());
((Selector) findViewById(R.id.selector_10)).setGroup("multiple", multipleGroup);
((Selector) findViewById(R.id.selector_20)).setGroup("multiple", multipleGroup);
((Selector) findViewById(R.id.selector_30)).setGroup("multiple", multipleGroup);
//單選
SelectorGroup singleGroup = new SelectorGroup();
singleGroup.setChoiceMode(SelectorGroup.MODE_SINGLE_CHOICE);
singleGroup.setStateListener(new SingleChoiceListener());
((Selector) findViewById(R.id.single10)).setGroup("single", singleGroup);
((Selector) findViewById(R.id.single20)).setGroup("single", singleGroup);
((Selector) findViewById(R.id.single30)).setGroup("single", singleGroup);
}
}
複製代碼
在activity_main.xml
中佈局了6個Selector
,其中三個用於單選,三個用於多選。
這一次新需求是多選和單選的組合:菜單選。這種模式將選項分紅若干組,組內單選,組間多選。看下使用策略模式重構後的SelectorGroup
是如何輕鬆應對的:
private class OderChoiceMode implements SelectorGroup.ChoiceAction {
@Override
public void onChoose(Selector selector, SelectorGroup selectorGroup, SelectorGroup.StateListener stateListener) {
//'取消同組的上次選中按鈕'
cancelPreSelector(selector, selectorGroup);
//'選中當前點擊按鈕'
selector.setSelected(true);
}
//'取消同組上次選中按鈕,同組的按鈕具備相同的groupTag'
private void cancelPreSelector(Selector selector, SelectorGroup selectorGroup) {
Selector preSelector = selectorGroup.getPreSelector(selector.getGroupTag());
if (preSelector!=null) {
preSelector.setSelected(false);
}
}
}
複製代碼
而後就能夠像這樣動態的爲SelectorGroup
擴展菜單選行爲了:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//'菜單選'
SelectorGroup orderGroup = new SelectorGroup();
orderGroup.setStateListener(new OrderChoiceListener());
orderGroup.setChoiceMode(new OderChoiceMode());
//'爲同組按鈕設置相同的groupTag'
((Selector) findViewById(R.id.selector_starters_duck)).setGroup("starters", orderGroup);
((Selector) findViewById(R.id.selector_starters_pork)).setGroup("starters", orderGroup);
((Selector) findViewById(R.id.selector_starters_springRoll)).setGroup("starters", orderGroup);
((Selector) findViewById(R.id.selector_main_pizza)).setGroup("main", orderGroup);
((Selector) findViewById(R.id.selector_main_pasta)).setGroup("main", orderGroup);
((Selector) findViewById(R.id.selector_soup_mushroom)).setGroup("soup", orderGroup);
((Selector) findViewById(R.id.selector_soup_scampi)).setGroup("soup", orderGroup);
}
}
複製代碼
效果以下:
其中單選按鈕經過繼承Selector
重寫onSwitchSelected()
,定義了選中效果爲愛心動畫。
至此,選項按鈕這個repository已經將兩種設計模式運用於實戰。
運用了模版方法模式將變化的按鈕佈局和點擊效果和按鈕自己隔離。
運用了策略模式將變化的選中行爲和選中組隔離。
在經歷屢次需求變動的忽然襲擊後,遍體鱗傷的咱們須要找出自救的方法:
實現需求前,經過分析需求識別出「會變的」和「不變的」邏輯,增長一層抽象將「會變的」邏輯封裝起來,以實現隔離和分層,將「不變的」邏輯和抽象的互動代碼在上層類中固定下來。需求發生變化時,經過在下層實現抽象以多態的方式來應對。這樣的代碼具備彈性,就能以「不變的」上層邏輯應對變化的需求。
實例代碼省略了一些非關鍵的細節,完整代碼在這裏