Android自定义ViewGroup实现流式布局


前言

在Android开发过程中,难免会遇到一些现有View实现不了的效果,这个时候我们就需要自定义View或自定义ViewGroup来实现各种各样的UI,来满足项目开发的需要,但是很多人可能又用不好自定义View或ViewGroup,面试中还经常被问到自定义View,真的很蛋疼,所以接下来把我学习到的自定义流式布局分享给大家!


话不多说,直接上干货!

一、自定义View分类

1.自定义View

在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View。
自定义View最基本的两个方法是:onMeasure()、onDraw();View在Activity显示出来,要经历测量、绘制两个步骤,分别对应上述两个动作。

  1. 测量:onMeasure()决定View的大小;
  2. 绘制:onDraw()决定绘制这个View。

2.自定义ViewGroup

自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout。
自定义ViewGroup最基本的两个方法是:onMeasure()、onLayout();View在Activity显示出来,要经历测量、绘制两个步骤,分别对应上述两个动作。

  1. 测量:onMeasure()决定View的大小;
  2. 布局:onLayout()决定View在ViewGroup中的位置。

二、自定义ViewGroup实现流式布局

1.测量(onMeasure)

代码如下(示例):

 /**
     * 度量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        clearMeasureParams();//内存 抖动

        int childCount = getChildCount();
        //先度量孩子
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        //解析父亲给ViewGroup的高度宽度
        int selfWidth = MeasureSpec.getSize(widthMeasureSpec);
        int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
        Log.d(TAG, "onMeasure: selfWidth = " + selfWidth + " ,selfHeight = " + selfHeight);
        //保存一行中的所有的view
        List<View> lineViews = new ArrayList<>();
        //记录这行已经使用了多宽的size
        int lineWidthUsed = 0;
        // 一行的行高
        int lineHeight = 0;
        // measure过程中,子View要求的父ViewGroup的宽
        int parentNeededWidth = 0;
        // measure过程中,子View要求的父ViewGroup的高
        int parentNeededHeight = 0;

        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            LayoutParams childViewLayoutParams = childView.getLayoutParams();
            if (childView.getVisibility() != GONE) {
                //将layoutParams转变成为 measureSpec
                int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childViewLayoutParams.width);
                int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childViewLayoutParams.height);
                childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                //获取子View的度量宽高
                int childViewMeasuredWidth = childView.getMeasuredWidth();
                int childViewMeasuredHeight = childView.getMeasuredHeight();
                //如果需要换行
                if (childViewMeasuredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
                    allLines.add(lineViews);
                    lineHeights.add(lineHeight);

                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + heightMeasureSpec);
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
                    //恢复初始化
                    lineViews = new ArrayList<>();
                    lineWidthUsed = 0;
                    lineHeight = 0;
                }
                // view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局
                lineViews.add(childView);
                lineWidthUsed = lineWidthUsed + childViewMeasuredWidth + mHorizontalSpacing;
                lineHeight = Math.max(lineHeight, childViewMeasuredHeight);

                //处理最后一行数据
                if (i == childCount - 1) {
                    allLines.add(lineViews);
                    lineHeights.add(lineHeight);
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
                }

            }

        }


        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int realWidth = widthMode == MeasureSpec.EXACTLY ? selfWidth : parentNeededWidth;
        int realHeight = heightMode == MeasureSpec.EXACTLY ? selfHeight : parentNeededHeight;

        setMeasuredDimension(realWidth, realHeight);

    }

2.布局(onLayout)

代码如下:

int lineCount = allLines.size();

        int curL = getPaddingLeft();
        int curT = getPaddingTop();

        for (int i = 0; i < lineCount; i++) {
            List<View> lineViews = allLines.get(i);

            int lineHeight = lineHeights.get(i);
            for (int j = 0; j < lineViews.size(); j++) {
                View view = lineViews.get(j);
                int left = curL;
                int top = curT;

                int right = left + view.getMeasuredWidth();
                int bottom = top + view.getMeasuredHeight();
                view.layout(left, top, right, bottom);
                curL = right + mHorizontalSpacing;
            }
            curT = curT + lineHeight + mVerticalSpacing;
            curL = getPaddingLeft();
        }
    /**
     * 每个item横向间距
     */
    private int mHorizontalSpacing = dp2px(16);
    /**
     * 每个item纵向间距
     */
    private int mVerticalSpacing = dp2px(8);
    /**
     * 记录所有的行,一行一行的存储,用于layout
     */

    private List<List<View>> allLines = new ArrayList<>();
    /**
     * 记录每一行的行高,用于layout
     */
    List<Integer> lineHeights = new ArrayList<>();

实现效果如下:

流式布局效果

完成FlowLayout代码如下:

import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.List;

/**
 * 自定义流式布局
 */
public class FlowLayout extends ViewGroup {
    private static final String TAG = "FlowLayout";
    /**
     * 每个item横向间距
     */
    private int mHorizontalSpacing = dp2px(16);
    /**
     * 每个item纵向间距
     */
    private int mVerticalSpacing = dp2px(8);
    /**
     * 记录所有的行,一行一行的存储,用于layout
     */

