Android自定义ViewGroup,onMeasureonLayout,实现流式布局

Posted SamDlex

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android自定义ViewGroup,onMeasureonLayout,实现流式布局相关的知识,希望对你有一定的参考价值。


其实自定义ViewGroup与自定义View很像。它们本质都是View,区别在于ViewGroup是用来组织显示View的。自定义ViewGroup也有几个关键的方法需要实现,而且onLayout方法是必须实现的。在自定义ViewGroup中我们常常需要重写onMeasure、onLayout,而onDraw一般不需要重写。

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

这个方法是用来测试量尺寸的,这两个参数是父布局传递过来的。如果你自定义的ViewGroup已经是根布局,但是它一样有父布局,因为android系统最后都会将其被添加到一个FrameLayout布局里。这个方法不仅要测量ViewGroup的尺寸,那还必须测量ViewGroup内每个视图。测试子视图把这两个参数传递过去,剩下的就是子视图自己的onMeasure方法来测量出它自己的尺寸。这方法的参数各自都包含了测量模式和具体尺寸大小。我们根据不同的测量模式来决定其具体的尺寸。具体的测量模式两三种:

  • UNSPECIFIED:父布局没有任何限制(在自定义View或ViewGroup中都不常用这个)
  • EXACTLY:父布局已经确定了确切的尺寸
  • AT_MOST:可以任意大小,直到指定确切的尺寸

我们只关注后面两种。这个模式的选定是根据xml布局文件中android:layout_width和android:layout_height的取值情况来决定的:

  • EXACTLY模式:
    (1)match_parent:占满整个父布局的宽或高
    (2)具体的数值:如300dp
  • AT_MOST模式:
    (1)wrap_content:期望内容多大,宽和高相应多大,反正是能够包裹住内容。这个模式给过来时,大小是不会像如期望一样大小,它就是整个父布局的大小,因为父布局没有办法知道你的内容有多大,你必须在onMeasure方法亲自确定其大小。

从两个参数取出各自的测量模式与尺寸:

int measuredWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measuredWidthSize = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredHeightSize = MeasureSpec.getSize(heightMeasureSpec);

对于EXACTLY模式,ViewGroup的尺寸直接用分离出来的尺寸。而对于AT_MOST模式的,分离出来的尺寸不能直接使用,因为它是传过来的尺寸是父布局的尺寸,明显不符合我们的要求。我们要根据需要,计算出ViewGroup中的子视图尺寸并累加起来作为最终的尺寸,在这种模式下,还要考虑各个视图之间的外边距margin(在自定义View,则只需要考虑内边距padding)。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measuredWidthSize = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredHeightSize = MeasureSpec.getSize(heightMeasureSpec);

int rowWidth = 0;// 临时记录行宽
int rowHeight = 0;// 临时记录行高
int maxWith = 0;
int maxHeight = 0;
measureChildren(widthMeasureSpec, heightMeasureSpec);// 测量children的大小
int count = getChildCount();
if (count != 0)
for (int i = 0; i < count; i++)
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + mlp.rightMargin + mlp.rightMargin;
int childHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;
if (childWidth + rowWidth > measuredWidthSize - getPaddingLeft() - getPaddingRight())
// 换行
maxWith = Math.max(maxWith, rowWidth);
rowWidth = childWidth;
maxHeight += rowHeight;
rowHeight = childHeight;
else
// 不换行
rowWidth += childWidth;
rowHeight = Math.max(childHeight, rowHeight);


//最后一个控件
if (i == count - 1)
maxWith = Math.max(maxWith, rowWidth);
maxHeight += rowHeight;




String widthModeStr = null;
switch (measuredWidthMode)
case MeasureSpec.AT_MOST:
widthModeStr = "AT_MOST";
if (count == 0)
mWidth = 0;
else
mWidth = maxWith + getPaddingLeft() + getPaddingRight();

break;
case MeasureSpec.EXACTLY:
widthModeStr = "EXACTLY";
mWidth = getPaddingLeft() + getPaddingRight() + measuredWidthSize;
break;
default:
throw new IllegalStateException("Unexpected value: " + measuredWidthMode);
case MeasureSpec.UNSPECIFIED:
break;

String heightModeStr = null;
switch (measuredHeightMode)
case MeasureSpec.AT_MOST:
heightModeStr = "AT_MOST";
if (count == 0)
mHeight = 0;
else
mHeight = maxHeight + getPaddingTop() + getPaddingBottom();

break;
case MeasureSpec.EXACTLY:
heightModeStr = "EXACTLY";
mHeight = getPaddingTop() + getPaddingBottom() + measuredHeightSize;
break;
default:
throw new IllegalStateException("Unexpected value: " + measuredHeightMode);
case MeasureSpec.UNSPECIFIED:
break;


setMeasuredDimension(mWidth, mHeight);
String str = "@widthMode#" + widthModeStr + ":" + measuredWidthSize + "@heightMode#" + heightModeStr + ":" + measuredHeightSize;
Log.d("Layout尺寸", str);

因为我们在处理AT_MOST模式时,必须获得每个子视图的外边距信息:

MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();

所以必须加入以下代码,否则会报错:

// 自定义ViewGroup必须要有以下这个方法,否则拿不到child的margin的信息
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)
return new MarginLayoutParams(getContext(), attrs);

onLayout(boolean changed, int l, int t, int r, int b)

在自定义View中一般不需要重写这个方法,但是在自定义ViewGroup中必须重写这个方法,因为ViewGroup是View的集合,必须处理它们的位置关系。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
if (changed)
int count = getChildCount();
for (int i = 0; i < count; i++)
View child = getChildAt(i);
Log.i("Child大小W", child.getMeasuredWidth() + "#H:" + child.getMeasuredHeight());
ChildPosition pos = mChildPos.get(i);
//设置View的左边、上边、右边底边位置
child.layout(pos.left, pos.top, pos.right, pos.bottom);


小结

所以自定义ViewGroup还是相当简单的。因为我们只需要量好ViewGroup的视图,在测量过程中,比较值得注意的就是当模式是AT_MOST时,需要计算各个子View的尺寸再累加起来,得到就是ViewGroup的尺寸。onMeasure的参数都是父布局传给子控件的,这就是为什么在自定义ViewGroup中,测量子View时的这两个参数是来自定义的ViewGroup。最后就是就是在onLayout中布局子View的位置。

流式布局

我们继承ViewGroup定义一个流式布局CustomFlowLayout。在流式布局中,我额外增加了滚动的功能,因为当我们的内容超过我们自定义ViewGroup的可视范围后,就需要用到滚动功能。滚动功能的实现思路

  1. 首先重写onTouchEvent方法,消费滑动事件,根据我们的手指滑动的方向,来移动我们的视图。
  2. 添加上边界的检查,一旦到达上边界就不允许再滚动
  3. 添加下边界的检查,一旦到达下边界就不允许再滚动

那么在处理边界问题时,需要我们有一定的知识储备,就是以下这个方面的知识:

  1. android的事件传递方面,Activity->View group ->View,这可以了事件传递的顺序。
  2. android的事件传递机制,要了解好dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent这个方法的工作过程,明白谁消费了事件,谁传递了事件。
  3. 理解好scrollTo与scrollBy的区别,前者是绝对位置移动,后者是相对位置移动。
  4. 要理解getY与getRawY与getScrollY它们的坐标系的特点。
  5. 最后一点,要知道android屏幕的坐标系是无限大,布局视图并没有边界,我们的屏幕只是这个坐标系的一部分,显示出来的视图只是刚好在这一部分而已。、
  6. 我们滚动视图,实质是改变了视图整体的位置而已。

下面是一个完全的流式布局CustomFlowLayout:

package com.wong.customtextview;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;

import java.util.ArrayList;
import java.util.List;

public class CustomFlowLayout extends ViewGroup

private int mWidth = 0;
private int mHeight = 0;
private int realHeight;
private boolean scrollable = false;
private boolean isInterceptedTouch;
/**
* 判定为拖动的最小移动像素数
*/
private int mTouchSlop;
private int topBorder; // 上边界
private int bottomBorder;// 下边界

//记录每个View的位置
private List<ChildPosition> mChildPos = new ArrayList<ChildPosition>();

private static class ChildPosition
int left, top, right, bottom;

public ChildPosition(int left, int top, int right, int bottom)
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;



public CustomFlowLayout(Context context)
this(context, null);


public CustomFlowLayout(Context context, AttributeSet attrs)
this(context, attrs, 0);


public CustomFlowLayout(Context context, AttributeSet attrs, int defStyleAttr)
super(context, attrs, defStyleAttr);
ViewConfiguration configuration = ViewConfiguration.get(context);
// 获取TouchSlop值,用于判断当前用户的操作是否是拖动
mTouchSlop = configuration.getScaledPagingTouchSlop();



@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measuredWidthSize = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredHeightSize = MeasureSpec.getSize(heightMeasureSpec);

int rowWidth = 0;// 临时记录行宽
int rowHeight = 0;// 临时记录行高
int maxWith = 0;
int maxHeight = 0;
measureChildren(widthMeasureSpec, heightMeasureSpec);// 测量children的大小
int count = getChildCount();
if (count != 0)
for (int i = 0; i < count; i++)
View child = getChildAt(i);
MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + mlp.rightMargin + mlp.rightMargin;
int childHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;
if (childWidth + rowWidth > measuredWidthSize - getPaddingLeft() - getPaddingRight())
// 换行
maxWith = Math.max(maxWith, rowWidth);
rowWidth = childWidth;
maxHeight += rowHeight;
rowHeight = childHeight;
else
rowWidth += childWidth;
rowHeight = Math.max(childHeight, rowHeight);


//最后一个控件
if (i == count - 1)
maxWith = Math.max(maxWith, rowWidth);
maxHeight += rowHeight;




String widthModeStr = null;
switch (measuredWidthMode)
case MeasureSpec.AT_MOST:
widthModeStr = "AT_MOST";
if (count == 0)
mWidth = 0;
else
mWidth = maxWith + getPaddingLeft() + getPaddingRight();

