浅谈自定义View之自定义布局FlowLayout

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;
            }
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值