习《Android开发艺术探索》中自定义ViewGroup章节
自定义ViewGroup总结的知识点
一.自定义ViewGroup中,onMeasure理解
onMeasure(int widthMeasureSpec,int heightMeasureSpec); 需要进行补充的逻辑
1.对布局设置为wrap_content的兼容,具体查看下一篇日志的构建MeasureSpec的方法
最终实现是在onMeasure(...)方法中对LayoutParams设置为wrap_content的实现,在构建MeasureSpec时将,这个转换为MeasureSpec.AT_MOST这样的设置模式
注:下面模式一般适用于单View(不包括ViewGroup),因为ViewGroup设置为wrap_content时,是测量所有子View高/宽的和
单View
1
2
3
4
5
6
7
if(widthMode == MeasureSpec.AT_MOST && height == MeasureSpec.AT_MOST){
setMeasureDimission(测量的宽,测量的高);
}else if(widthMode == MeasureSpec.AT_MOST ){
setMeasureDimission(测量的宽,heightMeasureSpec);//heightMeasureSpec是父布局指定
}else if(heightMode == MeasureSpec.AT_MOST ){
setMeasureDimission(widthMeasureSpec,测量的高);//widthMeasureSpec是父布局指定
}
ViewGroup(此方法在ViewGroup中已经实现,在自定义ViewGroup中可直接调用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* Utility to reconcile a desired size and state, with constraints imposed
* by a MeasureSpec. Will take the desired size, unless a different size
* is imposed by the constraints. The returned value is a compound integer,
* with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
* optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting
* size is smaller than the size the view wants to be.
*
* @param size How big the view wants to be
* @param measureSpec Constraints imposed by the parent
* @return Size information bit mask as defined by
* {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}.
*/
public static int resolveSizeAndState(int size,int measureSpec,int childMeasuredState) {
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:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
}else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result | (childMeasuredState&MEASURED_STATE_MASK);
}
2.onMeasure方法中参数的理解,widthMeasureSpec和heightMeasureSpec,在布局中去掉了margin参数后的值,将测量值通过setMeasureDimission设置该布局的宽和高
理解如下图
二,自定义ViewGroup中,onLayout的理解
1.onLayout(boolean changed, int l, int t, int r, int b) 对方法中参数,changed为当前布局是否改变
l,t,r,b是当前的布局的参数坐标,即有 当前控件宽度 width = r - l 高度 height = b - t;这里包括了padding的值
注意:在自定义ViewGroup的时候,实际计算得到的宽高均需要加入padding的值和子布局的margin值,而onMeasure或onLayout方法中传递过来的值,均不包含padding的值,这里要减去
总结:ViewGroup本身计算不用加入ViewGroup本身的margin,但要考虑padding变化,同时要考虑子View中margin的值
以上方法均需要调用子布局的measure和layout方法
例子: 自定义有自动换行功能的ViewGroup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package com.tongcheng.android.travel.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import com.tongcheng.android.R;
/**
* Created by lcl11718 on 2016/12/6.
* 横向实现自动换行的ViewGroup
*/
public class HorizontalWrapLineLayoutextends ViewGroup {
private int mVerticalSpace;
private int mHorizontalSpace;
public HorizontalWrapLineLayout(Context context) {
this(context,null);
}
public HorizontalWrapLineLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
setAttributeSet(context, attrs);
}
/**
* 设置自定义属性
*
* @param context
* @param attrs
*/
private void setAttributeSet(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HorizontalWrapLineLayout);
//属性中定义左右边距 padding margin
//属性中定义每一个距离垂直方向vertalSpace 和水平方向horizontalSpace
mVerticalSpace = (int) a.getDimension(R.styleable.HorizontalWrapLineLayout_verticalWrapSpace,0);
mHorizontalSpace = (int) a.getDimension(R.styleable.HorizontalWrapLineLayout_horizontalWrapSpace,0);
a.recycle();
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected boolean checkLayoutParams(LayoutParams p) {
return pinstanceof MarginLayoutParams;
}
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
// 这里的高度和宽度是去掉margin的值
int horizontalPadding = getPaddingLeft() + getPaddingRight();
int measureWidth = horizontalPadding;
int verticalPadding = getPaddingTop() + getPaddingBottom();
int measureHeight = verticalPadding;
final int childCount = getChildCount();
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
if (childCount ==0) {
setMeasuredDimension(0,0);
return;
}
for (int i =0; i < childCount; i++) {
View childView = getChildAt(i);
//测量子View
if (childView.getVisibility() != View.GONE) {
measureChildWithMargins(childView, widthMeasureSpec,0, heightMeasureSpec,0);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int childMeasuredHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childMeasuredWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
measureWidth += childMeasuredWidth + mHorizontalSpace;
if (measureWidth > widthSpaceSize) {
measureHeight += childMeasuredHeight + mVerticalSpace;
measureWidth = getPaddingLeft() + getPaddingRight();
}
if (childCount -1 == i && measureWidth >0) {
measureHeight += childMeasuredHeight + mVerticalSpace;
}
}
}
if (heightSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthMeasureSpec, measureHeight);
}else {
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed,int l,int t,int r,int b) {
//这里的四角参数,是去掉Margin的参数
int childCount = getChildCount();
int left = getPaddingLeft();
int top = getPaddingTop();
int right = r - getPaddingRight();
int currentLeft = left;
int currentTop = top;
int lineHeight =0;
for (int i =0; i < childCount; i++) {
View childView = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
//确定4个点
if (currentLeft + childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin >= right) {//换行
currentLeft = left;
currentTop += lineHeight + mVerticalSpace;
lineHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}else {
lineHeight = Math.max(lineHeight, childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
childView.layout(
currentLeft + lp.leftMargin,
currentTop + lp.topMargin,
currentLeft + lp.leftMargin + childView.getMeasuredWidth(),
currentTop + lp.topMargin + childView.getMeasuredHeight());
currentLeft += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + mHorizontalSpace;
}
}
}
上个版本是简略的实现,下面是优化之后,这个版本支持Grivity布局
package com.tongcheng.android.travel.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.LayoutDirection;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import com.tongcheng.android.R;
import java.util.ArrayList;
import java.util.List;
/**
* Created by lcl11718 on 2016/12/8.
* 自动换行容器
*/
public class AutoRowLayoutextends ViewGroup {
/**
* 平均分配
*/
public static final int AVERAGE =0;
/**
* 自适应
*/
public static final int ADAPTIVE =1;
/**
* style
*/
private int mStyleType;
/**
* 列之间间距
*/
private int mColumnSpace;
/**
* 行之间间距
*/
private int mRowSpace;
/**
* 列数量
*/
private int mColumnNum;
/**
* 行数
*/
private int mRowNum;
/**
* 最大行数
*/
private int mMaxLine;
/**
* 对齐方式
*/
private int mGravity = Gravity.START | Gravity.TOP;
/**
* 自适应测量算法 记录行数
*/
private List<Integer> mAdaptiveLines =new ArrayList<Integer>();
public AutoRowLayout(Context context) {
this(context,null,0);
}
public AutoRowLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public AutoRowLayout(Context context, AttributeSet attrs,int defStyleAttr) {
super(context, attrs, defStyleAttr);
setAttributes(context, attrs);
}
/**
* set basic attrs
*
* @param context
* @param attrs
*/
public void setAttributes(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.AutoRowLayout);
mStyleType = ta.getInt(R.styleable.AutoRowLayout_style_type,0);//0 是平均分
mColumnSpace = (int) ta.getDimension(R.styleable.AutoRowLayout_columnSpace,0);
mRowSpace = (int) ta.getDimension(R.styleable.AutoRowLayout_rowSpace,0);
mColumnNum = ta.getInt(R.styleable.AutoRowLayout_columnNum,0);
mRowNum = ta.getInt(R.styleable.AutoRowLayout_rowNum,0);
mMaxLine = ta.getInt(R.styleable.AutoRowLayout_maxLine,0);
mGravity = ta.getInt(R.styleable.AutoRowLayout_android_gravity,0);
ta.recycle();
}
/**
* set style type
*
* @param type
*/
public void setStyleType(int type) {
this.mStyleType = type;
}
public void setColumnSpace(int columnSpace) {
this.mColumnSpace = columnSpace;
}
public void setRowSpace(int rowSpace) {
this.mRowSpace = rowSpace;
}
public void setColumnNum(int columnNum) {
this.mColumnNum = columnNum;
}
public void setRowNum(int rowNum) {
this.mRowNum = rowNum;
}
public void setGravity(int gravity) {
this.mGravity = gravity;
}
/***********************************
* 加入Margin start
**************************************/
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected boolean checkLayoutParams(LayoutParams p) {
return p !=null && pinstanceof MarginLayoutParams;
}
/***********************************
* 加入Margin end
**************************************/
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
if (mStyleType == AVERAGE) {
measureAverage(widthMeasureSpec, heightMeasureSpec);
}else if (mStyleType == ADAPTIVE) {
measureAdaptive(widthMeasureSpec, heightMeasureSpec);
}
}
/**
* 平均测量算法
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
private void measureAverage(int widthMeasureSpec,int heightMeasureSpec) {
int count = getChildCount();
if (count ==0) {
setMeasuredDimension(0,0);
return;
}
int widthPadding = getPaddingLeft() + getPaddingRight();
int heightPadding = getPaddingTop() + getPaddingBottom();
int childState =0;
// get a max child width for widthMeasureSpec
int maxChildWidth = getMaxChildWidth(count, widthMeasureSpec, heightMeasureSpec, childState);
int maxWidth =0;
if (mColumnNum >0) {
maxWidth = maxChildWidth * mColumnNum + (mColumnNum -1) * mColumnSpace + widthPadding;
}else {
throw new RuntimeException("autoRowLayout must set a column num");
}
int widthMeasureSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
View firstChild = getChildAt(0);
int totalRowNumHeight = mRowNum * firstChild.getMeasuredHeight() + (mRowNum -1) * mRowSpace + heightPadding;
int maxHeight = mRowNum >0 ? totalRowNumHeight : getTotalHeightNoRows(count, heightPadding, firstChild);
int limitMaxWidth = (widthMeasureSize - widthPadding - (mColumnNum -1) * mColumnSpace) / mColumnNum;
int maxAllowWidth = MeasureSpec.EXACTLY == widthMeasureMode ? limitMaxWidth : Math.min(maxChildWidth, limitMaxWidth);
setWidthLayoutParams(count, maxAllowWidth);
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
int heightAndState = resolveSizeAndState(maxHeight, heightMeasureSpec,0);
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightAndState);
}
/**
* set per max width of views
*
* @param count
* @param maxChildWidth
*/
private void setWidthLayoutParams(int count,int maxChildWidth) {
for (int index =0; index < count; index++) {
View child = getChildAt(index);
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams lp =new MarginLayoutParams(maxChildWidth, child.getLayoutParams().height);
child.setLayoutParams(lp);
}
}
/**
* get max height not set row num
*
* @param count
* @param heightPadding
* @param child
* @return
*/
private int getTotalHeightNoRows(int count,int heightPadding, View child) {
int allowRowNums = count / mColumnNum;
int maxHeight = allowRowNums * child.getMeasuredHeight() + (allowRowNums -1) * mRowSpace + heightPadding;
if (count % mColumnNum >0) {
maxHeight += child.getMeasuredHeight() + mRowSpace;
}
return maxHeight;
}
/**
* get a max child width for this layout
*
* @param count
* @param widthMeasureSpec
* @param heightMeasureSpec
* @return
*/
private int getMaxChildWidth(int count,int widthMeasureSpec,int heightMeasureSpec,int childState) {
int maxChildWidth =0;
for (int index =0; index < count; index++) {
View child = getChildAt(index);
if (child.getVisibility() == View.GONE) {
continue;
}
measureChildWithMargins(child, widthMeasureSpec,0, heightMeasureSpec,0);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
childState = combineMeasuredStates(childState, child.getMeasuredState());
maxChildWidth = Math.max(maxChildWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
}
return maxChildWidth;
}
/**
* 自适应测量算法
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
private void measureAdaptive(int widthMeasureSpec,int heightMeasureSpec) {
int count = getChildCount();
if (count ==0) {
setMeasuredDimension(0,0);
return;
}
int widthPadding = getPaddingLeft() + getPaddingRight();
int heightPadding = getPaddingTop() + getPaddingBottom();
int width = widthPadding;
int height = heightPadding;
int lineMaxHeight =0;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int childState =0;
mAdaptiveLines.clear();
for (int index =0; index < count; index++) {
View child = getChildAt(index);
if (child.getVisibility() == View.GONE) {
continue;
}
measureChildWithMargins(child, widthMeasureSpec,0, heightMeasureSpec,0);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
if (width + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin > widthSize) {//换行
height += lineMaxHeight + mRowSpace;
width = widthPadding + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin + mColumnSpace;
lineMaxHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
mAdaptiveLines.add(index);
}else {
lineMaxHeight = Math.max(lineMaxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
width += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + mColumnSpace;
childState = combineMeasuredStates(childState, child.getMeasuredState());
}
}
mAdaptiveLines.add(count);
height += lineMaxHeight;
height = Math.max(height, getSuggestedMinimumHeight());
int heightAndState = resolveSizeAndState(height, heightMeasureSpec,0);
setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState), heightAndState);
}
@Override
protected void onLayout(boolean changed,int l,int t,int r,int b) {
if (mStyleType == AVERAGE) {
layoutAverage(l, t, r, b);
}else if (mStyleType == ADAPTIVE) {
layoutAdaptive(l, t, r, b);
}
}
/**
* 平均布局算法
*
* @param left
* @param top
* @param right
* @param bottom
*/
private void layoutAverage(int left,int top,int right,int bottom) {
int count = getChildCount();
if (count ==0) {
return;
}
int childLeft;
int childTop =0;
int lineMaxHeight =0;
int totalRowNum = count / mColumnNum + (count % mColumnNum ==0 ?0 :1);
int maxRowNum = mRowNum >0 ? mRowNum : totalRowNum;
for (int rowNum =0; rowNum < maxRowNum; rowNum++) {
childLeft = getPaddingLeft();
childTop += rowNum >0 ? lineMaxHeight + mRowSpace : getPaddingTop();
for (int columnNum =0; columnNum < mColumnNum; columnNum++) {
if (columnNum + mColumnNum * rowNum >= count) {
break;
}
View child = getChildAt(columnNum + mColumnNum * rowNum);
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
child.layout(
childLeft + lp.leftMargin,
childTop + lp.topMargin,
childLeft + lp.leftMargin + lp.width,
childTop + lp.topMargin + lp.height);
childLeft += lp.width + lp.leftMargin + lp.rightMargin + mColumnSpace;
lineMaxHeight = Math.max(lineMaxHeight, lp.height + lp.topMargin + lp.bottomMargin);
}
}
}
/**
* 自适应布局算法
*
* @param left
* @param top
* @param right
* @param bottom
*/
private void layoutAdaptive(int left,int top,int right,int bottom) {
int count = getChildCount();
if (count ==0) {
return;
}
int width = right - left;
int childSpace = width - getPaddingLeft() - getPaddingRight();
int limitLines = mMaxLine >0 ? Math.min(mMaxLine, mAdaptiveLines.size()) : mAdaptiveLines.size();
int[] childLefts =new int[limitLines];
int totalChildHeight =0;
for (int rowIndex =0; rowIndex < limitLines; rowIndex++) {
int startRowIndex = rowIndex >0 ? mAdaptiveLines.get(rowIndex -1) :0;
int endRowIndex = mAdaptiveLines.get(rowIndex);
int maxChildWidth =0;
int lineMaxHeight =0;
for (; startRowIndex < endRowIndex; startRowIndex++) {
View child = getChildAt(startRowIndex);
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
maxChildWidth += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
if (startRowIndex != endRowIndex -1) {
maxChildWidth += mColumnSpace;
}
lineMaxHeight = Math.max(lineMaxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
totalChildHeight += lineMaxHeight;
childLefts[rowIndex] = getChildLeft(childSpace - maxChildWidth);
}
int childTop = getChildTop(top, bottom, totalChildHeight);
for (int rowIndex =0; rowIndex < limitLines; rowIndex++) {
int startRowIndex = rowIndex >0 ? mAdaptiveLines.get(rowIndex -1) :0;
int endRowIndex = mAdaptiveLines.get(rowIndex);
int childLeft = childLefts[rowIndex];
for (; startRowIndex < endRowIndex; startRowIndex++) {
View child = getChildAt(startRowIndex);
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
child.layout(
childLeft + lp.leftMargin,
childTop + lp.topMargin,
childLeft + lp.leftMargin + child.getMeasuredWidth(),
childTop + lp.topMargin + child.getMeasuredHeight());
childLeft += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + mColumnSpace;
}
}
}
/**
* get top of child View
*
* @param top
* @param bottom
* @param totalChildHeight
* @return
*/
private int getChildTop(int top,int bottom,int totalChildHeight) {
int childTop;
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = getPaddingTop() + bottom - top - totalChildHeight;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = getPaddingTop() + (bottom - top - totalChildHeight) /2;
break;
case Gravity.TOP:
default:
childTop = getPaddingTop();
break;
}
return childTop;
}
/**
* get left value of row
*
* @param widthSpace
* @return
*/
private int getChildLeft(int widthSpace) {
int childLeft;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
final int absoluteGravity = Gravity.getAbsoluteGravity(minorGravity, LayoutDirection.LTR);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = getPaddingLeft() + (widthSpace /2);
break;
case Gravity.RIGHT:
childLeft = getPaddingLeft() + widthSpace;
break;
case Gravity.LEFT:
default:
childLeft = getPaddingLeft();
break;
}
return childLeft;
}
}
转自:https://www.cnblogs.com/ghhryr-lichl/p/6141223.html