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)?

  • 相关阅读:
    iOS 方便的宏定义
    IOS 推送消息 php做推送服务端
    iOS 7 动画UIDynamicAnimator
    iOS 适配
    ios 实现简单的解析xml网页
    用 MPMoviePlayerController 实现简单的视频下载播放功能
    ios 自定义弹出对话框效果
    ios国外大神
    git学习
    ios 7UI适配方法
  • 原文地址:https://www.cnblogs.com/yongdaimi/p/14511670.html
Copyright © 2011-2022 走看看