自定義View二篇,如何自定義一個規範的ViewGroup
- 2020 年 2 月 15 日
- 筆記
前言
在自定義View開篇,必須跨過的一道坎兒 中,我們介紹了自定義View的幾種方式,以及如何實現一個規範的自定義View,上文中也說了,實現一個規範的自定義ViewGroup是一件比較困難的事情,因為要考慮的情況包含 本身的padding以及子view的margin 與 本身wrap_content 問題。

如何實現一個規範的ViewGroup,以實現垂直布局的LinerLayout為例
- 新建LinerLayoutView 繼承自ViewGroup
首先我們讓LinerLayoutView 適應wrap_content的情況,在onMeasure中處理如下,同自定義View處理一樣,不同的是我們需要計算子View寬高,代碼如下所示:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); measureChildren(widthMeasureSpec,heightMeasureSpec); int totalheight = 0; int totalWidth = 0; for (int i = 0; i < getChildCount(); i++){ View childView = getChildAt(i); int childrWidth = childView.getMeasuredWidth(); int childHeight = childView.getMeasuredHeight(); totalheight = totalheight + childHeight; totalWidth = Math.max(totalWidth,childrWidth); } if (heightMode == MeasureSpec.AT_MOST && widthMode == MeasureSpec.AT_MOST){ setMeasuredDimension(totalWidth,totalheight); }else if (heightMode == MeasureSpec.AT_MOST){ setMeasuredDimension(measureWidth,totalheight); }else if (widthMode == MeasureSpec.AT_MOST){ setMeasuredDimension(totalWidth,measureHeight); } }
我們需要調用下面方法來測量子view
measureChildren(widthMeasureSpec,heightMeasureSpec);
因為這裡我們是垂直排列的,所以要循環計算view的總高度,wrap_content情況對應的寬度為子View最大的寬度,上面代碼比較簡單 我們主要來看onLayout方法。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int totalHeight = 0; for (int i = 0; i < getChildCount(); i++){ View childView = getChildAt(i); int childrWidth = childView.getMeasuredWidth(); int childHeight = childView.getMeasuredHeight(); childView.layout(0,totalHeight,childrWidth,totalHeight + childHeight); totalHeight = totalHeight + childHeight; } }
view.layout方法 是將view放置在什麼地方,分別對應left、top、right、bottom四個點,這裡我們需要注意的是,務必使用getMeasureWidth不能使用getWidth,因為前者是在測量的時候獲取的,後者在布局完成之後才能獲取到。
在布局文件中 引用這個ViewGroup,並且添加兩個子View,代碼如下所示:
<com.support.hlq.layout.LinerLayoutView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/colorPrimaryDark"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="kkko" android:textColor="@color/colorAccent" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="kkko" android:textColor="@color/colorAccent" /> </com.support.hlq.layout.LinerLayoutView>
運行結果如圖所示,可以看到我們已經適配了wrap_content的情況

考慮ViewGroup的padding問題
上面代碼,已經實現了最簡單的垂直排列,我們給LinerLayoutView設置大小為40的邊距,發現邊距並沒有生效,所以我們需要在onMeasure以及onLayout的方法中考慮padding問題。改寫onMeasure方法如下:
for (int i = 0; i < getChildCount(); i++){ View childView = getChildAt(i); int childrWidth = childView.getMeasuredWidth(); int childHeight = childView.getMeasuredHeight(); totalheight = totalheight + childHeight; totalWidth = Math.max(totalWidth,childrWidth); } totalheight = totalheight + getPaddingTop() + getPaddingBottom(); totalWidth = totalWidth + getPaddingLeft() + getPaddingRight();
因為我們處理的是ViewGroup的邊距,所以我們只需要對最終計算的高度和寬度分別加上上下邊距 和左右邊距即可,這裡你可能會有疑問
int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
為什麼上面兩處代碼不需要加上邊距呢,因為MeasureSpec獲取到的大小是已經包含過padding,所以我們不需要處理通過MeasureSpec獲取的寬高。
接下來,我們修改layout方法
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int totalHeight = getPaddingTop(); for (int i = 0; i < getChildCount(); i++){ View childView = getChildAt(i); int childrWidth = childView.getMeasuredWidth(); int childHeight = childView.getMeasuredHeight(); childView.layout(0 + getPaddingLeft(), totalHeight, childrWidth + getPaddingLeft(), totalHeight + childHeight + getPaddingTop()); totalHeight = totalHeight + childHeight; } }
left點我們加上getPaddingLeft,總高度由0修改為getPaddingTop,其他兩點也分別加上邊距布局即可,運行結果如下所示

我們可以看出ViewGroup的邊距已經生效了。
考慮子View的Margin問題
到這裡 這個自定義的ViewGroup還是不夠規範,不信我們來給第一個TextView設置下邊距為20dp
<com.support.hlq.layout.LinerLayoutView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/colorPrimaryDark"> <TextView android:layout_marginBottom="20dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="kkko" android:textColor="@color/colorAccent" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="kkko" android:textColor="@color/colorAccent" /> </com.support.hlq.layout.LinerLayoutView>
在這裡,因為要獲取到margin所以必須重寫
generateLayoutParams 方法和 generateDefaultLayoutParams 方法
@Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } @Override protected LayoutParams generateLayoutParams(LayoutParams p) { return new MarginLayoutParams(p); }
重寫上面三個方法後,我們才能獲取margin參數,同樣的我們首先在onMeasure中考慮子view邊距
for (int i = 0; i < getChildCount(); i++) { View childView = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); int childrWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; totalheight = totalheight + childHeight; totalWidth = Math.max(totalWidth, childrWidth); }
還要在獲取寬高的時候加上對應的邊距即可,同樣還需要在onLayout方法中考慮子view 的邊距問題,修改如下:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int totalHeight = getPaddingTop(); for (int i = 0; i < getChildCount(); i++) { View childView = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); int childrWidth = childView.getMeasuredWidth() ; int childHeight = childView.getMeasuredHeight() ; childView.layout(getPaddingLeft() + lp.leftMargin, totalHeight + lp.topMargin, childrWidth + getPaddingLeft() + lp.leftMargin, totalHeight + childHeight + getPaddingTop() + lp.topMargin); totalHeight = totalHeight + childHeight + lp.topMargin + lp.bottomMargin; } }
在layout的時候考慮子view的邊距,記得在計算總高度的時候 也要加上邊距和下邊距,運行結果如下圖所示

這樣一來,我們就定義了一個比較規範的ViewGroup,加上我們上篇文章講的自定義屬性,相信大家都掌握了自定義View的方法了。
