自定義ViewGroup考慮padding,margin
在View.java中:
publicfinal void measure(int widthMeasureSpec,int heightMeasureSpec){
...
onMeasure();
...
}
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED: //未指定
result = size;
break;
case MeasureSpec.AT_MOST: //至多
case MeasureSpec.EXACTLY: //精確
result = specSize;
break;
}
return result;
}
在ViewGroup中:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
說明:
1. measure是final修飾的方法,不可被重寫。
在外部直接呼叫view.measure(int wSpec, int hSpec)來對view進行自身寬高的測量。
measure()內部呼叫onMeasure(), 所以自定義view時,重寫onMeasure即可。
2.MeasureSpec 這是一個含mode和size的結合體,不需要我們來具體的關心。
當在測量時,可以呼叫MeasureSpec.getSize | getMode() 得到相應的size和mode。
然後使用MeasureSpec.makeMeasureSpec(size, mode); 來建立MeasureSpec物件。
mode是根據使用該自定義view時的layout_with| height引數決定的,不能隨便new一個。
size可以自己指定,也可以直接使用 measureSpec.getSize()。
3.如果是一個View,重寫onMeasure時要注意:
如果在使用自定義view時,用了wrap_content。那麼在onMeasure中就要呼叫setMeasuredDimension,
來指定view的寬高。如果使用的fill_parent或者一個具體的dp值。那麼直接使用super.onMeasure即可。
4.如果是一個ViewGroup,重寫onMeasure時要注意:
首先,結合上面的介紹測量子View的寬高。
然後,結合子view的測量寬高來設定自身的測量寬高。
測量子view的方式有:
getChildAt(int index).可以拿到index上的子view。
通過getChildCount()得到直接子view的數目,再迴圈遍歷出子subView。
接著,subView.measure(int wSpec, int hSpec); // 使用子view自身的測量方法
或者呼叫viewGroup的測量子view的方法:
// 某一個子view,寬,高, 內部加上了viewGroup的padding值
measureChild(subView, int wSpec, int hSpec);
//所有子view 寬,高, 內部呼叫了measureChild方法
measureChildren(int wSpec, int hSpec);
//某一個子view,寬,高, 內部加上了viewGroup的padding值、margin值和傳入的寬高wUsed、hUsed
measureChildWithMargins(subView, intwSpec, int wUsed, int hSpec, int hUsed);
ViewGroup中的子View 不支援margin
總結兩點
- 自定義View在onDraw裡面需要處理padding的影響,widthMeasureSpec和heightMeasureSpec是包含padding大小的。
- 子View的margin屬性是由ViewGroup處理的,ViewGroup在onMeasure和onLayout時一定要考慮 ViewGroup自己的padding和子View的margin的影響。
你可能遇到過下面這樣的錯誤。
java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
下面我們分析為什麼會遇到這種錯誤以及解決方法。
你可能見過很多人在自定義ViewGroup的onMeasure()
中使用measureChildren(widthMeasureSpec, heightMeasureSpec);
來測量所有子View的尺寸。
ViewGroup.measureChildren的原始碼如下:
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
// ******************* 注意這裡 ********************
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
measureChild是不是不太合適呢,查閱了FrameLayout和LinearLayout等都沒有用過這個measureChildren呢,幾乎全部都重寫了,我們的自定義ViewGroup的measureChildren是不是應該是改成下面這樣才對。
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
// ******************* 注意這裡 ********************
measureChildWithMargins(child, widthMeasureSpec, heightMeasureSpec);
}
}
你應該看到了區別,measureChild和measureChildWithMargins區別就是
測量child尺寸時,保證child的 最大可用尺寸,感覺這個with字首起的不太好。
- measureChild減去了 ViewGroup的padding 保證child最大可用空間
- measureChildWithMargins減去了ViewGroup的padding和子View的margin 保證child最大可用空間
至於 measureChild和measureChildWithMargins中是如何**生成child的MeasureSpec,並最終呼叫child.measure() -- > child.onMeasure()的,這裡就不貼原始碼了。
總結 : ViewGroup中測量child一定要用measureChildWithMargins而不是measureChild
使用measureChildWithMargins後卻產生異常
終於改成measureChildWithMargins了,卻突然產生了異常,這是為什麼?
找到異常產生的位置,追蹤到ViewGroup.addView()方法,原始碼如下:
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
// **************** 注意這裡 ****************
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
異常資訊是 ClassCastExceptioncannot be cast to android.view.ViewGroup$MarginLayoutParams
而addView中,如果child.getLayoutParams();獲取不到,則預設生成一個generateDefaultLayoutParams();
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
這個預設生成的肯定不能強制轉換為MarginLayoutParams了。
再來看addView中的其他方法
private void addViewInner(View child, int index, LayoutParams params,boolean preventRequestLayout) {
if (!checkLayoutParams(params)) {
// **************** 注意這裡 ****************
params = generateLayoutParams(params);
}
if (preventRequestLayout) {
child.mLayoutParams = params;
} else {
child.setLayoutParams(params);
}
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
................
................
}
裡面還有檢測這個child的LayoutParams 是不是為空的,乾脆全部重寫得了。
在你的自定義ViewGroup中加入如下程式碼即可令 子View 的margin生效。
public class MyViewGroup extends ViewGroup {
// ..................... 其他程式碼省略 .....................
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MyLayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new MyLayoutParams(lp);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MyLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
public static class MyLayoutParams extends MarginLayoutParams {
public MyLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public MyLayoutParams(int width, int height) {
super(width, height);
}
public MyLayoutParams(LayoutParams lp) {
super(lp);
}
}
}
另外在ViewGroup.onLayout()時中千萬別忘記根據 ViewGroup的padding和子View的margin 靈活給子View佈局。
ViewGroupX.java
public class ViewGroupX extends ViewGroup {
private int mPaddingLeft;
private int mPaddingTop;
private int mPaddingRight;
private int mPaddingBottom;
public ViewGroupX(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ViewGroupX(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/**
* getPaddingLeft() 當前容器的paddingLeft
* widthMeasureSpec.size 當前容器的寬度(parentWidth - parentPadding - this.margin)
* view.getMeasuredHeight() (widthMeasureSpec.size - this.padding - viewMargin)
*/
mPaddingLeft = getPaddingLeft();
mPaddingTop = getPaddingTop();
mPaddingRight = getPaddingRight();
mPaddingBottom = getPaddingBottom();
int desireWidth = 0, desireHeight = 0;
int childCount = getChildCount(); // 直接子元素的個數
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
if (view.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) view.getLayoutParams();
measureChildWithMargins(view, widthMeasureSpec, 0, heightMeasureSpec, 0);
desireWidth = Math.max(desireWidth, view.getMeasuredWidth());
desireHeight += view.getMeasuredHeight() + (params.bottomMargin + params.topMargin);
}
}
// count with padding
desireWidth += mPaddingLeft + mPaddingRight;
desireHeight += mPaddingTop + mPaddingBottom;
// see if the size is big enough
desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());
setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
/**
* l this(相對父佈局)left
* r this(相對父佈局)right
*/
int top = mPaddingTop;
int childCount = getChildCount(); // 直接子元素的個數
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
if (view.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) view.getLayoutParams();
int viewLeft = mPaddingLeft + params.leftMargin;
int viewTop = top + params.topMargin;
view.layout(viewLeft, viewTop, viewLeft + view.getMeasuredWidth(), viewTop + view.getMeasuredHeight());
top += params.topMargin + view.getMeasuredHeight() + params.bottomMargin;
}
}
}
@Override
protected android.view.ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
}
@Override
public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected android.view.ViewGroup.LayoutParams generateLayoutParams(
android.view.ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
public static class LayoutParams extends MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(android.view.ViewGroup.LayoutParams source) {
super(source);
}
}
}
LineViewGroup.java
public class LineViewGroup extends ViewGroup {
private int mPaddingLeft;
private int mPaddingTop;
private int mPaddingRight;
private int mPaddingBottom;
public LineViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LineViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desireWidth = 0, desireHeight = 0;
mPaddingLeft = getPaddingLeft();
mPaddingTop = getPaddingTop();
mPaddingRight = getPaddingRight();
mPaddingBottom = getPaddingBottom();
int childCount = getChildCount(); // 直接子元素的個數
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
if (view.getVisibility() != GONE) {
ViewGroupX.LayoutParams params = (ViewGroupX.LayoutParams) view.getLayoutParams();
measureChildWithMargins(view, widthMeasureSpec, 0, heightMeasureSpec, 0);
desireWidth = Math.max(desireWidth, view.getMeasuredWidth());
desireHeight += view.getMeasuredHeight() + params.bottomMargin + params.topMargin;
}
}
// count with padding
desireWidth += mPaddingLeft + mPaddingRight;
desireHeight += mPaddingTop + mPaddingBottom;
// see if the size is big enough
desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());
setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
/**
* l this相對父佈局的left
*/
int top = mPaddingTop;
int childCount = getChildCount(); // 直接子元素的個數
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
if (view.getVisibility() != GONE) {
ViewGroupX.LayoutParams params = (ViewGroupX.LayoutParams) view.getLayoutParams();
int viewLeft = mPaddingLeft + params.leftMargin;
int viewTop = top + params.topMargin;
view.layout(viewLeft, viewTop, viewLeft + view.getMeasuredWidth(), viewTop + view.getMeasuredHeight());
top += params.topMargin + view.getMeasuredHeight() + params.bottomMargin;
}
}
}
@Override
protected android.view.ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new ViewGroupX.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
}
@Override
public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new ViewGroupX.LayoutParams(getContext(), attrs);
}
@Override
protected android.view.ViewGroup.LayoutParams generateLayoutParams(
android.view.ViewGroup.LayoutParams p) {
return new ViewGroupX.LayoutParams(p);
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:padding="@dimen/bottom_padding"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.lenovo.ext.views.ViewGroupX
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#123"
android:layout_margin="@dimen/margin"
android:padding="@dimen/view_group_padding">
<LinearLayout
android:id="@+id/top_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#234"
android:layout_margin="@dimen/layout_padding"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/view_group_text"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FFF"
android:src="@mipmap/ic_launcher"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/view_group_text"/>
</LinearLayout>
<LinearLayout
android:id="@+id/middle_layout"
android:background="#749"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/view_group_middle_text"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#000"
android:src="@mipmap/ic_launcher_foreground"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/view_group_middle_text"/>
</LinearLayout>
<com.lenovo.ext.views.LineViewGroup
android:layout_margin="@dimen/line_padding"
android:padding="@dimen/line_padding"
android:background="#264"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/view_group_bottom_text"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#000"
android:src="@mipmap/ic_launcher_round"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/view_group_bottom_text"/>
</com.lenovo.ext.views.LineViewGroup>
</com.lenovo.ext.views.ViewGroupX>
</LinearLayout>
</ScrollView>
dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="bottom_height">120dp</dimen>
<dimen name="layout_padding">10dp</dimen>
<dimen name="padding">5dp</dimen>
<dimen name="margin">5dp</dimen>
<dimen name="line_padding">7dp</dimen>
<dimen name="view_group_padding">15dp</dimen>
<dimen name="bottom_padding">8dp</dimen>
<dimen name="divider_height">0.5dp</dimen>
</resources>
string.xml
<resources>
<string name="app_name">DateEngine</string>
<string name="view_group_text">因此只有hashCode()的低位參加運算,發生不同的hash值,但是得到的index相同的情況的機率會大大增加,這種情況稱之為hash碰撞。 即,碰撞率會增大。</string>
<string name="view_group_middle_text">對於擴容導致需要新建陣列存放更多元素時,除了要將老陣列中的元素遷移過來,也記得將老陣列中的引用置null,以便GC</string>
<string name="view_group_bottom_text">當手指MotionEvent.ACTION_DOWN,需要確定手指點選的位置是否在兩個滑塊中的其中一個。而滑塊實際上是通過 Canvas.drawRect() 函式繪製的矩形,於是可以將手指觸控的座標與左右滑塊的 top、bottom、left 和 right 進行判斷,如果觸控,將 Slider.isTouching 設定為false</string>
</resources>