1. 程式人生 > >Android 自定義View實現多行RadioGroup單選(多選)

Android 自定義View實現多行RadioGroup單選(多選)

我們都知道RadioGroup可以實現選擇框,但它有一個侷限性,由於它是繼承自LinearLayout的,所以只能有一個方向,橫向或者縱向;

好在我們可以自定義View來實現多行的一個RadioGroup(我把它命令為MultiLineRadioGroup);

在貼出程式碼之前,先來分析一下思路:

1、首先自定義一個View繼承自ViewGroup,並且重寫onMeasure方法和onLayout方法,分別用於測量Child尺寸和在ViewGroup中放置Child;

2、指定單個child元素,通過自定義屬性的方式,由於要實現選擇,即child是checkable的,我這裡選擇使用CheckBox作為child,使用的時候先在layout下指定一個xml檔案並且設定它的根節點為CheckBox,然後把這個layout配置到MultiLineRadioGroup節點的child節點對應的屬性中;

3、onMeasure方法中,我們只需要遍歷ViewGroup的child並且呼叫measureChild方法對child進行測量;

4、onLayout方法中,我們根據child的尺寸來對child進行放置,具體來講就是分別定義兩個變數來記錄上一個child的左上角Y座標和右下角X座標,並且根據當前要layout的child的尺寸進行是否需要換行的判斷,如果當前要layout的child的寬度加上前一個View的右下角X座標值大於當前MultiLineRadioGroup的寬度,則換行;擺放一個child完成之後需要對兩個變數進行更新;

5、在onLayout的基礎上,我們加入了child的水平間距和垂直間距的設定,通過自定義屬性的方式;

6、在上面的基礎上,對child進行統一化的管理,管理它的選擇狀態,以及新增、刪除、選中一個child等常用方法;

再來預覽一下程式介面效果圖;


一、MultiLineRadioGroup.java

// org.ccflying.MultiLineRadioGroup
public class MultiLineRadioGroup extends ViewGroup implements OnClickListener {
	private int mX, mY;
	private List<CheckBox> viewList;
	private int childMarginHorizontal = 0;
	private int childMarginVertical = 0;
	private int childResId = 0;
	private int childCount = 0;
	private int childValuesId = 0;
	private boolean singleChoice = false;
	private int mLastCheckedPosition = -1;
	private OnCheckedChangedListener listener;
	private List<String> childValues;
	private boolean forceLayout;

	public MultiLineRadioGroup(Context context, AttributeSet attrs,
			int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		viewList = new ArrayList<CheckBox>();
		childValues = new ArrayList<String>();
		TypedArray arr = context.obtainStyledAttributes(attrs,
				R.styleable.MultiLineRadioGroup);
		childMarginHorizontal = arr.getDimensionPixelSize(
				R.styleable.MultiLineRadioGroup_child_margin_horizontal, 15);
		childMarginVertical = arr.getDimensionPixelSize(
				R.styleable.MultiLineRadioGroup_child_margin_vertical, 5);
		childResId = arr.getResourceId(
				R.styleable.MultiLineRadioGroup_child_layout, 0);
		childCount = arr.getInt(R.styleable.MultiLineRadioGroup_child_count, 0);
		singleChoice = arr.getBoolean(
				R.styleable.MultiLineRadioGroup_single_choice, true);
		childValuesId = arr.getResourceId(
				R.styleable.MultiLineRadioGroup_child_values, 0);
		if (childResId == 0) {
			throw new RuntimeException(
					"The attr 'child_layout' must be specified!");
		}
		if (childValuesId != 0) {
			String[] childValues_ = getResources()
					.getStringArray(childValuesId);
			for (String str : childValues_) {
				childValues.add(str);
			}
		}
		if (childCount > 0) {
			boolean hasValues = childValues != null;
			for (int i = 0; i < childCount; i++) {
				View v = LayoutInflater.from(context).inflate(childResId, null);
				if (!(v instanceof CheckBox)) {
					throw new RuntimeException(
							"The attr child_layout's root must be a CheckBox!");
				}
				CheckBox cb = (CheckBox) v;
				viewList.add(cb);
				addView(cb);
				if (hasValues && i < childValues.size()) {
					cb.setText(childValues.get(i));
				} else {
					childValues.add(cb.getText().toString());
				}
				cb.setTag(i);
				cb.setOnClickListener(this);
			}
		} else {
			Log.d("tag", "childCount is 0");
		}
		arr.recycle();
	}

