變化是永恆的,產品需求穩定不變是不可能的,和產品經理互懟是沒有用的,但有一個方向是可以努力的:讓程式碼更有彈性,以不變應萬變。
繼上一次發版前突然變更單選按鈕樣式之後,又新增了兩個和選項按鈕有關的需求。它們分別是多選和選單選。多選類似於原生CheckBox
,而選單選是多選和單選的組合,類似於西餐點菜,西餐選單將食物分為前菜、主食、湯,每種只能選擇 1 個(即同組內單選,多組間多選)。
上一篇中的自定義單選按鈕Selector + SelectorGroup
完美 hold 住按鈕樣式的變化,這一次能否從容應對新增需求?
自定義單選按鈕
回顧下Selector + SelectorGroup
的效果:
其中每一個選項就是Selector
,它們的狀態被SelectorGroup
管理。
這組自定義控制元件突破了原生單選按鈕的佈局限制,選項的相對位置可以用 xml 定義(原生控制元件只能是垂直或水平鋪開),而且還可以方便地更換按鈕樣式以及定義選中效果(上圖中選中後有透明度動畫)
實現關鍵邏輯如下:
- 單個按鈕是一個抽象容器控制元件,它可以被點選並藉助
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
中的行為無法被替換。
每次行為擴充套件都重新寫一個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已經將兩種設計模式運用於實戰。
-
運用了模版方法模式將變化的按鈕佈局和點選效果和按鈕本身隔離。
-
運用了策略模式將變化的選中行為和選中組隔離。
在經歷多次需求變更的突然襲擊後,遍體鱗傷的我們需要找出自救的方法:
實現需求前,通過分析需求識別出“會變的”和“不變的”邏輯,增加一層抽象將“會變的”邏輯封裝起來,以實現隔離和分層,將“不變的”邏輯和抽象的互動程式碼在上層類中固定下來。需求發生變化時,通過在下層實現抽象以多型的方式來應對。這樣的程式碼具有彈性,就能以“不變的”上層邏輯應對變化的需求。
talk is cheap, show me the code
例項程式碼省略了一些非關鍵的細節,完整程式碼在這裡