Android RadioGroup 源码分析

  Android 的 RadioGroup 可用于容纳 RadioButton,并为其中的 RadioButton 提供统一的行为管理(选中/取消选中)和事件监听(OnCheckedChangeListener)。RadioGroup 可以很方便地管理 RadioButton,但也仅限于 RadioButton,RadioGroup 无法为其他 View 提供类似的功能。

  最近在做一个类似 RadioGroup 的 ViewGroup,用于管理任意实现了 Checkable 接口的 View,以解决 RadioGroup 不兼容非 RadioButton 的问题,趁此机会阅读了一下 RadioGroup 的源码,把要点整理如下。

1. Layout

  RadioGroup 继承自 LinearLayout,故可以通过 android:orientation 设定水平或垂直布局,默认使用垂直布局。

public RadioGroup(Context context, AttributeSet attrs) {
    super(context, attrs);

    // retrieve selected radio button as requested by the user in the
    // XML layout file
    TypedArray attributes = context.obtainStyledAttributes(
            attrs, com.android.internal.R.styleable.RadioGroup, com.android.internal.R.attr.radioButtonStyle, 0);

    int value = attributes.getResourceId(R.styleable.RadioGroup_checkedButton, View.NO_ID);
    if (value != View.NO_ID) {
        mCheckedId = value;
    }

    final int index = attributes.getInt(com.android.internal.R.styleable.RadioGroup_orientation, VERTICAL);
    setOrientation(index);

    attributes.recycle();
    init();
}

可以通过 android:checkedButton 属性指定默认选中的按钮,从上面的代码也可以看到,这个属性的值被保存在 mCheckedId 中,mCheckedId 用于保存当前选中的 RadioButton 的 id,之后会反复出现。

2. onFinishInflate()

  RadioGroup 重写了 onFinishInflate(),用于设置初始选中的 RadioButton:

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

    // checks the appropriate radio button as requested in the XML file
    if (mCheckedId != -1) {
        mProtectFromCheckedChange = true;
        setCheckedStateForView(mCheckedId, true);
        mProtectFromCheckedChange = false;
        setCheckedId(mCheckedId);
    }
}

这里如果 mCheckedId 不为 -1,说明开发者通过 android:checkedButton 设置了要初始选中的 RadioButton,则通过 setCheckedStateForView() 选中具有该 id 的按钮,并通过 setCheckedId() 保存并通知选中状态的变化。至于 mProtectFromCheckedChange,之后会再做说明。setCheckedStateForView() 和 setCheckedId() 的代码很简单:

private void setCheckedStateForView(int viewId, boolean checked) {
    View checkedView = findViewById(viewId);
    if (checkedView != null && checkedView instanceof RadioButton) {
        ((RadioButton) checkedView).setChecked(checked);
    }
}

private void setCheckedId(@IdRes int id) {
    mCheckedId = id;
    if (mOnCheckedChangeListener != null) {
        mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
    }
}

值得注意的是,setCheckedStateForView() 中调用了 checkedView 的 setChecked(),setChecked() 是 Checkable 接口中的方法(RadioButton 继承自 CompoundButton,后者实现了 Checkable),但却通过 “instanceof RadioButton” 要求 checkedView 是 RadioButton 的实例,而没有使用 “instanceof Checkable” 这一更宽泛的限制,具体原因后面会说明。

3. addView()

  RadioGroup 重写了 addView():

@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
    if (child instanceof RadioButton) {
        final RadioButton button = (RadioButton) child;
        if (button.isChecked()) {
            mProtectFromCheckedChange = true;
            if (mCheckedId != -1) {
                setCheckedStateForView(mCheckedId, false);
            }
            mProtectFromCheckedChange = false;
            setCheckedId(button.getId());
        }
    }

    super.addView(child, index, params);
}

这里有一个细节:如果加入的 RadioButton 已经是选中的状态,那么在把这个 RadioButton 加入到 RadioGroup中时,就保持该 RadioButton 的选中状态,并取消掉之前选中的 RadioButton。

4. onChildViewAdded()

  RadioGroup 还通过 OnHierarchyChangeListener 来监听 ChildView 的加入:

private class PassThroughHierarchyChangeListener implements
        ViewGroup.OnHierarchyChangeListener {
    private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;

    public void onChildViewAdded(View parent, View child) {
        if (parent == RadioGroup.this && child instanceof RadioButton) {
            int id = child.getId();
            // generates an id if it's missing
            if (id == View.NO_ID) {
                id = View.generateViewId();
                child.setId(id);
            }
            ((RadioButton) child).setOnCheckedChangeWidgetListener(
                    mChildOnCheckedChangeListener);
        }

        if (mOnHierarchyChangeListener != null) {
            mOnHierarchyChangeListener.onChildViewAdded(parent, child);
        }
    }
    // ...
}

这里有两个关键逻辑。其一是如果所要加入的 child 没有自己的 id,就为它设置一个 id。如前所述,RadioGroup 使用 mCheckId 记录被选中的 RadioButton,这就要求 RadioGroup 中的所有 RadioButton 都有自己的 id。其二是通过 setOnCheckedChangeWidgetListener() 监听每个 RadioButton 的选中状态变化。

  setOnCheckedChangeWidgetListener() 是 RadioButton 继承自 CompoundButton 的方法,用于内部监听选中状态变化,独立于通过公用方法 setOnCheckedChangeListener() 设置的 OnCheckedChangeListener,由此 RadioGroup 获得了监听其中 RadioButton 的选中状态的能力,也使得 RadioGroup 依赖于 CompoundButton,无法适用于一般的的仅实现了 Checkable 接口的 View。又由于 RadioButton 覆盖了 CompoundButton 的 toggle() 方法:

