Android 打造属于自己的流式布局

          在平时的开发过程中,系统为我们提供了五大布局,基本上可以解决在工作中遇到的问题。但是,随着公司对产品的要求越来越高,界面也变的越来越好看,对应的写起来就变的越来越复杂,这个时候系统提供的布局也就不够用了,特别是对于下面的这种格式。因此,到了这个时候,我们只能通过自定义来实现了

今天我们要实现的就是这个功能

首先写一个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();
        }
    }
}

经过检测,发现和我上面的效果一模一样。当然,这个其实还有很多可以完善的地方,比如给每一个条目添加点击事件,点击的时候改变背景等等,这些就靠你们自己去完善了。当然,如果最后你实在做不出来,可以在下面留言给我,我会把完整的代码发给你,包括点击事件,背景变色等等

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值