一、項目概況
我們都知道RadioGroup可以實現選擇框,但它有一個局限性,由於它是繼承自LinearLayout的,所以只能有一個方向,橫向或者縱向;但有時候僅一行的RadioGroup並不能滿足實際的需求,比如在一行的寬度下顯示不完所有的選項,設計上又不允許左右滑動,這時候RadioGroup就不能滿足這樣的功能設計了;基於此,我寫了這個MultiLineRadioGroup並且開源出來;
1、程序界面效果圖

2、功能接口
在Api開發上,能夠用到的功能及我能想到的,基本都已經添加完畢;具體如下:
- child選項添加,刪除
- child選項選中,取消選中
- child對齊方式(左|中|右)
- child行間距,左右間距
- 設置選擇模式(單選|多選)
- 獲取已選選項
- child選擇狀態的監聽回調查
3、Demo鏈接地址
https://github.com/a284628487/MultiLineRadioGroup
二、項目分析
1、基於上面的功能設計,為了設計方便,添加了一些自定義屬性;
<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" /> <attr name="gravity" format="integer" /> </declare-styleable>
上面的幾個自定義屬性分別表示
- child水平間距
- child上下間距
- child對應的layout布局文件(后面會講到,此屬性必須配置)
- 初始元素個數
- 初始元素值列表
- 選擇模式(單選|多選)
- child對齊方式
2、在layout中使用MultiLineRadioGroup
(1)、定義一個包含MultiLineRadioGroup的xml文件
<org.ccflying.MultiLineRadioGroup android:id="@+id/content" android:layout_width="match_parent" android:layout_height="wrap_content" 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>
(2)、定義一個根節點為CheckBox的layout文件,並把該文件id設置到MultiLineRadioGroup的child_layout屬性中(注:該屬性必須設置)
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg" android:button="@null" android:padding="8.0dip" android:textColor="@color/text_color" > </CheckBox>
在MultiLineRadiaGroup中,它的子child元素為CheckBox,所以,必須指定一個要布局為CheckBox的child_layout,這個CheckBox可以根據你的需求設置它的不同狀態下的樣式;
3、MultiLineRadioGroup 核心方法分析
(1)、onMeasure
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); childCount = getChildCount(); int flagX = 0, flagY = 0, sheight = 0; if (childCount > 0) { for (int i = 0; i < childCount; i++) { View v = getChildAt(i); measureChild(v, widthMeasureSpec, heightMeasureSpec); int w = v.getMeasuredWidth() + childMarginHorizontal * 2 + flagX + getPaddingLeft() + getPaddingRight(); if (w > getMeasuredWidth()) { flagY++; flagX = 0; } sheight = v.getMeasuredHeight(); flagX += v.getMeasuredWidth() + childMarginHorizontal * 2; } rowNumber = flagY; } int height = (flagY + 1) * (sheight + childMarginVertical) + childMarginVertical + getPaddingBottom() + getPaddingTop(); setMeasuredDimension(getMeasuredWidth(), height); }
遍歷所有的child,並且調用measureChild來對child進行寬高的測量,再通過對寬度的累加與getWidth的值進行比較來判斷是否需要換行,並且對需要用到的行數進行記錄;
(2)、onLayout
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (!changed && !forceLayout) { Log.d("tag", "onLayout:unChanged"); return; } childCount = getChildCount(); int[] sX = new int[rowNumber + 1]; if (childCount > 0) { if (gravity != LEFT) { for (int i = 0; i < childCount; i++) { View v = getChildAt(i); int w = v.getMeasuredWidth() + childMarginHorizontal * 2 + mX + getPaddingLeft() + getPaddingRight(); if (w > getWidth()) { if (gravity == CENTER) { sX[mY] = (getWidth() - mX) / 2; } else { // right sX[mY] = (getWidth() - mX); } mY++; mX = 0; } mX += v.getMeasuredWidth() + childMarginHorizontal * 2; if (i == childCount - 1) { if (gravity == CENTER) { sX[mY] = (getWidth() - mX) / 2; } else { // right sX[mY] = (getWidth() - mX); } } } mX = mY = 0; } for (int i = 0; i < childCount; i++) { View v = getChildAt(i); int w = v.getMeasuredWidth() + childMarginHorizontal * 2 + mX + getPaddingLeft() + getPaddingRight(); if (w > getWidth()) { mY++; mX = 0; } int startX = mX + childMarginHorizontal + getPaddingLeft() + sX[mY]; int startY = mY * v.getMeasuredHeight() + (mY + 1) * childMarginVertical; v.layout(startX, startY, startX + v.getMeasuredWidth(), startY + v.getMeasuredHeight()); mX += v.getMeasuredWidth() + childMarginHorizontal * 2; } } mX = mY = 0; forceLayout = false; }
和onMeasure一樣,onLayout方法也需要對child進行遍歷,不過,在這里的遍歷就不是進行測量了,而是對child進行擺放,擺放的時候就需要用到onMeasure方法里面所測量出的子元素的寬高等屬性;
遍歷可能會遍歷兩次,如果child對齊方式是非Left的情況下,第一次遍歷計算出每行的空隙,然后根據對齊方式算出每行的第一個child的偏移left的距離,第二次遍歷的時候,再根據之前算出的偏移距離對child進行layout;
(3)、其它方法
- append(String str) 附加一個child;
- insert(int position, String str) 往指定位置插入child;
- getCheckedValues()|getCheckedItems() 獲取選中項;
- remove(int position) 刪除指定位置的child;
- setItemChecked(int position) 選中指定位置的child;
- setGravigy(int gravity) 設置child對齊方式;
這些方法都是根據常用或者可能用到的方法來進行實現的,比較簡單,就不再貼出代碼,上面的Demo鏈接中都有;

Over!
