android自定义流式布局解析与源码

原创 2015年11月18日 22:24:16

  今天给大家解析一下自定义流式布局的编写,以及分析一下写代码过程遇到的难点。该布局支持水平垂直方向和子view gravity选择,先看一下运行的效果,左边是垂直布局,右边是水平布局,套一个scrollview就支持滑动了

垂直布局 水平布局

说一下遇到的两个难点:

  • 自定义LayoutParams类

编写过程中需要自定义一个LayoutParams,这个LayoutParams类是要继承子父类的LayoutParams,在编码过程中需要将子view的ViewGroup.LayoutParams转成自定义的LayoutParams

    LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();

这行代码如果没有进行处理就会报错,这时候就需要重写几个函数

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attributeSet) {
        return new LayoutParams(getContext(), attributeSet);
    }
    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

为什么需要重写这几个函数呢?我们去看看ViewGroup的源码就知道了

    private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) {
        ....
        //注意该params为child的LayoutParams
        if (!checkLayoutParams(params)) {
            params = generateLayoutParams(params);
        }
        ....
    }

在添加每个子view的时候都会先检测该子view的params是不是该父view的LayoutParams类型,如果不是,则会调用generateLayoutParams函数将ViewGroup.LayoutParams类型转换为自定义的LayoutParams类型。而generateDefaultLayoutParams函数则是子view无layoutparmas的时候生成一个默认layoutparams。复写这四个函数之后,上面那行代码就不会出异常了。

  • 自定义attr

为了方便使用,需要自定义attr,但是attr的使用不是很熟悉,这里有个链接:
http://stackoverflow.com/questions/3441396/defining-custom-attrs
参考上面的方式,自定了自己的attr

<declare-styleable name="FlowLayout">
        <attr name="orientation" format="enum">
            <enum name="vertical" value="0"/>
            <enum name="horizontal" value="1"/>
        </attr>
        <attr name="childGravity">
            <flag name="top" value="0x30"/>
            <flag name="bottom" value="0x50"/>
            <flag name="left" value="0x03"/>
            <flag name="right" value="0x05"/>
            <flag name="center" value="0x11"/>
        </attr>
        <attr name="verticalSpacing" format="dimension"/>
        <attr name="horizontalSpacing" format="dimension"/>
    </declare-styleable>

关于gravity的使用,自己看了一下源码,具体了解了一下原理,它总共用了8位二进制来表示,前面4位用来表示y轴,后4位用来表示x轴,具体可以看Gravity类,这个类中有两个掩码HORIZONTAL_GRAVITY_MASK(00000111)和VERTICAL_GRAVITY_MASK(01110000),接着如果把所有的gravity变量都用二进制来表示,就很明了地知道这些变量与和或之后的结果是另外哪个gravity变量。
  

源码分析

最后就来直接看看代码,整个类最复杂的就是onMeasure函数了,这个函数中需要做大量的计算和处理,先贴出来代码:

FlowLayout.class类

public class FlowLayout extends ViewGroup{
    public static final int VERTICAL = 0;
    public static final int HORIZONTAL = 1;
    private final int CENTER = 1;
    private final int TOP = 2;
    private final int BOTTOM =3;
    private final int LEFT = 4;
    private final int RIGHT = 5;

    //默认间隙
    private int verticalSpacing = 10;
    private int horizontalSpacing = 10;
    //布局方向
    private int orientation = HORIZONTAL;
    //子view放置gravity
    private int childGravity = 1;

    public FlowLayout(Context context) {
        this(context, null);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getAttrValue(attrs);
    }

    private void getAttrValue(AttributeSet attrs){
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.FlowLayout);

        verticalSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_verticalSpacing, 10);
        horizontalSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_horizontalSpacing, 10);
        orientation = typedArray.getInt(R.styleable.FlowLayout_orientation, HORIZONTAL);

        int gravity = typedArray.getInt(R.styleable.FlowLayout_childGravity, Gravity.TOP);
        if (orientation == HORIZONTAL) {
            gravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
            if (gravity == Gravity.TOP)
                childGravity = TOP;
            else if (gravity == Gravity.BOTTOM)
                childGravity = BOTTOM;
            else
                childGravity = CENTER;
        }else{
            gravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
            if (gravity == Gravity.LEFT)
                childGravity = LEFT;
            else if (gravity == Gravity.RIGHT)
                childGravity = RIGHT;
            else
                childGravity = CENTER;
        }

        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (getChildCount() <= 0){
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }

        int paddingTop = getPaddingTop();
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();

        int width;
        int height;

        int childWidth;
        int childHeight;

        //该行最大子view大小
        int maxChildSize = 0;
        //剩余大小
        int lastSize;

        //水平布局,宽度固定,高度变化
        if (orientation == HORIZONTAL) {
            width = MeasureSpec.getSize(widthMeasureSpec);
            height = 0;
            lastSize = width - paddingLeft - paddingRight;

            //如果第一个子view的大小已经超过容器大小
            if (lastSize < getChildAt(0).getLayoutParams().width)
                throw new ChildSizeTooLongException("the 0 child's width too long");
        }
        //垂直布局,高度固定,宽度变化
        else{
            width = 0;
            height = MeasureSpec.getSize(heightMeasureSpec);
            lastSize = height - paddingTop - paddingBottom;

            //如果第一个子view的大小已经超过容器大小
            if (lastSize < getChildAt(0).getLayoutParams().height)
                throw new ChildSizeTooLongException("the 0 child's height too long");
        }

        //每行的第一个item的序号
        int firstItemOfLine = 0;

        //x,y坐标
        int x = paddingLeft;
        int y = paddingTop;

        int childSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);

            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            childHeight = lp.height;
            childWidth = lp.width;

            if (childHeight <= 0 || childWidth <= 0) {
                child.measure(childSpec, childSpec);
                childWidth = child.getMeasuredWidth();
                childHeight = child.getMeasuredHeight();
            }
            child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));

            if (orientation == HORIZONTAL) {
                lastSize = lastSize - childWidth - horizontalSpacing;
            }else{
                lastSize = lastSize - childHeight - verticalSpacing;
            }

            //需要换行
            if (lastSize < 0) {
                if (orientation == HORIZONTAL) {
                    //根据gravity将上一行的子view放置在正确的位置上
                    for (int j=firstItemOfLine; j<i; j++){
                        View lineChild = getChildAt(j);
                        LayoutParams childLayoutParams = (LayoutParams) lineChild.getLayoutParams();
                        if (childGravity == TOP){
                            //默认,无需处理
                        }else if (childGravity == BOTTOM){
                            childLayoutParams.y += maxChildSize - lineChild.getMeasuredHeight();
                        }else if (childGravity == CENTER){
                            childLayoutParams.y += (maxChildSize - lineChild.getMeasuredHeight())/2;
                        }
                    }

                    //将大小重置
                    lastSize = width - paddingLeft - paddingRight - childWidth;

                    //换行之后该行的第一个view大小超过整体父view大小
                    if (lastSize < 0)
                        throw new ChildSizeTooLongException("the " + i + " child's width too long");

                    //高换行
                    height += maxChildSize + verticalSpacing;
                    //换行之后的第一行坐标
                    x = paddingLeft;
                    y += maxChildSize + verticalSpacing;
                    //将最大高度值置为这第一个view的高度
                    maxChildSize = childHeight;
                }else{
                    //根据gravity将上一行的子view放置在正确的位置上
                    for (int j=firstItemOfLine; j<i; j++){
                        View lineChild = getChildAt(j);
                        LayoutParams childLayoutParams = (LayoutParams) lineChild.getLayoutParams();
                        if (childGravity == LEFT){
                            //默认,无需处理
                        }else if (childGravity == RIGHT){
                            childLayoutParams.x += maxChildSize - lineChild.getMeasuredWidth();
                        }else if (childGravity == CENTER){
                            childLayoutParams.x += (maxChildSize - lineChild.getMeasuredWidth())/2;
                        }
                    }

                    //将大小重置
                    lastSize = height - paddingTop - paddingBottom - childHeight;

                    //换行之后该行的第一个view大小超过整体父view大小
                    if (lastSize < 0)
                        throw new ChildSizeTooLongException("the " + i + " child's height too long");

                    //宽换列
                    width += maxChildSize + horizontalSpacing;
                    //换列之后的第一列坐标
                    x += maxChildSize + horizontalSpacing;
                    y = paddingTop;
                    //将最大宽度值置为这第一个view的宽度
                    maxChildSize = childWidth;
                }

                //换行之后的第一个item序号
                firstItemOfLine= i;
            }
            //不需要换行
            else {
                if (orientation == HORIZONTAL) {
                    //计算出这一行子view中高度最大的view
                    maxChildSize = maxChildSize > childHeight ? maxChildSize : childHeight;
                }else{
                    //计算出这一列子view中宽度最大的view
                    maxChildSize = maxChildSize > childWidth ? maxChildSize : childWidth;
                }
            }
            lp.setXY(x, y);
            if (orientation == HORIZONTAL) {
                x += childWidth + horizontalSpacing;
            }else{
                y += childHeight + verticalSpacing;
            }
        }
        if (orientation == HORIZONTAL) {
            height += maxChildSize;
            height += + paddingBottom + paddingTop;
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
            //不要忘记最后一行
            for (int i=firstItemOfLine; i<getChildCount(); i++){
                View lineChild = getChildAt(i);
                LayoutParams childLayoutParams = (LayoutParams) lineChild.getLayoutParams();
                if (childGravity == TOP){
                    //默认,无需处理
                }else if (childGravity == BOTTOM){
                    childLayoutParams.y += maxChildSize - lineChild.getMeasuredHeight();
                }else if (childGravity == CENTER){
                    childLayoutParams.y += (maxChildSize - lineChild.getMeasuredHeight())/2;
                }
            }
        }else{
            width += maxChildSize;
            width += paddingLeft + paddingRight;
            //不要忘记最后一列
            for (int i=firstItemOfLine; i<getChildCount(); i++){
                View lineChild = getChildAt(i);
                LayoutParams childLayoutParams = (LayoutParams) lineChild.getLayoutParams();
                if (childGravity == LEFT){
                    //默认,无需处理
                }else if (childGravity == RIGHT){
                    childLayoutParams.x += maxChildSize - lineChild.getMeasuredWidth();
                }else if (childGravity == CENTER){
                    childLayoutParams.x += (maxChildSize - lineChild.getMeasuredWidth())/2;
                }
            }
        }
        setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y + child.getMeasuredHeight());
        }
    }
    /**
     * 设置布局方向
     * @param orientation {@link #HORIZONTAL}or{@link #VERTICAL}
     */
    public void setOrientation(int orientation){
        if (orientation!=HORIZONTAL && orientation!=VERTICAL)
            throw new IllegalArgumentException("orientation error");
        this.orientation = orientation;
        invalidate();
    }
    public int getOrientation(){
        return orientation;
    }
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attributeSet) {
        return new LayoutParams(getContext(), attributeSet);
    }
    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }
    public static class LayoutParams extends ViewGroup.LayoutParams{
        public int x;
        public int y;
        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }
        public LayoutParams(int width, int height) {
            super(width, height);
        }
        public void setXY(int x, int y){
            this.x = x;
            this.y = y;
        }
    }
    public static class ChildSizeTooLongException extends RuntimeException{
        public ChildSizeTooLongException(String message){
            super(message);
        }
    }
}

