赞
踩
View在Android中还是比较大的一个点,当然其中的内容是异常的多,而且使用也是十分的灵活。网上很多大神都已经有了自己对View的总结,那么作为一个新司机,我也打算开始这个模块的总结(尽可能的说明白),并且以后会逐渐的推出有关于View的一系列文章。
当然其中总有不当之处,还请各位多多指教,鄙人不胜感激。
ok,本次给大家带来的是一个自定义ViewGroup的案例,主要是实现选项功能,这种效果在网上也被很多人实现过了,比如hongyang大神,那么我就来说一下我实现的思路。
先上效果图:
源码下载地址:
https://github.com/fuyunwang/FlowLayout
(求个star,谢谢啦)
对于这种效果,我们必然是通过自定义一个ViewGroup来存放不同的子View而实现的。
那么好,
1.首先自定义ViewGroup,我们一般都是先新建一个类,继承自ViewGroup,然后覆写其中的抽象方法onLayout(),同时我们要覆写其中的构造方法,这里分别实现一个参数、两个参数和三个参数的构造方法即可。
2.接下来,我们要考虑的是子View要按照一定的规则摆放在自定义的ViewGroup中,那么必不可少的就是测量的过程和摆放的过程,我们已经覆写了onLayout(),这个方法稍后用于摆放子控件,在此之前,我们首先应该覆写onMeasure()方法来测量控件的大小。
onMeasure()方法的第一步是首先得到当前控件的测量模式和内容的宽高(控件一般会指定padding值),这个值接下来会用到:
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec)
- getPaddingRight() - getPaddingLeft();
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec)
- getPaddingTop() - getPaddingBottom();
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
测量了父View的宽和高,这个时候,要遍历所有的子View并且分别测量每一个控件。注意父控件会影响子View的大小和测量的模式,这也就是我们为什么要首先得到父控件测量模式和内容宽高的原因。
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeWidth,modeWidth == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeWidth);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight,modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST: modeHeight);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
这样,我们的控件就初步测量好了,注意这只是初步测量,因为之后view要合理的占满所有的空间,我们需要再进一步的调整。
3.那么为了保证所有的子View能够合理的利用所有的空间,我们需要一个行对象来维护每一行的子View的空间使用和子View的布局。
对于addView方法,主要是记录下该行中添加的子View,然后以该行中子View的最高高度最为该行的高度。
对于layoutView布局方法:
我们需要记录下当前行所使用的宽度以及剩余的所有宽度,当每一行剩下的距离无法放下一个子View的时候,我们需要将多余的控件平均分配给每一个子View;此外,由于每一个子View的高度可能不同,我们需要保证每一个子View的中轴线在一条线上,所以需要计算纵向的偏移,然后使每一个子View能够移动合适的距离。
/**
* 代表着一行,封装了一行所占高度,该行子View的集合,以及所有View的宽度总和
*/
class Line {
int mWidth = 0;
int mHeight = 0;
List<View> views = new ArrayList<View>();
public void addView(View view) {
// 往该行中添加一个子View
views.add(view);
mWidth += view.getMeasuredWidth();
int childHeight = view.getMeasuredHeight();
mHeight = mHeight < childHeight ? childHeight : mHeight;// 高度等于一行中最高的View
}
public int getViewCount() {
//返回每一行中子View的数量
return views.size();
}
/**
* 下面的方法是给子View布局摆放,注意多余的空间应该平均分配给其他的控件
* @param l
* @param t
*/
public void layoutView(int l, int t) {
int left = l;
int top = t;
int count = getViewCount();
int layoutWidth = getMeasuredWidth() - getPaddingLeft()
- getPaddingRight();
int surplusWidth = layoutWidth - mWidth - mHorizontalSpacing
* (count - 1);
if (surplusWidth >= 0) {
int splitSpacing = (int) (surplusWidth / count + 0.5);
for (int i = 0; i < count; i++) {
final View view = views.get(i);
int childWidth = view.getMeasuredWidth();
int childHeight = view.getMeasuredHeight();
int topOffset = (int) ((mHeight - childHeight) / 2.0 + 0.5);
if (topOffset < 0) {
topOffset = 0;
}
childWidth = childWidth + splitSpacing;
view.getLayoutParams().width = childWidth;
if (splitSpacing > 0) {
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
childWidth, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(
childHeight, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
}
// 布局View
view.layout(left, top + topOffset, left + childWidth, top
+ topOffset + childHeight);
left += childWidth + mHorizontalSpacing;
}
} else {
//最后一行要单独处理
View view = views.get(0);
view.layout(left, top, left + view.getMeasuredWidth(), top
+ view.getMeasuredHeight());
}
}
}
这个时候,我们就明确了子View在满足 “当前行的剩余空间不能摆放下一个子View” 这一条件之后需要进行换行操作,也就是在下一行重新摆放View。
换行的关键在于将当前行添加到行的集合中保存,然后将当前行的已使用宽度置为0并且在已使用的高度上添加当前行的高度:
/** 新增加一行 */
private boolean newLine() {
mLines.add(mLine);
if (mLines.size() < mMaxLinesCount) {
mLine = new Line();
mUsedWidth = 0;
return true;
}
return false;
}
这样每一行的子View的宽和高就都已经测量完毕,我们从而就得到了每一行所使用的空间,所有的控件相加就得到了我们父控件的整体宽高:
int childWidth = child.getMeasuredWidth();
mUsedWidth += childWidth;// 增加使用的宽度
if (mUsedWidth <= sizeWidth) {// 使用宽度小于总宽度,该child属于这一行。
mLine.addView(child);// 添加child
mUsedWidth += mHorizontalSpacing;// 加上间隔
if (mUsedWidth >= sizeWidth) {// 加上间隔后如果大于等于总宽度,需要换行
if (!newLine()) {
break;
}
}
} else {
if (mLine.getViewCount() == 0) {
mLine.addView(child);
if (!newLine()) {
break;
}
} else {
if (!newLine()) {
break;
}
mLine.addView(child);
mUsedWidth += childWidth + mHorizontalSpacing;
}
}
}
if (mLine != null && mLine.getViewCount() > 0
&& !mLines.contains(mLine)) {
mLines.add(mLine);
}
int totalWidth = MeasureSpec.getSize(widthMeasureSpec);
int totalHeight = 0;
final int linesCount = mLines.size();
for (int i = 0; i < linesCount; i++) {
totalHeight += mLines.get(i).mHeight;
}
totalHeight += mVerticalSpacing * (linesCount - 1);
totalHeight += getPaddingTop() + getPaddingBottom();
// 设置布局的宽高,宽度直接采用父view传递过来的最大宽度,而不用考虑子view是否填满宽度,因为该布局的特性就是填满一行后,再换行
// 高度根据设置的模式来决定采用所有子View的高度之和还是采用父view传递过来的高度
setMeasuredDimension(totalWidth,resolveSize(totalHeight, heightMeasureSpec));
4.到此测量工作完成,并且我们在测量的时候也涉及到了每一行的子View的摆放,下面我们就借助上面的layoutView方法来完成onLayout工作:
其实大多数工作我们都在layoutView中完成了,我们这里只需要规划好每一行的子View的开始摆放的位置(也就是每一行左上角的坐标),然后遍历每一行,让每一行布局好自己的子View,整个控件的布局也就完成了。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
int left = getPaddingLeft();
int top = getPaddingTop();
final int linesCount = mLines.size();
for (int i = 0; i < linesCount; i++) {
final Line oneLine = mLines.get(i);
oneLine.layoutView(left, top);
top += oneLine.mHeight + mVerticalSpacing;
}
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。