Android自定义ViewGroup的布局,往往都是从流式布局开始

news2025/7/7 16:54:01

前言

前面几篇我们简单的复习了一下自定义 View 的测量与绘制,并且回顾了常见的一些事件的处理方式。

那么如果我们想自定义 ViewGroup 的话,它和自定义View又有什么区别呢?其实我们把 ViewGroup 当做 View 来用的话也不是不可以。但是既然我们用到了容器 ViewGroup 当时是想用它的一些特殊的特性了。

比如 ViewGroup 的测量,ViewGroup的布局,ViewGroup的绘制。

  1. ViewGroup的测量:与 View 的测量不同,ViewGroup 的测量会遍历子 View ,获取子 View 的大小,从而决定自己的大小。当然我们也可以通过指定的模式来指定自身的大小。
  2. ViewGroup的布局:这个是 ViewGroup 核心与常用的功能。找到对于的子View 布局到指定的位置。
  3. ViewGroup的绘制:一般我们不会重写这个方法,因为一般来说它本身不需要绘制,并且当我们没有设置ViewGroup的背景的时候,onDraw()方法都不会被调用,一般来说 ViewGroup 只是会使用 dispatchDraw()方法来绘制其子View,其过程同样是通过遍历所有子View,并调用子View的绘制方法来完成绘制工作。

下面我们一起复习一下ViewGroup的测量布局方式。我们以入门级的 FlowLayout 为例,看看流式布局是如何测量与布局的。

话不多说,Let’s go

一、基本的测量与布局

我们先回顾一下ViewGroup的

一个经典的ViewGroup测量是怎样实现?一般来说,最简单的测量如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for(int i = 0; i < getChildCount(); i++){
          View childView = getChildAt(i);
          measureChild(childView,widthMeasureSpec,heightMeasureSpec);
        }

    }

或者我们直接使用封装之后的默认方法

measureChildren(widthMeasureSpec,heightMeasureSpec);

其内部也是遍历子View来实现的。当然如果有自定义的一些宽高测量规则,就不能使用这个方法,就需要自己遍历找到View自定义实现了。

需要注意的是,这里我们测量子布局传递的 widthMeasureSpec 和 heightMeasureSpec 是父布局的测量模式。

当父布局设置为固定宽度的时候,子View是不能超过这个宽度的,比如父控件设置为match_parent,自定义View无论是match_parent 还是 wrap_content 都是一样的,充满整个父控件。

相当于父布局调用子控件的onMeasure方法的时候告诉子控件,我就这么大,你看着办,不能超过它。

而父布局传递的是自适应AT_MOST模式,那么就是由子View来决定父布局的宽高。

相当于父布局调用子控件的onMeasure方法的时候问子控件,我也不知道我多大,你需要多大的位置?我又需要多大的地方才能容纳你?

其实也很好理解。那么一个经典的ViewGroup布局又是怎样实现?重写 onLayout 并且遍历拿到每一个View,进行Layout操作。

比如如下的代码,我们每一个View的高度设置为固定高度,并且垂直排列,类似一个ListView 的布局:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int childCount = getChildCount();
        //设置子View的高度
        MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
        params.height = mFixedHeight * childCount;
        setLayoutParams(params);

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

            if (child.getVisibility() != View.GONE) {
                child.layout(l, i * mFixedHeight, r, (i + 1) * mFixedHeight);
            }

        }

    }

注意我们 onLayout() 的参数

展示的效果就是这样:

二、流式的布局的layout

首先我们先不管测量,我们先指定ViewGroup的宽高为固定宽高,指定为match_parent。我们先做布局的操作:

