鸿蒙布局第三篇--详细介绍ArkUI中的栅格布局,列表以及媒体查询

3,540 阅读18分钟

前言

最近在忙个又大又赶的Web项目,基本都抽不出闲暇时间来写文章,这篇文章开始写第一个字已经是两个礼拜以前的事情了,中间就是断断续续的,挤点时间写点挤点时间再写点,好不容易才把这篇鸿蒙布局的最后一篇文章写完了,这篇文章讲的是鸿蒙布局里面的栅格布局,列表以及媒体查询,三种布局基本都体现出了一个共同点,那就是屏幕适配,一起来看下吧。

栅格布局(GridRow/GridCol)

栅格布局看组件名字有点跟之前介绍过的Grid组件很像,整体看上去栅格布局也是将设备屏幕分成一小格一小格的展示子试图,区别在于,Grid当你设置好了交叉轴方向上的子视图个数的时候,无论屏幕大小怎么变,交叉轴方向上的视图数量是不会变的,变的是子视图的大小,但是在一些宽屏设备上,子试图如果变得太大,也影响视觉上的体验,所以栅格布局在这一点上做了区别,它会在设备的水平宽度上设置一套断点规则,每一种断点就相当于是一种屏幕大小区间,在每个区间里面,最终页面呈现的布局是不一样的,那么什么是断点呢?不是平时我们说的断点调试那种断点,这个断点相当于给一种屏幕起了一个名字,总共有下面几个名字

  • xs:最小宽度类型设备
  • sm:小宽度类型设备
  • md:中等宽度类型设备
  • lg:大宽度类型设备
  • xl:特大宽度类型设备
  • xxl:超大宽度类型设备

写过Web的一定对这些名字都不陌生,在Web里面Col组件也可以通过设置断点来适配屏幕,而且用法同栅格布局基本一致,在栅格布局中,通过设置breakpoints来设置断点,用法如下

image.png

breakpoints里面有个value,我们给value设置了一个数组,这个数组啥意思呢,它代表着一个个区间,当设备宽度小于200vp的时候,当前断点就是xs,当设备宽度大于200vp小于500vp的时候,设备断点是sm,以此类推下去,所以由于我们总共就六个断点,所以value这个数组的长度最大不能超过5,那么这个时候可能有的人就有疑问了,干啥要设置这些区间呢,愣麻烦的...还记得刚才我们说到的栅格布局会根据不同的屏幕大小区间来呈现不同的布局吗?关键时刻到了,栅格布局GridRow默认将屏幕宽度分成12列,而它的子视图规定使用GridCol组件,GridCol会根据设置好的断点,来给每个断点设置每一个GridCol占的列数,比如这里给五个断点分别设置不同列数

image.png

小屏幕xs上单个视图占了2格,再大点的屏幕sm上占了3格,然后后面依次设置相应的列数,这里还创建了12个色块,下面来看下在不同尺寸的屏幕上的布局排列情况

0913aa1.gif

可以看到在正常手机上竖屏的时候,我们断点是来到了sm,所以在水平方向上展示四个,当手机屏幕变为横屏时候,断点就变成了md,水平方向上展示的个数也发生了变化,而变成平板横屏的时候,区间断点就来到了lg,这里是按照默认列数为12在计算每一项占的列数,我们还可以自定义栅格布局的列数,通过设置column参数

image.png

在上面的代码中仅仅只增加了column:24的设置,将GridRow占的列数增加到了24列,这个时候我们的布局又发生了变化

0913aa2.gif

由于我们已经知道手机设备上竖屏时候断点是在sm上,sm我们是设置了3列,所有当GridRow的默认列数变成24的时候,我们可以从效果图上看到手机设备竖屏时候水平方向上的子项数量变成了八个,同理在旋转设备以及切换至平板时候,水平方向上的子项数量都增加了

span

到现在为止我们GridColspan属性都是设置的GridColColumnOptionspan代表着一个子项所占的列数,默认占1列,我们还可以给span单独设置个number,那么这个就表示在任何断点区间内,子项占的列数都是固定的,比如我们将上面的例子更改下,让每个子项占2列,代码如下

image.png

这里仍旧把GridRow的默认列数变回12,每个GridCol占2列,那么水平方向一行应该永远最多只有六个,看下效果

0913aa3.gif

gutter

当我们想要给GridRow里面的子项设置间距的时候,可以使用GridRowOptions提供的gutter属性

image.png

发现gutter既可以设置一个Length,也可以设置一个GutterOption,当设置一个Length的时候,可以给gutter设置一个number类型的值,也可以是一个string,也可以是Resource

