学习《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
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中可直接调用)
/**
* 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
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 HorizontalWrapLineLayout extends 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 p instanceof 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 AutoRowLayout extends 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 mAdaptiveLines = new ArrayList();
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 && p instanceof 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;
}
}