我们自定义 ViewGroup 中重写测量与布局的方法:

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

        measureChildren(widthMeasureSpec,heightMeasureSpec);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * @param changed 当前ViewGroup的尺寸或者位置是否发生了改变
     * @param l,t,r,b 当前ViewGroup相对于父控件的坐标位置,
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int mViewGroupWidth = getMeasuredWidth(); //当前ViewGroup的总宽度

        int layoutChildViewCurX = l; //当前绘制View的X坐标
        int layoutChildViewCurY = t; //当前绘制View的Y坐标

        int childCount = getChildCount(); //子控件的数量

        //遍历所有子控件,并在其位置上绘制子控件
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            //子控件的宽和高
            int width = childView.getMeasuredWidth();
            int height = childView.getMeasuredHeight();

            //如果剩余控件不够,则移到下一行开始位置
            if (layoutChildViewCurX + width > mViewGroupWidth) {
                layoutChildViewCurX = l;
                //如果换行,则需要修改当前绘制的高度位置
                layoutChildViewCurY += height;
            }

            //执行childView的布局与绘制(右和下的位置加上自身的宽高即可)
            childView.layout(layoutChildViewCurX, layoutChildViewCurY, layoutChildViewCurX + width, layoutChildViewCurY + height);

            //布局完成之后,下一次绘制的X坐标需要加上宽度
            layoutChildViewCurX += width;
        }

    }

最后我们就能得到对应的换行效果,如下:

通过上面我们的基础学习,我们应该能理解这样的布局方式,跟上面的基础布局方式相比,就是多了一个 layoutChildViewCurX 和 layoutChildViewCurY 。关于其它的逻辑这里已经注释的非常清楚了。

但是这样的效果好丑,我们加上间距 margin 试试?

并没有效果,其实是内部 View 的 LayoutParams 就不支持 margin,我们需要定义一个内部类继承 ViewGroup.MarginLayoutParams,并重写generateLayoutParams() 方法。

    //要使子控件的margin属性有效必须继承此LayoutParams,内部还可以定制一些别的属性
    public static class LayoutParams extends MarginLayoutParams {

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

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

        public LayoutParams(ViewGroup.LayoutParams layoutParams) {
            super(layoutParams);
        }
    }

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

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

然后修改一下代码,在 layout 子布局的时候我们手动的把 margin 加上。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int mViewGroupWidth = getMeasuredWidth(); //当前ViewGroup的总宽度

        int layoutChildViewCurX = l; //当前绘制View的X坐标
        int layoutChildViewCurY = t; //当前绘制View的Y坐标

        int childCount = getChildCount(); //子控件的数量

        //遍历所有子控件,并在其位置上绘制子控件
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            //子控件的宽和高
            int width = childView.getMeasuredWidth();
            int height = childView.getMeasuredHeight();
            final LayoutParams lp = (LayoutParams) childView.getLayoutParams();

            //如果剩余控件不够,则移到下一行开始位置
            if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > mViewGroupWidth) {
                layoutChildViewCurX = l;
                //如果换行,则需要修改当前绘制的高度位置
                layoutChildViewCurY += height + lp.topMargin + lp.bottomMargin;
            }

            //执行childView的布局与绘制(右和下的位置加上自身的宽高即可)
            childView.layout(
                    layoutChildViewCurX + lp.leftMargin,
                    layoutChildViewCurY + lp.topMargin,
                    layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin,
                    layoutChildViewCurY + height + lp.topMargin + lp.bottomMargin);

            //布局完成之后,下一次绘制的X坐标需要加上宽度
            layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;
        }

    }

此时的效果就能生效了:

三、流式的布局的Measure

前面的设置我们都是使用的宽高 match_parent。那我们修改 ViewGroup 的高度为 wrap_content ,能实现高度自适应吗?

这…并不是我们想要的效果。并没有自适应高度。因为我们没有写测量的逻辑。

我们想一下,如果我们的宽度是固定的,想要高度自适应,那么我们就需要测量每一个子View的高度,计算出对应的高度,当换行之后我们再加上行的高度。

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

        final int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - this.getPaddingRight() - this.getPaddingLeft();
        final int modeWidth = MeasureSpec.getMode(widthMeasureSpec);

        final int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - this.getPaddingTop() - this.getPaddingBottom();
        final int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        if (modeWidth == MeasureSpec.EXACTLY && modeHeight == MeasureSpec.EXACTLY) {

            measureChildren(widthMeasureSpec, heightMeasureSpec);

            super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        } else if (modeWidth == MeasureSpec.EXACTLY && modeHeight == MeasureSpec.AT_MOST) {

            int layoutChildViewCurX = this.getPaddingLeft();

            int totalControlHeight = 0;

            for (int i = 0; i < getChildCount(); i++) {
                final View childView = this.getChildAt(i);
                if (childView.getVisibility() == GONE) {
                    continue;
                }

                final LayoutParams lp = (LayoutParams) childView.getLayoutParams();
                childView.measure(
                        getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight(), lp.width),
                        getChildMeasureSpec(heightMeasureSpec, this.getPaddingTop() + this.getPaddingBottom(), lp.height)
                );

                int width = childView.getMeasuredWidth();
                int height = childView.getMeasuredHeight();

                if (totalControlHeight == 0) {
                    totalControlHeight = height + lp.topMargin + lp.bottomMargin;
                }

                //如果剩余控件不够,则移到下一行开始位置
                if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > sizeWidth) {
                    layoutChildViewCurX = this.getPaddingLeft();
                    totalControlHeight += height + lp.topMargin + lp.bottomMargin;
                }
                layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;

            }

            //最后确定整个布局的高度和宽度
            int cachedTotalWith = resolveSize(sizeWidth, widthMeasureSpec);
            int cachedTotalHeight = resolveSize(totalControlHeight, heightMeasureSpec);

            this.setMeasuredDimension(cachedTotalWith, cachedTotalHeight);

        }