@Override
public void toggle() {
    // we override to prevent toggle when the radio is already
    // checked (as opposed to check boxes widgets)
    if (!isChecked()) {
        super.toggle();
    }
}

点击未选中的 RadioButton 会选中 RadioButton,点击已选中的 RadioButton 则不会取消选中国内。由于 RadioButton 不可通过重复点击取消选中状态的特点,RadioGroup 便在各处做了前面所见的 “instanceof RadioButton” 的限制,仅支持 RadioButton。

5. onCheckedChanged()

  前面提到 RadioGroup 通过 setOnCheckedChangeWidgetListener() 监听其中 RadioButton 的选中状态,具体处理逻辑如下:

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);
    }
}

当点击一个未被选中的 RadioButton 时,RadioButton 会自己切换至选中状态,并触发上面的 onCheckedChanged()。由于之前选中的 RadioButton 并不会自动取消选中,首先要通过 setCheckedStateForView() 取消之前被选中的 RadioButton,然后通过 setCheckedId() 保存并通知当前选中的 RadioButton 的 id。点击已被选中的 RadioButton 不会触发 onCheckedChanged(),原因就在上面所列的 RadioButton 的 toggle()。

  这里又遇到了 mProtectFromCheckedChange。在 RadioGroup 中可以看到,在每次调用 setCheckedStateForView() 前后,都会先后设置 mProtectFromCheckedChange 为 true 和 false,如果 mProtectFromCheckedChange 为 true,onCheckedChanged() 就会直接 return,否则 setCheckedStateForView() 本身就会触发 onCheckedChanged(),导致无限循环。

6. RadioGroup 的一个问题

  通过阅读源码,可以发现 RadioGroup 的一个问题。RadioGroup 通过 mCheckedId 记录选中的 RadioButton,并通过 OnHierarchyChangeListener 确保加入的 RadioButton 都有自己的 id,按照 RadioGroup 的 addView() 逻辑,如果通过 addView() 加入的 RadioButton 已经处于选中状态,则保持该 RadioButton 的状态,并取消之前选中的 RadioButton:

@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
    if (child instanceof RadioButton) {
        final RadioButton button = (RadioButton) child;
        if (button.isChecked()) {
            mProtectFromCheckedChange = true;
            if (mCheckedId != -1) {
                setCheckedStateForView(mCheckedId, false);
            }
            mProtectFromCheckedChange = false;
            setCheckedId(button.getId());
        }
    }

    super.addView(child, index, params);
}

这里有一个例外情况,addView() 发生在 OnHierarchyChangeListener 的 onChildViewAdded() 之前,而 addView() 并不检查所加入的 RadioButton 是否有 id。如果通过 addView() 加入一个未指定 id 的、已经被选中的 RadioButton,那么上面 setCheckedId(button.getId()) 相当于 setCheckedId(-1),RadioGroup 无法在 mCheckedId 记录新加入的已选中的 RadioButton,该 RadioButton 会一直处于选中状态,用户是无法通过界面来取消选中的。该问题可以在 Android 7.0 上复现。

7. 总结

  以上就是 RadioGroup 的主要源码分析,由于 RadioGroup 依赖 CompoundButton 的 setOnCheckedChangeListener() 来监听其中按钮的选中状态,导致 RadioGroup 仅能支持 CompoundButton;又由于 RadioButton 实现了自己的 toggle() 逻辑,RadioGroup 便把自己限制在 RadioButton,仅能管理其中 RadioButton 的状态变化。

  Checkable 接口的本意是扩展 View 以支持选中状态,但该接口仅有三个方法:

public interface Checkable {
    
    /**
     * Change the checked state of the view
     * 
     * @param checked The new checked state
     */
    void setChecked(boolean checked);
        
    /**
     * @return The current checked state of the view
     */
    boolean isChecked();
    
    /**
     * Change the checked state of the view to the inverse of its current state
     *
     */
    void toggle();
}

并没有要求实现该接口的 View 提供监听其选中状态的方法,这也是实现通用的“CheckableGroup”的难点。解决方法可以是像 RadioGroup 一样,要求其中的子 View 不但要实现 Checkable,还要提供一个类似 OnCheckedChangeListener 的 Listener(最好是内部的,不影响对外接口),以监听 Checkable 的选中状态。另一种解决方法是利用现有的 View 的接口,比如 setOnCheckedChangeListener(),来监听子 View 的选中状态,但这样就要要求“CheckableGroup”外部不能再使用子 View 的 setOnCheckedChangeListener(),否则就会导致“CheckableGroup”的监听失效。

  此外,Checkable 也无法规定实现该接口的 View 是否要能够自行切换选中状态,如 RadioButton 可以通过点击事件来更改选中状态,点击未选中的 RadioButton 后,RadioButton 会自己切换到选中状态;而其他实现了 Checkable 的 View 可能并不具有此行为,而是依赖外部设置(通过 Checkable 的 setChecked() 和 toggle()),这也进一步加大了实现通用“CheckableGroup”的复杂度。