在平时的开发过程中,系统为我们提供了五大布局,基本上可以解决在工作中遇到的问题。但是,随着公司对产品的要求越来越高,界面也变的越来越好看,对应的写起来就变的越来越复杂,这个时候系统提供的布局也就不够用了,特别是对于下面的这种格式。因此,到了这个时候,我们只能通过自定义来实现了
今天我们要实现的就是这个功能
首先写一个StreamLayout类继承自ViewGroup重写onMeasure()和onLayout()方法
/**
* 自定义View实现流式布局
*/
public class StreamLayout extends ViewGroup {
/**
* 宽度使用的长度
*/
private int mUseWidth;
/**
* 最后的高度
*/
private int mTotalHeight;
/**
* 最后的宽度
*/
private int mTotalWidth;
/**
* 横向间距
*/
private int mHorizontalSpace = 5;
/**
* 纵向间距
*/
private int mVerticalSpace = 5;
/**
* 保存所有的View
*/
private List<List<View>> mLineList = new ArrayList<>();
/**
* 保存每一行的View
*/
private List<View> mViewList;
public StreamLayout(Context context) {
this(context, null);
}
public StreamLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public StreamLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StreamLayout);
//mTextColor = a.getColor(R.styleable.StreamLayout_textColor, Color.BLACK);
//mTextSize = a.getDimensionPixelSize(R.styleable.StreamLayout_textSize, FontUtil.px2dp(context, 14));
mHorizontalSpace = a.getDimensionPixelSize(R.styleable.StreamLayout_horizontalSpace, FontUtil.px2dp(context, 10));
mVerticalSpace = a.getDimensionPixelSize(R.styleable.StreamLayout_verticalSpace, FontUtil.px2dp(context, 5));
a.recycle();
}
/**
* 进行测量
*
* @param widthMeasureSpec 宽度的MeasureSpec
* @param heightMeasureSpec 高度的MeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取控件的宽度
int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
// 获取控件的高度
int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
// 宽度的测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 高度的测量模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 遍历所有的子控件
int childCount = getChildCount();
if (mLineList.size() == 0) {
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 获取子控件的尺寸与测量模式
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode);
// 测量子控件
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// 拿到子View的宽度和高度
int childMeasureWidth = childView.getMeasuredWidth();
int childMeasureHeight = childView.getMeasuredHeight();
// 计算使用的宽度
mUseWidth += childMeasureWidth;
// 将每一行加入到集合中
if (mViewList == null) {
mViewList = new ArrayList<>();
mLineList.add(mViewList);
mTotalHeight += childMeasureHeight + getPaddingTop();
}
// 添加的子View超过当前行
if (mUseWidth >= width) {
// 新建一行
mViewList = new ArrayList<>();
mLineList.add(mViewList);
mUseWidth = childMeasureWidth;
mTotalHeight += childMeasureHeight + mVerticalSpace;
}
if (!mViewList.contains(childView)) {
mViewList.add(childView);
mUseWidth += mHorizontalSpace;
}
}
}
mTotalHeight = mTotalHeight + getPaddingBottom();
mTotalWidth = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(mTotalWidth,mTotalHeight);
}
/**
* @param changed 摆放是否发生改变
* @param l left
* @param t top
* @param r right
* @param b bottom
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = getPaddingTop();
int left = getPaddingLeft();
for (int i = 0; i < mLineList.size(); i++) {
List<View> list = mLineList.get(i);
int childTop = top + (mTotalHeight - getPaddingBottom() - getPaddingTop() + mVerticalSpace) / mLineList.size() * i;
int childLeft = left;
for (int j = 0; j < list.size(); j++) {
View view = list.get(j);
view.layout(childLeft, childTop, childLeft + view.getMeasuredWidth(), childTop + view.getMeasuredHeight());
childLeft += view.getMeasuredWidth() + mHorizontalSpace;
}
}
}
}
实现思路也很简单:
第一步:测量
因为我们要测量最终的高和宽,所以我们必须要知道填充的内容有多少行。
最终的高度 = getPaddingTop()+getPaddingBottom()+行高(内容+垂直间距)
那么行数要怎么确定呢?肯定是通过宽度,当我们添加内容的总宽度超过我们设置的最大宽度时就另起一行
第二步:摆放
因为在测量的时候我们把每一行要填充的内容都通过集合保存起来了,所以我们只需要去遍历集合即可
布局文件如下
<com.steven.baselibrary.view.streamlayout.StreamLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:horizontalSpace="40dp"
app:verticalSpace="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="头条" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="态度公开课" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="房产" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="人工智能" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="军事" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="段子" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="营销态度" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="社会" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="航空" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="独家" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NBA" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="社会" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="冰雪运动" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="国际足球" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="段子" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="体育" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="女人" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="政务" />
</com.steven.baselibrary.view.streamlayout.StreamLayout>
那么我们先来看看效果如何
是不是有那么一点点像了,不用急,我们现在来加上边距和边框,注意加上边框的时候只需要改变TextView的一些属性就行了,这里我就不贴代码了,把效果图贴出来看看
哈哈哈,是不是很像了,有木有感觉胜利的曙光就在前方。但是别高兴的太早了,因为现在的内容是写死的,大部分情况下这些数据是可变的,是由后台传递过来的,这个时候我们肯定不能写在里面了,那么这个时候我们应该怎么做呢?其实也很简单,我们使用适配器模式给我们的StreamLayout加上一个适配器不就行了嘛。当然,要配合着观察者模式一起使用,如果还有同学不太懂观察者模式的话可以看下我的这篇文章观察者模式
好了,不废话了,直接把所有代码提出来吧
/**
* 被观察者
*/
class StreamObservable extends Observable<StreamObserver> {
public void notifyChanged() {
synchronized (mObservers) {
for (int i = 0; i < mObservers.size(); i++) {
mObservers.get(i).onChanged();
}
}
}
}
/**
* 观察者
*/
public abstract class StreamObserver {
public void onChanged() {
}
}
/**
* 适配器
*/
public abstract class AbsStreamAdapter {
private final StreamObservable mStreamObservable = new StreamObservable();
public void registerDataSetObserver(StreamObserver observer) {
mStreamObservable.registerObserver(observer);
}
public void unregisterDataSetObserver(StreamObserver observer) {
mStreamObservable.unregisterObserver(observer);
}
public void notifyDataSetChanged() {
mStreamObservable.notifyChanged();
}
public abstract int getCount();
public abstract Object getItem(int position);
public abstract long getItemId(int position);
public abstract View getView(int position, View convertView, ViewGroup parent);
}
public class StreamAdapter extends AbsStreamAdapter {
private LayoutInflater mInflater;
private List<String> mList;
public StreamAdapter(Context context) {
mInflater = LayoutInflater.from(context);
}
public void setList(List<String> list) {
this.mList = list;
if (mList != null && mList.size() > 0) {
notifyDataSetChanged();
}
}
@Override
public int getCount() {
return mList != null ? mList.size() : 0;
}
@Override
public String getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView tv = (TextView) mInflater.inflate(R.layout.stream_layout, parent, false);
tv.setText(mList.get(position));
return tv;
}
}
/**
* 自定义View实现流式布局
*/
public class StreamLayout extends ViewGroup {
/**
* 宽度使用的长度
*/
private int mUseWidth;
/**
* 最后的高度
*/
private int mTotalHeight;
/**
* 最后的宽度
*/
private int mTotalWidth;
/**
* 横向间距
*/
private int mHorizontalSpace = 5;
/**
* 纵向间距
*/
private int mVerticalSpace = 5;
/**
* 保存所有的View
*/
private List<List<View>> mLineList = new ArrayList<>();
/**
* 保存每一行的View
*/
private List<View> mViewList;
/**
* 刷新的适配器
*/
private StreamAdapter mStreamAdapter;
/**
* 观察者
*/
private StreamObserver mStreamDataSetObserver;
public StreamLayout(Context context) {
this(context, null);
}
public StreamLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public StreamLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StreamLayout);
mHorizontalSpace = a.getDimensionPixelSize(R.styleable.StreamLayout_horizontalSpace, FontUtil.px2dp(context, 10));
mVerticalSpace = a.getDimensionPixelSize(R.styleable.StreamLayout_verticalSpace, FontUtil.px2dp(context, 5));
a.recycle();
}
/**
* 进行测量
*
* @param widthMeasureSpec 宽度的MeasureSpec
* @param heightMeasureSpec 高度的MeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取控件的宽度
int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
// 获取控件的高度
int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
// 宽度的测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 高度的测量模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 遍历所有的子控件
int childCount = getChildCount();
if (mLineList.size() == 0) {
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 获取子控件的尺寸与测量模式
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode);
// 测量子控件
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// 拿到子View的宽度和高度
int childMeasureWidth = childView.getMeasuredWidth();
int childMeasureHeight = childView.getMeasuredHeight();
// 计算使用的宽度
mUseWidth += childMeasureWidth;
// 将每一行加入到集合中
if (mViewList == null) {
mViewList = new ArrayList<>();
mLineList.add(mViewList);
mTotalHeight += childMeasureHeight + getPaddingTop();
}
// 添加的子View超过当前行
if (mUseWidth >= width) {
// 新建一行
mViewList = new ArrayList<>();
mLineList.add(mViewList);
mUseWidth = childMeasureWidth;
mTotalHeight += childMeasureHeight + mVerticalSpace;
}
if (!mViewList.contains(childView)) {
mViewList.add(childView);
mUseWidth += mHorizontalSpace;
}
}
}
mTotalHeight = mTotalHeight + getPaddingBottom();
mTotalWidth = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(mTotalWidth, mTotalHeight);
}
/**
* @param changed 摆放是否发生改变
* @param l left
* @param t top
* @param r right
* @param b bottom
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = getPaddingTop();
int left = getPaddingLeft();
for (int i = 0; i < mLineList.size(); i++) {
List<View> list = mLineList.get(i);
int childTop = top + (mTotalHeight - getPaddingBottom() - getPaddingTop() + mVerticalSpace) / mLineList.size() * i;
int childLeft = left;
for (int j = 0; j < list.size(); j++) {
View view = list.get(j);
view.layout(childLeft, childTop, childLeft + view.getMeasuredWidth(), childTop + view.getMeasuredHeight());
childLeft += view.getMeasuredWidth() + mHorizontalSpace;
}
}
}
/**
* 进行重绘
*
* @param canvas 画笔
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
/**
* 设置适配器
*/
public void setAdapter(StreamAdapter streamAdapter) {
if (mStreamAdapter != null && mStreamDataSetObserver != null) {
mStreamAdapter.unregisterDataSetObserver(mStreamDataSetObserver);
}
this.mStreamAdapter = streamAdapter;
if (mStreamAdapter != null) {
mStreamDataSetObserver = new AdapterDataSetObserver();
mStreamAdapter.registerDataSetObserver(mStreamDataSetObserver);
requestLayout();
}
}
class AdapterDataSetObserver extends StreamObserver {
@Override
public void onChanged() {
int count = mStreamAdapter.getCount();
if (count > 0) {
for (int i = 0; i < count; i++) {
View view = mStreamAdapter.getView(i, null, StreamLayout.this);
if (view != null) {
StreamLayout.this.addView(view);
}
}
}
StreamLayout.this.requestLayout();
}
}
}
经过检测,发现和我上面的效果一模一样。当然,这个其实还有很多可以完善的地方,比如给每一个条目添加点击事件,点击的时候改变背景等等,这些就靠你们自己去完善了。当然,如果最后你实在做不出来,可以在下面留言给我,我会把完整的代码发给你,包括点击事件,背景变色等等