	public MultiLineRadioGroup(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public MultiLineRadioGroup(Context context) {
		this(context, null, 0);
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		childCount = getChildCount();
		if (childCount > 0) {
			for (int i = 0; i < childCount; i++) {
				View v = getChildAt(i);
				measureChild(v, widthMeasureSpec, heightMeasureSpec);
			}
		}
	}

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		if (!changed && !forceLayout) {
			Log.d("tag", "onLayout:unChanged");
			return;
		}
		mX = mY = 0;
		childCount = getChildCount();
		if (childCount > 0) {
			for (int i = 0; i < childCount; i++) {
				View v = getChildAt(i);
				if (v.getMeasuredWidth() + childMarginHorizontal * 2 + mX > getWidth()) {
					mY++;
					mX = 0;
				}
				int startX = mX + childMarginHorizontal;
				int startY = mY * v.getMeasuredHeight() + (mY + 1)
						* childMarginVertical;
				v.layout(startX, startY, startX + v.getMeasuredWidth(), startY
						+ v.getMeasuredHeight());
				mX += v.getMeasuredWidth() + childMarginHorizontal * 2;
			}
		}
		forceLayout = false;
	}

	@Override
	public void onClick(View v) {
		if (singleChoice) {
			try {
				int i = (Integer) v.getTag();
				if (mLastCheckedPosition == i) {
					return;
				}
				if (mLastCheckedPosition >= 0
						&& mLastCheckedPosition < viewList.size()) {
					viewList.get(mLastCheckedPosition).setChecked(false);
				}
				mLastCheckedPosition = i;
				if (listener != null) {
					listener.onItemChecked(this, i);
				}
			} catch (Exception e) {
			}
		} else { // multiChoice

		}
	}

	public void setOnCheckChangedListener(OnCheckedChangedListener l) {
		this.listener = l;
	}

	public boolean setItemChecked(int position) {
		if (position >= 0 && position < viewList.size()) {
			if (singleChoice) {
				if (position == mLastCheckedPosition) {
					return true;
				}
				if (mLastCheckedPosition >= 0
						&& mLastCheckedPosition < viewList.size()) {
					viewList.get(mLastCheckedPosition).setChecked(false);
				}
			}
			viewList.get(position).setChecked(true);
			return true;
		}
		return false;
	}

	public boolean isSingleChoice() {
		return singleChoice;
	}

	public void setChoiceMode(boolean isSingle) {
		this.singleChoice = isSingle;
		if (singleChoice) {
			if (getCheckedValues().size() > 1) {
				clearChecked();
			}
		}
	}

	public int[] getCheckedItems() {
		if (singleChoice && mLastCheckedPosition >= 0
				&& mLastCheckedPosition < viewList.size()) {
			return new int[] { mLastCheckedPosition };
		}
		SparseIntArray arr = new SparseIntArray();
		for (int i = 0; i < viewList.size(); i++) {
			if (viewList.get(i).isChecked()) {
				arr.put(i, i);
			}
		}
		if (arr.size() != 0) {
			int[] r = new int[arr.size()];
			for (int i = 0; i < arr.size(); i++) {
				r[i] = arr.keyAt(i);
			}
			return r;
		}
		return null;
	}

	public List<String> getCheckedValues() {
		List<String> list = new ArrayList<String>();
		if (singleChoice && mLastCheckedPosition >= 0
				&& mLastCheckedPosition < viewList.size()) {
			list.add(viewList.get(mLastCheckedPosition).getText().toString());
			return list;
		}
		for (int i = 0; i < viewList.size(); i++) {
			if (viewList.get(i).isChecked()) {
				list.add(viewList.get(i).getText().toString());
			}
		}
		return list;
	}

	public int append(String str) {
		View v = LayoutInflater.from(getContext()).inflate(childResId, null);
		if (!(v instanceof CheckBox)) {
			throw new RuntimeException(
					"The attr child_layout's root must be a CheckBox!");
		}
		CheckBox cb = (CheckBox) v;
		cb.setText(str);
		cb.setTag(childCount);
		cb.setOnClickListener(this);
		viewList.add(cb);
		addView(cb);
		childValues.add(str);
		childCount++;
		forceLayout = true;
		postInvalidate();
		return childCount - 1;
	}

