自定义View实例——FlowLayout(流布局)的实现

简介

简单来说,流布局就是按照指定的对齐方式,将所有子view根据加入顺序依次排列,一行放不下则转入下一行。这种排列方式常见于各种标签栏、吐槽版的设计中。
展示
上图是本文实现的一个简单流布局,支持以下功能:

  • 支持左对齐、居中对齐、右对齐三种全局对齐方式
  • 子view支持居于上方、居于中间、居于下方三种位置选择
  • 支持开关分隔线
  • 左右、上下、子view之间、行与行之间均有间隔;

下面部分将讲解实现该布局的全部步骤,并在最后附上完整代码。

准备工作

  1. 创建继承自ViewGroup的新class,并命名为FlowLayout。

创建自定义属性集

  1. 在res/value文件夹下创建attrs.xml;
  2. 在resources根标签下添加子标签declare-styleable声明新属性集,并设置其name属性;
  3. 依次添加各个自定义属性,并设置其数据格式。

本文中的流布局实现了alignment(对齐方式)和hasSplitLines(有无分隔线)两种属性,如下:

<declare-styleable name="FlowLayout">
    <attr name="alignment">
        <enum name="left" value="0"/>
        <enum name="center" value="1"/>
        <enum name="right" value="2"/>
    </attr>
    <attr name="hasSplitLines" format="boolean"/>
</declare-styleable>

实现LayoutParams

  1. 创建继承自ViewGroup.MarginLayoutParams的静态内部类LayoutParams;
  2. 在attrs.xml文件夹中创建属性集FlowLayout_Layout,并加入提供给子view的自定义属性。本文中提供了layout_gravity用于设置子view在纵向的位置;
  3. 覆盖构造器,主要是在LayoutParams(Context c, AttributeSet attrs)这个构造器中利用TypedArray提取自定义属性;
  4. 实现FlowLayout的generateDefaultLayoutParams(),checkLayoutParams(),generateLayoutParams()几个方法。

属性集如下:

<declare-styleable name="FlowLayout_Layout">
    <attr name="layout_gravity">
        <enum name="top" value="0"/>
        <enum name="center" value="1"/>
        <enum name="bottom" value="2"/>
    </attr>
</declare-styleable>

onMeasure()方法实现

  1. 根据widthMeasureSpec确定FlowLayout自身的宽度;
  2. 利用measureChildWithMargins()方法对所有子view的宽高进行测量,测量时考虑左右、上下的间隔;
  3. 根据FlowLayout的宽度以及所有子view的宽度,确定出每个子view所在的行;
  4. 将每行的高度确定为该行高度最大的子view的高度,并将该行中所有layout_height属性为MATCH_PARENT的子view的高度重新设定为该行高度;
  5. 根据heightMeasureSpec以及所有行总高度(注意上下间隔与行间隔)确定出FlowLayout的高度。

onLayout()方法实现

下面为布置某一行的方法:
1. 计算出该行中子view的总宽度(注意子view的间隔);
2. 根据整体对齐方式,计算出该行第一个子view左侧的位置坐标;
3. 使用layout方法依次布置该行每个子view(注意考虑子view的layout_gravity属性,会对y坐标产生影响);

onDraw()方法实现

  1. 判断是否需要分隔线,需要的话则计算出每条分隔线的位置并利用drawLine()方法画上。

以上就是所有核心方法的实现思路了,具体细节部分可以参照代码及代码注释,示例程序可见FlowLayout,GitHub

FlowLayout完整代码

package com.example.swt369.flowlayout;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;

/**
 * Created by swt369 on 2017/8/25.
 */

public class FlowLayout extends ViewGroup {
    /**
     * left-aligned
     */
    public static final int ALIGNMENT_LEFT = 0;
    /**
     * center-aligned
     */
    public static final int ALIGNMENT_CENTER = 1;
    /**
     * right-aligned
     */
    public static final int ALIGNMENT_RIGHT = 2;

    private int mAlignment;

    private int mSpaceLeftAndRight;
    private int mSpaceTopAndBottom;
    private int mSpaceBetweenChildren;
    private int mSpaceBetweenLevels;

    private int mWidth;
    private int mHeight;

    boolean mHasSplitLines;
    Paint paintForSplitLines;
    private static final int DEFAULT_SPLIT_LINE_COLOR = Color.argb(255,176,48,96);
    private static final int DEFAULT_SPLIT_LINE_THICKNESS = 4;


