Android自定义ViewGroup,实现自动换行
学习《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<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 && 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; } }