break;
case MeasureSpec.EXACTLY:
widthModeStr = "EXACTLY";
mWidth = getPaddingLeft() + getPaddingRight() + measuredWidthSize;
break;
default:
throw new IllegalStateException("Unexpected value: " + measuredWidthMode);
case MeasureSpec.UNSPECIFIED:
break;

String heightModeStr = null;
switch (measuredHeightMode)
case MeasureSpec.AT_MOST:
heightModeStr = "AT_MOST";
if (count == 0)
mHeight = 0;
else
mHeight = maxHeight + getPaddingTop() + getPaddingBottom();

break;
case MeasureSpec.EXACTLY:
heightModeStr = "EXACTLY";
mHeight = getPaddingTop() + getPaddingBottom() + measuredHeightSize;
break;
default:
throw new IllegalStateException("Unexpected value: " + measuredHeightMode);
case MeasureSpec.UNSPECIFIED:
break;

//真实高度
realHeight = maxHeight + getPaddingTop() + getPaddingBottom();
//测量高度
if (measuredHeightMode == MeasureSpec.EXACTLY)
scrollable = realHeight > mHeight;
else
scrollable = realHeight > measuredHeightSize;

if (scrollable)
// 初始化上下边界值
MarginLayoutParams lp1 = (MarginLayoutParams) getChildAt(0).getLayoutParams();
topBorder = getChildAt(0).getTop() - lp1.topMargin;
if (measuredHeightMode == MeasureSpec.EXACTLY)
bottomBorder = realHeight - mHeight + getPaddingBottom();
else
bottomBorder = realHeight - measuredHeightSize + getPaddingBottom();


setMeasuredDimension(mWidth, mHeight);
String str = "@widthMode#" + widthModeStr + ":" + measuredWidthSize + "@heightMode#" + heightModeStr + ":" + measuredHeightSize;
Log.d("Layout尺寸", str);


@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
mChildPos.clear();
int rowWidth = 0;// 临时记录行宽
int rowHeight = 0;// 临时记录行高
int maxWith = 0;
int maxHeight = 0;
int count = getChildCount();
for (int i = 0; i < count; i++)
View child = getChildAt(i);
MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + mlp.rightMargin + mlp.rightMargin;
int childHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;
if (childWidth + rowWidth > getMeasuredWidth() - getPaddingLeft() - getPaddingRight())
// 换行
maxWith = Math.max(maxWith, rowWidth);
rowWidth = childWidth;
maxHeight += rowHeight;
rowHeight = childHeight;
mChildPos.add(new ChildPosition(
getPaddingLeft() + mlp.leftMargin,
getPaddingTop() + maxHeight + mlp.topMargin,
getPaddingLeft() + childWidth - mlp.rightMargin,
getPaddingTop() + maxHeight + childHeight - mlp.bottomMargin
));

else
// 不换行
mChildPos.add(new ChildPosition(
getPaddingLeft() + rowWidth + mlp.leftMargin,
getPaddingTop() + maxHeight + mlp.topMargin,
getPaddingLeft() + rowWidth + childWidth - mlp.rightMargin,
getPaddingTop() + maxHeight + childHeight - mlp.bottomMargin
));
rowWidth += childWidth;
rowHeight = Math.max(childHeight, rowHeight);




// 布局每一个child
for (int i = 0; i < count; i++)
View child = getChildAt(i);
Log.i("Child大小W", child.getMeasuredWidth() + "#H:" + child.getMeasuredHeight());
ChildPosition pos = mChildPos.get(i);
//设置View的左边、上边、右边底边位置
child.layout(pos.left, pos.top, pos.right, pos.bottom);



// 自定义ViewGroup必须要有以下这个方法,否则拿不到child的margin的信息
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)
return new MarginLayoutParams(getContext(), attrs);



private float mLastYMove;
private float currentY;

@Override
public boolean onTouchEvent(MotionEvent event)
if (scrollable)
switch (event.getAction())
case MotionEvent.ACTION_DOWN:
this.mLastYMove = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
this.currentY = event.getRawY();
int scrolledY = getScrollY();
float diff = Math.abs(this.mLastYMove - this.currentY);
// 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件
if (diff > mTouchSlop)
int dy = (int) (this.mLastYMove - this.currentY);
if (scrolledY + dy < topBorder)
dy = 0;
scrollTo(0, topBorder);
return true;
//最顶端,超过0时,不再下拉,要是不设置这个,getScrollY一直是负数
else if (scrolledY + dy > bottomBorder)
dy = 0;
scrollTo(0, bottomBorder);
return true;

scrollBy(0, dy);
this.mLastYMove = event.getRawY();

break;


return true;


以上是关于Android自定义ViewGroup,onMeasureonLayout,实现流式布局的主要内容,如果未能解决你的问题,请参考以下文章

Android自定义ViewGroup及自定义属性

Android自定义ViewGroup,onMeasureonLayout,实现流式布局

Android自定义ViewGroup

Android 手把手教您自定义ViewGroup

Android自定义ViewGroup-入门

Android 中自定义ViewGroup实现流式布局的效果