前言:
本篇博客讲解内容主要是来自 鸿洋的博客Android 自定义ViewGroup 实战篇 -> 实现FlowLayout
上篇文章 自定义view(二),继承view。今天来看自定义View的第二种情况,继承自viewgroup,虽然viewGroup也是继承view控件,但是ViewGroup和View还是有很多方法区别的,顾名思义,这是一个控件的集合控件。
1.
我们来通过自定义viewGroup来实现瀑布流的效果。效果图如下:
2.
继承viewGroup主要来重写onMearsure和onLayout,剩下的onDraw更多是用来绘图的,所以不需要来重写。所以我们来看看代码
onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
//获取测量模式和测量大小
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
/**以下代码主要是针对测量模式是wrap_content时需要动态的计算,如果是一些具体的大小或者match_parent时根本不不需要写onMearsure
*/
//整个自定义viewgroup的宽,高的最大值进行记录
int width = 0;
int height = 0;
//判断是否要换行时使用
int lineWidth = 0;
int lineHeight = 0;
//计算子view的个数
int cCount = getChildCount();
//for循环计算每个子view在viewgroup中的大小
for (int i = 0; i < cCount; i++)
{
View child = getChildAt(i);
//measureChild这个方法来计算子view的大小和mode,这个viewGroup定义的一个方法,是个很重要的方法
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 得到child的lp,MarginLayoutParams可以得到Margin数据。
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
//measureChild方法过后,child.getMeasuredWidth()才能计算得到值,不然会为0
int childWidth = child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin
+ lp.bottomMargin;
//判断是否需要换行
if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight())
{
//需要换行了,记录宽的最大值
width = Math.max(width, lineWidth);
//换行后的新的lineWidth等于控件的宽
lineWidth = childWidth;
height += lineHeight;
lineHeight = childHeight;
} else
{ //不需要换行
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
//最后一个view
if (i == cCount - 1)
{
width = Math.max(lineWidth, width);
height += lineHeight;
}
}
//不同的mode进行一个简单的判断,viewGroup的真正的大小实际上就是通过这个方法进行设置的
setMeasuredDimension(
//
modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()//
);
}
上面的注释其实也是十分的详细了,上面有个很关键的方法,是进行测试子view的
measureChild(child, widthMeasureSpec, heightMeasureSpec);这个viewgroup的方法,我们来看下源码:
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
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);
}
getChildMeasureSpec()和measure()这两个方法的源码大家可以自己看看,主要是对子view进行测量。
3.
测量好了大小后,我们来看看是如何布局的,此时需要重写onLayout方法:
//记录所有的子view,并且再按行来进行一个区别记录
private List<List<View>> mAllViews = new ArrayList<List<View>>();
//记录没行的最大高度
private List<Integer> mLineHeight = new ArrayList<Integer>();
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
mAllViews.clear();
mLineHeight.clear();
int width = getWidth();
int lineWidth = 0;
int lineHeight = 0;
// 存储每一行所有的childView
List<View> lineViews = new ArrayList<View>();
int cCount = getChildCount();
// 遍历所有的孩子
for (int i = 0; i < cCount; i++)
{
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 如果已经需要换行
if (childWidth + lp.leftMargin + lp.rightMargin + lineWidth > width)
{
// 记录这一行所有的View以及最大高度
mLineHeight.add(lineHeight);
// 将当前行的childView保存,然后开启新的ArrayList保存下一行的childView
mAllViews.add(lineViews);
lineWidth = 0;// 重置行宽
lineViews = new ArrayList<View>();
}
/**
* 如果不需要换行,则累加
*/
lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
lineHeight = Math.max(lineHeight, childHeight + lp.topMargin
+ lp.bottomMargin);
lineViews.add(child);
}
// 记录最后一行
mLineHeight.add(lineHeight);
mAllViews.add(lineViews);
int left = 0;
int top = 0;
// 得到总行数
int lineNums = mAllViews.size();
for (int i = 0; i < lineNums; i++)
{
// 每一行的所有的views
lineViews = mAllViews.get(i);
// 当前行的最大高度
lineHeight = mLineHeight.get(i);
// 遍历当前行所有的View
for (int j = 0; j < lineViews.size(); j++)
{
View child = lineViews.get(j);
if (child.getVisibility() == View.GONE)
{
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
//计算childView的left,top,right,bottom
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc =lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
//以上所有的计算都是为了获取这四个参数,来进行子view的布局。
child.layout(lc, tc, rc, bc);
left += child.getMeasuredWidth() + lp.rightMargin
+ lp.leftMargin;
}
left = 0;
top += lineHeight;
}
}
代码也不是很难,细心一点。
4.
实际上,我们可以发现,该自定义控件实现的逻辑挺简单的,主要是:
(1)onMeasure中,通过计算每个子view的大小才确定viewgroup的大小。
(1)onLayout中,动态的计算子view的位置坐标然后进行一个布局。
由此也可以发现,自定义viewgroup最重要的也就是这两个方法,但是,其中很多小的细节也是需要我们注意到的,比如:
(a)只有子view调用了measure方法,才能调用getMeasureSpec获取到值,不然为0.
(b)用MarginLayoutParams来支持margin属性。
代码:
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)
{
return new MarginLayoutParams(getContext(), attrs);
}
以上代码就可以简单实现自定义view的流式布局了。
最近搞了个Android技术分享的公众号,欢迎关注投稿。