前言

前面几篇我们简单的复习了一下自定义 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测量是怎样实现?一般来说,最简单的测量如下:

    @Overrideprotected 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 的布局:

    @Overrideprotected 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 中重写测量与布局的方法:

    @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {measureChildren(widthMeasureSpec,heightMeasureSpec);super.onMeasure(widthMeasureSpec, heightMeasureSpec);}/*** @param changed 当前ViewGroup的尺寸或者位置是否发生了改变* @param l,t,r,b 当前ViewGroup相对于父控件的坐标位置,*/@Overrideprotected 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);}}@Overridepublic ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {return new ViewGroup2.LayoutParams(getContext(), attrs);}@Overrideprotected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {return new LayoutParams(p);}

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

    @Overrideprotected 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的高度,计算出对应的高度,当换行之后我们再加上行的高度。

 @Overrideprotected 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 的最终宽度,而高度的计算是和上面的方式一样的。

    @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {...else if (modeWidth == MeasureSpec.AT_MOST && modeHeight == MeasureSpec.AT_MOST) {//如果宽高都是Wrap-Contentint 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

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

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

  1. flowlayout java_Java图形化界面设计——布局管理器之FlowLayout(流式布局)

    前文讲解了JFrame.JPanel,其中已经涉及到了空布局的使用.Java虽然可以以像素为单位对组件进行精确的定位,但是其在不同的系统中将会有一定的显示差异,使得显示效果不尽相同,为此java提供了 ...

  2. java flowlayout 左对齐_Java Swing组件布局管理器之FlowLayout(流式布局)入门教程

    本文实例讲述了Java Swing组件布局管理器之FlowLayout(流式布局).分享给大家供大家参考,具体如下: FlowLayout应该是Swing布局管理器学习中最简单.最基础的一个.所谓流式 ...

  3. flowlayout java_Java Swing组件布局管理器之FlowLayout(流式布局)入门教程

    本文实例讲述了Java Swing组件布局管理器之FlowLayout(流式布局).分享给大家供大家参考,具体如下: FlowLayout应该是Swing布局管理器学习中最简单.最基础的一个.所谓流式 ...

  4. 自己定义ViewGroup控件(一)-----gt;流式布局进阶(一)

    main.xml <? xml version="1.0" encoding="utf-8"?> <com.example.SimpleLay ...

  5. java流式布局换行_自动换行的流式布局

    1.[代码][Java]代码 package com.robert; import android.content.Context; import android.util.AttributeSet; ...

  6. 响应式布局(Responsive Layout)/流式布局(Fluid Layout)/自适应布局(Adaptive)

    1.使用媒体查询来适应不同视口的固定宽度设计,例如bootstrap的container类. 2.将固定像素布局转换成灵活的百分比布局,才能让页面元素根据视口大小在一个又一个媒体查询间伸缩修正样式. ...

  7. Web前端学习笔记09:移动web开发流式布局_flex布局

    文章目录 移动web开发流式布局 1.0 移动端基础 1.1浏览器现状 1.2 手机屏幕的现状 1.3移动端调试方法 2.0 视口 2.1 布局视口 layout viewport 2.3理想视口 i ...

  8. 从上往下 流式布局_教大家怎么写前端布局

    一.静态布局(Static Layout) 1. 布局概念 最传统.原始的Web布局设计.网页最外层容器(outer)有固定的大小,所有的内容以该容器为标准,超出宽高的部分用滚动条(overflow: ...

  9. 京东首页案例(流式布局)

    我是表哥Harker,表妹我来咯~ 上篇说过,现在主流的移动端开发是单独制作移动端页面,响应式虽然有但是很麻烦,但是我们学肯定都要学到,那么这篇开始我们先讲解单独制作的几个布局. 这篇讲解流式布局(其 ...

最新文章

  1. maven3 手动安装本地jar到仓库
  2. 【jquery】一款不错的音频播放器——Amazing Audio Player
  3. spring cloud config动态刷新_SpringCloud-Config
  4. degree of freedom of a leg of a dog
  5. Python爬虫开发:url中文字符编码的两种解决方式
  6. python自建模块导入_Python模块的使用及自建模块的导入方法举例
  7. Android中获取应用程序(包)的信息-----PackageManager的使用(一)
  8. dataframe去重复 python_python – 在DataFrame中组合重复的列
  9. IIS6 mysql速度_Win 2003下IIS6+Mysql+php5.2  isapi搭建 升级php5.2到5.3测试 借助fastcgi实现...
  10. 扩展坞和hub集线器的区分
  11. 组策略开启计算机管理员账号,怎么用组策略禁用本地管理员|组策略提升管理员权限方法...
  12. 记录一次实战破解无线wifi——Aircrack-ng
  13. 一些奇怪的东西以及寄几需要注意的地方
  14. Android 一个简单手机响铃功能实现
  15. Elasticsearch+Spring Boot集成实践
  16. 【论文翻译】Learning to Navigate in Cities Without a Map
  17. [读书报告]构建之法(九)
  18. 【云原生】设备入云之基于FlexManager的应用开发
  19. Python3,我用这种方式讲解python模块,80岁的奶奶都说能理解。建议收藏 ~ ~
  20. 第一部分 思科九年 一(8)

热门文章

  1. 百度图片时看到一张很眼熟,竟然是自己发的,这收录效率!
  2. c++面试常见题·Part 1 基础
  3. 安装 Git 之后系统自动添加自定义快捷命令列表
  4. 在路上●我的年轻●勇往直前●匆匆十年
  5. 解析ARM中OS_CPU_A.S(中断级方式)
  6. Progressive Layered Extraction: A Novel Multi-TaskLearning Model for Personalized Recommendations
  7. 图片分类-K近邻分类器
  8. 浏览器自动转到外国服务器,通过HSTS实现浏览器自动跳转https(非服务器响应跳转)...
  9. Keycloak Gatekeeper:Keycloak通用代理
  10. HUAWEI Mate bookD 加装固态