言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

何番目のRadioButtonがチェックされているかうっかり取得されてしまう話

RadioGroup#getCheckedRadioButtonId() が、チェックが入ったviewのidではなく、何番目にチェックが入っているかを取得できると勘違いして、実際その勘違い通りの振る舞いをするように見えるケースが存在した調査ログです。

結論から言ってしまうと、RadioButtonにidを振らないとView#generateViewId() が1から順に勝手に新規idを振ってしまい、それを返してしまうという話です。

現象

RadioButtonを3つ配置したRadioGroupが存在しており、そのうち1番目にチェックをつけた状態でRadioGroup#getCheckedRadioButtonId()が1を返すケースがある。 この振る舞いになる条件は、

  1. RadioButtonにidをふらない
  2. 初回起動時のみ(n回目に対象Activityを起動した際には3n-2が返ってくる)

具体的にはこんなレイアウト

    <RadioGroup
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/radiogroup_sample">
        <RadioButton android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:text="@string/sample1"/>
        <RadioButton android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:text="@string/sample2"/>
        <RadioButton android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:text="@string/sample3"/>
    </RadioGroup>

何がおこったか

RadioButtonがviewのhierarchyに追加される時点で、idが存在するかチェックし 存在しない場合は新規に追加します。

RadioGroup.java

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

        /**
         * {@inheritDoc}
         */
        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);
                }

View.java

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

    public static int 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;
            }
        }
    }

generateViewId() は1で初期化したAtomicIntegerから順にインクリメントした値を返すので、それぞれn番目がチェックされているかのように振舞っていたのです。(returnするのがnewValueではなくresultなので初回は1が返ってきます。) ただし、例えばActivityを再起動するなどして再度viewのhierarchyに追加されるときは1からではなく続きからインクリメントされるので、初回起動時のみn番目がチェックされているかのように見えていたというオチでした。

余談

何番目がチェックされているか返すmethodはRadioGroupを拡張したクラスでaddView() をoverrideしてやれば作れそう。 ただし、「何番目がチェックされているか」ではなく「何がチェックされているか」の方がどう考えても重要なのであまり使い道はないかもしれない。

public class CustomRadioGroup extends RadioGroup {

  private List<RadioButton> mChildren = new ArrayList<>();
  public CustomRadioGroup(Context context) {
    super(context);
  }

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

  @Override
  public void addView(View child, int index, ViewGroup.LayoutParams params) {
    if (child instanceof RadioButton) {
      mChildren.add((RadioButton) child);
    }
    super.addView(child, index, params);
  }

  public int getSelectedPosition() {
    int selectedViewId = getCheckedRadioButtonId();
    if (selectedViewId == -1 || mChildren.isEmpty()) {
      return -1;
    }
    for (int i = 0; i < mChildren.size() -1; i++) {
      if (mChildren.get(i).getId() == selectedViewId) {
        return i + 1;
      }
    }
    return 0;
  }
}