这段时间一直在学习自定义控件,之前就想完成这篇博客,后来看了鸿洋和爱哥的自定义控件系列,发现里面有很多很多的东西是我之前没有想到的,内容比较多,这篇博客就算是当做一篇入门笔记来写。我刚开始写博客,没什么文采,也是想到哪就写到哪,如果觉得比较混乱什么的,见谅。
之前写过了一篇关于自定义View的博客,其实自定义ViewGroup和自定义View是有很多的相似性的,毕竟ViewGroup也是View的一种。但是ViewGroup和View还是有区别的,光从名字就看出来不一样了(笑)。ViewGroup从名字上看就是一组View,更正式的点就是放View的一个容器,他的职责就是给他包含的View即childview计算出建议的宽和高,并决定childview的位置布局。为什么是建议的宽和高,是因为当我们设置为wrap_content的时候,只有childview自己才能计算出自己的宽和高。对于ViewGroup如何对childview进行布局,则要使用onlayout方法,下面会具体介绍.
LayoutParams
自定义ViewGroup的时候,经常使用到LayoutParams,这个又是什么东西。在刚开始学习的时候我也很困惑,不知道这玩意是干什么用的,后来才明白他的意思。我们可以想想我们之前用过哪些ViewGroup,比如LinearLayout,RelativeLayout,这些都会有一些自己特定的属性,比如LinearLayout的layout_gravity,RelativeLayout的layout_centerInParent。这个就是我们用到的LayoutParams
下面说一下自定义viewgroup的几个步骤,用鸿洋的一个例子
Android 自定义ViewGroup 实战篇 -> 实现FlowLayout
1.首先我们考虑使用什么LayoutParams,这里我们只用到Margin,所有使用MarginLayoutParams就可以了。
2.重写onMeasure方法,这里的onMeasure方法就是负责测量子控件的模式和大小,然后根据子控件确定自己的宽和高,贴一下代码
/**
* 负责设置子控件的测量模式和大小 根据所有子控件设置自己的宽和高
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获得它的父容器为它设置的测量模式和大小
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
Log.e(TAG, sizeWidth + "," + sizeHeight);
// 如果是warp_content情况下,记录宽和高
int width = 0;
int height = 0;
/**
* 记录每一行的宽度,width不断取最大宽度
*/
int lineWidth = 0;
/**
* 每一行的高度,累加至height
*/
int lineHeight = 0;
int cCount = getChildCount();
// 遍历每个子元素
for (int i = 0; i < cCount; i++)
{
View child = getChildAt(i);
// 测量每一个child的宽和高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 得到child的lp
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
// 当前子空间实际占据的宽度
int childWidth = child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
// 当前子空间实际占据的高度
int childHeight = child.getMeasuredHeight() + lp.topMargin
+ lp.bottomMargin;
/**
* 如果加入当前child,则超出最大宽度,则的到目前最大宽度给width,类加height 然后开启新行
*/
if (lineWidth + childWidth > sizeWidth)
{
width = Math.max(lineWidth, childWidth);// 取最大的
lineWidth = childWidth; // 重新开启新行,开始记录
// 叠加当前高度,
height += lineHeight;
// 开启记录下一行的高度
lineHeight = childHeight;
} else
// 否则累加值lineWidth,lineHeight取最大高度
{
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
// 如果是最后一个,则将当前记录的最大宽度和当前lineWidth做比较
if (i == cCount - 1)
{
width = Math.max(width, lineWidth);
height += lineHeight;
}
}
setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth
: width, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight
: height);
}
在方法中,首先得到宽和高的size和mode,然后遍历childview,测量childview的宽高,然后根据mode设置 setMeasuredDimension
3.重写onLayout方法,这个方法主要是用来对childview的位置和大小的制定
贴一下代码
/**
* 存储所有的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);
Log.e(TAG, "第" + i + "行 :" + lineViews.size() + " , " + lineViews);
Log.e(TAG, "第" + i + "行, :" + lineHeight);
// 遍历当前行所有的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();
Log.e(TAG, child + " , l = " + lc + " , t = " + t + " , r ="
+ rc + " , b = " + bc);
child.layout(lc, tc, rc, bc);
left += child.getMeasuredWidth() + lp.rightMargin
+ lp.leftMargin;
}
left = 0;
top += lineHeight;
}
}
在方法中,也是遍历childview,然后计算childview的left,top,right,bottom ,最后在调用child.layout方法传入参数
写在最后的话:
感觉自定义view和viewgroup还是挺相似的,步骤都差不多,了解了这些算是个入门。接下来打算把鸿洋的自定义控件系列跟着博客把代码全部撸一遍,加深一下了解。关于爱歌的自定义专栏,感觉还是很深奥,看起来比较费劲,往后面放一放。然后学习一下关于网络相关的内容,尝试使用流行的框架,加油吧
相关博客:
Android 自定义ViewGroup 实战篇 -> 实现FlowLayout