自定义View —— FlowLayout

自定义View —— FlowLayout


摘要

最近研究了一下 View 和 ViewGroup 这个东东, 然后想着动手写点什么。
于是实现了一下 FlowLayout —— 自动换行的标签布局。先上效果图:

这里写图片描述

背景知识

关于自定义 view,你需要了解 view 的构造流程。
我总结了一下,大体是这样的 onMeasure -> onLayout -> onDraw。
我们通过重写这三个方法来自定义控件。
1. onMeasure: 自定义宽高
2. onLayout: 自定义子控件的排列方式
3. onDraw: 自定义绘图方式,只有你想自己画圆圈、正方形之类的,才需要重写它


思路

那么我们怎么实现这个自动换行的控件呢?其实思路已经很清晰了。
1. 在 onMeasure 的时候,计算总行高 —— 每次宽度到头, 就另起一行,最后 set 一下计算出来的宽高。
2. 在 onLayout 的时候,调整子控件的起始位置 —— 也就是到头的时候要调整一下,另起一行。


实现


  1. onMeasure:

搞了一个类 MeasureSpecEntry 保存一下 measureSpec,会方便一点:
    private static final int MOD_ON_MEASURE = 0;
    private static final int MOD_ON_LAYOUT = 1;

    MeasureSpecEntry mWidthEntry = new MeasureSpecEntry(),
            mHeightEntry = new MeasureSpecEntry();
    ...
    private class MeasureSpecEntry{
        public int mMeasureSpec;
        public int mMode;
        public int mSize;

        public MeasureSpecEntry(){}

        public MeasureSpecEntry(int measureSpec) {
            init(measureSpec);
        }

        public void init(int measureSpec){
            this.mMeasureSpec = measureSpec;
            this.mMode = MeasureSpec.getMode(measureSpec);
            this.mSize = MeasureSpec.getSize(measureSpec);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        mWidthEntry.init(widthMeasureSpec);
        mHeightEntry.init(heightMeasureSpec);

        manageChild(MOD_ON_MEASURE); // 关于换行的处理
    }

我们主要看 manageChild 这个函数。因为 measure 和 layout 的时候,关于换行处理是一致的,
于是我弄了一个 mod 来切换: manageChild(int mod)。

    // 我注释掉了 mod == MOD_ON_LAYOUT 的部分
    public void manageChild(int mod){
        int widthStart=0, heightStart=0;

        for(int i=0; i<getChildCount(); i++){
            View child = getChildAt(i);

            // measure 之后, getMeasuredWidth 和 getMeasuredHeight 才有值,
            // 请不要用 getWidth 和 getHeight, 它们此时为0
            if(mod == MOD_ON_MEASURE){
                measureChild(child, mWidthEntry.mMeasureSpec, mHeightEntry.mMeasureSpec);
                mlastChildHeight = child.getMeasuredHeight();
            }

            // 超出边界, 换行
            if(widthStart + child.getMeasuredWidth() + mItemSpace > mWidthEntry.mSize) {
                widthStart = 0; // 重置起始 width
                heightStart += child.getMeasuredHeight() + mItemSpace;  
                // 换行后, 高度增加 child.getHeight() + mItemSpace
            }

            // layout
            //if(mod == MOD_ON_LAYOUT) {
            //    child.layout(widthStart, heightStart,
            //            widthStart + child.getMeasuredWidth(),
            //            heightStart + child.getMeasuredHeight());
            //}

            // 更新起点
            widthStart += child.getMeasuredWidth() + mItemSpace;
        }

        if(mod == MOD_ON_MEASURE){
            // TODO: match_parent 失效, 需做一些特殊处理
            setMeasuredDimension(mWidthEntry.mSize, heightStart + (widthStart==0? 0: mlastChildHeight));
        }
    }

大体上维护一个 widthStart 和 heightStart,遍历 child 来计算宽高
最后用 setMeasuredDimension( newWidth, newHeight)把宽高设上去。
代码很清晰,不多做解释。



  1. onLayout

跟 onMeasure 基本一致,就切换一下 mod, 直接看代码:
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        manageChild(MOD_ON_LAYOUT); // 还是调用 manageChild,切换一下 mod
    }
    // 我注释掉了 mod == MOD_ON_MEASURE 的部分
    public void manageChild(int mod){
        int widthStart=0, heightStart=0;

        for(int i=0; i<getChildCount(); i++){
            View child = getChildAt(i);

            // measure 之后, getMeasuredWidth 和 getMeasuredHeight 才有值,
            // 请不要用 getWidth 和 getHeight, 它们此时为0
            //if(mod == MOD_ON_MEASURE){
            //    measureChild(child, mWidthEntry.mMeasureSpec, mHeightEntry.mMeasureSpec);
            //    mlastChildHeight = child.getMeasuredHeight();
            //}

            // 超出边界, 换行
            if(widthStart + child.getMeasuredWidth() + mItemSpace > mWidthEntry.mSize) {
                widthStart = 0; // 重置起始 width
                heightStart += child.getMeasuredHeight() + mItemSpace;  
                // 换行后, 高度增加 child.getHeight() + mItemSpace
            }

            // layout
            if(mod == MOD_ON_LAYOUT) {
                child.layout(widthStart, heightStart,
                        widthStart + child.getMeasuredWidth(),
                        heightStart + child.getMeasuredHeight());
            }

            // 更新起点
            widthStart += child.getMeasuredWidth() + mItemSpace;
        }

        //if(mod == MOD_ON_MEASURE){
            // TODO: match_parent 失效, 需做一些特殊处理
        //    setMeasuredDimension(mWidthEntry.mSize, 
        //            heightStart + (widthStart==0? 0: mlastChildHeight));
        //}
    }

