我們都知道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;