image.png

这些都表示子项水平方向垂直方向的间距是一样的,比如给上面例子中的GridRow添加10vp的间距,代码可以这样写

image.png image.png

最终效果就如上图所示,水平垂直方向的间距都是相同的,那如果水平跟垂直方向的间距不一样,我们就得设置GutterOption,分别定义xy方向上的间距

image.png

x,y的取值也有两种,Length就不用多说了,GridRowSizeOption是什么东西呢?点进去一看,发现GridRowSizeOption也是可以根据断点区间来设置大小的

image.png

下面来写点例子,来给上面的栅格布局的水平与垂直方向设置不同的间距值,先设置Lendth类型

image.png image.png

效果图上可以看到垂直方向的间距已经比水平方向的要大了,我们再用GridRowSizeOption来试一下

image.png 0913aa4.gif

效果图上可以看出,在不断改变屏幕宽度下,GridRow的子项之间的间距也都发生了变化

offset

GridCol还可以设置相对于前一项的偏移量,这个偏移量也相当于偏移的列数,比如我们给每个子项偏移2列,代码可以这样写

image.png

偏移了两列以后,效果图如下

image.png

由于每个GridCol默认占1列,然后偏移了两列,也就是说每一项需要3列的空间,然后我们GridRow默认列数为12列,所以最终展示出来的效果才是屏幕宽度上一行有四个子项

order

order可以设置子组件在GridRow里面的排列顺序,当不设置order或者设置相同的order的时候,子组件排列的顺序按照代码顺序进行,当子组件设置了不同的order的时候,order越小的子组件排列越靠前,不设置order的子组件要比设置order的子组件排列靠前,下面来看个例子

image.png

这里有个长度为12的数组orderValues,数组里面的值从0依次到11,然后在GridRow里面遍历数组并创建个Text去展示值,这里GridCol没有设置order属性,最终Text的排列顺序应该是同orderValues顺序一样的,我们看下

image.png

然后我们给GridCol添加上order属性,每一个GridColorder值就是数组长度减去子项的下标,代码如下

image.png image.png

从效果图上可以看出,设置了order值以后,第一个被创建的Text被排列到了布局的最后一个,最后一个Text排列到了第一个,我们再加点逻辑,比如奇数项不设置order,偶数项的order的计算方式不变

image.png image.png

跟之前说的一样,不设置order的子项还是按照代码顺序排列并且排在设置过order的偶数项前面

列表(List)

列表组件List是比较常用的组件之一,在需要展示比较大的数量的时候,列表组件往往都是首选,ArkUI中的列表组件也跟其他前端框架一样拥有一些相同的特性,下面来逐个了解下

约束与布局

列表组件中的子组件只能是ListItemListItemGroup,前者是展示单个子项布局,后者则是一个ListItem的集合,可以将多个子项合并在一起展示,最常见的就是手机里面的通讯录,下面是一个简单的列表展示数据的代码

image.png image.png

这里有个长度为10的number类型的数组,通过在List里面使用ForEach遍历数组逐个生成ListItem,由于每个子项的高度设置成60vp,而List没有设置高度,所以最终List将所有数据都展示了出来,并且它的高度就是所有子项的高度和,也就是600vp,如果想要让列表里面的子项可以滚动起来,一个方式就是扩大数组的数据量,或者加大子项的高度,让所有子项高度的和大于屏幕,这样List就可以滚动起来,另一个方式就是给List设置一个高度值,这样里面的子项高度和如果大于这个高度值的话,List里面的子项也可以滚动,比如给上面的列表增加个300vp的高度值后,效果如下

image.png 0905aa1.gif

整个列表的高度已经被限制在了300vp内,里面的子项要通过滚动才能查看所有数据,这个就是List设置与不设置高度的区别,其实这里有个用词不当的地方,这里的高度其实应该换成主轴方向的大小,由于List默认主轴方向是垂直方向,所以列表组件默认的主轴方向大小就是它的高度大小,如果我们想要改变列表的主轴方向,可以通过设置listDirection来切换

image.png

如果不设置listDirection,列表的默认主轴属性就是Axis.Vertical,而除了在主轴方向布局之外,List组件还支持在交叉轴方向布局,也就是交叉轴方向上的子项数量可以大于一个,通过lanes函数来设置,lanes函数接收的参数类型有两种,一种是直接传一个number,比如上面的列表我想让它交叉轴方向上展示三个子项,可以这样设置

image.png image.png