跟之前一样,在遍历 child 的时候,用 child.layout( left, top, right, bottom) 来放置我们的 child。


附录

最后放上完整的代码,感兴趣的童鞋可以拷过去把玩一下。如图,用它包裹一坨 TextView 或是别的。。。

这里写图片描述

完整代码:

package com.example.jinliangshan.littlezhihu.home.widget;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by jinliangshan on 16/9/1.
 */
public class MyFlowLayout extends ViewGroup {
    private static final String TAG = "MyFlowLayout";
    private static final int MOD_ON_MEASURE = 0;
    private static final int MOD_ON_LAYOUT = 1;

    private int mItemSpace = 30;

    MeasureSpecEntry mWidthEntry = new MeasureSpecEntry(),
            mHeightEntry = new MeasureSpecEntry();
    private int mlastChildHeight;

    public MyFlowLayout(Context context) {
        super(context);
    }

    public MyFlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public MyFlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        mWidthEntry.init(widthMeasureSpec);
        mHeightEntry.init(heightMeasureSpec);

        manageChild(MOD_ON_MEASURE);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        manageChild(MOD_ON_LAYOUT);
    }

    public void manageChild(int mod){
        int widthStart=0, heightStart=0;

        for(int i=0; i<getChildCount(); i++){
            View child = getChildAt(i);

            // measure 之后, getMeasuredWidth 和 getMeasuredHeight 才有值,
            // 请不要用 getWidth 和 getHeight, 它们此时为0
            if(mod == MOD_ON_MEASURE){
                measureChild(child, mWidthEntry.mMeasureSpec, mHeightEntry.mMeasureSpec);
                mlastChildHeight = child.getMeasuredHeight();
            }

            // 超出边界, 换行
            if(widthStart + child.getMeasuredWidth() + mItemSpace > mWidthEntry.mSize) {
                widthStart = 0;                                         // 重置起始 width
                heightStart += child.getMeasuredHeight() + mItemSpace;  
                // 换行后, 高度增加 child.getHeight() + mItemSpace
            }

            // layout
            if(mod == MOD_ON_LAYOUT) {
                child.layout(widthStart, heightStart,
                        widthStart + child.getMeasuredWidth(),
                        heightStart + child.getMeasuredHeight());
            }

            // 更新起点
            widthStart += child.getMeasuredWidth() + mItemSpace;
        }

        if(mod == MOD_ON_MEASURE){
            // TODO: match_parent 失效, 需做一些特殊处理
            setMeasuredDimension(mWidthEntry.mSize, heightStart + (widthStart==0? 0: mlastChildHeight));
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

    private class MeasureSpecEntry{
        public int mMeasureSpec;
        public int mMode;
        public int mSize;

        public MeasureSpecEntry(){}

        public MeasureSpecEntry(int measureSpec) {
            init(measureSpec);
        }

        public void init(int measureSpec){
            this.mMeasureSpec = measureSpec;
            this.mMode = MeasureSpec.getMode(measureSpec);
            this.mSize = MeasureSpec.getSize(measureSpec);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值