Android View系列---RadioGroup與RadioButton

weixin_34129145發表於2018-11-07

RadioGroup與RadioButton配合實現一組資料的單選問題。


插播一條資訊,在設定RadioButton的textColor的選中效果時,不能在drawable中建立想xml,得在res/color檔案中建立xml,然後引用。
radioButton.setTextColor(getResources().getColorStateList(R.color.xxx));


這個過程中,需要注意幾點。

  • RadioButton 設定前面小圓點消失 radioButton.setButtonDrawable(null)
  • RadioGroup 下面的RadioButton不能用其他控制元件包裹,否則就會是一個一個單獨的RadioButton。
  • 在動態建立RadioButton的時候,需要設定margin時,需要使用RadioGroup.LayoutParams 來建立佈局引數,不然設定margin不起作用。
  • 如何實現在RadioGroup與RadioButton配合時,現在點選兩次RadioButton,取消選中

對以上問題,具體我們分析下:

  1. 為什麼RadioGroup的直接子控制元件必須是RadioButt呢?
    先來看下RadioGroup的init()方法

    private void init() {
       // tracks children radio buttons checked state  追蹤RadioButton的選中狀態
       mChildOnCheckedChangeListener = new CheckedStateTracker(); 
       // 監聽層級變化 ViewGroup的子View移除和新增都會觸發相對應的方法。
       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;
     }
    

    再看下,PassThroughHierarchyChangeListener 實現的新增和移除的方法

    private class PassThroughHierarchyChangeListener implements
     ViewGroup.OnHierarchyChangeListener {
       private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
       /**
        * {@inheritDoc}
        */
       public void onChildViewAdded(View parent, View child) {
         // 關鍵點一: 如果child是RadioButton,才會執行
         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選擇狀態的監聽,設定到radiobutoon上,以實現監聽
           ((RadioButton) child).setOnCheckedChangeWidgetListener(
                 mChildOnCheckedChangeListener);
         }
         // 如果使用者自己定義,會繼續走使用者自己定義的方法
         if (mOnHierarchyChangeListener != null) {
           mOnHierarchyChangeListener.onChildViewAdded(parent, child);
         }
       }
       。。。。
     }  
    

接著,我們來看下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());
    }
  }
  // 父類ViewGroup的addView方法
  super.addView(child, index, params);
}

ViewGroup.java
從原始碼裡可以 addView()方法裡面呼叫了 addViewInner()方法,在裡面呼叫了disparchViewAdd(child)方法。
addView() —> addViewInner() —>dispatchViewAdd()

void dispatchViewAdded(View child) {
  onViewAdded(child);
  // 在RadioGroup init方法中,設定了該listener。在這裡回撥到onChildViewAdded()方法。再為radiobutton設定了OnCheckedChangeWidgetListener
  if (mOnHierarchyChangeListener != null) {
      mOnHierarchyChangeListener.onChildViewAdded(this, child);
  }
}

理清楚他們之間的關聯,現在該是點選的時候了。

之前介紹過onclick方法,之所以能執行,是整個事件分發完之後,執行了performClick()方法,然後執行clicklistener的onclick回撥方法。
RadioButton繼承了CompoundButton。 裡面的performClick方法
CompoundButton.java

@Override
public boolean performClick() {
   // 先執行radioButton的toggle方法
  toggle();
  // 執行 onclick方法。
  final boolean handled = super.performClick();
  if (!handled) {
      // View only makes a sound effect if the onClickListener was
      // called, so we'll need to make one here instead.
      playSoundEffect(SoundEffectConstants.CLICK);
  }
  return handled;
}

public void toggle() {
  setChecked(!mChecked);
}

RadioButton.java

/**
 * {@inheritDoc}
 * <p>
 * If the radio button is already checked, this method will not toggle the radio button.
 */
@Override
public void toggle() {
   // we override to prevent toggle when the radio is already
  // checked (as opposed to check boxes widgets)
  // 關鍵點四:如果該RadioButton已經選中了,就不執行了,這也是RadioButton一旦被選擇了,就不能取消選中的根本原因。
  if (!isChecked()) {
     super.toggle();
  }
}

