一、前言
流式布局可以让内部的组件自行换行。官方文档提供了一个自定义流式布局的例子,但我相信,很多初学者肯定没看懂。事实上,不管是安卓的官方文档还是鸿蒙的官方文档,对初学者来说并不是那么的友好,官方文档更加适合有经验的开发者。当年我刚接触安卓的时候,在自定义组件的方面遇到各种问题。我相信,当年我遇到的问题,对于现在初学鸿蒙的开发者来说,同样会遇到。本文将以自定义流式布局为例,详细的介绍自定义组件的各个方面知识,确保读者在阅读完文章后,当遇到自定义组件的时候,能够有自己的思路,不会无从下手。
二、说点题外话
我从事安卓开发四五年了,为什么从安卓程序员转为鸿蒙程序员?主要有两个原因。
第一、一个国家、一个民族、一家企业,只有掌握核心技术才能不受制于人。华为在被美国制裁之前,就以高瞻远瞩的战略目光,加大技术投入,孵化备胎产品,而在经历美国的四轮制裁后,仍然能够持续的加大技术投入,这是我所佩服的。自从2019年华为发布鸿蒙1.0以来(当时的鸿蒙只用在了智慧屏上),我就打算当一名鸿蒙开发者。
第二、从哲学的角度来说,要用发展的眼光看问题,新事物必然取代旧事物,要与时俱进。十多年前,安卓和iOS能够取代塞班,就是因为塞班跟不上时代发展的潮流,塞班一直固守着功能机,随着时代的发展,人们对手机的需求已经不仅仅是打电话和发短信了,安卓和iOS正好满足当时人们的需求。而在今天,万物互联的时代已经来临。对于开发者来说,同样需要适应时代发展潮流,你固守一门技术,当这门技术已经不能适应时代发展潮流时,也就意味你落后了。当一门技术能够引领时代潮流,甚至有着革命性的功能,那这门技术是值得学习的。
目前,已经有一些有经验的开发者参与到鸿蒙的生态建设当中来,我们希望能够有更多的开发者参与到鸿蒙的生态建设当中来。但仅仅靠这些开发者远远不够。所谓后生可畏,鸿蒙的生态建设必须要有新鲜的血液注入进来,必须要有初学者的参与,必须要有后浪的参与。
三、一些重要的基础知识
3、1 组件树
在开发的时候,我们总是先在布局文件中写用户界面。鸿蒙的用户界面由组件和布局组成,也就是由Component
和ComponentContainer
组成。Component
是所有组件的父类,ComponentContainer
、文本、按钮、图片等这些组件都继承于Component
,线性布局、相对布局、栈布局等都继承于ComponentContainer
。例如下面的布局,线性布局里面添加了文字、按钮,同时,线性布局里面又添加了一个线性布局和一个栈布局。也就说,一个布局里面可以添加任意的组件,同时也可以添加任意的布局。
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:orientation="vertical"
ohos:width="match_parent">
<!-- 在线性布局里面添加文字-->
<Text
ohos:height="match_content"
ohos:width="match_content"/>
<!-- 在线性布局里面添加按钮-->
<Button
ohos:height="match_parent"
ohos:width="match_parent"/>
<!-- 在线性布局里面添加线性布局-->
<DependentLayout
ohos:height="match_parent"
ohos:width="match_parent">
<!-- 在线性布局里面添加文字-->
<Text
ohos:height="match_content"
ohos:width="match_content"/>
<!-- 在线性布局里面添加按钮-->
<Button
ohos:height="match_parent"
ohos:width="match_parent"/>
</DependentLayout>
<!-- 在线性布局里面添加栈布局-->
<StackLayout
ohos:height="match_parent"
ohos:width="match_parent">
<Button
ohos:height="match_parent"
ohos:width="match_parent"/>
</StackLayout>
</DirectionalLayout>
通过上面的布局,可以发现布局和组件形成了下图的结构。布局里面可以添加组件,可以添加布局,内部的布局又可以添加组件。这些布局和组件就形成了一棵树,组件和布局形成的树就是组件树。在计算机里面,树是一种非常重要的数据结构。组件和布局以树状的层级结构进行组织,组件树的特点是仅有一个根组件,其他组件有且仅有一个父节点。
3、2 内边距和外边距的区别
事实上,每个组件都是一个矩形,给组件添加背景,就看到每个组件都是一个矩形。padding
就是内边距,margin
就是外边距,什么是内边距?什么是外边距?
3、2、1 内边距
如下图所示,我们给组件添加了浅色的背景,这个时候就能看出组件其实是个矩形。此时,我们发现,组件的内容与边界重合在一起。
如下图所示,
Text
组件的内容就是它的文字,我们给组件添加内边距后,组件的文字与边界有了间距。内边距.png
至此,就可以知道了,内边距指的是组件的内容到边界的距离。当组件需要添加背景,同时又希望组件的内容与边界有间距,此时就可以使用内边距。内边距有以下几个属性,
padding
表示为组件的四边设置相同的内边距,left_padding
表示为组件的左边设置内边距,top_padding
表示为组件的上边设置内边距,right_padding
表示为组件的右边设置内边距,bottom_padding
表示为组件的下边设置内边距。
ohos:padding="10vp" 为组件的四边设置相同的内边距
ohos:left_padding="10vp" 为组件的左边设置内边距
ohos:top_padding="10vp" 为组件的上边设置内边距
ohos:right_padding="10vp" 为组件的右边设置内边距
ohos:bottom_padding="10vp" 为组件的下边设置内边距
除了在布局文件中给组件设内边距,也可以在代码里面为组件是内边距
// 为组件的四边设置内边距
component.setPadding(int left, int top, int right, int bottom)
// 为组件的左边设置内边距
conponent.setPaddingLeft(int left)
// 为组件的上边设置内边距
conponent.setPaddingTop(int top)
// 为组件的右边设置内边距
conponent.setPaddingRight(int right)
// 为组件的下边设置内边距
conponent.setPaddingBottom(int bottom)
为组件设置内边距后,就可以在代码里面获取到组件的内边距了
// 获取组件四边的内边距
component.getPadding()
// 获取组件的左边的内边距
conponent.getPaddingLeft()
// 获取组件的上边的内边距
conponent.getPaddingTop()
// 获取组件的右边的内边距
conponent.getPaddingRight()
// 获取组件的下边的内边距
conponent.getPaddingBottom()
3、2、2 外边距
在上面的图中,我们发现组件贴在屏幕左边,能不能让组件距离屏幕有些间距了呢?此时就可以使用外边距,如下图所示,给组件添加了外边距,组件距离屏幕就有了间距。
再添加一个新的组件,同时给新的组件添加外边距,新的组件和之前的组件也有了间距。
外边距.png
至此,就可以知道了,外边距指的是不同组件之间的间距。如果两个组件之间要有间距,那就可以使用外边距。外边距有以下几个属性,
margin
表示为组件的四边设置相同的外边距,left_margin
表示为组件的左边设置外边距,top_margin
表示为组件的上边设置外边距,right_margin
表示为组件的右边设置外边距,bottom_margin
表示为组件的下边设置外边距。
ohos:margin="10vp" 为组件的四边设置相同的外边距
ohos:left_margin="10vp" 为组件的左边设置外边距
ohos:top_margin="10vp" 为组件的上边设置外边距
ohos:right_margin="10vp" 为组件的右边设置外边距
ohos:bottom_margin="10vp" 为组件的下边设置外边距
除了在布局文件中给组件设外边距,也可以在代码里面为组件是外边距,先获取到组件布局参数对象,然后就可以设置外边距了。
// 获取布局参数
LayoutConfig layoutConfig = component.getLayoutConfig();
// 为组件的四边设置外边距
layoutConfig.setMargins(int left, int top, int right, int bottom)
// 为组件的左边设置外边距
layoutConfig.setMarginLeft(int left)
// 为组件的上边设置外边距
layoutConfig.setMarginTop(int top)
// 为组件的右边设置外边距
layoutConfig.setMarginRight(int right)
// 为组件的下边设置外边距
layoutConfig.setMarginBottom(int bottom)
为组件设置外边距后,就可以在代码里面获取到组件的外边距了,先获取到组件的布局参数对象,然后就可以获取到外边距了。
LayoutConfig layoutConfig = component.getLayoutConfig();
// 获取组件四边的外边距
layoutConfig.getMargins()
// 获取组件的左边的外边距
layoutConfig.getMarginLeft()
// 获取组件的上边的外边距
layoutConfig.getMarginTop()
// 获取组件的右边的外边距
layoutConfig.getMarginRight()
// 获取组件的下边的外边距
layoutConfig.getMarginBottom()
3、3 坐标系
有两种坐标系,一种屏幕坐标系,一种是组件的坐标系。
3、3、1 屏幕坐标系
屏幕坐标系以屏幕的左上角为圆心,如下图所示,蓝色区域是设备屏幕,以左上角为圆心,往右是横坐标的正方向,往左是横坐标的负方向,往下是纵坐标的正方向,往上是纵坐标的负方向。
屏幕坐标系.png
3、3、2 组件的坐标系
组件的坐标系是相对于父组件而言的,也就是说,组件的坐标系的原点是父组件的左上角。坐标原点往右是横坐标的正方向,坐标原点往下是纵坐标的正方向。请注意,坐标原点的坐标是(0,0),坐标原点的坐标是(0,0),坐标原点的坐标是(0,0)。如下图所示,蓝色区域是设备屏幕,绿色区域是父组件,红色区域是子组件。每个组件都有自己的坐标系,组件的坐标系是相对于父组件的。请注意,请注意,请注意,对于红色组件来说,绿色组件的左上角的坐标是(0,0),因为红色组件的父组件是绿色组件,红色组件的的坐标原点是绿色组件的左上角。对于绿色组件来说,蓝色组件的左上角的坐标是(0,0),因为绿色组件的父组件是蓝色组件,绿色组件的的坐标原点是蓝色组件的左上角。
3、3、2 组件的左上右下
知道了组件的坐标系后,就需要知道组件的左上右下。每个组件都有自己的左上右下,getLeft
、getTop
、getRight
、getBottom
分别用于获取组件的左上右下。组件的左上右下也是相对于父组件的。如下图所示,下图画出了红色组件的左上右下,图中的四根黑线就是红色组件的左上右下。红色组件的父组件是绿色组件,红色组件的坐标原点是绿色组件的左上角,所以红色组件的左上右下就是相对于绿色组件的距离。从图中可以看出,getLeft
方法返回的是红色组件左上角到绿色组件左边的距离,getTop
方法返回的是红色组件左上角到绿色组件上边的距离,getRight
方法返回的是红色组件右下角到绿色组件左边的距离,getBottom
方法返回的是红色组件右下角到绿色组件上边的距离。
在自定义组件的时候,经常需要用到组件的左上右下,开发者必须理解组件的左上右下。
四、自定义布局
鸿蒙系统提供了一系列的组件和布局,Text
,Image
等组件都继承于Component
,如果系统提供的组件不能满足要求,这时就需要自定义组件。自定义组件是通过继承Component
或者继承Compnent
的子类,由开发者定义的具有一定特性的组件。
线性布局、相对布局、栈布局等都布局继承于ComponentContainer
,如果系统提供的布局不能满足要求,这时候就需要自定义布局,自定义布局是通过继承ComponentContainer
或者继承ComponentContainer
的子类,由开发者定义的具有特定布局规则的容器类组件。本文将主要介绍自定义布局。自定义布局分为两个步骤,一是测量自定义布局的宽高,为什么需要测量自定义布局的宽高呢?不测量,宽高可能就不是我们想要的了。二是确定布局里面子组件的摆放位置,由于布局里面可以添加子组件,所以就需要确定布局里面的子组件摆放位置。
4、1 测量自定义布局的宽高
在布局文件里面添加布局的时候,必须指定宽高,宽高可以设置成match_parent
或者match_content
,也可以设置成具体的数值。需要注意的是,宽高需要的是具体的数值,而match_parent
和match_content
只是枚举值而已,并不是具体的数值,那为什么还能给宽高设置match_parent
和match_content
呢?这是因为开发者需要在Java代码里面测量宽高,也就是将match_parent
和match_content
转换成具体的宽高。系统提供的组件和布局都在代码里面测量了组件和布局的宽高。
4、1、1 测量规格
在测量的时候需要用到EstimateSpec
,EstimateSpec
就是测量规格。系统将组件的布局参数根据父容器所施加的规格转换成对应的EstimateSpec
(测量规格)。每个组件或者布局都有自己的测量模式和测量大小。EstimateSpec
封装了子组件从父组件继承的排列要求,从EstimateSpec
里面可以获取组件或者布局的测量模式和测量大小。
EstimateSpec
的getMode
方法用于获取测量模式
public static int getMode(int estimateSpec) {
}
EstimateSpec
的getSize
方法用于获取测量大小
public static int getSize(int estimateSpec) {
}
EstimateSpec
的getSizeWithMode
方法根据测量大小和测量模式生成新的测量规格。
public static int getSizeWithMode(int size, int mode) {
}
EstimateSpec
的getChildSizeWithMode
方法根据测量大小和测量模式为子组件生成的新测量规格。
public static int getChildSizeWithMode(int size, int mode) {
}
4、1、2 测量模式
测量规格有三种测量模式
测量模式 | 含义 |
---|---|
EstimateSpec.UNCONSTRAINT | UNCONSTRAINT的意思是不受约束。父组件对子组件没有约束,表示子组件可以任意大小。一般用于系统内部,或者ListContainer、ScrollView等组件。 |
EstimateSpec.PRECISE | PRECISE是精确的意思。父组件已确定子组件的宽高。在PRECISE模式下,组件的测量大小就是通过EstimateSpec.getSize方法得到的数值。 |
EstimateSpec.NOT_EXCEED | NOT_EXCEED的意思是不超过。父组件没有确定子组件的宽高,但父组件已为子组件指定了最大的宽高,子组件不能超过指定的宽高。 |
通过上表可以知道,子组件的测量模式由父组件的测量模式和子组件本身的布局参数共同确定,要想确定子组件的测量模式,就需要知道父组件的测量模式。每个组件都有宽高,所以就会有宽度的测量模式和高度的测量模式。
- 如果父组件的宽高设置了
match_parent
或者具体的数值,那么父组件的测量模式就是PRECISE
。在父组件的测量模式为PRECISE
的情况下,如果子组件的宽高设置了match_parent
或者设置了具体的数值,那么子组件的测量模式就是PRECISE
。如果子组件的宽高设置了match_content
,那么子组件的测量模式就是NOT_EXCEED
。 - 如果父组件的宽高设置了
match_content
,那么父组件的测量模式就是NOT_EXCEED
。在父组件的测量模式为NOT_EXCEED
的情况下,如果子组件的宽高设置了具体的数值,那么子组件的测量模式就是PRECISE
。如果子组件的宽高设置了match_parent
或者match_content
,那么子组件的测量模式就是NOT_EXCEED
。 -
UNCONSTRAINT
比较少用,如果父组件的测量模式就是UNCONSTRAINT
。在父组件的测量模式为UNCONSTRAINT
的情况下,如果子组件的宽高设置了具体的数值,那么子组件的测量模式就是PRECISE
。如果子组件的宽高设置了match_parent
或者match_content
,那么子组件的测量模式就是UNCONSTRAINT
。
4、1、3 测量方法
为了能够测量组件或者布局的宽高,需要实现Component.EstimateSizeListener
接口,重写onEstimateSize
方法,在onEstimateSize
方法中进行组件测量,并通过setEstimatedSize
方法将测量的宽高设置给父组件。
onEstimateSize
方法就是用来测量组件或者布局的宽高。onEstimateSize
方法有两个参数,分别是widthEstimatedConfig
,heightEstimatedConfig
。两个参数的含义如下:
参数 | 含义 |
---|---|
widthEstimatedConfig | 父组件提供给自定义布局的宽度的测量规格 |
heightEstimatedConfig | 父组件提供给自定义布局的高度的测量规格 |
如何在onEstimateSize
方法中测量宽高?
- 第一、调用
EstimateSpec.getMode(widthEstimatedConfig)
,传入widthEstimatedConfig
参数,得到宽度的测量模式; - 第二、调用
EstimateSpec.getMode(heightEstimatedConfig)
,传入heightEstimatedConfig
参数,得到高度的测量模式; - 第三、调用
EstimateSpec.getSize(widthEstimatedConfig)
,传入widthEstimatedConfig
参数,得到宽度的测量大小; - 第四、调用
EstimateSpec.getSize(heightEstimatedConfig)
,传入heightEstimatedConfig
参数,得到高度的测量大小; - 第五、判断宽高的测量模式,如果宽高都是精确的测量模式,那么自定义布局的宽高就是调用
EstimateSpec.getSize
方法得到的数值; - 第六、如果自定义布局的宽高不是精确的测量模式,就需要手动计算自定义布局的宽高。如何计算?遍历所有的布局里面的所有子组件,获取子组件的布局参数,调用
EstimateSpec.getChildSizeWithMode
得到子组件宽高的测量模式,调用子组件的estimateSize
方法来测量子组件。子组件测量完成后,就能得到子组件测量后的宽高和外边距。有了子组件的宽高和外边距,就能根据子组件的宽高和外边距来计算布局的宽高; - 第七、如何根据子组件的宽高和外边距来计算布局的宽高?不同布局的计算方式不同,需要根据实际情况进行计算。后面会以自定义流式布局为例,详细的介绍;
- 第八、当计算宽高完成后,调用
setEstimatedSize
方法来将测量好的宽高传递给父组件,并且让onEstimateSize
方法返回true
使得测量的宽高生效。
上面的计算步骤,不管自定义什么样的布局,除了第七步的代码需要根据实际情况进行计算外,其它步骤的代码可以直接拿过来用。
public class FlowLayout extends ComponentContainer implements Component.EstimateSizeListener {
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttrSet attrSet) {
this(context, attrSet, "");
}
public FlowLayout(Context context, AttrSet attrSet, String styleName) {
super(context, attrSet, styleName);
setEstimateSizeListener(this);
}
/**
* 测量自定义布局的宽高
*
* @param widthEstimatedConfig 父组件提供给自定义布局的宽度的测量规格
* @param heightEstimatedConfig 父组件提供给自定义布局的高度的测量规格
* @return 调用setEstimatedSize方法来将测量好宽高传递给父组件,并且返回true让测量的宽高生效
*/
@Override
public boolean onEstimateSize(int widthEstimatedConfig, int heightEstimatedConfig) {
// 得到宽度的测量模式
int widthMode = EstimateSpec.getMode(widthEstimatedConfig);
// 得到高度的测量模式
int heightMode = EstimateSpec.getMode(heightEstimatedConfig);
// 得到宽度的测量大小
int width = EstimateSpec.getSize(widthEstimatedConfig);
// 得到高度的测量大小
int height = EstimateSpec.getSize(heightEstimatedConfig);
// 自定义布局的宽度
int estimateWidth = 0;
// 自定义布局高度
int estimateHeight = 0;
// 宽度都是精确的模式,此时流式布局的宽高就是
if (widthMode == EstimateSpec.PRECISE && heightMode == EstimateSpec.PRECISE) {
// 此时自定义布局的宽高就是调用EstimateSpec.getSize方法得到的数值
estimateWidth = width;
estimateHeight = height;
} else {
// 不是精确的测量模式,需要手动计算自定义布局的宽高
for (int i = 0; i < childCount; i++) {
// 得到每一个子组件
Component child = getComponentAt(i);
// 获取子组件的布局参数
LayoutConfig layoutConfig = child.getLayoutConfig();
// 获取子组件宽度的测量模式
int childWidthMeasureSpec = EstimateSpec.getChildSizeWithMode(
layoutConfig.width, widthEstimatedConfig, EstimateSpec.UNCONSTRAINT);
// 获取子组件高度的测量模式
int childHeightMeasureSpec = EstimateSpec.getChildSizeWithMode(
layoutConfig.height, heightEstimatedConfig, EstimateSpec.UNCONSTRAINT);
// 测量子组件
child.estimateSize(childWidthMeasureSpec, childHeightMeasureSpec);
// 子组件测量完成后,就能得到子组件测量后的宽高
int estimatedWidth = child.getEstimatedWidth();
int estimatedHeight = child.getEstimatedHeight();
// 还可以获取子组件的外边距
int[] margins = child.getMargins();
// 根据子组件的宽高和外边距来计算布局的宽高
// 不同布局的计算方式不同,需要根据实际情况进行计算
// 等下文讲自定义流式布局的时候再来补充代码
}
}
// 调用setEstimatedSize方法来将测量好的宽高传递给父组件
setEstimatedSize(estimateWidth, estimateHeight);
// 返回true让测量的宽高生效
return true;
}
}
4、2 确定子组件的摆放位置
上面讲解完了自定义布局的测量,现在讲解自定义布局的摆放。由于布局里面可以添加子组件,所以就需要子组件的摆放位置。比如,我们常用的线性布局,如果把线性布局设置为垂直方向,那么线性布局里面的子组件就会垂直摆放。如果把线性布局设置为水平方向,那么线性布局里面的子组件就会水平摆放。
4、2、1 确定子组件的摆放位置
为了能够确定子组件的摆放位置,需要实现ComponentContainer.ArrangeListener
接口,重写onArrange
方法,在onArrange
方法中确定子组件的摆放位置。onArrange
方法有四个参数,其含义如下:
参数 | 含义 |
---|---|
left | 自定义布局的左上角到父组件左边的距离 |
top | 自定义布局的左上角到父组件上边的距离 |
width | 自定义布局的宽度 |
height | 自定义布局的高度 |
4、2、2 arrange
方法
还记得组件的坐标系吗?还记得组件的左上右下吗?不记得的话再去复习下。arrange
方法用来确定子组件的摆放位置,arrange
方法有四个参数,其含义如下
参数 | 含义 |
---|---|
left | 子组件的左上角到父组件左边的距离 |
top | 子组件的左上角到父组件上边的距离 |
width | 子组件的宽度,由于在onEstimateSize 里面已经测量子组件的宽度,所以直接调用子组件的getEstimatedWidth 来得到子组件的宽度 |
height | 子组件的高度,由于在onEstimateSize 里面已经测量子组件的高度,所以直接调用子组件的getEstimatedHeight 来得到子组件的高度 |
如何在onArrange
方法中确定子组件的摆放位置?
- 第一、遍历所有的子组件,得到子组件的外边距以及测量后德宽高;
- 第二、计算子组件左上角到父组件左边的距离,计算子组件左上角到父组件上边的距离;
- 第三、调用子组件的
arrange
方法确定子组件的摆放位置; - 第四、按照前三步确定下一个子组件的摆放位置。
- 第五、所有的子组件摆放完毕后,
onArrange
方法返回true,表示组件已在onArrange
方法中处理完成。
请注意,下面的代码是一些伪代码,只是介绍了确定子组件摆放位置的一般步骤,不要复制下面的代码,后面会介绍自定义流式布局,到时再来详细讲解下相关代码。
public class FlowLayout extends ComponentContainer implements ComponentContainer.ArrangeListener {
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttrSet attrSet) {
this(context, attrSet, "");
}
public FlowLayout(Context context, AttrSet attrSet, String styleName) {
super(context, attrSet, styleName);
setArrangeListener(this);
}
/**
* 确定子组件的摆放位置
*
* @param left 自定义布局的左上角到父组件左边的距离
* @param top 自定义布局的左上角到父组件上边的距离
* @param width 自定义布局测量出来宽度
* @param height 自定义布局测量出来高度
* @return true 此组件已在onArrange方法中处理布局
*/
@Override
public boolean onArrange(int left, int top, int width, int height) {
// 这里的是伪代码,只是介绍了确定子组件摆放位置的一般步骤,不要复制这里面的代码
// 后面会介绍自定义流式布局,到时再来详细讲解下相关代码
int curLeft = 0;
int curTop = 0;
int l, t;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
// 遍历所有的子组件
Component component = getComponentAt(i);
// 得到布局参数对象
LayoutConfig layoutConfig = component.getLayoutConfig();
// 得到左外边距
int marginLeft = layoutConfig.getMarginLeft();
// 得到上外边距
int marginTop = layoutConfig.getMarginTop();
// 得到右外边距
int marginRight = layoutConfig.getMarginRight();
// 得到子组件测量后的宽度
int estimatedWidth = component.getEstimatedWidth();
// // 得到子组件测量后的高度
int estimatedHeight = component.getEstimatedHeight();
l = curLeft + marginLeft;
t = curTop + marginTop;
// 计算完子组件的左上以及子组件的宽高,调用arrange方法确定子组件的位置
component.arrange(l, t, estimatedWidth, estimatedHeight);
curLeft += estimatedWidth + marginLeft + marginRight;
}
return true;
}
}
总结下,自定义布局需要经过三个步骤:
- 第一、继承
ComponentContainer
或者继承ComponentContainer
的子类; - 第二、实现
Component.EstimateSizeListener
接口,重写onEstimateSize
方法,在onEstimateSize
方法中进行组件测量,由于是自定义布局,所以在onEstimateSize方法里面不仅要测量自定义布局的宽高,还需要测量子组件的宽高。大家在测量之前,先在脑海里想一下或者纸上画一下自定义布局的宽高,当在脑海里或者纸上确定好了宽高后,才在onEstimateSize
方法里面计算布局的宽高; -
第三、实现
ComponentContainer.ArrangeListene
接口,重写onArrange
方法,确定子组件的摆放位置。大家在确定子组件的摆放位置之前,先在脑海里想一下或者纸上画一下,当在脑海里或者纸上确定好子组件的摆放位置后,才在onArrange
方法里面确定子组件的摆放位置。
至此,自定义布局的相关知识都介绍完了,如果你还没理解上面的知识,建议你多看几遍。下面我们来自定义一个流式布局,这个流式布局就需要用到上面介绍的知识。
五、自定义流式布局
先看下效果图,流式布局里面的子组件也是一行一行的横向显示,如果当前行已经没有足够的位置来显示下一个子组件,那下一个子组件就会显示在下一行。
流式布局.png
5、1 创建流式布局的类
新建一个类叫做FlowLayout
继承ComponentContainer
,实现Component.EstimateSizeListener
接口,重写onEstimateSize
方法,实现ComponentContainer.ArrangeListener
接口,重写onArrange
方法。为了能够调用onEstimateSize
方法和onArrange
方法,需要在构造方法里面调用setEstimateSizeListener(this);
和setArrangeListener(this);
在onArrange
方法里面需要通过每行的高度来确定每个子组件的摆放位置,这里创建一个存储每行高度的集合,在测量的时候会把当前行的高度添加到集合。
在onArrange
方法里面需要获取到流式布局中所有的子组件,这里创建一个集合,用于存储流式布局中每一行的子组件,一行一行的存储,在测量的时候会把当前行的所有子组件添加到集合。
定义两个变量,分别表示流式布局的宽度和高度。
public class FlowLayout extends ComponentContainer implements Component.EstimateSizeListener, ComponentContainer.ArrangeListener {
/**
* 在onArrange方法里面需要通过每行的高度来确定每个子组件的摆放位置,这里创建一个存储每行高度的集合
*/
private final List<Integer> lineHeight = new ArrayList<>();
/**
* 在onArrange方法里面需要获取到流式布局中所有的子组件,这里创建一个集合,用于存储流式布局中每一行的子组件,一行一行的存储
*/
private final List<List<Component>> listLineComponent = new ArrayList<>();
/**
* 流式布局的宽度
*/
private int mEstimateFlowLayoutWidth = 0;
/**
* 流式布局布局高度
*/
private int mEstimateFlowLayoutHeight = 0;
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttrSet attrSet) {
this(context, attrSet, "");
}
public FlowLayout(Context context, AttrSet attrSet, String styleName) {
super(context, attrSet, styleName);
// 让onEstimateSize方法能够执行
setEstimateSizeListener(this);
// 让onArrange方法能够执行
setArrangeListener(this);
}
/**
* 测量流式布局的宽高,由于是自定义布局,所以不仅需要测量流式布局自身的宽高,还需要测量子组件的宽高
*
* @param widthEstimatedConfig 父组件提供给流式布局的宽度的测量规格
* @param heightEstimatedConfig 父组件提供给流式布局的宽度的测量规格
* @return 调用setEstimatedSize方法来将测量好宽高传递给父组件,并且返回true让测量的宽高生效
*/
@Override
public boolean onEstimateSize(int widthEstimatedConfig, int heightEstimatedConfig) {
return true;
}
/**
* 确定子组件的摆放位置
*
* @param left 流式布局的左上角到父组件左边的距离
* @param top 流式布局的左上角到父组件上边的距离
* @param width 流式布局的宽度
* @param height 流式布局的高度
* @return true表示此组件已在onArrange方法中处理布局
*/
@Override
public boolean onArrange(int left, int top, int width, int height) {
return true;
}
}
5、2 在布局文件使用流式布局
在布局文件使用流式布局,在自定义的流式布局添加一堆的Text
,这些Text
就是流式布局的子组件,同时给每个Text
组件设置内边距和外边距,后面可以通过遍历得到这些子组件。
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:orientation="vertical"
ohos:width="match_parent">
<!-- 自定义的流式布局-->
<com.pyf.flowlayout.FlowLayout
ohos:height="match_content"
ohos:width="match_content">
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="我的"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="我是你的"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="好的"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="你好呀"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="我很不好"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="小样"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="佩服佩服佩服"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="佩服佩服佩"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="嗯嗯嗯嗯嗯嗯嗯"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="你的"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="你的"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="你的"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="你的"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
<Text
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text="你的"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content"/>
</com.pyf.flowlayout.FlowLayout>
</DirectionalLayout>
5、3 在onEstimateSize
方法中测量宽度
在onEstimateSize
方法里面测量流式布局的宽高和子组件的宽高。流式布局的宽高到底是多少呢?请看下图,图中共有5行,流式布局的高度就是每一行的高度相加。哪一行最宽,哪一行的宽度就是流式布局的宽度,图中第三行最宽,第三行的宽度就是流式布局的宽度。
知道了流式布局的宽高后,再来确定下流式布局宽高的测量模式。再看下刚刚添加的布局文件,如下图所示,流式布局的父组件是线性布局,线性布局的宽度都是
match_parent
,所以父组件宽高的的测量模式都是PRECISE
,流式布局的宽度都是match_content
。还记得上面说的测量模式吗?父组件的宽度是match_parent
,父组件宽度的的测量模式是PRECISE
,子组件的宽度是match_content
,所以流式布局宽度的测量模式是NOT_EXCEED
。父组件的高度是match_parent
,父组件高度的的测量模式是PRECISE
,子组件的高度是match_content
,所以流式布局高度的测量模式是NOT_EXCEED
。流式布局的测量模式.png
接下来就可以在
onEstimateSize
方法里面计算流式布局的宽高。上文介绍了在onEstimateSize
方法里面计算布局宽高的步骤,还记得吗?再来复习下。第一、调用
EstimateSpec.getMode(widthEstimatedConfig)
,传入widthEstimatedConfig
参数,得到宽度的测量模式;第二、调用
EstimateSpec.getMode(heightEstimatedConfig)
,传入heightEstimatedConfig
参数,得到高度的测量模式;第三、调用
EstimateSpec.getSize(widthEstimatedConfig)
,传入widthEstimatedConfig
参数,得到宽度的测量大小;第四、调用
EstimateSpec.getSize(heightEstimatedConfig)
,传入heightEstimatedConfig
参数,得到高度的测量大小;第五、判断宽高的测量模式,如果宽高都是精确的测量模式,那么自定义布局的宽高就是调用
EstimateSpec.getSize
方法得到的数值;第六、如果自定义布局的宽高不是精确的测量模式,就需要手动计算自定义布局的宽高。如何计算?遍历所有的布局里面的所有子组件,获取子组件的布局参数,调用
EstimateSpec.getChildSizeWithMode
得到子组件宽高的测量模式,调用子组件的estimateSize
方法来测量子组件。子组件测量完成后,就能得到子组件测量后的宽高和外边距。有了子组件的宽高和外边距,就能根据子组件的宽高和外边距来计算布局的宽高;第七、如何根据子组件的宽高和外边距来计算布局的宽高?不同布局的计算方式不同,需要根据实际情况进行计算;
第八、当计算宽高完成后,调用
setEstimatedSize
方法来将测量好的宽高传递给父组件,并且让onEstimateSize
方法返回true
使得测量的宽高生效。按照这八个步骤,我们来计算流式布局的宽高和子组件的宽高。
如下代码,根据父组件给出的宽度测量规格和高度测量规格,分别获取到宽度的测量模式、高度的测量模式、宽度的测量大小、高度的测量大小。
如果宽高都是精确的测量模式,此时流式布局的宽高就是调用
EstimateSpec.getSize
方法得到的数值,然后在精确模式下测量子组件的宽高。如果不是精确模式,那就需要手动测量流式布局的宽高和子组件的宽高。
计算宽高完成后,调用
setEstimatedSize
方法来将测量好的宽高传递给父组件,并且让onEstimateSize
方法返回true
使得测量的宽高生效。
/**
* 测量流式布局的宽高,由于是自定义布局,所以不仅需要测量流式布局自身的宽高,还需要测量子组件的宽高
*
* @param widthEstimatedConfig 父组件提供给流式布局的宽度的测量规格
* @param heightEstimatedConfig 父组件提供给流式布局的宽度的测量规格
* @return 调用setEstimatedSize方法来将测量好宽高传递给父组件,并且返回true让测量的宽高生效
*/
@Override
public boolean onEstimateSize(int widthEstimatedConfig, int heightEstimatedConfig) {
// 得到宽度的测量模式
int widthMode = EstimateSpec.getMode(widthEstimatedConfig);
// 得到高度的测量模式
int heightMode = EstimateSpec.getMode(heightEstimatedConfig);
// 得到宽度的测量大小
int width = EstimateSpec.getSize(widthEstimatedConfig);
// 得到高度的测量大小
int height = EstimateSpec.getSize(heightEstimatedConfig);
// 宽高都是精确的模式
if (widthMode == EstimateSpec.PRECISE && heightMode == EstimateSpec.PRECISE) {
// 此时流式布局的宽高就是提供EstimateSpec.getSize方法得到的数值
mEstimateFlowLayoutWidth = width;
mEstimateFlowLayoutHeight = height;
// 在精确模式下测量子组件的宽高
estimateChildByPrecise(width, widthEstimatedConfig, heightEstimatedConfig);
} else {
// 不是精确模式
estimateChildByNotExceed(widthEstimatedConfig, heightEstimatedConfig, width);
}
// 调用setEstimatedSize方法来将测量好的宽高传递给父组件
setEstimatedSize(mEstimateFlowLayoutWidth, mEstimateFlowLayoutHeight);
// 返回true让测量的宽高生效
return true;
}
5、4 测量模式不是精确模式,测量流式布局的宽高和子组件的宽高
如下代码,遍历所有的子组件,获取子组件的布局参数,调用getChildSizeWithMode
方法获取子组件的宽高的测量规格,调用子组件的estimateSize
方法测量子组件,子组件的estimateSize
方法会调用到子组件重写的onEstimateSize
方法。测量完成后,子组件最终的宽度等于子组件测量后的宽度 + 子组件的左边的外边距 + 子组件的右边的外边距。子组件最终的高度等于子组件测量后的高度 + 子组件的上边的外边距 + 子组件的底部的外边距。
如果当前行没有足够位置来显示下一个子组件,那么就需要换行,把下一个组件显示在下一行。如何判断当前行没有足够位置来显示下一个子组件?如果子组件的宽度 + 当前行的宽度 > 测量出来的宽度,那就说明当前行没有足够位置来显示下一个子组件,需要换行了。换行之前,先保存当前行的信息,确定流式布局的宽高,再把当前行的高度保存到集合,把当前行里面所有的子组件保存到集合。保存当前行信息后,更新新行信息。新行的宽度就是下一个子组件的宽度,新行的高度就是下一个子组件的高度
如果当前行还有位置来显示下一个子组件,那就不需要换行,计算当前行的宽高。
上面的计算方式会漏掉最后一个子组件,需要计算最后一个子组件。确定流式布局的宽高,将当前行的高度保存到集合,将当前行的所有子组件添加到集合。
/**
* 测量模式不是精确模式,需要测量流式布局的宽高和子组件的宽高
*
* @param widthEstimatedConfig 流式布局宽度的测量规格
* @param heightEstimatedConfig 流式布局高度的测量规格
* @param width 通过调用EstimateSpec.getSize得到的宽度
*/
private void estimateChildByNotExceed(int widthEstimatedConfig, int heightEstimatedConfig, int width) {
// 子组件的宽度
int childWidth;
// 子组件的高度
int childHeight;
// 流式布局可以有多行,这个变量表示当前行的宽度
int curLineWidth = 0;
// 流式布局可以有多行,这个变量表示当前行的高度
int curLineHeight = 0;
// 子组件的总数
int childCount = getChildCount();
// 用于存储每行的子组件
List<Component> lineComponent = new ArrayList<>();
for (int i = 0; i < childCount; i++) {
// 得到布局里面的每一个子组件
Component child = getComponentAt(i);
// 获取子组件的布局参数
LayoutConfig layoutConfig = child.getLayoutConfig();
/*
* 调用getChildSizeWithMode获取子组件的宽度的测量规格,getChildSizeWithMode方法有三个参数,
* 第一个参数是子组件的大小,由于希望获取子组件的宽度的测量规格,所以第一个参数传递子组件的宽度。
* 第二个参数是父组件的测量规格,由于希望获取子组件的宽度的测量规格,所以第二个参数传递父组件的宽度的测量规格。
* 第三个参数是子组件的测量规格,对于流式布局里面的子组件来说,我们并不希望流式布局去限定子组件的宽度,子组件想要多宽就有多宽,
* 所以第三个参数就传EstimateSpec.UNCONSTRAINT
*/
int childWidthMeasureSpec = EstimateSpec.getChildSizeWithMode(
layoutConfig.width, widthEstimatedConfig, EstimateSpec.UNCONSTRAINT);
/*
* 调用getChildSizeWithMode获取子组件的高度的测量规格,getChildSizeWithMode方法有三个参数,
* 第一个参数是子组件的大小,由于希望获取子组件的高度的测量规格,所以第一个参数传递子组件的高度。
* 第二个参数是父组件的测量规格,由于希望获取子组件的高度的测量规格,所以第二个参数传递父组件的高度的测量规格。
* 第三个参数是子组件的测量规格,对于流式布局里面的子组件来说,我们并不希望流式布局去限定子组件的高度,子组件想要多高就有多高,
* 所以第三个参数就传EstimateSpec.UNCONSTRAINT
*/
int childHeightMeasureSpec = EstimateSpec.getChildSizeWithMode(
layoutConfig.height, heightEstimatedConfig, EstimateSpec.UNCONSTRAINT);
/*
* 调用子组件的estimateSize方法测量子组件,子组件的estimateSize方法会调用到子组件重写的onEstimateSize方法,
* 所有的组件都是在onEstimateSize方法进行测量。如果子组件是系统提供的组件,比如Text,那就不需要开发者手动在
* 子组件的onEstimateSize方法进行测量了,因为系统已经帮开发者测量好了。如果子组件是开发者自定义的,
* 那就需要开发者手动在自定义的子组件的onEstimateSize方法进行测量
*/
child.estimateSize(childWidthMeasureSpec, childHeightMeasureSpec);
/*
* 测量完成后,就可以获取到子组件测量后的宽度了,由于子组件设置了外边距,
* 子组件最终的宽度等于子组件测量后的宽度 + 子组件的左边的外边距 + 子组件的右边的外边距
*/
childWidth = child.getEstimatedWidth() + layoutConfig.getMarginLeft() + layoutConfig.getMarginRight();
/*
* 测量完成后,就可以获取到子组件测量后的高度了,由于子组件设置了外边距,
* 子组件最终的高度等于子组件测量后的高度 + 子组件的上边的外边距 + 子组件的底部的外边距
*/
childHeight = child.getEstimatedHeight() + layoutConfig.getMarginTop() + layoutConfig.getMarginBottom();
/*
* 如果当前行没有足够位置来显示下一个子组件,那么就需要换行,把下一个组件显示在下一行
* 如何判断当前行没有足够位置来显示下一个子组件?如果子组件的宽度 + 当前行的宽度 > 测量出来的宽度,
* 那就说明当前行没有足够位置来显示下一个子组件,需要换行了。
* 换行之前,先保存当前行的信息,先判断流式布局的宽高,对比当前流式布局的宽度和当前行的宽度,
* 哪个大,哪个就是流式布局的宽度,而流式布局的高度就是当前流式布局的高度加上当前行的高度
* 再把当前行的高度保存到集合,把当前行里面所有的子组件保存到集合。
* 保存当前行信息后,更新新行信息。由于换行了,新行的宽度就是下一个子组件的宽度,新行的高度就是下一个子组件的高度,
* 同时需要创建一个新的存储每行子组件的集合。
*/
if (childWidth + curLineWidth > width) {
// 换行之前,保存当前行信息
// 判断流式布局的宽度,对比当前流式布局的宽度和当前行的宽度,哪个大,哪个就是流式布局的宽度
mEstimateFlowLayoutWidth = Math.max(mEstimateFlowLayoutWidth, curLineWidth);
// 判断流式布局的高度,流式布局的高度就是当前流式布局的高度加上当前行的高度
mEstimateFlowLayoutHeight += curLineHeight;
// 把当前行的高度保存到集合
lineHeight.add(curLineHeight);
// 把当前行里面所有的子组件保存到集合
listLineComponent.add(lineComponent);
// 更新新行信息
// 由于换行了,新行的宽度就是下一个子组件的宽度
curLineWidth = childWidth;
// 由于换行了,新行的高度就是下一个子组件的高度
curLineHeight = childHeight;
// 由于换行了,创建一个新的存储每行子组件的集合
lineComponent = new ArrayList<>();
} else {
// 当前行还有位置来显示下一个子组件,那就计算当前行的宽度
// 当前行的宽度就是当前行的宽度 + 子组件的宽高
curLineWidth += childWidth;
// 对比当前行的高度和子组件的高度,哪个高,哪个就是当前行的高度
curLineHeight = Math.max(curLineHeight, childHeight);
}
// 将子组件添加到集合
lineComponent.add(child);
/*
* 上面的计算方式会漏掉最后一个子组件,需要计算最后一个子组件。先判断流式布局的宽高,
* 对比当前流式布局的宽度和当前行的宽度,哪个大,哪个就是流式布局的宽度,
* 而流式布局的高度就是当前流式布局的高度加上当前行的高度
* 最后将当前行的高度保存到集合,将当前行的所有子组件添加到集合
*/
if (i == childCount - 1) {
// 判断流式布局的宽度,对比当前流式布局的宽度和当前行的宽度,哪个大,哪个就是流式布局的宽度
mEstimateFlowLayoutWidth = Math.max(mEstimateFlowLayoutWidth, curLineWidth);
// 流式布局的高度就是当前流式布局的高度加上当前行的高度
mEstimateFlowLayoutHeight += curLineHeight;
// 将当前行的高度保存到集合
lineHeight.add(curLineHeight);
// 将当前行的所有子组件添加到集合
listLineComponent.add(lineComponent);
}
}
}
5、5 精确模式下测量子组件的宽高
如下代码,在精确模式下测量子组件的宽高,在调用estimateChildByPrecise
方法之前,就已经确定好了流式布局的宽高,所以不需要再次处理流式布局的宽高了,在estimateChildByPrecise
方法里面只需要测量子组件的宽高。
遍历所有的子组件,获取子组件的布局参数,调用getChildSizeWithMode
方法获取子组件的宽高的测量规格,调用子组件的estimateSize
方法测量子组件,子组件的estimateSize
方法会调用到子组件重写的onEstimateSize
方法。测量完成后,子组件最终的宽度等于子组件测量后的宽度 + 子组件的左边的外边距 + 子组件的右边的外边距。子组件最终的高度等于子组件测量后的高度 + 子组件的上边的外边距 + 子组件的底部的外边距。
如果当前行没有足够位置来显示下一个子组件,那么就需要换行,把下一个组件显示在下一行。如何判断当前行没有足够位置来显示下一个子组件?如果子组件的宽度 + 当前行的宽度 > 测量出来的宽度,那就说明当前行没有足够位置来显示下一个子组件,需要换行了。换行之前,先保存当前行的信息,把当前行的高度保存到集合,把当前行里面所有的子组件保存到集合。保存当前行信息后,更新新行信息,新行的宽度就是下一个子组件的宽度,新行的高度就是下一个子组件的高度。
如果当前行还有位置来显示下一个子组件,那就不需要换行,计算当前行的宽高。当前行的宽度就是当前行的宽度 + 子组件的宽度,对比当前行的高度和子组件的高度,哪个高,哪个就是当前行的高度。
上面的计算方式会漏掉最后一个子组件,需要将最后一个子组件添加到集合。
/**
* 在精确模式下测量子组件的宽高,在调用estimateChildByPrecise方法之前,就已经确定好了流式布局的宽高,
* 所以不需要再次处理流式布局的宽高了,在estimateChildByPrecise方法里面只需要测量子组件的宽高
*
* @param width 通过调用EstimateSpec.getSize得到的宽度
* @param widthEstimatedConfig 流式布局宽度的测量规格
* @param heightEstimatedConfig 流式布局高度的测量规格
*/
private void estimateChildByPrecise(int width, int widthEstimatedConfig, int heightEstimatedConfig) {
// 子组件的宽度
int childWidth;
// 子组件的高度
int childHeight;
// 流式布局可以有多行,这个变量表示当前行的宽度
int curLineWidth = 0;
// 流式布局可以有多行,这个变量表示当前行的高度
int curLineHeight = 0;
// 子组件的总数
int childCount = getChildCount();
// 用于存储每行的子组件
List<Component> lineComponent = new ArrayList<>();
// 遍历子组件
for (int i = 0; i < childCount; i++) {
// 得到每个子组件
Component child = getComponentAt(i);
// 得到子组件的布局参数
LayoutConfig layoutConfig = child.getLayoutConfig();
/*
* 调用getChildSizeWithMode获取子组件的宽度的测量规格,getChildSizeWithMode方法有三个参数,
* 第一个参数是子组件的大小,由于希望获取子组件的宽度的测量规格,所以第一个参数传递子组件的宽度。
* 第二个参数是父组件的测量规格,由于希望获取子组件的宽度的测量规格,所以第二个参数传递父组件的宽度的测量规格。
* 第三个参数是子组件的测量规格,对于流式布局里面的子组件来说,我们并不希望流式布局去限定子组件的宽度,子组件想要多宽就有多宽,
* 所以第三个参数就传EstimateSpec.UNCONSTRAINT
*/
int childWidthMeasureSpec = EstimateSpec.getChildSizeWithMode(
layoutConfig.width, widthEstimatedConfig, EstimateSpec.UNCONSTRAINT);
/*
* 调用getChildSizeWithMode获取子组件的高度的测量规格,getChildSizeWithMode方法有三个参数,
* 第一个参数是子组件的大小,由于希望获取子组件的高度的测量规格,所以第一个参数传递子组件的高度。
* 第二个参数是父组件的测量规格,由于希望获取子组件的高度的测量规格,所以第二个参数传递父组件的高度的测量规格。
* 第三个参数是子组件的测量规格,对于流式布局里面的子组件来说,我们并不希望流式布局去限定子组件的高度,子组件想要多高就有多高,
* 所以第三个参数就传EstimateSpec.UNCONSTRAINT
*/
int childHeightMeasureSpec = EstimateSpec.getChildSizeWithMode(
layoutConfig.height, heightEstimatedConfig, EstimateSpec.UNCONSTRAINT);
/*
* 调用子组件的estimateSize方法测量子组件,子组件的estimateSize方法会调用到子组件重写的onEstimateSize方法,
* 所有的组件都是在onEstimateSize方法进行测量。如果子组件是系统提供的组件,比如Text,那就不需要开发者手动在
* 子组件的onEstimateSize方法进行测量了,因为系统已经帮开发者测量好了。如果子组件是开发者自定义的,
* 那就需要开发者手动在自定义的子组件的onEstimateSize方法进行测量
*/
child.estimateSize(childWidthMeasureSpec, childHeightMeasureSpec);
/*
* 测量完成后,就可以获取到子组件测量后的宽度了,由于子组件设置了外边距,
* 子组件最终的宽度等于子组件测量后的宽度 + 子组件的左边的外边距 + 子组件的右边的外边距
*/
childWidth = child.getEstimatedWidth() + layoutConfig.getMarginLeft() + layoutConfig.getMarginRight();
/*
* 测量完成后,就可以获取到子组件测量后的高度了,由于子组件设置了外边距,
* 子组件最终的高度等于子组件测量后的高度 + 子组件的上边的外边距 + 子组件的底部的外边距
*/
childHeight = child.getEstimatedHeight() + layoutConfig.getMarginTop() + layoutConfig.getMarginBottom();
/*
* 如果当前行没有足够位置来显示下一个子组件,那么就需要换行,把下一个组件显示在下一行
* 如何判断当前行没有足够位置来显示下一个子组件?如果子组件的宽度 + 当前行的宽度 > 测量出来的宽度,
* 那就说明当前行没有足够位置来显示下一个子组件,需要换行了。
* 换行之前,先保存当前行的信息,把当前行的高度保存到集合,把当前行里面所有的子组件保存到集合。
* 保存当前行信息后,更新新行信息。由于换行了,新行的宽度就是下一个子组件的宽度,新行的高度就是下一个子组件的高度,
* 同时需要创建一个新的存储每行子组件的集合。
*/
if (childWidth + curLineWidth > width) {
// 换行之前,保存当前行信息
// 把当前行的高度保存到集合
lineHeight.add(curLineHeight);
// 把当前行里面所有的子组件保存到集合
listLineComponent.add(lineComponent);
// 更新新行信息
// 由于换行了,新行的宽度就是下一个子组件的宽度
curLineWidth = childWidth;
// 由于换行了,新行的高度就是下一个子组件的高度
curLineHeight = childHeight;
// 由于换行了,创建一个新的存储每行子组件的集合
lineComponent = new ArrayList<>();
} else {
// 当前行还有位置来显示下一个子组件,那就计算当前行的宽度
// 当前行的宽度就是当前行的宽度 + 子组件的宽度
curLineWidth += childWidth;
// 对比当前行的高度和子组件的高度,哪个高,哪个就是当前行的高度
curLineHeight = Math.max(curLineHeight, childHeight);
}
// 将子组件添加到集合
lineComponent.add(child);
/*
* 上面的计算方式会漏掉最后一个子组件,需要将最后一个子组件添加到集合。
* 将当前行的高度保存到集合,将当前行的所有子组件添加到集合
*/
if (i == childCount - 1) {
// 将当前行的高度保存到集合
lineHeight.add(curLineHeight);
// 将当前行的所有子组件添加到集合
listLineComponent.add(lineComponent);
}
}
}
5、6 在onArrange
确定子组件的摆放位置
还记得如何在onArrange
方法中确定子组件的摆放位置?再来复习下。
- 第一、遍历所有的子组件,得到子组件的外边距以及测量后的宽高;
- 第二、计算子组件左上角到父组件左边的距离,计算子组件左上角到父组件上边的距离;
- 第三、调用子组件的
arrange
方法确定子组件的摆放位置; - 第四、按照前三步确定下一个子组件的摆放位置。
- 第五、所有的子组件摆放完毕后,
onArrange
方法返回true,表示组件已在onArrange
方法中处理完成。
按照这五个步骤,我们确定子组件的摆放位置。如下代码
/**
* 确定子组件在流式布局中的摆放位置
*
* @param left 流式布局的左上角到父组件左边的距离,也就是流式布局的左上角到父组件左边的距离
* @param top 流式布局的左上角到父组件上边的距离,也就是流式布局的左上角到父组件上边的距离
* @param width 自定义布局测量出来宽度,也就是流式布局的宽度
* @param height 自定义布局测量出来高度,也就是流式布局的高度
* @return true表示子组件已在onArrange方法中处理完成
*/
@Override
public boolean onArrange(int left, int top, int width, int height) {
// 当前行到流式布局左边的距离
int curLineLeft = 0;
// 当前行到流式布局上边的距离
int curLineTop = 0;
// 子组件左上角到流式布局左边的距离
int l;
// 子组件左上角到流式布局上边的距离
int t;
/*
* 我们需要把子组件一行一行的显示出来,之前用集合存储了每行的子组件,此时就可以遍历了,
* 先拿到每行的子组件,再遍历每行中具体的一个子组件
*/
for (int i = 0; i < listLineComponent.size(); i++) {
// 得到每行的子组件
List<Component> lineComponent = listLineComponent.get(i);
// 得到每行中具体的一个子组件
for (Component component : lineComponent) {
if (component.getVisibility() == Component.HIDE) {
// 子组件隐藏了,不做处理
continue;
}
LayoutConfig layoutConfig = component.getLayoutConfig();
// 得到子组件的左边的外边距
int marginLeft = layoutConfig.getMarginLeft();
// 得到子组件的上边的外边距
int marginTop = layoutConfig.getMarginTop();
// 得到子组件的右边的外边距
int marginRight = layoutConfig.getMarginRight();
// 得到子组件测量后的宽度
int estimatedWidth = component.getEstimatedWidth();
// 得到子组件测量后的高度
int estimatedHeight = component.getEstimatedHeight();
// 子组件左上角到流式布局左边的距离 = 当前行到流式布局左边的距离 + 子组件的左边的外边距
l = curLineLeft + marginLeft;
// 子组件左上角到流式布局上边的距离 = 当前行到流式布局上边的距离 + 子组件的上边的外边距
t = curLineTop + marginTop;
// 计算完子组件的左上以及子组件的宽高,调用arrange方法确定子组件的位置
component.arrange(l, t, estimatedWidth, estimatedHeight);
// 更新当前行到流式布局左边的距离,当前行到流式布局左边的距离 = 子组件测量后的宽度 + 子组件的左边的外边距 + 子组件的右边的外边距
curLineLeft += estimatedWidth + marginLeft + marginRight;
}
// 遍历完一行后,当前行到流式布局左边的距离就要为0,因为又要从头开始了
curLineLeft = 0;
// 遍历完一行后,当前行到流式布局上边的距离 = 前行到流式布局上边的距离 + 当前行的高度,因为开始在下一行确定子组件的摆放位置了
curLineTop += lineHeight.get(i);
}
// 子组件处理完成
return true;
}
六、将服务端返回的数据显示在流式布局中
在上文的讲解中,我们是把流式布局的子组件直接添加到布局文件中,但在实际开发中,流式布局里面的内容是由服务端返回的,接下来我们就将服务端返回的数据显示在流式布局中。
6、1 效果图
流式布局.png效果图里面的内容是从服务器获取的,我们使用蒹葭网络库来请求服务器,蒹葭是鸿蒙系统上一款网络请求框架,本质上是从
retrofit
移植过来的,蒹葭的用法跟retrofit
是一样,如果不熟悉蒹葭的用法,可以阅读鸿蒙系统网络请求框架—蒹葭这篇文章。在示例代码中,我们会访问搜索热词这个接口,这个接口用于获取搜索热词。
6、2 示例代码讲解
在配置文件中添加访问网络的权限
"ohos.permission.INTERNET"
打开entry目录下的build.gradle文件中,在build.gradle文件中的dependencies闭包下添加下面的依赖。
// 蒹葭的核心代码
implementation 'io.gitee.zhongte:jianjia:1.0.1'
// 数据转换器,数据转换器使用gson来帮我们解析json,不需要我们手动解析json
implementation 'io.gitee.zhongte:converter-gson:1.0.1'
implementation "com.google.code.gson:gson:2.8.2"
在布局文件添加流式布局
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:orientation="vertical">
<com.pyf.flowlayout.FlowLayout
ohos:id="$+id:flow_layout"
ohos:height="match_content"
ohos:width="match_parent"/>
</DirectionalLayout>
在布局文件中创建流式布局的子组件
<?xml version="1.0" encoding="utf-8"?>
<Text
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:background_element="$graphic:background_ability_main"
ohos:height="match_content"
ohos:margin="10vp"
ohos:padding="10vp"
ohos:text_alignment="center"
ohos:text_color="#ffffff"
ohos:text_size="15fp"
ohos:width="match_content">
</Text>
创建接口
/**
* @author 裴云飞
* @date 2021/5/16
*/
public interface Wan {
@GET("/hotkey/json")
Call<HotKey> getHotKey();
}
在AbilityPackage
中创建蒹葭对象,并创建接口的实例对象
public class MyApplication extends AbilityPackage {
private static MyApplication application;
private JianJia mJianJia;
private Wan mWan;
public static MyApplication getInstance() {
return application;
}
@Override
public void onInitialize() {
super.onInitialize();
application = this;
// 创建蒹葭对象
mJianJia = new JianJia.Builder()
.baseUrl("https://www.wanandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
// 创建接口的实例对象
mWan = mJianJia.create(Wan.class);
}
/**
* 获取蒹葭对象
*
* @return 蒹葭对象
*/
public JianJia getJianJia() {
return mJianJia;
}
/**
* 获取接口的实例对象
*
* @return
*/
public Wan getWan() {
return mWan;
}
}
在MainAbilitySlice
里面使用蒹葭访问服务器,将搜索热词显示到流式布局里面
public class MainAbilitySlice extends AbilitySlice {
private FlowLayout mFlowLayout;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);
mFlowLayout = (FlowLayout) findComponentById(ResourceTable.Id_flow_layout);
// 请求服务端的搜索热词
MyApplication.getInstance().getWan().getHotKey().enqueue(new Callback<HotKey>() {
@Override
public void onResponse(Call<HotKey> call, Response<HotKey> response) {
// 请求成功
if (response.isSuccessful()) {
HotKey hotKey = response.body();
// 设置搜索热词
setHotKey(hotKey);
}
}
@Override
public void onFailure(Call<HotKey> call, Throwable throwable) {
// 请求失败
LogUtils.info("yunfei", throwable.getMessage());
}
});
}
/**
* 设置搜索热词
*
* @param hotKey
*/
private void setHotKey(HotKey hotKey) {
if (hotKey == null || hotKey.data == null || hotKey.data.isEmpty()) {
// 判空操作
return;
}
List<Data> hotKeys = hotKey.data;
for (Data data : hotKeys) {
// 将布局文件转换成组件对象,并强转为Text组件
Text text = (Text) LayoutScatter.getInstance(this).
parse(ResourceTable.Layout_item_text, null, false);
if (data != null && !TextUtils.isEmpty(data.name)) {
// 显示组件的内容
text.setText(data.name);
// 将组件添加到流式布局
mFlowLayout.addComponent(text);
}
}
}
@Override
public void onActive() {
super.onActive();
}
@Override
public void onForeground(Intent intent) {
super.onForeground(intent);
}
}
七、总结
本文以自定义流式布局为例,非常详细的介绍了自定义布局的各方面知识,非常适合鸿蒙的初学者。大家需要掌握自定义布局的步骤:
- 第一、继承
ComponentContainer
或者继承ComponentContainer
的子类; - 第二、实现
Component.EstimateSizeListener
接口,重写onEstimateSize
方法,在onEstimateSize
方法中进行组件测量,由于是自定义布局,所以在onEstimateSize方法里面不仅要测量自定义布局的宽高,还需要测量子组件的宽高。大家在测量之前,先在脑海里想一下或者纸上画一下自定义布局的宽高,当在脑海里或者纸上确定好了宽高后,才在onEstimateSize
方法里面计算布局的宽高; -
第三、实现
ComponentContainer.ArrangeListene
接口,重写onArrange
方法,确定子组件的摆放位置。大家在确定子组件的摆放位置之前,先在脑海里想一下或者纸上画一下,当在脑海里或者纸上确定好子组件的摆放位置后,才在onArrange
方法里面确定子组件的摆放位置。
最后奉上源码
网友评论