宽度固定和高度自适应的情况下,我们是这么处理的。计算出子View的总高度,然后设置 setMeasuredDimension 为ViewGroup的测量宽度和子View的总高度。即为最终 ViewGroup 的宽高。

这样我们就能实现高度的自适应了。那么宽度能不能自适应呢?

当然可以,我们只需要记录每一行的宽度,然后最终 setMeasuredDimension 的时候传入所有行中的最大宽度,就是 ViewGroup 的最终宽度,而高度的计算是和上面的方式一样的。

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

        ...

       else if (modeWidth == MeasureSpec.AT_MOST && modeHeight == MeasureSpec.AT_MOST) {

            //如果宽高都是Wrap-Content
            int layoutChildViewCurX = this.getPaddingLeft();
            //总宽度和总高度
            int totalControlWidth = 0;
            int totalControlHeight = 0;
            //由于宽度是非固定的,所以用一个List接收每一行的最大宽度
            List<Integer> lineLenghts = new ArrayList<>();

            for (int i = 0; i < getChildCount(); i++) {
                final View childView = this.getChildAt(i);
                if (childView.getVisibility() == GONE) {
                    continue;
                }

                final LayoutParams lp = (LayoutParams) childView.getLayoutParams();
                childView.measure(
                        getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight(), lp.width),
                        getChildMeasureSpec(heightMeasureSpec, this.getPaddingTop() + this.getPaddingBottom(), lp.height)
                );

                int width = childView.getMeasuredWidth();
                int height = childView.getMeasuredHeight();

                if (totalControlHeight == 0) {
                    totalControlHeight = height + lp.topMargin + lp.bottomMargin;
                }

                //如果剩余控件不够,则移到下一行开始位置
                if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > sizeWidth) {
                    lineLenghts.add(layoutChildViewCurX);
                    layoutChildViewCurX = this.getPaddingLeft();
                    totalControlHeight += height + lp.topMargin + lp.bottomMargin;
                }
                layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;

            }

            //计算每一行的宽度,选出最大值
            YYLogUtils.w("每一行的宽度 :" + lineLenghts.toString());
            totalControlWidth = Collections.max(lineLenghts);
            YYLogUtils.w("选出最大宽度 :" + totalControlWidth);

            //最后确定整个布局的高度和宽度
            int cachedTotalWith = resolveSize(totalControlWidth, widthMeasureSpec);
            int cachedTotalHeight = resolveSize(totalControlHeight, heightMeasureSpec);

            this.setMeasuredDimension(cachedTotalWith, cachedTotalHeight);

        }

    }

为了效果,我们把第一行的最后一个View宽度多一点,方便查看效果。

这样就可以得到ViewGroup自适应的宽度和高度了。并不复杂对不对!

后记

这样是不是就能实现一个简单的流式布局了呢?当然这些只是为方便学习和理解,真正的实战中并不推荐直接这样使用,因为内部还有一些兼容的逻辑没处理,一些逻辑没有封装,属性没有抽取。甚至连每一个View的高度,和每一行的最大高度也没有处理,其实这样健壮性并不好。

Android 知识点归整

Android 性能调优系列https://0a.fit/dNHYY

Android 车载学习指南https://0a.fit/jdVoy

