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
MultiLineRadioGroup裡面的方法都不得太難,不再細說;// 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); } }
二、自定義屬性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;