我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Android实现简单的自定义ViewGroup流式布局

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Android实现简单的自定义ViewGroup流式布局

前言

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

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

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

  • ViewGroup的测量:与 View 的测量不同,ViewGroup 的测量会遍历子 View ,获取子 View 的大小,从而决定自己的大小。当然我们也可以通过指定的模式来指定自身的大小。
  • ViewGroup的布局:这个是 ViewGroup 核心与常用的功能。找到对于的子View 布局到指定的位置。
  • 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);
    }

    
    @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实现简单的自定义ViewGroup流式布局的详细内容,更多关于Android ViewGroup流式布局的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Android实现简单的自定义ViewGroup流式布局

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android实现简单的自定义ViewGroup流式布局

本文我们将一起复习一下ViewGroup的测量布局方式。然后会以入门级的FlowLayout为例,来看看流式布局是如何测量与布局的,感兴趣的可以了解一下
2022-12-09

Android自定义ViewGroup之实现FlowLayout流式布局

整理总结自鸿洋的博客:http://blog.csdn.net/lmj623565791/article/details/38352503/ 一、FlowLayout介绍 所谓FlowLayout,就是控件根据ViewGroup的宽,自
2022-06-06

Android简单实现自定义流式布局的方法

本文实例讲述了Android简单实现自定义流式布局的方法。分享给大家供大家参考,具体如下: 首先来看一下 手淘HD - 商品详情 - 选择商品属性 页面的UI商品有很多尺码,而且展现每个尺码所需要的View的大小也不同(主要是宽度),所以在
2022-06-06

Android自定义ViewGroup实现可滚动的横向布局(2)

上一篇文章自定义viewgroup(1)地址://www.jb51.net/article/100608.htm 这里直接代码:package com.example.libingyuan.horizontallistview.Scroll
2022-06-06

Android编程实现Toast自定义布局简单示例

本文实例讲述了Android编程实现Toast自定义布局的方法。分享给大家供大家参考,具体如下: 不知道各位客官是不是觉得系统的toast的信息很难看呢,默认的但黑色背景,毫无色彩。 那么接下来我就教大家用最简单的方式自定义toast布局吧
2022-06-06

Android自定义ViewGroup实现侧滑菜单

这篇文章主要为大家详细介绍了Android如何通过自定义ViewGroup实现侧滑菜单,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
2023-01-05

Android自定义ViewGroup(侧滑菜单)详解及简单实例

自定义侧滑菜单的简单实现 不少APP中都有这种侧滑菜单,例如QQ这类的,比较有名开源库如slidingmenu。 有兴趣的可以去研究研究这个开源库。 这里我们将一种自己的实现方法,把学习的 东西做个记录,O(∩_∩)O! 首先看效果图:这里
2022-06-06

Android自定义ViewGroup实现标签流容器FlowLayout

本篇文章讲的是Android 自定义ViewGroup之实现标签流式布局-FlowLayout,开发中我们会经常需要实现类似于热门标签等自动换行的流式布局的功能,网上也有很多这样的FlowLayout,但不影响我对其的学习。和往常一样,主要
2022-06-06

Android自定义ViewGroup的实现方法

在android中提供了常见的几种ViewGroup的实现,包括LinearLayout、Relativeayout、FrameLayout等。这些ViewGroup可以满足我们一般的开发需求,但是对于界面要求复杂的,这几个布局就
2022-06-06

Android自定义实现BaseAdapter的优化布局

上一篇中我们介绍了自定义实现BaseAdapter的普通实现布局,然而上一章也说了普通实现的方式效率会很低,而且对系统开销也很大,所以,那样的实现是为了让初学者能知道可以这样使用,在实际项目中不可能使用那种方式的,要是你在做项目的时候使用普
2022-06-06

Android 自定义流式布局FlowLayout 自己造的轮子真香!

效果图二话不说,先上效果图原创文章 25获赞 45访问量 10万+关注私信展开阅读全文作者:A类函数
2022-06-06

Android 自定义布局竖向的ViewPager的实现

Android 自定义布局竖向的ViewPager的实现效果图:这个自定义控件涉及到的知识点:自定义ViewGroup中onMeasure和onLayout的写法 弹性滚动Scroller的用法 速度轨迹追踪器VelocityTracker
2023-05-31

Android自定义ViewGroup实现带箭头的圆角矩形菜单

本文和大家一起做一个带箭头的圆角矩形菜单,大概长下面这个样子: 要求顶上的箭头要对准菜单锚点,菜单项按压反色,菜单背景色和按压色可配置。 最简单的做法就是让UX给个三角形的图片往上一贴,但是转念一想这样是不是太low了点,而且不同分辨率也不
2022-06-06

在Android开发中使用Toast怎么实现自定义布局简单示例

在Android开发中使用Toast怎么实现自定义布局简单示例?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。首先加载一个自定义的布局LayoutInflater
2023-05-31

Android简单实现自定义弹框(PopupWindow)

一:一般都是先上效果图二:实现步骤: 1.xml布局实现 2022-06-06

Android自定义dialog简单实现方法

本文实例讲述了Android自定义dialog简单实现方法。分享给大家供大家参考,具体如下:@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreat
2022-06-06

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录