摘要:這種模式將選項(xiàng)分成若干組,組內(nèi)單選,組間多選??偨Y(jié)至此,選項(xiàng)按鈕這個(gè)已經(jīng)將兩種設(shè)計(jì)模式運(yùn)用于實(shí)戰(zhàn)。運(yùn)用了策略模式將變化的選中行為和選中組隔離。這樣的代碼具有彈性,就能以不變的上層邏輯應(yīng)對(duì)變化的需求。
變化是永恒的,產(chǎn)品需求穩(wěn)定不變是不可能的,和產(chǎn)品經(jīng)理互懟是沒(méi)有用的,但有一個(gè)方向是可以努力的:讓代碼更有彈性,以不變應(yīng)萬(wàn)變。
繼上一次發(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 定義(原生控件只能是垂直或水平鋪開(kāi)),而且還可以方便地更換按鈕樣式以及定義選中效果(上圖中選中后有透明度動(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通過(guò)模版方法模式,將構(gòu)建按鈕視圖和按鈕選中效果延遲到子類構(gòu)建。所以當(dāng)按鈕內(nèi)部元素布局發(fā)生改變時(shí)不需要修改Selector,只需要新建它的子類。
單選組持有所有按鈕,當(dāng)按鈕被點(diǎn)擊時(shí),選中組遍歷其余按鈕并取消選中狀態(tài),以此來(lái)實(shí)現(xiàn)單選效果
public class SelectorGroup { //持有所有按鈕 private Set
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中的行為無(wú)法被替換。
每次行為擴(kuò)展都重新寫一個(gè)SelectorGroup怎么樣?不行!因?yàn)?b>Selector是和SelectorGroup耦合的,這意味著Selector的代碼也要跟著改動(dòng),這不符合開(kāi)閉原則。
SelectorGroup中除了會(huì)變的“選中行為”之外,也有不會(huì)變的成分,比如“持有所有的按鈕”。是不是可以增加一層抽象將變化的行為封裝起來(lái),使得SelectorGroup與變化隔離?
接口是封裝行為的最佳選擇,可以運(yùn)用策略模式將選中行為封裝起來(lái)
策略模式的詳細(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
selectors, Selector selector, StateListener stateListener); } }
將具體行為替換成接口后就好像是在原本嚴(yán)嚴(yán)實(shí)實(shí)的SelectorGroup中挖了一個(gè)洞,只要符合這個(gè)洞形狀的東西都可以塞進(jìn)來(lái)。這樣就很靈活了。
如果每次使用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; //通過(guò)這個(gè)方法設(shè)置自定義行為 public void setChoiceMode(ChoiceAction choiceMode) { this.choiceMode = choiceMode; } //通過(guò)這個(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
selectors, Selector selector, StateListener stateListener) { //將自己選中 selector.setSelected(true); //將除了自己外的其他按鈕設(shè)置為未選中 cancelPreSelector(selector, selectors); } } //多選行為 private class MultipleAction implements ChoiceAction { @Override public void onChoose(Set 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)單選,組間多選??聪率褂貌呗阅J街貥?gòu)后的SelectorGroup是如何輕松應(yīng)對(duì)的:
class OrderChoiceMode implements SelectorGroup.ChoiceAction { @Override public void onChoose(Set
selectors, Selector selector, SelectorGroup.StateListener stateListener) { //同組互斥選中 String tagPrefix = getTagPrefix(selector.getSelectorTag()); cancelPreSelectorBySameTag(selectors, tagPrefix, stateListener); selector.setSelected(true); } //在同一組中取消之前的選擇(要求同一組按鈕的tag具有相同的前綴) private void cancelPreSelectorBySameTag(Set 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è)置相同前綴的做法:
starters_pork starters_duck starters_springRoll main_pizza main_pasta soup_mushroom soup_scampi
前菜、主食、湯分別采用了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); } }
效果如下:
其中單選按鈕通過(guò)繼承Selector重寫onSwitchSelected(),定義了選中效果為愛(ài)心動(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)需求前,通過(guò)分析需求識(shí)別出“會(huì)變的”和“不變的”邏輯,增加一層抽象將“會(huì)變的”邏輯封裝起來(lái),以實(shí)現(xiàn)隔離和分層,將“不變的”邏輯和抽象的互動(dòng)代碼在上層類中固定下來(lái)。需求發(fā)生變化時(shí),通過(guò)在下層實(shí)現(xiàn)抽象以多態(tài)的方式來(lái)應(yīng)對(duì)。這樣的代碼具有彈性,就能以“不變的”上層邏輯應(yīng)對(duì)變化的需求。
talk is cheap, show me the code
實(shí)例代碼省略了一些非關(guān)鍵的細(xì)節(jié),完整代碼在這里
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/6689.html
摘要:年加入微軟中國(guó)有限公司,任職軟件開(kāi)發(fā)測(cè)試工程師,負(fù)責(zé)微軟在線業(yè)務(wù)與商業(yè)智能產(chǎn)品的測(cè)試工作。目前,史亮正從事下一代產(chǎn)品的研發(fā)工作。在他們的熱心幫助下,我獲得了去北京面試微軟測(cè)試開(kāi)發(fā)工程師,簡(jiǎn)稱的機(jī)會(huì)。 非商業(yè)轉(zhuǎn)載請(qǐng)注明作譯者、出處,并保留本文的原始鏈接:http://www.ituring.com.cn/article/114546 史亮,東南大學(xué)計(jì)算機(jī)軟件與理論專業(yè)博士,研究...
摘要:本屆工作坊,我們邀請(qǐng)到了擁有年資深設(shè)計(jì)經(jīng)驗(yàn)的高級(jí)體驗(yàn)設(shè)計(jì)專家朱斌,他將作為產(chǎn)品場(chǎng)講師為我們分享如何有效的管理用戶注意力的話題。美國(guó)設(shè)計(jì)同行對(duì)創(chuàng)新的追求和包容是最能體現(xiàn)設(shè)計(jì)魅力的地方。 導(dǎo)讀:7月6-7日,由msup主辦的第43屆MPD工作坊將于北京召開(kāi)。MPD工作坊是一個(gè)圍繞崗位角色發(fā)展的實(shí)踐課堂,按照軟件研發(fā)中心的崗位職能劃分,以產(chǎn)品經(jīng)理、團(tuán)隊(duì)經(jīng)理、 架構(gòu)師、開(kāi)發(fā)經(jīng)理、測(cè)試經(jīng)理作為五...
摘要:其中負(fù)載均衡那一節(jié),基本上是參考的權(quán)威指南負(fù)載均衡的內(nèi)容。開(kāi)發(fā)指南讀了一半,就是看這本書理解了的事件循環(huán)。哈哈創(chuàng)京東一本騙錢的書。 歡迎大家前往騰訊云+社區(qū),獲取更多騰訊海量技術(shù)實(shí)踐干貨哦~ 本文由騰訊IVWEB團(tuán)隊(duì) 發(fā)表于云+社區(qū)專欄作者:link 2014年一月以來(lái),自己接觸web前端開(kāi)發(fā)已經(jīng)兩年多了,記錄一下自己前端學(xué)習(xí)路上看過(guò)的,以及道聽(tīng)途說(shuō)的一些書,基本上按照由淺入深來(lái)介紹...
摘要:其中負(fù)載均衡那一節(jié),基本上是參考的權(quán)威指南負(fù)載均衡的內(nèi)容。開(kāi)發(fā)指南讀了一半,就是看這本書理解了的事件循環(huán)。哈哈創(chuàng)京東一本騙錢的書。歡迎大家前往騰訊云+社區(qū),獲取更多騰訊海量技術(shù)實(shí)踐干貨哦~ 本文由騰訊IVWEB團(tuán)隊(duì)發(fā)表于云+社區(qū)專欄 作者:link 2014年一月以來(lái),自己接觸web前端開(kāi)發(fā)已經(jīng)兩年多了,記錄一下自己前端學(xué)習(xí)路上看過(guò)的,以及道聽(tīng)途說(shuō)的一些書,基本上按照由淺入深來(lái)介紹。...
閱讀 2964·2021-10-15 09:41
閱讀 1620·2021-09-22 15:56
閱讀 2104·2021-08-10 09:43
閱讀 3273·2019-08-30 13:56
閱讀 1779·2019-08-30 12:47
閱讀 648·2019-08-30 11:17
閱讀 2770·2019-08-30 11:09
閱讀 2193·2019-08-29 16:19