onMeasure函数第一步根据布局方向来确定整个view的宽或高,固定一个值不变,去计算另外一个值大小。第二步,循环该view所有子view,先获取子view宽和高,如果未知就让子view自己去计算宽和高;接着根据子view的大小去计算是否需要换行,垂直布局和水平布局的处理方式有些许差异;最后计算出子view的x和y轴坐标,并且赋值到该子view的layoutparams中即可。第三步,根据以上的计算结果最后统计出整个父view的大小并且调用setMeasuredDimension方法收尾即可。

onLayout函数非常简单,就是根据onMeasure函数中的计算结果x和y来布局所有子view。

问题讨论

最后还有一个问题就是

Method 'onMeasure' is too complex to analyze by data flow algorithm

由于在onMeasure函数里面的计算和处理代码有点多,导致在实际onMeasure操作时有些耗时,经测算,在HTC one m8t上面加入1000个子view,onMeasure函数会执行400ms左右,在500个以上就能明显感觉到卡顿,这个需要怎么处理,onMeasure函数我已经优化过了,留下的都是一些必要的操作,求指点~~

整体源码下载

我将代码放在了自己不才写的一个框架中:
https://github.com/zhaozepeng/Android_framework

布局类FlowLayout.class代码位于com.android.libcore_ui.widget.FlowLayout中
测试类FlowLayoutActivity.class代码位于com.android.sample.test_widget.FlowLayoutActivity中。