    private List<List<View>> allLines = new ArrayList<>();
    /**
     * 记录每一行的行高,用于layout
     */
    List<Integer> lineHeights = new ArrayList<>();

    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);
    }

    private void clearMeasureParams() {
        allLines.clear();
        lineHeights.clear();
    }

    /**
     * 度量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        clearMeasureParams();//内存 抖动

        int childCount = getChildCount();
        //先度量孩子
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        //解析父亲给ViewGroup的高度宽度
        int selfWidth = MeasureSpec.getSize(widthMeasureSpec);
        int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
        Log.d(TAG, "onMeasure: selfWidth = " + selfWidth + " ,selfHeight = " + selfHeight);
        //保存一行中的所有的view
        List<View> lineViews = new ArrayList<>();
        //记录这行已经使用了多宽的size
        int lineWidthUsed = 0;
        // 一行的行高
        int lineHeight = 0;
        // measure过程中,子View要求的父ViewGroup的宽
        int parentNeededWidth = 0;
        // measure过程中,子View要求的父ViewGroup的高
        int parentNeededHeight = 0;

        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            LayoutParams childViewLayoutParams = childView.getLayoutParams();
            if (childView.getVisibility() != GONE) {
                //将layoutParams转变成为 measureSpec
                int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childViewLayoutParams.width);
                int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childViewLayoutParams.height);
                childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                //获取子View的度量宽高
                int childViewMeasuredWidth = childView.getMeasuredWidth();
                int childViewMeasuredHeight = childView.getMeasuredHeight();
                //如果需要换行
                if (childViewMeasuredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
                    allLines.add(lineViews);
                    lineHeights.add(lineHeight);

                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + heightMeasureSpec);
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
                    //恢复初始化
                    lineViews = new ArrayList<>();
                    lineWidthUsed = 0;
                    lineHeight = 0;
                }
                // view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局
                lineViews.add(childView);
                lineWidthUsed = lineWidthUsed + childViewMeasuredWidth + mHorizontalSpacing;
                lineHeight = Math.max(lineHeight, childViewMeasuredHeight);

                //处理最后一行数据
                if (i == childCount - 1) {
                    allLines.add(lineViews);
                    lineHeights.add(lineHeight);
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
                }

            }

        }


        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int realWidth = widthMode == MeasureSpec.EXACTLY ? selfWidth : parentNeededWidth;
        int realHeight = heightMode == MeasureSpec.EXACTLY ? selfHeight : parentNeededHeight;

        setMeasuredDimension(realWidth, realHeight);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int lineCount = allLines.size();

        int curL = getPaddingLeft();
        int curT = getPaddingTop();

        for (int i = 0; i < lineCount; i++) {
            List<View> lineViews = allLines.get(i);

            int lineHeight = lineHeights.get(i);
            for (int j = 0; j < lineViews.size(); j++) {
                View view = lineViews.get(j);
                int left = curL;
                int top = curT;

                int right = left + view.getMeasuredWidth();
                int bottom = top + view.getMeasuredHeight();
                view.layout(left, top, right, bottom);
                curL = right + mHorizontalSpacing;
            }
            curT = curT + lineHeight + mVerticalSpacing;
            curL = getPaddingLeft();
        }
    }

    public static int dp2px(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
    }
}

完整xml布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:text="搜索历史"
            android:textColor="@android:color/black"
            android:textSize="18sp" />

        <com.lcy.base.customview.view.FlowLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:paddingLeft="10dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="水果味孕妇奶粉" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="儿童洗衣机" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="洗衣机全自动" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="小度" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="儿童汽车可坐人" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="抽真空收纳袋" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="儿童滑板车" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="稳压器 电容" />


            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="羊奶粉" />


            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="奶粉1段" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="图书勋章日" />
        </com.lcy.base.customview.view.FlowLayout>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:text="搜索发现"
            android:textColor="@android:color/black"
            android:textSize="18sp" />

        <com.lcy.base.customview.view.FlowLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="惠氏3段" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="奶粉2段" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="图书勋章日" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="伯爵茶" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="阿迪5折秒杀" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="蓝胖子" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="婴儿洗衣机" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="小度在家" />


            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="遥控车可坐" />


            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="搬家袋" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="剪刀车" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="滑板车儿童" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="空调风扇" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="空鼓锤" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/shape_button_circular"
                android:text="笔记本电脑" />
        </com.lcy.base.customview.view.FlowLayout>
    </LinearLayout>

</ScrollView>

总结

自定义流式布局难点1: 在onMeasure方法中,需根据子View的LayoutParams的宽高属性,调用子View的measure方法;
//将layoutParams转变成为 measureSpec
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childViewLayoutParams.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childViewLayoutParams.height);
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

自定义流式布局难点2: 在onMeasure方法中,再次获取子View的宽高,需获取测量完成后的宽高,而不是:
int childViewMeasuredWidth = childView.getWidth();
int childViewMeasuredHeight = childView.getHeight();
自定义流式布局难点3: 在onMeasure方法中,最后一行,需单独处理:
    //处理最后一行数据
if (i == childCount - 1) {
     allLines.add(lineViews);
     lineHeights.add(lineHeight);
     parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
     parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
    }

自定义流式布局难点4:
在onMeasure方法中,需关注子View是否为GONE状态。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值