一下子List就变成了Grid,其实lanes这个函数拿来适配各种屏幕比较合适,比如说当子项的布局不是很复杂的时候,在宽屏设备上就可以通过设置lanes来让List在交叉轴方向上多显示几个子项,美观还节省空间,lanes函数还可以接收LengthConstrain类型的参数,会设置一个最小交叉轴长度与最大交叉轴长度,用这两个值与List的尺寸一起决定交叉轴上应该展示的列数,比如上面这个例子我们改一下

image.png

设置了最小交叉轴长度为100vp,最大交叉轴长度为200vp,然后由于List没有设置listDirection,所以List的交叉轴方向为水平方向,width150vp作为交叉轴方向上的大小,那么最终List会展示几列呢?我们看下

image.png

最终会展示一列,因为List的最小交叉轴长度为100vpList交叉轴方向上大小为150vp,小于最小交叉轴长度的两倍,所以只能展示一列,如果我们把List交叉轴上的大小改成200vp或者比200vp大,那么列数就会改变了,我们试一下

image.png image.png

可以看到现在列数已经变成两列了,所以LengthConstrain也可以作为适配各种屏幕的一种方式

列表样式

经常在使用列表的时候,避免不了要调整一些列表样式,下面来介绍下ArkUI中List组件是如何设置与调整这些样式的

设置间距

List组件中使用space属性给列表的子项之间添加间距,下面来举个例子,给列表的子项之间添加15vp的间距

image.png image.png

可以看到子项之间立马就出现了15vp大小的间距,非常简单

设置分割线

除了间距之外,分割线也是经常会去设置的,列表组件里提供了divider来设置分割线,其中strokeWidthcolor分别设置分割线的粗细以及颜色,startMarginendMargin分别设置分割线的左右边距,来看下具体使用

image.png

这里给列表设置了分割线,其中分割线的粗细是5vp,色值为红色,左边距为20vp,右边距为10vp,效果如下

image.png

分组列表

在前面有说过,列表组件里面的子项只能是ListItemListItemGroup,而后者就是用来给列表设置分组的,常见的就是我们手机通讯录列表,下面来简单做一个带分组的列表,首先准备好需要展示的数据

image.png

然后通过一次ForEach遍历,创建对应的ListItemGroup,并且给ListItemGroup设置header属性

image.png

注意的是这里的header接收的是一个CustomBuilder类型的参数,也就是不能直接给它这是个字符串,需要传入一个自定义组件,所以我们在build函数外面新建一个@Builder的自定义组件head,这个组件就用来创建一个简单的展示title的布局

image.png

创建好header之后,在ListItemGroup内部在做一次ForEach遍历,将每一个members里面的字符串都显示在对应的分组里面,这里也给每一个分组子项也新建了一个@Builder的自定义组件child

image.png image.png

这样一个分组列表就做好了,效果如下

0906aa1.gif

但是大多数情况下,这样的分组列表都要支持一个功能,那就是header吸顶,而这样的功能在我们这个列表组件中实现起来很容易,使用sticky函数就可以完成了,内部接收参数StickyStyle.Header

image.png 0906aa2.gif

响应滚动位置

在列表的一些业务场景中,有些场景就需要要求能够监听列表的滚动事件来做出相应的操作,最简单的例子,当一个列表开始滚动的时候,底部会出现一个按钮,点击后会让列表重新回到顶部,那么首先第一步就需要监听列表的滚动事件,在List组件中提供了onScrollIndex函数来监听列表的滑动事件,然后会返回当前列表处于最顶部的子项的下标值,具体用法如下

image.png

这是一个简单展示十五条数据的列表,当列表滚动的时候,在onScrollIndex中就会返回最顶部的子项下标值,那么我们就来做个功能,当列表第三项出现在列表的最顶部的时候,屏幕右下方会出现一个按钮,显示“回到顶部”,既然要在屏幕右下方放个按钮,我们就要让List组件外面在套个Stack组件,让List与回到顶部的按钮在Stack里面是同级关系

image.png image.png

接着创建一个@State的状态变量,并在onScrollIndex中随着列表滚动去更新这个状态变量,并且判断,只有当这个变量大于2的时候,回到顶部的按钮才出现,否则就隐藏

image.png 0909aa1.gif

按钮的隐藏出现逻辑完成后,我们就要思考如何点击按钮后让列表回到顶部,这种控制列表滚动位置的事情,我们交给Scroller去做,所以我们再创建一个Scroller并将它作为参数传给List组件

image.png image.png