那RadioButton怎麼和RadioGroup聯絡到一起的呢?

RadioButton的父類CompoundButton的toggle

public void toggle() {
  setChecked(!mChecked);
}

/**
 * <p>Changes the checked state of this button.</p>
 *    真正改變RadioButton狀態的方法。
 * @param checked true to check the button, false to uncheck it
 */
public void setChecked(boolean checked) {
  if (mChecked != checked) {
     mChecked = checked;
     // 重新整理選中態的UI
     refreshDrawableState();
     notifyViewAccessibilityStateChangedIfNeeded(
            AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);

    // Avoid infinite recursions if setChecked() is called from a listener
    if (mBroadcasting) {
        return;
    }

    mBroadcasting = true;
    if (mOnCheckedChangeListener != null) {
        mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
    }
    // 由前面關鍵點二可知,在RadioGroup中設定這個listener,所以回撥到RadioGroup中
    if (mOnCheckedChangeWidgetListener != null) {
        mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked);
    }

    mBroadcasting = false;            
  }
}

再來看下CheckedChangeWidgetListener的具體實現 CheckedStateTracker

private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
  public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    // prevents from infinite recursion
    if (mProtectFromCheckedChange) {
        return;
    }

    mProtectFromCheckedChange = true;
    //關鍵點三:將其他的RadioButton置為未選中 mCheckedId是之前選擇的View Id, 
    if (mCheckedId != -1) {
        setCheckedStateForView(mCheckedId, false);
    }
    mProtectFromCheckedChange = false;

    int id = buttonView.getId();
    // 更新 mCheckedId
    setCheckedId(id);
  }
}

private void setCheckedStateForView(int viewId, boolean checked) {
  // 找到之前選中的RadioButton。設定它的選中狀態。
  View checkedView = findViewById(viewId);
  if (checkedView != null && checkedView instanceof RadioButton) {
    ((RadioButton) checkedView).setChecked(checked);
  }
}

以上分析了整個RadioGroup為什麼子View必須是RadioButton 才能起到一組單選的效果,以及整個流程。

2、怎麼實現RadioButton點選兩次,取消選中呢?

由關鍵點四,可以知道如果RadioButton選中了,就不會執行setChecked()方法也就無法通知RadioGroup。
但是他扔會走touch方法、click方法。

我寫的思路就是,在RadioButton的點選事件裡面,人為的設定radiobutton的選中狀態。
但是RadioButton的click事件是發生在RadioButton自己的setChecked()之後,所以

radioButton.setOnClickListener(new OnClickListener(){
  @Override
  public void onClick(View v){
    // 不管該控制元件的狀態之前是選中態還是未選中態,這個時候,都已經是選中狀態了
    // 所以這個時候,radioButton的isChecked()得到的都是true
    // 所以關鍵是找到之前的選中的是哪個控制元件。
    if (checkedId == v.getId()){
        // 說明是第二次點選該控制元件,所以要做清除選中邏輯
        // 清除選中,其實縱觀整個RadioGroup,只有當前的RadioButton被選中了,而此時恰恰需要清除選中,則只需要把RadioGroup全部清空狀態即可。
        radioGroup.clearCheck();
    }
  }
})

根據View的整個事件分發機制,click事件,是最後觸發的。
所以我們可以在touch事件中獲得到之前選中的狀態。
又根據RadioGroup提供了一個方法getCheckedRadioButtonId(),返回選中態RadioButton的id

int checkedId = -1;
radioButton.setOnTouchListener(new OnTouchListener(){
    @Override
    public boolean onTouch(View v, MotionEvent event){
        // 這時候,新的button狀態還沒有設定,所以返回的是之前選中的button
        checkedId = radioGroup.getCheckedRadioButtonId();
        // 不消耗掉該事件,接著往下傳遞給click事件。
        return false;
    }
})

這樣就是整個過程了。

相關文章