最后可以厚颜无耻的要一颗星么~(@^_^@)~

版权声明:转载请标明出处http://blog.csdn.net/self_study,对技术感兴趣的同鞋加群544645972一起交流

这一定是最简单的自定义布局

说明 上半部分下载按钮为主页面 下半部分进度条和取消按钮为引用的自定义布局 简介 啥?自定义 View 和自定义布局...
  • qq_35902556
  • qq_35902556
  • 2017年12月20日 10:42
  • 38

流失布局

自定义布局 package com.example.liushi; import android.content.Context; import android.util.Attrib...
  • qikaikai0324
  • qikaikai0324
  • 2017年12月30日 09:14
  • 52

【Android】掌握自定义LayoutManager(二) 实现流式布局

转载请标明出处: http://blog.csdn.net/zxt0601/article/details/52956504 本文出自:【张旭童的博客】 本系列文章相关代码传送门: 自...
  • zxt0601
  • zxt0601
  • 2016年10月28日 17:58
  • 14020

100行Android代码自定义一个流式布局-FlowLayout

通过不到100行代码,模仿css3的flexbox功能,实现一个Android的流式布局。同时介绍了如何把打出来的aar包upload到jCenter。...
  • FeeLang
  • FeeLang
  • 2015年02月16日 22:00
  • 16340

Android流式布局的简单实现

这就是我们要实现的流式布局。 难点就在于如何控制子View的布局,动态的判断是否将子View放置在下一行。很明显,我们需要写一个类来继承ViewGroup。package com.example.d...
  • zhoujiadick
  • zhoujiadick
  • 2015年08月17日 10:50
  • 1043

Android 自定义ViewGroup 实战篇 -> 实现FlowLayout

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38352503 ,本文出自【张鸿洋的博客】1、概述上一篇已经基本给大家介绍了如何自...
  • lmj623565791
  • lmj623565791
  • 2014年08月04日 09:13
  • 123849

Android Studio 奇技淫巧

这篇文章篇幅较长,可以使用版权声明下面的目录,找到感兴趣的进行阅读 目录 3.1 Android Studio使用初探  Project面板Stucture面板Android...
  • dj0379
  • dj0379
  • 2016年11月26日 13:44
  • 6294

Android自定义控件--流式布局(FlowLayout)--自动适配

在android开发中,随着开发需求的不断提升,android原生的控件在很大程度上已不能满足开发者以及用户的需求,为了更好的增加用户体验,更有利的维护UI,在一个完整的程序中,自定义控件往往是不可或...
  • MyLoveyaqiong
  • MyLoveyaqiong
  • 2016年11月03日 01:37
  • 1852

Android自定义ViewGroup实现流式布局

实现宽度不足自动换行的流式布局: FlowLayout.java package com.jackie.flowlayout; import android.content.Context; imp...
  • shineflowers
  • shineflowers
  • 2015年08月28日 18:02
  • 1983

自定义控件:流式布局

实现代码public class FlowLayout extends ViewGroup { private List mLines = new ArrayList(); // 用来记录描述...
  • axi295309066
  • axi295309066
  • 2017年01月17日 14:16
  • 236
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:android自定义流式布局解析与源码
举报原因:
原因补充:

(最多只允许输入30个字)