RecyclerView支持单选复选的容器实现ButtonGroupRecyclerView

先上图和视频看效果:
在这里插入图片描述

屏幕录制2021-01-25 08.51.42

(原创作品,转载请声明出处:https://blog.csdn.net/hegan2010/article/details/113103183)

谷歌已经提供了 MaterialButtonToggleGroup, 但是 MaterialButtonToggleGroup 是继承 LinearLayout 的,并且没有增加子View排列的的属性定义,说明只支持线性排列,不能满足需要多行排列的需求。

无奈自己仿写了 MaterialButtonToggleGroup 来实现支持各种布局 ButtonGroupRecyclerView。

ButtonGroupRecyclerView 继承自 RecyclerView。内部通过 mDefaultCheckedPos (单选),mCheckedItemIdSet (复选) 来记录已选的 Button 的 Item Id,所以这里要求 Adapter 要 setHasStableIds(true), 并且要每个 Item View 返回独特的 Item Id,即要求实现 public long getItemId(int position) 方法,因此ButtonGroupRecyclerView 已提供 ButtonCheckedAdapter 作为 Adapter 的 Base 类。ButtonCheckedAdapter 在 ViewHolder 被回收的时候(onViewRecycled) 对已选 Button 进行 check 状态重置,以避免 Button 复用的时候保留了之前的 check 状态。实际是否能避免这个问题还有待进一步测试。所以,可能还存在 bug

提供了 OnButtonCheckedListener 监听器,注意监听器回掉方法的参数 checkedPos 指的是 Item Position 而不是 Item Id。

好了,上代码:

import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;

import android.content.Context;
import android.content.res.TypedArray;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.BoolRes;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.customview.view.AbsSavedState;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.material.button.MaterialButton;
import com.hym.tuluowan.R;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.WeakHashMap;
import java.util.function.Predicate;

public class ButtonGroupRecyclerView extends RecyclerView {
    /**
     * Interface definition for a callback to be invoked when a {@link MaterialButton} is checked or
     * unchecked in this group.
     */
    public interface OnButtonCheckedListener {
        /**
         * Called when a {@link MaterialButton} in this group is checked or unchecked.
         *
         * @param group      The group in which the MaterialButton's checked state was changed
         * @param checkedPos The position of the MaterialButton whose check state changed
         * @param isChecked  Whether the MaterialButton is currently checked
         */
        void onButtonChecked(ButtonGroupRecyclerView group, int checkedPos, boolean isChecked);
    }

    private static final String LOG_TAG = ButtonGroupRecyclerView.class.getSimpleName();

    private static final int DEF_STYLE_RES = R.style.Widget_ButtonGroupRecyclerView;

    private final CheckedStateTracker mCheckedStateTracker = new CheckedStateTracker();
    private final LinkedHashSet<OnButtonCheckedListener> mOnButtonCheckedListeners =
            new LinkedHashSet<>();

    private boolean mSkipCheckedStateTracker = false;
    private boolean mSingleSelection;
    private boolean mSelectionRequired;

    private long mCheckedItemId = NO_ID;
    private final int mDefaultCheckedPos;
    private final SortedSet<Long> mCheckedItemIdSet = new TreeSet<>();

    public ButtonGroupRecyclerView(@NonNull Context context) {
        this(context, null);
    }

    public ButtonGroupRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, R.attr.buttonGroupRecyclerViewStyle);
    }

    public ButtonGroupRecyclerView(
            @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr);
        // Ensure we are using the correctly themed context rather than the context that was
        // passed in.
        context = getContext();
        TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.ButtonGroupRecyclerView, defStyleAttr, DEF_STYLE_RES);
        boolean single = a.getBoolean(R.styleable.ButtonGroupRecyclerView_singleSelection, false);
        setSingleSelection(single);
        mDefaultCheckedPos =
                a.getInteger(R.styleable.ButtonGroupRecyclerView_checkedButtonPos, NO_POSITION);
        recordCheckedItemId(mCheckedItemId, true);
        mSelectionRequired = a.getBoolean(R.styleable.ButtonGroupRecyclerView_selectionRequired,
                false);
        setChildrenDrawingOrderEnabled(true);
        a.recycle();

        ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        // Checks the appropriate button as requested via XML
        if (mCheckedItemId != NO_ID) {
            checkForced(mCheckedItemId);
        }
    }

    /**
     * This override prohibits Views other than {@link MaterialButton} to be added.
     */
    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if (!(child instanceof MaterialButton)) {
            Log.e(LOG_TAG, "Child views must be of type MaterialButton.");
            return;
        }

        super.addView(child, index, params);
        MaterialButton buttonChild = (MaterialButton) child;
        // Sets sensible default values and an internal checked change listener for this child
        setupButtonChild(buttonChild);

        // Reorders children if a checked child was added to this layout
        if (buttonChild.isChecked()) {
            long childItemId = getChildItemId(buttonChild);
            updateCheckedStates(childItemId, true);
            setCheckedItemId(childItemId);
        } else if (isChildChecked(buttonChild) || (mCheckedItemIdSet.isEmpty()
                && mDefaultCheckedPos == getChildLayoutPosition(buttonChild))) {
            MaterialButtonHelper.setButtonCheckedWithoutNotifyListeners(buttonChild, true);
            long childItemId = getChildItemId(buttonChild);
            updateCheckedStates(childItemId, true);
            setCheckedItemId(childItemId);
        }
    }

    @Override
    public void onViewRemoved(View child) {
        super.onViewRemoved(child);

        ((MaterialButton) child).removeOnCheckedChangeListener(mCheckedStateTracker);
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        SavedState state = new SavedState(super.onSaveInstanceState());
        state.checkedItemId = mCheckedItemId;
        state.checkedItemIdList = new ArrayList<>(mCheckedItemIdSet);
        return state;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }
        SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());
        resetChecked();
        setCheckedItemId(savedState.checkedItemId, false);
        for (long checkedItemId : savedState.checkedItemIdList) {
            setCheckedItemId(checkedItemId, false);
        }
    }

    private static class SavedState extends AbsSavedState {
        private long checkedItemId;
        private List<Long> checkedItemIdList;

        /**
         * Constructor called from {@link ButtonGroupRecyclerView#onSaveInstanceState()}
         */
        private SavedState(Parcelable superState) {
            super(superState);
        }

        /**
         * Constructor called from {@link #CREATOR}
         */
        private SavedState(Parcel in, ClassLoader loader) {
            super(in, loader);
            checkedItemId = in.readLong();
            checkedItemIdList = in.readArrayList(null);
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeLong(checkedItemId);
            dest.writeList(checkedItemIdList);
        }

        public static final Parcelable.Creator<SavedState> CREATOR
                = new ClassLoaderCreator<SavedState>() {
            public SavedState createFromParcel(Parcel in, ClassLoader loader) {
                return new SavedState(in, loader);
            }

            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in, null);
            }

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

    @NonNull
    @Override
    public CharSequence getAccessibilityClassName() {
        return ButtonGroupRecyclerView.class.getName();
    }

    /**
     * Sets the {@link MaterialButton} whose id is passed in to the checked state. If this
     * RadioGroupView is in {@link #isSingleSelection() single selection mode}, then all
     * other MaterialButtons in this group will be unchecked. Otherwise, other MaterialButtons will
     * retain their checked state.
     *
     * @param itemId View item id of {@link MaterialButton} to set checked
     * @see #uncheck(long)
     * @see #clearChecked()
     * @see #getCheckedButtonItemIds()
     * @see #getCheckedButtonItemId()
     */
    public void check(long itemId) {
        if (itemId == mCheckedItemId) {
            return;
        }

        checkForced(itemId);
    }

    /**
     * Sets the {@link MaterialButton} whose id is passed in to the unchecked state.
     *
     * @param itemId View item id of {@link MaterialButton} to set unchecked
     * @see #check(long)
     * @see #clearChecked()
     * @see #getCheckedButtonItemIds()
     * @see #getCheckedButtonItemId()
     */
    public void uncheck(long itemId) {
        setCheckedStateForView(itemId, false);
        updateCheckedStates(itemId, false);
        mCheckedItemId = NO_ID;
        // recordCheckedItemId(mCheckedItemId, true);
        dispatchOnButtonChecked(itemId, false);
    }

    /**
     * Clears the selections. When the selections are cleared, no {@link MaterialButton} in this
     * group is checked and {@link #getCheckedButtonItemIds()} returns an empty list.
     *
     * @see #check(long)
     * @see #uncheck(long)
     * @see #getCheckedButtonItemIds()
     * @see #getCheckedButtonItemId()
     */
    public void clearChecked() {
        mSkipCheckedStateTracker = true;
        for (int i = 0; i < getChildCount(); i++) {
            MaterialButton child = getChildButton(i);
            child.setChecked(false);

            dispatchOnButtonChecked(getChildLayoutPosition(child), false);
        }
        mSkipCheckedStateTracker = false;

        mCheckedItemIdSet.clear();
        setCheckedItemId(NO_ID);
    }

    /**
     * When in {@link #isSingleSelection() single selection mode}, returns the identifier of the
     * selected button in this group. Upon empty selection, the returned value is {@link
     * RecyclerView#NO_ID}.
     * If not in single selection mode, the return value is {@link RecyclerView#NO_ID}.
     *
     * @return The item id of the selected {@link MaterialButton} in this group in {@link
     * #isSingleSelection() single selection mode}. When not in {@link #isSingleSelection() single
     * selection mode}, returns {@link RecyclerView#NO_ID}.
     * @attr ref R.styleable#RadioGroupRecyclerView_checkedButton
     * @see #check(long)
     * @see #uncheck(long)
     * @see #clearChecked()
     * @see #getCheckedButtonItemIds()
     */
    public long getCheckedButtonItemId() {
        return mSingleSelection ? mCheckedItemId : NO_ID;
    }

    /**
     * Returns the identifiers of the selected {@link MaterialButton}s in this group. Upon empty
     * selection, the returned value is an empty list.
     *
     * @return The item ids of the selected {@link MaterialButton}s in this group. When in {@link
     * #isSingleSelection() single selection mode}, returns a list with a single item id. When no
     * {@link MaterialButton}s are selected, returns an empty list.
     * @see #check(long)
     * @see #uncheck(long)
     * @see #clearChecked()
     * @see #getCheckedButtonItemId()
     */
    @NonNull
    public List<Long> getCheckedButtonItemIds() {
        return new ArrayList<>(mCheckedItemIdSet);
    }

    /**
     * Add a listener that will be invoked when the check state of a {@link MaterialButton} in this
     * group changes. See {@link OnButtonCheckedListener}.
     *
     * <p>Components that add a listener should take care to remove it when finished via {@link
     * #removeOnButtonCheckedListener(OnButtonCheckedListener)}.
     *
     * @param listener listener to add
     */
    public void addOnButtonCheckedListener(@NonNull OnButtonCheckedListener listener) {
        mOnButtonCheckedListeners.add(listener);
    }

    /**
     * Remove a listener that was previously added via {@link
     * #addOnButtonCheckedListener(OnButtonCheckedListener)}.
     *
     * @param listener listener to remove
     */
    public void removeOnButtonCheckedListener(@NonNull OnButtonCheckedListener listener) {
        mOnButtonCheckedListeners.remove(listener);
    }

    /** Remove all previously added {@link OnButtonCheckedListener}s. */
    public void clearOnButtonCheckedListeners() {
        mOnButtonCheckedListeners.clear();
    }

    /**
     * Returns whether this group only allows a single button to be checked.
     *
     * @return whether this group only allows a single button to be checked
     * @attr ref R.styleable#RadioGroupRecyclerView_singleSelection
     */
    public boolean isSingleSelection() {
        return mSingleSelection;
    }

    /**
     * Sets whether this group only allows a single button to be checked.
     *
     * <p>Calling this method results in all the buttons in this group to become unchecked.
     *
     * @param singleSelection whether this group only allows a single button to be checked
     * @attr ref R.styleable#RadioGroupRecyclerView_singleSelection
     */
    public void setSingleSelection(boolean singleSelection) {
        if (mSingleSelection != singleSelection) {
            mSingleSelection = singleSelection;
            clearChecked();
        }
    }

    /**
     * Sets whether we prevent all child buttons from being deselected.
     *
     * @attr ref R.styleable#RadioGroupRecyclerView_selectionRequired
     */
    public void setSelectionRequired(boolean selectionRequired) {
        mSelectionRequired = selectionRequired;
    }

    /**
     * Returns whether we prevent all child buttons from being deselected.
     *
     * @attr ref R.styleable#RadioGroupRecyclerView_selectionRequired
     */
    public boolean isSelectionRequired() {
        return mSelectionRequired;
    }

    /**
     * Sets whether this group only allows a single button to be checked.
     *
     * <p>Calling this method results in all the buttons in this group to become unchecked.
     *
     * @param id boolean resource ID of whether this group only allows a single button to be checked
     * @attr ref R.styleable#RadioGroupRecyclerView_singleSelection
     */
    public void setSingleSelection(@BoolRes int id) {
        setSingleSelection(getResources().getBoolean(id));
    }

    private void setCheckedStateForView(long itemId, boolean checked) {
        ViewHolder holder = findViewHolderForItemId(itemId);
        if (holder == null) {
            return;
        }
        View checkedView = holder.itemView;
        mSkipCheckedStateTracker = true;
        ((MaterialButton) checkedView).setChecked(checked);
        recordCheckedItemId(itemId, checked);
        mSkipCheckedStateTracker = false;
    }

    private void setCheckedItemId(long checkedItemId) {
        setCheckedItemId(checkedItemId, true);
    }

    private void setCheckedItemId(long checkedItemId, boolean dispatch) {
        mCheckedItemId = checkedItemId;
        recordCheckedItemId(checkedItemId, true);

        if (dispatch) {
            dispatchOnButtonChecked(checkedItemId, true);
        }
    }

    private void recordCheckedItemId(long checkedItemId, boolean checked) {
        if (checkedItemId == NO_ID) {
            return;
        }
        if (checked) {
            mCheckedItemIdSet.add(checkedItemId);
        } else {
            mCheckedItemIdSet.remove(checkedItemId);
        }
    }

    private void resetChecked() {
        mCheckedItemId = NO_ID;
        mCheckedItemIdSet.clear();
    }

    private boolean isChildChecked(View child) {
        ViewHolder holder = getChildViewHolder(child);
        if (holder != null) {
            return mCheckedItemIdSet.contains(holder.getItemId());
        }
        return false;
    }

    private MaterialButton getChildButton(int index) {
        return (MaterialButton) getChildAt(index);
    }

    private int getFirstVisibleChildIndex() {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            if (isChildVisible(i)) {
                return i;
            }
        }

        return -1;
    }

    private int getLastVisibleChildIndex() {
        int childCount = getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            if (isChildVisible(i)) {
                return i;
            }
        }

        return -1;
    }

    private boolean isChildVisible(int i) {
        View child = getChildAt(i);
        return child.getVisibility() != View.GONE;
    }

    private int getVisibleButtonCount() {
        int count = 0;
        for (int i = 0; i < getChildCount(); i++) {
            if (isChildVisible(i)) {
                count++;
            }
        }
        return count;
    }

    private int getIndexWithinVisibleButtons(@Nullable View child) {
        int index = 0;
        for (int i = 0; i < getChildCount(); i++) {
            if (getChildAt(i) == child) {
                return index;
            }
            if (isChildVisible(i)) {
                index++;
            }
        }
        return -1;
    }

    /**
     * When a checked child is added, or a child is clicked, updates checked state and draw order of
     * children to draw all checked children on top of all unchecked children.
     *
     * <p>If {@code singleSelection} is true, this will unselect any other children as well.
     *
     * <p>If {@code selectionRequired} is true, and the last child is unchecked it will undo the
     * deselection.
     *
     * @param childItemId    item id of child whose checked state may have changed
     * @param childIsChecked Whether the child is checked
     * @return Whether the checked state for childId has changed.
     */
    private boolean updateCheckedStates(long childItemId, boolean childIsChecked) {
        List<Long> checkedButtonPositions = getCheckedButtonItemIds();
        if (mSelectionRequired && checkedButtonPositions.isEmpty()) {
            // undo deselection
            setCheckedStateForView(childItemId, true);
            mCheckedItemId = childItemId;
            recordCheckedItemId(childItemId, true);
            return false;
        }

        // un select previous selection
        if (childIsChecked && mSingleSelection) {
            checkedButtonPositions.remove(childItemId);
            for (long buttonItemId : checkedButtonPositions) {
                setCheckedStateForView(buttonItemId, false);
                dispatchOnButtonChecked(buttonItemId, false);
            }
        }
        return true;
    }

    private void dispatchOnButtonChecked(long buttonItemId, boolean checked) {
        ViewHolder holder = findViewHolderForItemId(buttonItemId);
        if (holder != null) {
            dispatchOnButtonChecked(holder.getLayoutPosition(), checked);
        }
    }

    private void dispatchOnButtonChecked(int buttonPos, boolean checked) {
        for (OnButtonCheckedListener listener : mOnButtonCheckedListeners) {
            listener.onButtonChecked(this, buttonPos, checked);
        }
    }

    private void checkForced(long checkedItemId) {
        setCheckedStateForView(checkedItemId, true);
        updateCheckedStates(checkedItemId, true);
        setCheckedItemId(checkedItemId);
    }

    /**
     * Sets sensible default values for {@link MaterialButton} child of this group, set child to
     * {@code checkable}, and set internal checked change listener for this child.
     *
     * @param buttonChild {@link MaterialButton} child to set up to be added to this {@link
     *                    ButtonGroupRecyclerView}
     */
    private void setupButtonChild(@NonNull MaterialButton buttonChild) {
        buttonChild.setMaxLines(1);
        buttonChild.setEllipsize(TextUtils.TruncateAt.END);
        buttonChild.setCheckable(true);

        buttonChild.addOnCheckedChangeListener(mCheckedStateTracker);

        // Enables surface layer drawing for semi-opaque strokes
        // buttonChild.setShouldDrawSurfaceColorStroke(true);
        MaterialButtonHelper.setShouldDrawSurfaceColorStroke(buttonChild, true);
    }

    private class CheckedStateTracker implements MaterialButton.OnCheckedChangeListener {
        @Override
        public void onCheckedChanged(@NonNull MaterialButton button, boolean isChecked) {
            // Prevents infinite recursion
            if (mSkipCheckedStateTracker) {
                return;
            }

            long childItemId = getChildItemId(button);
            if (mSingleSelection) {
                mCheckedItemId = isChecked ? childItemId : NO_ID;
                if (!isChecked) {
                    recordCheckedItemId(childItemId, false);
                }
                recordCheckedItemId(mCheckedItemId, true);
            } else {
                recordCheckedItemId(childItemId, isChecked);
            }

            boolean buttonCheckedStateChanged = updateCheckedStates(childItemId, isChecked);
            if (buttonCheckedStateChanged) {
                // Dispatch button.isChecked instead of isChecked in case its checked state was
                // updated internally.
                dispatchOnButtonChecked(childItemId, button.isChecked());
            }

            invalidate();
        }
    }

    private final AdapterDataObserver mAdapterDataObserver = new AdapterDataObserver();

    private class AdapterDataObserver extends RecyclerView.AdapterDataObserver {
        @Override
        public void onChanged() {
            long checkedItemId = NO_ID;
            List<Long> checkedItemIds = new LinkedList<>();
            Adapter adapter = getAdapter();
            if (adapter != null) {
                int count = adapter.getItemCount();
                for (int pos = 0; pos < count; pos++) {
                    ViewHolder holder = findViewHolderForAdapterPosition(pos);
                    if (holder == null) {
                        continue;
                    }
                    long itemId = holder.getItemId();
                    if (mCheckedItemId == itemId) {
                        checkedItemId = itemId;
                    }
                    if (mCheckedItemIdSet.contains(itemId)) {
                        checkedItemIds.add(itemId);
                    }
                }
                setItemViewCacheSize(count);
            } else {
                setItemViewCacheSize(0);
            }
            resetChecked();
            if (checkedItemId != NO_ID) {
                mCheckedItemId = checkedItemId;
            }
            mCheckedItemIdSet.addAll(checkedItemIds);
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            onChanged();
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
            onItemRangeChanged(positionStart, itemCount);
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            onChanged();
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            onChanged();
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            onChanged();
        }
    }

    @Override
    public void swapAdapter(@Nullable Adapter adapter, boolean removeAndRecycleExistingViews) {
        Adapter oldAdapter = getAdapter();
        if (oldAdapter != null) {
            oldAdapter.unregisterAdapterDataObserver(mAdapterDataObserver);
        }

        super.swapAdapter(adapter, removeAndRecycleExistingViews);

        if (adapter != null) {
            setItemViewCacheSize(adapter.getItemCount());
            adapter.registerAdapterDataObserver(mAdapterDataObserver);
        }
    }

    @Override
    public void setAdapter(@Nullable Adapter adapter) {
        Adapter oldAdapter = getAdapter();
        if (oldAdapter != null) {
            oldAdapter.unregisterAdapterDataObserver(mAdapterDataObserver);
        }

        super.setAdapter(adapter);

        if (adapter != null) {
            setItemViewCacheSize(adapter.getItemCount());
            adapter.registerAdapterDataObserver(mAdapterDataObserver);
        }
    }

    public abstract static class ButtonCheckedAdapter<VH extends RecyclerView.ViewHolder>
            extends RecyclerView.Adapter<VH> {
        private final Map<VH, MaterialButton.OnCheckedChangeListener> mButtonListenerMap
                = new WeakHashMap<>();

        public ButtonCheckedAdapter() {
            setHasStableIds(true);
        }

        @Override
        public abstract long getItemId(int position);

        public abstract MaterialButton.OnCheckedChangeListener getOnCheckedChangeListener(
                @NonNull VH holder, int position);

        @CallSuper
        @Override
        public void onBindViewHolder(@NonNull VH holder, int position) {
            MaterialButton.OnCheckedChangeListener oldListener = mButtonListenerMap.remove(holder);
            MaterialButton button = (MaterialButton) holder.itemView;
            if (oldListener != null) {
                button.removeOnCheckedChangeListener(oldListener);
            }
            MaterialButton.OnCheckedChangeListener newListener = getOnCheckedChangeListener(holder,
                    position);
            if (newListener != null) {
                mButtonListenerMap.put(holder, newListener);
                button.addOnCheckedChangeListener(newListener);
            }
        }

        @CallSuper
        @Override
        public void onViewRecycled(@NonNull VH holder) {
            mButtonListenerMap.remove(holder);
            MaterialButton button = (MaterialButton) holder.itemView;
            button.clearOnCheckedChangeListeners();
            if (button.isChecked()) {
                button.toggle();
            }
        }
    }

    private static class MaterialButtonHelper {
        private static final Method setShouldDrawSurfaceColorStroke;
        private static final Field onCheckedChangeListeners;

        static {
            Method method = null;
            try {
                method = MaterialButton.class.getDeclaredMethod(
                        "setShouldDrawSurfaceColorStroke", boolean.class);
                method.setAccessible(true);
            } catch (ReflectiveOperationException e) {
                Log.w(LOG_TAG, "get MaterialButton.setShouldDrawSurfaceColorStroke failed", e);
            }
            setShouldDrawSurfaceColorStroke = method;

            Field field = null;
            try {
                field = MaterialButton.class.getDeclaredField("onCheckedChangeListeners");
                field.setAccessible(true);
            } catch (ReflectiveOperationException e) {
                Log.w(LOG_TAG, "get MaterialButton.onCheckedChangeListeners failed", e);
            }
            onCheckedChangeListeners = field;
        }

        public static void setShouldDrawSurfaceColorStroke(MaterialButton button,
                boolean shouldDrawSurfaceColorStroke) {
            if (setShouldDrawSurfaceColorStroke == null) {
                Log.w(LOG_TAG, "setShouldDrawSurfaceColorStroke failed: method is null");
                return;
            }
            try {
                setShouldDrawSurfaceColorStroke.invoke(button, shouldDrawSurfaceColorStroke);
            } catch (ReflectiveOperationException e) {
                Log.w(LOG_TAG, "setShouldDrawSurfaceColorStroke failed", e);
            }
        }

        public static Set<MaterialButton.OnCheckedChangeListener>
        removeAllOnCheckedChangeListeners(MaterialButton button) {
            if (onCheckedChangeListeners == null) {
                Log.w(LOG_TAG, "removeAllOnCheckedChangeListeners failed: field is null");
                return Collections.emptySet();
            }
            try {
                Object obj = onCheckedChangeListeners.get(button);
                Set<MaterialButton.OnCheckedChangeListener> oriSet = (Set) obj;
                final Set<MaterialButton.OnCheckedChangeListener> retSet = new LinkedHashSet<>();
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
                    oriSet.removeIf(new Predicate<MaterialButton.OnCheckedChangeListener>() {
                        @Override
                        public boolean test(
                                MaterialButton.OnCheckedChangeListener onCheckedChangeListener) {
                            retSet.add(onCheckedChangeListener); // add to retSet
                            return true; // remove all
                        }
                    });
                } else {
                    retSet.addAll(oriSet);
                    oriSet.clear();
                }
                return retSet;
            } catch (ReflectiveOperationException e) {
                Log.w(LOG_TAG, "removeAllOnCheckedChangeListeners failed", e);
                return Collections.emptySet();
            }
        }

        public static void restoreOnCheckedChangeListeners(MaterialButton button,
                Set<MaterialButton.OnCheckedChangeListener> set) {
            if (onCheckedChangeListeners == null) {
                Log.w(LOG_TAG, "restoreOnCheckedChangeListeners failed: field is null");
            }
            try {
                Object obj = onCheckedChangeListeners.get(button);
                ((Set) obj).addAll(set);
            } catch (ReflectiveOperationException e) {
                Log.w(LOG_TAG, "restoreOnCheckedChangeListeners failed", e);
            }
        }

        public static void setButtonCheckedWithoutNotifyListeners(MaterialButton button,
                boolean checked) {
            Set<MaterialButton.OnCheckedChangeListener> set = removeAllOnCheckedChangeListeners(
                    button);
            button.setChecked(checked);
            restoreOnCheckedChangeListeners(button, set);
        }
    }
}

还需要添加属性定义:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="buttonGroupRecyclerViewStyle" format="reference" />

    <declare-styleable name="ButtonGroupRecyclerView">
        <attr name="singleSelection" format="boolean" />
        <attr name="checkedButtonPos" format="integer" />
        <attr name="selectionRequired" format="boolean" />
    </declare-styleable>
</resources>

singleSelection 表示是单选还是复选。
checkedButtonPos 设置默认选中 Button 的 position (注意这里不是 Item Id),默认是NO_POSITION。
selectionRequired 表示是否至少选中一个。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值