每個 Button 都需寫 selector XML (btn_background.xml),項目中按鈕眾多時極其繁瑣,維護成本高。
public class StateButton extends AppCompatButton {
private Drawable normalDrawable, pressedDrawable, focusedDrawable, disabledDrawable;
private StateListDrawable stateListDrawable;
public StateButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StateButton);
normalDrawable = a.getDrawable(R.styleable.StateButton_normalDrawable);
pressedDrawable = a.getDrawable(R.styleable.StateButton_pressedDrawable);
focusedDrawable = a.getDrawable(R.styleable.StateButton_focusedDrawable);
disabledDrawable = a.getDrawable(R.styleable.StateButton_disabledDrawable);
a.recycle();
buildStateListDrawable();
setBackground(stateListDrawable); // 背景而非 src
}
private void buildStateListDrawable() {
stateListDrawable = new StateListDrawable();
// 順序重要:具體狀態在前,默認在後
if (pressedDrawable != null)
stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, pressedDrawable);
if (focusedDrawable != null)
stateListDrawable.addState(new int[]{android.R.attr.state_focused}, focusedDrawable);
if (disabledDrawable != null)
stateListDrawable.addState(new int[]{-android.R.attr.state_enabled}, disabledDrawable);
if (normalDrawable != null)
stateListDrawable.addState(StateSet.WILD_CARD, normalDrawable); // 默認
}
// 運行時動態修改
public void setNormalDrawable(Drawable d) {
normalDrawable = d;
buildStateListDrawable();
}
// ... 其他 setter
}
所有 Button 自動套用主題風格,單個覆蓋只需在佈局加 style="@style/Widget.App.Button.Secondary"。
優勢:內建 Ripple、ShapeAppearance、狀態屬性映射、無需自定義類、符合 Material Design 規範。
| 方案 | 優點 | 缺點 | 推薦場景 |
|---|---|---|---|
| 每個 Button 寫 selector | 完全可控 | 重複代碼、維護地獄 | 極少量特殊按鈕 |
| 自定義 StateButton + 屬性 | 佈局簡潔、運行時可改、邏輯集中 | 需維護自定義類 | 中型專案、需動態換膚 |
| 主題統一 buttonStyle | 全局一致、零佈局侵入、易換膚 | 靈活性稍低 | 規範化團隊、標準化 UI 庫 |
| MaterialButton | 現代、功能豐富、官方維護、無樣板代碼 | 需依賴 material 库 | 所有新專案首選 |