    private ArrayList<ArrayList<View>> mLevels;
    private ArrayList<Integer> mLevelHeights;

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

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.FlowLayout);
        try {
            mAlignment = ta.getInt(R.styleable.FlowLayout_alignment,ALIGNMENT_LEFT);
            mHasSplitLines = ta.getBoolean(R.styleable.FlowLayout_hasSplitLines,true);
        }finally {
            ta.recycle();
        }

        float density = context.getResources().getDisplayMetrics().density;
        mSpaceLeftAndRight = (int)(10 * density);
        mSpaceTopAndBottom = (int)(5 * density);
        mSpaceBetweenChildren = (int)(5 * density);
        mSpaceBetweenLevels = (int)(5 * density);

        paintForSplitLines = new Paint();
        paintForSplitLines.setColor(DEFAULT_SPLIT_LINE_COLOR);
        paintForSplitLines.setStrokeWidth(DEFAULT_SPLIT_LINE_THICKNESS);
    }

    public void setAlignment(int alignment){
        if(alignment == mAlignment){
            return;
        }
        if(alignment == ALIGNMENT_LEFT || alignment == ALIGNMENT_CENTER || alignment == ALIGNMENT_RIGHT){
            mAlignment = alignment;
            requestLayout();
        }
    }

    public int getAlignment(){
        return mAlignment;
    }

    public void openSplitLines(){
        if(!mHasSplitLines){
            mHasSplitLines = true;
            invalidate();
        }
    }

    public void closeSplitLines(){
        if(mHasSplitLines){
            mHasSplitLines = false;
            invalidate();
        }
    }

    public boolean hasSplitLines(){
        return mHasSplitLines;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //step 1,determine the width of this layout.
        //make the width equal widthSize whatever the width mode is.
        if(widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED){
            mWidth = widthSize;
        }

        //step 2,measure all the children in order to get their width and height
        int count = getChildCount();
        int[] childWidths = new int[count];
        for(int i = 0 ; i < count ; i++){
            View child = getChildAt(i);
            if(child.getVisibility() != GONE){
                measureChildWithMargins(
                        child,
                        widthMeasureSpec,2 * mSpaceLeftAndRight,
                        heightMeasureSpec,2 * mSpaceTopAndBottom
                );
//                child.measure(
//                        getChildMeasureSpec(widthMeasureSpec,2 * mSpaceLeftAndRight,child.getLayoutParams().width),
//                        getChildMeasureSpec(heightMeasureSpec,2 * mSpaceTopAndBottom,child.getLayoutParams().height)
//                );
                int height = child.getMeasuredHeight();
                childWidths[i] = child.getMeasuredWidth();
            }
        }

        //step 3,determine which level the children will be in.
        mLevels = new ArrayList<>();
        mLevels.add(new ArrayList<View>());
        int curLevel = 0;
        int curX = mSpaceLeftAndRight;
        for(int i = 0 ; i < count ; i++){
            View child = getChildAt(i);
            if(child.getVisibility() != GONE){
                if(childWidths[i] > mWidth - 2 * mSpaceLeftAndRight){
                    //this view is too big to be put in even an empty level,so give it up.
                    continue;
                }
                if(curX + childWidths[i] <= mWidth - mSpaceLeftAndRight){
                    //current level has enough space to put this view in.
                    mLevels.get(curLevel).add(child);
                    curX += (childWidths[i] + mSpaceBetweenChildren);
                }else {
                    //current level doesn't have enough space to add this view,
                    //so switch into next level.
                    mLevels.add(new ArrayList<View>());
                    curLevel++;
                    curX = mSpaceLeftAndRight;
                    mLevels.get(curLevel).add(child);
                    curX += (childWidths[i] + mSpaceBetweenChildren);

                }
                if(curX > mWidth - mSpaceLeftAndRight){
                    //current level doesn't have enough space to add any view,
                    //so switch into next level.
                    mLevels.add(new ArrayList<View>());
                    curLevel++;
                    curX = mSpaceLeftAndRight;
                }
            }
        }

        //step 4,adjust the height of the children according to their layout_height.
        mLevelHeights = new ArrayList<>();
        for(int i = 0 ; i < mLevels.size() ; i++){
            //obtain the maximum height of this level
            int maxHeight = 0;
            for(View child : mLevels.get(i)){
                if(child.getVisibility() != GONE){
                    maxHeight = Math.max(maxHeight,child.getMeasuredHeight());
                }
            }
            mLevelHeights.add(maxHeight);
            //if a child's layout_height equals MATCH_PARENT
            //and its height doesn't equal the maximum height of this level,
            //then remeasure it.
            for(View child : mLevels.get(i)){
                if(child.getVisibility() != GONE){
                    if(child.getLayoutParams().height == ViewGroup.LayoutParams.MATCH_PARENT
                            && child.getMeasuredHeight() != maxHeight){
                        child.measure(
                                MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),MeasureSpec.EXACTLY),
                                MeasureSpec.makeMeasureSpec(maxHeight,MeasureSpec.EXACTLY)
                        );
                    }
                }
            }
        }

        //step 5,determine the height of this layout.
        //be the size of the parent
        if(heightMode == MeasureSpec.EXACTLY){
            mHeight = heightSize;
        }else {
            //be the size of all the levels.
            mHeight = 2 * mSpaceTopAndBottom;
            for(Integer integer : mLevelHeights){
                mHeight += (integer + mSpaceBetweenLevels);
            }
            mHeight -= mSpaceBetweenLevels;
        }

        //step 6,set the width and height of this layout.
        setMeasuredDimension(mWidth,mHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int curY = mSpaceTopAndBottom;
        for(int i = 0 ; i < mLevels.size() ; i++){
            //calculate the total width of the views in this level(including space between them).
            int curX = 0;
            int totalWidth = 0;
            for(View child : mLevels.get(i)){
                totalWidth += (child.getMeasuredWidth() + mSpaceBetweenChildren);
            }
            totalWidth -= mSpaceBetweenChildren;
            //use different ways to determine the start position
            //so as to realize different ways of alignment.
            switch (mAlignment){
                case ALIGNMENT_LEFT:
                    curX = mSpaceLeftAndRight;
                    break;
                case ALIGNMENT_CENTER:
                    curX = (mWidth - totalWidth) / 2;
                    break;
                case ALIGNMENT_RIGHT:
                    curX = mWidth - mSpaceLeftAndRight - totalWidth;
                    break;
            }
            //determine the accurate position of the views according to their layout_gravity
            for(View view : mLevels.get(i)){
                if(view.getVisibility() != GONE){
                    LayoutParams params = (FlowLayout.LayoutParams)view.getLayoutParams();
                    switch (params.gravity){
                        case LayoutParams.GRAVITY_TOP:
                            view.layout(
                                    curX,
                                    curY,
                                    curX + view.getMeasuredWidth(),
                                    curY + view.getMeasuredHeight()
                            );
                            break;
                        case LayoutParams.GRAVITY_CENTER:
                            int space = (mLevelHeights.get(i) - view.getMeasuredHeight()) / 2;
                            view.layout(
                                    curX,
                                    curY + space,
                                    curX + view.getMeasuredWidth(),
                                    curY + mLevelHeights.get(i) - space
                            );
                            break;
                        case LayoutParams.GRAVITY_BOTTOM:
                            view.layout(
                                    curX,
                                    curY + mLevelHeights.get(i) - view.getMeasuredHeight(),
                                    curX + view.getMeasuredWidth(),
                                    curY + mLevelHeights.get(i)
                            );
                            break;
                    }
                    curX += (view.getMeasuredWidth() + mSpaceBetweenChildren);
                }
            }
            curY += (mLevelHeights.get(i) + mSpaceBetweenLevels);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if(mHasSplitLines){
            //draw split lines
            int curY = mSpaceTopAndBottom / 2;
            for(int i = 0 ; i < mLevelHeights.size() ; i++){
                canvas.drawLine(0,curY,mWidth,curY,paintForSplitLines);
                curY += (mLevelHeights.get(i) + mSpaceBetweenLevels);
            }
            canvas.drawLine(0,curY,mWidth,curY,paintForSplitLines);
        }
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof FlowLayout.LayoutParams;
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p.width,p.height);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(),attrs);
    }

    public static class LayoutParams extends ViewGroup.MarginLayoutParams{
        public static final int GRAVITY_TOP = 0;
        public static final int GRAVITY_CENTER = 1;
        public static final int GRAVITY_BOTTOM = 2;
        public int gravity = GRAVITY_BOTTOM;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            TypedArray ta = c.obtainStyledAttributes(attrs,R.styleable.FlowLayout_Layout);
            try {
                gravity = ta.getInt(R.styleable.FlowLayout_Layout_layout_gravity,GRAVITY_CENTER);
            }finally {
                ta.recycle();
            }
        }

        public LayoutParams(int width,int height) {
            super(width, height);
        }

        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值