当前位置: 首页 > 工具软件 > CheckboxGroup > 使用案例 >

CheckBoxGroup

陆高峰
2023-12-01

之前做个一个投票应用,投票可以多选,但是又有上限,比如5选3,跟多选题一样。当时就写了个组合控件。
关键代码如下:

    public void init(List<String> texts, int maxCheckedNum) {
        setOrientation(VERTICAL);

        this.maxCheckedNum = maxCheckedNum;

        if (maxCheckedNum == 1) {
            for (int i = 0; i < texts.size(); i++) {
                RadioButton radioButton = new RadioButton(getContext());
                radioButton.setText(texts.get(i));
                radioButton.setTag(i);
                radioButton.setOnCheckedChangeListener(this);
                addView(radioButton, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
            }
        } else {
            for (int i = 0; i < texts.size(); i++) {
                CheckBox checkBox = new CheckBox(getContext());
                checkBox.setText(texts.get(i));
                checkBox.setTag(i);
                checkBox.setOnCheckedChangeListener(this);
                addView(checkBox, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
            }
        }
    }

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (maxCheckedNum == 1) {
            if (isChecked) {
                if (currentCheckedRadioButton != null) {
                    currentCheckedRadioButton.setChecked(false);
                }
                currentCheckedRadioButton = buttonView;
                checkedPositions.clear();
                checkedPositions.add((Integer) buttonView.getTag());
            } else {
                currentCheckedRadioButton = null;
                checkedPositions.clear();
            }
        } else {
            if (isChecked) {
                if (checkedPositions.size() < maxCheckedNum) {
                    checkedPositions.add((Integer) buttonView.getTag());
                } else {
                    Toast.makeText(getContext(), "You cann't select more!", Toast.LENGTH_SHORT).show();
                    buttonView.setChecked(false);
                }
            } else {
                checkedPositions.remove(((Integer) buttonView.getTag()));
            }
        }
    }

    public List<Integer> getCheckedPositions() {
        return checkedPositions;
    }

最近看文档,觉得这里面存在状态保存的问题。摘要如下:

However, even if you do nothing and do not implement onSaveInstanceState(), some of the activity state is restored by the Activity class’s default implementation of onSaveInstanceState(). Specifically, the default implementation calls the corresponding onSaveInstanceState() method for every View in the layout, which allows each view to provide information about itself that should be saved. Almost every widget in the Android framework implements this method as appropriate, such that any visible changes to the UI are automatically saved and restored when your activity is recreated. For example, the EditText widget saves any text entered by the user and the CheckBox widget saves whether it’s checked or not. The only work required by you is to provide a unique ID (with the android:id attribute) for each widget you want to save its state. If a widget does not have an ID, then the system cannot save its state.

You can also explicitly stop a view in your layout from saving its state by setting the android:saveEnabled attribute to “false” or by calling the setSaveEnabled() method. Usually, you should not disable this, but you might if you want to restore the state of the activity UI differently.
Although the default implementation of onSaveInstanceState() saves useful information about your activity’s UI, you still might need to override it to save additional information. For example, you might need to save member values that changed during the activity’s life (which might correlate to values restored in the UI, but the members that hold those UI values are not restored, by default).

Because the default implementation of onSaveInstanceState() helps save the state of the UI, if you override the method in order to save additional state information, you should always call the superclass implementation of onSaveInstanceState() before doing any work. Likewise, you should also call the superclass implementation of onRestoreInstanceState() if you override it, so the default implementation can restore view states.

注意If a widget does not have an ID, then the system cannot save its state.,我的组合控件中checkbox就属于这种情况。此外,根据the members that hold those UI values are not restored, by default,类中的checkedPositions也不会被保存。
所以我参考链接1链接2进行了完善:

  • 手动为 checkbox 分配 id
  • override onSavedInstanceState 和 onRestoreInstanceState 方法

关键代码如下:

public class CheckBoxGroup extends LinearLayout implements CompoundButton.OnCheckedChangeListener {

    private List<String> texts;
    private int maxCheckedNum;
    private ArrayList<Integer> checkedPositions = new ArrayList<Integer>();
    private CompoundButton currentCheckedRadioButton;

    public CheckBoxGroup(Context context) {
        super(context);
    }

    public CheckBoxGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CheckBoxGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void init(List<String> texts, int maxCheckedNum) {
        setOrientation(VERTICAL);

        this.maxCheckedNum = maxCheckedNum;

        if (maxCheckedNum == 1) {
            for (int i = 0; i < texts.size(); i++) {
                RadioButton radioButton = new RadioButton(getContext());
                radioButton.setText(texts.get(i));
                radioButton.setTag(i);
                radioButton.setOnCheckedChangeListener(this);
                radioButton.setId(generateViewId());
                addView(radioButton, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
            }
        } else {
            for (int i = 0; i < texts.size(); i++) {
                CheckBox checkBox = new CheckBox(getContext());
                checkBox.setText(texts.get(i));
                checkBox.setTag(i);
                checkBox.setOnCheckedChangeListener(this);
                checkBox.setId(generateViewId());
                addView(checkBox, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
            }
        }
    }

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (maxCheckedNum == 1) {
            if (isChecked) {
                if (currentCheckedRadioButton != null) {
                    currentCheckedRadioButton.setChecked(false);
                }
                currentCheckedRadioButton = buttonView;
                checkedPositions.clear();
                checkedPositions.add((Integer) buttonView.getTag());
            } else {
                currentCheckedRadioButton = null;
                checkedPositions.clear();
            }
        } else {
            if (isChecked) {
                if (checkedPositions.size() < maxCheckedNum) {
                    checkedPositions.add((Integer) buttonView.getTag());
                } else {
                    Toast.makeText(getContext(), "You cann't select more!", Toast.LENGTH_SHORT).show();
                    buttonView.setChecked(false);
                }
            } else {
                checkedPositions.remove(((Integer) buttonView.getTag()));
            }
        }
    }

    public List<Integer> getCheckedPositions() {
        return checkedPositions;
    }

    private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

    /**
     * Generate a value suitable for use in {@link #setId(int)}.
     * This value will not collide with ID values generated at build time by aapt for R.id.
     *
     * @return a generated ID value
     */
    public static int generateViewId() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            return  View.generateViewId();
        }

        for (;;) {
            final int result = sNextGeneratedId.get();
            // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
            int newValue = result + 1;
            if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
            if (sNextGeneratedId.compareAndSet(result, newValue)) {
                return result;
            }
        }
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        if (state instanceof SavedState){
            SavedState savedState = (SavedState) state;
            super.onRestoreInstanceState(savedState.getSuperState());
            this.checkedPositions = savedState.checkedPositions;
        } else {
            super.onRestoreInstanceState(state);
        }
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState savedState = new SavedState(superState);
        savedState.checkedPositions = this.checkedPositions;
        return savedState;
    }

    static class SavedState extends BaseSavedState implements Parcelable {
        private ArrayList<Integer> checkedPositions;

        public SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeSerializable(this.checkedPositions);
        }

        private SavedState(Parcel in) {
            super(in);
            this.checkedPositions = (ArrayList<Integer>) in.readSerializable();
        }

        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
            public SavedState createFromParcel(Parcel source) {
                return new SavedState(source);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}

ps, Android Studio 有个好用的插件可以帮你给你的类实现 parcelable 接口,就叫 Android Parcelable code generator

相关阅读

相关文章

相关问答