Android Framework核心知识点笔记https://0a.fit/acnLL

Android 音视频学习笔记https://0a.fit/BzPVh

Jetpack全家桶(含Compose)https://0a.fit/GQJSl

Kotlin 入门到精进https://0a.fit/kdfWR

Flutter 基础到进阶实战https://0a.fit/xvcHV

Android 八大知识体系https://0a.fit/mieWJ

Android 中高级面试题锦https://0a.fit/YXwVq

后续如有新知识点,将会持续更新,尽请期待……

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/109136.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

端到端网络全链路监控方案

结构日渐复杂&#xff0c;设备类型、设备数量逐渐增加&#xff0c;设备间的连接关系随之复杂化&#xff0c;同时随着无线网络的发展&#xff0c;网络中的连接关系逐渐去“线”化&#xff0c;如何可观、高效的对网络间复杂的连接关系进行监控和管理&#xff0c;成为用户不可忽视…

2022年最好用的五款设备管理软件

工厂是典型的设备密集型组织&#xff0c;设备固定资产具有数量多、种类多、使用周期长、使用地点分散等特征。如果依然在使用传统的手工记录数据、手工巡检、纸质维保、电话维修的方式&#xff0c;势必给企业带来损失。 设备是众多企业经营中支出的主要组成部分&#xff0c;在…

(二十)Vue之非单文件组件

文章目录基本使用一、如何定义一个组件&#xff1f;二、如何注册组件&#xff1f;三、如何使用组件&#xff1f;演示程序普通Vue程序单文件组件程序局部注册全局注册几个注意点1.关于组件名2.关于组件标签3.一个简写方式组件的嵌套使用关于VueComponent一个重要的内置关系&…

Shape详解

Spape详解 1.自定义背景shape 1.1gradient 1.简介 定义渐变色&#xff0c;可以定义两色渐变和三色渐变&#xff0c;及渐变样式&#xff0c;它的属性有下面几个2.属性 angle&#xff0c;只对线性渐变是有效的放射性渐变必须指定放射性的半径&#xff0c;gradientRadiouscentetX和…

Ubuntu安装redis服务器

官网下载redis服务器的压缩包redis-6.0.16.tar.gz 点击download 6.2.8或任意版本即可。 上传下载的压缩包到服务器或者本地虚拟机 解压压缩包&#xff0c;并安装gcc tar -zxvf redis-6.0.16.tar.gz解压之后可以看到redis的目录结构&#xff1a; 没有bin目录&#xff0c;而redi…

Meta CTO专访:2023年AR/VR、元宇宙的下一步怎么走

2022年对于Meta来说注定是不平凡的一年&#xff0c;它经历了股价大跌、万人大裁员、项目重组、季度营收首次下滑、Reality Labs季度亏损破纪录&#xff0c;甚至前不久Meta AR/VR业务的元老级人物、Reality Labs顾问CTO John Carck也宣布离职&#xff0c;这件事对于Meta甚至整个…

HEVC学习之CTU划分

一,CTU相关概念 H.265将图像划分为“树编码单元&#xff08;coding tree units, CTU&#xff09;”&#xff0c;而不是像H.264那样的1616的宏块。根据不同的编码设置&#xff0c;树编码块的尺寸可以被设置为6464或有限的3232或1616。 上图就是一个6464树编码块的分区示例&am…

数字ic验证|SoC的功能验证

随着设计的进行&#xff0c;越接近最后的产品&#xff0c;修正一个设计缺陷的成本就会越高。 1.功能验证概述 在IC设计与制造领域&#xff0c;通常所说的验证&#xff08;Verification&#xff09;和测试&#xff08;Test&#xff09;是两种不同的事 验证 在设计过程中确认…

PHP 实现PDF转图片

目录 1.环境配置&#xff1a; 2.实现原理&#xff1a; 3.安装php扩展imagick 4.安装ghostscript 5.pdf转图片 1.环境配置&#xff1a; 2.实现原理&#xff1a; Php使用扩展插件imagick进行图片处理&#xff0c;处理pdf时使用imagick去调用ghostscript 3.安装php扩展imag…

spring-boot如何自行写一个starter并且使用