再给按钮添加点击事件,在事件中只需要调用ScrollerscrolToIndex函数就好了

image.png 0909aa2.gif

媒体查询(mediaquery)

在日常做一些ui方面的适配工作上,总是会根据设备的不同状态展示不一样的ui样式,这些状态比如像是设备分辨率,设备的宽高,以及横竖屏切换之类,特别像是横竖屏切换,展示的布局完全就是两个样子,所以如何正确的去判断设备处于什么状态才能帮助我们有效的去完成适配工作,在鸿蒙当中,会使用媒体查询去做这些事情,简单的说,媒体查询就是通过一些条件语句来让屏幕去判断当前是否符合该条件,是就执行一段逻辑,不是就执行另外一条逻辑,这个条件大致的结构如下

image.png

这里的media-type目前只有一个screen值,所以可写可不写,media-logic-operation是媒体逻辑操作符,使用这些操作符可以将不同的媒体特征组合起来形成更加复杂的条件语句,媒体逻辑操作符有如下几种

  • and:可以将不同的媒体特征以“与”的逻辑连在一起,只有当所有媒体特征都会true,整个媒体查询条件才成立
  • or:将不同的媒体特征以“or”的逻辑连在一起,只要存在一条特征为true,整个查询条件就成立
  • not:取反媒体查询结果,媒体查询结果不成立时返回true,否则返回false
  • only:当整个表达式都匹配时,才会应用选择的样式,可以应用在防止某些较早的版本的浏览器上产生歧义的场景
  • **comma(, **):与上面的or一样

media-feature是媒体特征,像分辨率,屏幕的宽高,横竖屏都属于媒体特征,以下是所有媒体特征以及其解释

  • height:应用页面可绘制区域的高度
  • min-height:应用页面可绘制区域的最小高度
  • max-height:应用页面可绘制区域的最大高度
  • width:应用页面可绘制区域的宽度
  • min-width:应用页面可绘制区域的最小宽度
  • max-width:应用页面可绘制区域的最大宽度
  • resolution:设备的分辨率
  • min-resolution:设备的最小分辨率
  • max-resolution:设备的最大分辨率
  • orientation:屏幕的方向
  • device-height:设备的高度
  • min-device-height:设备的最小高度
  • max-device-height:设备的最大高度
  • device-width:设备的宽度
  • device-type:设备的类型
  • min-device-width:设备的最小宽度
  • max-device-width:设备的最大宽度
  • round-screen:屏幕类型,圆形屏幕为true,非圆形屏幕为false
  • dark-mode:系统为深色模式时为true,否则为false

介绍完这些概念之后,我们就开始动手试试看这个媒体查询究竟怎么使用的,首先是导入这个媒体查询模块,直接import进来

image.png

然后调用matchMediaSync函数去设置媒体查询条件,目前手里只有模拟器,找来找去只能测试个横竖屏切换功能,比如条件为当屏幕为横屏的时候,条件语句就是下面这样

image.png

注意在条件语句里面,每一个媒体特征都必须用括号括起来,多个媒体特征在一起用媒体逻辑操作符连起来,这里只判断是否为横屏,所以只有一个媒体特征,再将这个条件语句塞入到matchMediaSync函数里去,完整的代码如下

image.png

listener就是一个监听器,监听这个查询条件成立的时机,然后还要让listener绑定一个回调函数observeLandscape,这个回调用来处理当媒体条件成立时候做什么事情,不成立时候做什么事情

image.png

绑定的时机就放在aboutToAppear函数中

image.png

主要的代码都写好了,现在可以写个功能验证下,比如界面上有个色块,还有段文字,当屏幕为横屏状态时候,色块显示绿色,文字显示“现在是横屏”,反之色块就显示红色,文字显示“现在是竖屏”,所以我们先创建两个状态变量来记录颜色与文案这两个值

image.png

然后在回调函数observeLandscape里写上各个状态的逻辑

image.png

最后再简单创建两个组件分别展示颜色与文案

image.png

所有代码都写完了,最后看下效果

0912aa1.gif

总结

至此,所有的鸿蒙布局都挨个介绍了一遍,都是些基本用法,当然也遇到些比较难理解的概念,这些还是得需要我们通过多实践多揣摩才能把这些东西都理解透,另外在学习过程中,我们还是可以从鸿蒙这些布局的用法上能看到一些Flutter,Compose和Web的一些影子,尤其是这篇文章里介绍的栅格布局,断点概念完全跟Web一摸一样,所以对于有过声明式UI开发经验的人来说,学鸿蒙的确是会轻松不少