zoukankan      html  css  js  c++  java
  • android: 分享一个带多行选择功能的RadioGroup

    分享一个带多行选择功能的RadioGroup, Github上看到的。

    android 的RadioGroup有两个缺陷:

    1. 不支持多行选择
    什么意思呢?比如要实现下图这样的效果:

    上面的10组选项其实都只能单选,但是要用RadioGroup去做的话,最少要定义3个RadioGroup, 原生的RadioGroup只支持单向排列,要么是横向,要么是纵向, 且不能自动换行。

    2. 没办法自动区分选中状态是由代码触发的还是用户触发的

    这个问题其实也存在于SwitchButton、CheckBox等其它原生控件中。这点我觉得非常不好,因为这个需求其实很常见,比如我进入一个新界面,需要更新RadioGroup中RadioButton的选中状态,如果你直接使用RadioGroup#check()或RadioButton#setChecked()方法,这两个方法都会触发监听器的 onCheckedChanged() 回调,而我们一般是将选中后执行的动作写在这个回调方法里的,这样就会导致一个很奇葩的现象:比如选中按钮后需要下一个setter指令,我更新界面时调用了RadioButton#setChecked()方法,本来是来更新界面的,结果却莫名其妙下了一个setter指令。

    另外,使用RadioGroup#check()方法还会触发多次 onCheckedChanged() 回调!这是最坑的,完全跟我们的预期效果不一样,你可以说是我们用法上的问题,但我觉得一个好的api设计最起码从名称上就让人一目了然,光看名字就知道能达到什么效果, 而不是用了发现跟预期不一致,睬了个坑,再回去翻源码,发现原来是这样设计的,关于这个问题可参考:Android-为什么 RadioGroup.onCheckedChanged() 会调用多次?

    下面直接贴代码:

    code 是基于 https://github.com/pheng/android_radiogroup_MutilRadioGroup/blob/master/MutilRadioGroup/src/com/pheng/mutilradiogroup/MyRadioGroup.java

    我在上面加了一段code, 用于在代码选中时不会触发 onCheckedChanged()方法。 setCheckWithoutNotif 

    /**
     * RadioGroup with multi-line function and Code selection function
     * <br/>
     */
    public class MultiLineRadioGroup extends LinearLayout {
    
        // holds the checked id; the selection is empty by default
        private int mCheckedId = -1;
    
        // tracks children radio buttons checked state
        private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener;
    
        // when true, mOnCheckedChangeListener discards events
        private boolean mProtectFromCheckedChange = false;
    
        private OnCheckedChangeListener mOnCheckedChangeListener;
    
        private PassThroughHierarchyChangeListener mPassThroughListener;
    
        /**
         * {@inheritDoc}
         */
        public MultiLineRadioGroup(Context context) {
            super(context);
            setOrientation(VERTICAL);
            init();
        }
    
        /**
         * {@inheritDoc}
         */
        public MultiLineRadioGroup(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        private void init() {
            mChildOnCheckedChangeListener = new CheckedStateTracker();
            mPassThroughListener = new PassThroughHierarchyChangeListener();
            super.setOnHierarchyChangeListener(mPassThroughListener);
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
            // the user listener is delegated to our pass-through listener
            mPassThroughListener.mOnHierarchyChangeListener = listener;
        }
    
        /**
         * set the default checked radio button, without notification the listeners
         */
        public void setCheckWithoutNotif(int id) {
            if (id != -1 && (id == mCheckedId)) {
                return;
            }
    
            mProtectFromCheckedChange = true;
            if (mCheckedId != -1) {
                setCheckedStateForView(mCheckedId, false);
            }
    
            if (id != -1) {
                setCheckedStateForView(id, true);
            }
    
            mCheckedId = id;
            mProtectFromCheckedChange = false;
        }
    
        @Override
        public void addView(View child, int index, ViewGroup.LayoutParams params) {
            List<RadioButton> btns = getAllRadioButton(child);
            if (btns != null && btns.size() > 0) {
                for (RadioButton button : btns) {
                    if (button.isChecked()) {
                        mProtectFromCheckedChange = true;
                        if (mCheckedId != -1) {
                            setCheckedStateForView(mCheckedId, false);
                        }
                        mProtectFromCheckedChange = false;
                        setCheckedId(button.getId());
                    }
                }
            }
            super.addView(child, index, params);
        }
    
        /**
         * get all radio buttons which are in the view
         *
         * @param child
         */
        private List<RadioButton> getAllRadioButton(View child) {
            List<RadioButton> btns = new ArrayList<RadioButton>();
            if (child instanceof RadioButton) {
                btns.add((RadioButton) child);
            } else if (child instanceof ViewGroup) {
                int counts = ((ViewGroup) child).getChildCount();
                for (int i = 0; i < counts; i++) {
                    btns.addAll(getAllRadioButton(((ViewGroup) child).getChildAt(i)));
                }
            }
            return btns;
        }
    
        /**
         * Use program to check a radioButton, the {@link OnCheckedChangeListener#onCheckedChanged(MultiLineRadioGroup, int)}
         * method will not be triggered
         *
         * @param rationButtonId Selected radio button ID.
         * @param checked        Checked state.
         */
        public void setChecked(int rationButtonId, boolean checked) {
            RadioButton radioButton = findViewById(rationButtonId);
            if (radioButton != null) {
                OnCheckedChangeListener tmpListener = mOnCheckedChangeListener;
                setOnCheckedChangeListener(null);
                radioButton.setChecked(checked);
                setOnCheckedChangeListener(tmpListener);
            }
        }
    
        /**
         * <p>Sets the selection to the radio button whose identifier is passed in
         * parameter. Using -1 as the selection identifier clears the selection;
         * such an operation is equivalent to invoking {@link #clearCheck()}.</p>
         *
         * @param id the unique id of the radio button to select in this group
         * @see #getCheckedRadioButtonId()
         * @see #clearCheck()
         */
        public void check(int id) {
            // don't even bother
            if (id != -1 && (id == mCheckedId)) {
                return;
            }
    
            if (mCheckedId != -1) {
                setCheckedStateForView(mCheckedId, false);
            }
    
            if (id != -1) {
                setCheckedStateForView(id, true);
            }
    
            setCheckedId(id);
        }
    
        private void setCheckedId(int id) {
            mCheckedId = id;
            if (mOnCheckedChangeListener != null) {
                mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
            }
        }
    
        private void setCheckedStateForView(int viewId, boolean checked) {
            View checkedView = findViewById(viewId);
            if (checkedView != null && checkedView instanceof RadioButton) {
                ((RadioButton) checkedView).setChecked(checked);
            }
        }
    
        /**
         * <p>Returns the identifier of the selected radio button in this group.
         * Upon empty selection, the returned value is -1.</p>
         *
         * @return the unique id of the selected radio button in this group
         * @attr ref android.R.styleable#MyRadioGroup_checkedButton
         * @see #check(int)
         * @see #clearCheck()
         */
        public int getCheckedRadioButtonId() {
            return mCheckedId;
        }
    
        /**
         * <p>Clears the selection. When the selection is cleared, no radio button
         * in this group is selected and {@link #getCheckedRadioButtonId()} returns
         * null.</p>
         *
         * @see #check(int)
         * @see #getCheckedRadioButtonId()
         */
        public void clearCheck() {
            check(-1);
        }
    
        /**
         * <p>Register a callback to be invoked when the checked radio button
         * changes in this group.</p>
         *
         * @param listener the callback to call on checked state change
         */
        public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
            mOnCheckedChangeListener = listener;
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new MultiLineRadioGroup.LayoutParams(getContext(), attrs);
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
            return p instanceof MultiLineRadioGroup.LayoutParams;
        }
    
        @Override
        protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
            return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        }
    
        @Override
        public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
            super.onInitializeAccessibilityEvent(event);
            event.setClassName(MultiLineRadioGroup.class.getName());
        }
    
        @Override
        public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
            super.onInitializeAccessibilityNodeInfo(info);
            info.setClassName(MultiLineRadioGroup.class.getName());
        }
    
        /**
         * <p>This set of layout parameters defaults the width and the height of
         * the children to {@link #WRAP_CONTENT} when they are not specified in the
         * XML file. Otherwise, this class ussed the value read from the XML file.</p>
         */
        public static class LayoutParams extends LinearLayout.LayoutParams {
            /**
             * {@inheritDoc}
             */
            public LayoutParams(Context c, AttributeSet attrs) {
                super(c, attrs);
            }
    
            /**
             * {@inheritDoc}
             */
            public LayoutParams(int w, int h) {
                super(w, h);
            }
    
            /**
             * {@inheritDoc}
             */
            public LayoutParams(int w, int h, float initWeight) {
                super(w, h, initWeight);
            }
    
            /**
             * {@inheritDoc}
             */
            public LayoutParams(ViewGroup.LayoutParams p) {
                super(p);
            }
    
            /**
             * {@inheritDoc}
             */
            public LayoutParams(MarginLayoutParams source) {
                super(source);
            }
    
            /**
             * <p>Fixes the child's width to
             * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
             * height to  {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
             * when not specified in the XML file.</p>
             *
             * @param a          the styled attributes set
             * @param widthAttr  the width attribute to fetch
             * @param heightAttr the height attribute to fetch
             */
            @Override
            protected void setBaseAttributes(TypedArray a,
                                             int widthAttr, int heightAttr) {
    
                if (a.hasValue(widthAttr)) {
                    width = a.getLayoutDimension(widthAttr, "layout_width");
                } else {
                    width = WRAP_CONTENT;
                }
    
                if (a.hasValue(heightAttr)) {
                    height = a.getLayoutDimension(heightAttr, "layout_height");
                } else {
                    height = WRAP_CONTENT;
                }
            }
        }
    
        /**
         * <p>Interface definition for a callback to be invoked when the checked
         * radio button changed in this group.</p>
         */
        public interface OnCheckedChangeListener {
            /**
             * <p>Called when the checked radio button has changed. When the
             * selection is cleared, checkedId is -1.</p>
             *
             * @param group     the group in which the checked radio button has changed
             * @param checkedId the unique identifier of the newly checked radio button
             */
            public void onCheckedChanged(MultiLineRadioGroup group, int checkedId);
        }
    
        private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                // prevents from infinite recursion
                if (mProtectFromCheckedChange) {
                    return;
                }
    
                mProtectFromCheckedChange = true;
                if (mCheckedId != -1) {
                    setCheckedStateForView(mCheckedId, false);
                }
                mProtectFromCheckedChange = false;
    
                int id = buttonView.getId();
                setCheckedId(id);
            }
        }
    
        /**
         * <p>A pass-through listener acts upon the events and dispatches them
         * to another listener. This allows the table layout to set its own internal
         * hierarchy change listener without preventing the user to setup his.</p>
         */
        private class PassThroughHierarchyChangeListener implements
                ViewGroup.OnHierarchyChangeListener {
            private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
    
            /**
             * {@inheritDoc}
             */
            @SuppressLint("NewApi")
            public void onChildViewAdded(View parent, View child) {
                if (parent == MultiLineRadioGroup.this) {
                    List<RadioButton> btns = getAllRadioButton(child);
                    if (btns != null && btns.size() > 0) {
                        for (RadioButton btn : btns) {
                            int id = btn.getId();
                            // generates an id if it's missing
                            if (id == View.NO_ID && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                                id = View.generateViewId();
                                btn.setId(id);
                            }
                            btn.setOnCheckedChangeListener(
                                    mChildOnCheckedChangeListener);
                        }
                    }
                }
    
                if (mOnHierarchyChangeListener != null) {
                    mOnHierarchyChangeListener.onChildViewAdded(parent, child);
                }
            }
    
            /**
             * {@inheritDoc}
             */
            public void onChildViewRemoved(View parent, View child) {
                if (parent == MultiLineRadioGroup.this) {
                    List<RadioButton> btns = getAllRadioButton(child);
                    if (btns != null && btns.size() > 0) {
                        for (RadioButton btn : btns) {
                            btn.setOnCheckedChangeListener(null);
                        }
                    }
                }
    
                if (mOnHierarchyChangeListener != null) {
                    mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
                }
            }
        }
    }

    使用这份code, 可以很轻易实现多行的RadioButton效果,另外如果想选中RadioButton时不触发  onCheckedChanged 回调,可以调用: public void setChecked(int rationButtonId, boolean checked) 方法,其实这个code的原始作者也加了这个功能,
     setCheckWithoutNotif , 但我最开始没看到,所以就自己实现了一份,也可以实现相同的效果。

    参考链接:

    1. Android-为什么 RadioGroup.onCheckedChanged() 会调用多次?

    2. How can I distinguish whether Switch,Checkbox Value is changed by user or programmatically (including by retention)?

  • 相关阅读:
    -bash: fork: Cannot allocate memory 问题的处理
    Docker top 命令
    docker常见问题修复方法
    The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)
    What's the difference between encoding and charset?
    hexcode of é î Latin-1 Supplement
    炉石Advanced rulebook
    炉石bug反馈
    Sidecar pattern
    SQL JOIN
  • 原文地址:https://www.cnblogs.com/yongdaimi/p/14511670.html
Copyright © 2011-2022 走看看