这里说的starter是pom中引入的一系列starter包&#xff0c;比如spring-boot-starter-web、mybatis-plus-boot-starter等。本文先已mybatis-spring-boot-starter的使用进行说明&#xff0c;然后得到使用的流程&#xff08;套路&#xff09;&#xff0c;然后根据该流程&#xff0…

ddim原理及代码(Denoising diffusion implicit models)

前言 之前学习了 DDPM(DDPM原理与代码剖析)和 IDDPM(IDDPM原理和代码剖析)&#xff0c; 这次又来学习另一种重要的扩散模型。它的采样速度比DDPM快很多(respacing)&#xff0c;扩散过程不依赖马尔科夫链。 Denoising diffusion implicit models, ICLR 2021 理论 Astract和Int…

百果园通过港交所上市聆讯:八成营收来自加盟店,余惠勇为董事长

撰稿|汤汤 来源|贝多财经 近日&#xff0c;深圳百果园实业&#xff08;集团&#xff09;股份有限公司&#xff08;下称“百果园”&#xff09;通过港交所上市聆讯&#xff0c;并披露了聆讯后招股书。 根据招股书介绍&#xff0c;百果园是中国最大的水果零售经营商。根据弗若斯…

数据库实验6 存储过程实验

前言&#xff1a;游标的mysql代码不懂写&#xff0c;所以没有运行结果 实验6 存储过程实验 实验6.1 存储过程实验 1.实验目的 掌握数据库 PL/SQL 编程语言&#xff0c;以及数据库存储过程的设计和使用方法。 2.实验内容和要求 存储过程定义,存储过程运行,存储过程更名,存…

股票价、量走势图绘制

在证券投资分析领域中价、量走势分布图是投资者常用的一个参考方面。本案例主要介绍股票每日收盘价格、成交量的走势图以及月交易量分布饼图的绘制技能&#xff0c;并进一步介绍了子图的绘制方法。今有股票代码600000行情交易数据表&#xff08;trd.xlsx&#xff09;&#xff0…

SPDK线程模型

一、reactor线程 与传统的reactor线程模型相比&#xff0c;SPDK的reactor在功能实现上还是有很大区别的&#xff0c;线程不在基于流水线形式进行作业&#xff0c;而是采用Run-To-Complete来做运行处理。 如图所示&#xff0c;每个reactor线程会绑定一个cpu core&#xff0c;线…

力扣刷题笔记day7(数组中重复的数字+在排序数组中查找数字+0~n-1中缺失的数字)

文章目录数组中重复的数字题目思路代码在排序数组中查找数据题目思路代码0&#xff5e;n-1中缺失的数字题目思路代码数组中重复的数字 题目 思路 创建一个哈希表&#xff0c;将数组遍历&#xff0c;如果不存在则添加到哈希表中&#xff0c;如果存在则直接返回 代码 var fin…

电磁功率流和坡印廷矢量

回顾&#xff1a; 场源的影响周围的快慢取决于距离和传播速度 场源变化引起电磁波&#xff0c;电磁波传输能量 电磁能量的流动满足能量守恒定律 我们关心的是 体积V里面和体积外怎么交换能量&#xff0c;S是包围的闭合面 大家想一想&#xff0c;体积里面有场源 随着能量的…

Spring 中使用Nacos服务发现

引入依赖 <dependency><groupId>com.alibaba.nacos</groupId><artifactId>nacos-spring-context</artifactId><version>${latest.version}</version> </dependency>本文使用的版本为&#xff1a;1.1.1&#xff08;与Spring 中…

微服务(二)——注册中心(Eureka、Nacos)

目录1. 概念2. Eureka1. 服务搭建1. 依赖导入2. 配置文件3. 启动项目2. 服务注册1. 依赖导入2. 配置文件3. 启动多个实例3. 服务发现1. 依赖导入2. 配置文件3. 服务拉取和负载均衡4. 小结3. Ribbon1. 负载均衡流程2. 负载均衡策略3. 加载策略4. Nacos1. 下载安装2. 注册中心1. …

css:隐藏input file标签并触发点击上传文件事件

目录方式一&#xff1a;将input标签覆盖到按钮的最上层方式二&#xff1a;通过label标签触发点击事件方式三&#xff1a;js触发文件上传的点击事件总结通用的按钮样式 /* button样式来自element-ui */ .button {color: #fff;background-color: #409eff;display: inline-block;…