	public void addAll(List<String> list) {
		if (list != null && list.size() > 0) {
			for (String str : list) {
				append(str);
			}
		}
	}

	public boolean remove(int position) {
		if (position >= 0 && position < viewList.size()) {
			CheckBox cb = viewList.remove(position);
			removeView(cb);
			childValues.remove(cb.getText().toString());
			childCount--;
			forceLayout = true;
			if (position <= mLastCheckedPosition) { // before LastCheck
				if (mLastCheckedPosition == position) {
					mLastCheckedPosition = -1;
				} else {
					mLastCheckedPosition--;
				}
			}
			for (int i = position; i < viewList.size(); i++) {
				viewList.get(i).setTag(i);
			}
			postInvalidate();
			return true;
		}
		return false;
	}

	public boolean insert(int position, String str) {
		if (position < 0 || position > viewList.size()) {
			return false;
		}
		View v = LayoutInflater.from(getContext()).inflate(childResId, null);
		if (!(v instanceof CheckBox)) {
			throw new RuntimeException(
					"The attr child_layout's root must be a CheckBox!");
		}
		CheckBox cb = (CheckBox) v;
		cb.setText(str);
		cb.setTag(position);
		cb.setOnClickListener(this);
		viewList.add(position, cb);
		addView(cb, position);
		childValues.add(position, str);
		childCount++;
		forceLayout = true;
		if (position <= mLastCheckedPosition) { // before LastCheck
			mLastCheckedPosition++;
		}
		for (int i = position; i < viewList.size(); i++) {
			viewList.get(i).setTag(i);
		}
		postInvalidate();
		return true;
	}

	public void clearChecked() {
		if (singleChoice) {
			if (mLastCheckedPosition >= 0
					&& mLastCheckedPosition < viewList.size()) {
				viewList.get(mLastCheckedPosition).setChecked(false);
				mLastCheckedPosition = -1;
				return;
			}
		}
		for (CheckBox cb : viewList) {
			if (cb.isChecked()) {
				cb.setChecked(false);
			}
		}
	}

	public interface OnCheckedChangedListener {
		public void onItemChecked(MultiLineRadioGroup group, int position);
	}
}
MultiLineRadioGroup裡面的方法都不得太難,不再細說;

二、自定義屬性attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="MultiLineRadioGroup">
        <attr name="child_margin_horizontal" format="dimension" />
        <attr name="child_margin_vertical" format="dimension" />
        <attr name="child_layout" format="integer" />
        <attr name="child_count" format="integer" />
        <attr name="child_values" format="integer" />
        <attr name="single_choice" format="boolean" />
    </declare-styleable>

</resources>
在這裡我定義了6個自定義屬性,其中child_layout是必須指定的,並且child_layout對應的layout檔案的要節點必須是CheckBox,我們可以在這裡對child進行樣式的統一設定;其它的幾個屬性分別是水平間距、垂直間距、child元素個數,child(CheckBox)元素的Text陣列,單選/多選(預設是單選);

三、MultiLineRadioGroup使用

    <org.ccflying.MultiLineRadioGroup
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="0.0dip"
        android:layout_weight="1"
        app:child_count="8"
        app:child_layout="@layout/child"
        app:child_margin_horizontal="6.0dip"
        app:child_margin_vertical="2.0dip"
        app:child_values="@array/childvalues"
        app:single_choice="true" >
    </org.ccflying.MultiLineRadioGroup>
child.xml
<?xml version="1.0" encoding="utf-8"?>
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Child" >

</CheckBox>
childvalues
    <string-array name="childvalues">
        <item>child1</item>
        <item>child2</item>
        <item>childchild3</item>
        <item>child4</item>
        <item>childchild5</item>
        <item>childchildchild6</item>
        <item>child7</item>
        <item>child8</item>
    </string-array>


四、部分方法說明
  • append(String str) 附加一個child;
  • insert(int position, String str) 往指定位置插入child;
  • getCheckedValues()|getCheckedItems() 獲取選中項;
  • remove(int position) 刪除指定位置的child;
  • setItemChecked(int position) 選中指定位置的child;
五、Demo下載連結