鸿蒙应用开发-初见:ArkUI

2024-06-15 21:49:22 浏览数 (2)

编程范式:命令式->声明式

以一个卡片的实现做下讲解

命令式

简单讲就是需要开发用代码一步一步进行布局,这个过程需要开发全程参与。

  • Objective-C
代码语言:ts复制
UIView *cardView = [[UIView alloc] init];
cardView.backgroundColor = [UIColor whiteColor];
cardView.layer.cornerRadius = 16;
cardView.clipsToBounds = YES;
[self.view addSubview:cardView];
[cardView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.mas_equalTo(16);
    make.right.mas_offset(-16);
    make.height.mas_equalTo(116);
    make.top.mas_equalTo(100);
}];
    
NSString *imgUrl = @"https://ke-image.ljcdn.com//110000-inspection//pc1_nBllrJgGj_1.jpg.280x210.jpg";
UIImageView *imgView = [[UIImageView alloc] init];
imgView.backgroundColor = [UIColor lightGrayColor];
[imgView sd_setImageWithURL:[NSURL URLWithString:imgUrl]];
[cardView addSubview:imgView];
[imgView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.bottom.mas_offset(0);
    make.left.mas_equalTo(0);
    make.width.mas_equalTo(107);
}];

UILabel *titleLbl = [[UILabel alloc] init];
titleLbl.font = [UIFont systemFontOfSize:14 weight:UIFontWeightBold];
titleLbl.textColor = [UIColor blackColor];
titleLbl.text = @"万柳书院新一区 南北向满五唯一";
[cardView addSubview:titleLbl];
[titleLbl mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.mas_equalTo(imgView.mas_right).mas_offset(12);
    make.right.mas_offset(-12);
    make.top.mas_equalTo(16);
}];

UILabel *subTitleLbl = [[UILabel alloc] init];
subTitleLbl.textColor = [UIColor blackColor];
subTitleLbl.font = [UIFont systemFontOfSize:12 weight:UIFontWeightRegular];
subTitleLbl.text = @"4室2厅/278.35㎡/南 北/万柳书院";
[cardView addSubview:subTitleLbl];
[subTitleLbl mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.right.mas_equalTo(titleLbl);
    make.top.mas_equalTo(titleLbl.mas_bottom).mas_offset(8);
}];

UILabel *priceLbl = [[UILabel alloc] init];
priceLbl.font = [UIFont systemFontOfSize:14 weight:UIFontWeightBold];
priceLbl.textColor = [UIColor redColor];
priceLbl.text = @"4238万";
[cardView addSubview:priceLbl];
[priceLbl mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.mas_equalTo(titleLbl);
    make.bottom.mas_offset(-16);
}];

UILabel *avgPriceLbl = [[UILabel alloc] init];
avgPriceLbl.textColor = [UIColor lightGrayColor];
avgPriceLbl.font = [UIFont systemFontOfSize:12 weight:UIFontWeightRegular];
avgPriceLbl.text = @"155,445元/平";
[cardView addSubview:avgPriceLbl];
[avgPriceLbl mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.mas_equalTo(priceLbl.mas_right).mas_offset(2);
    make.right.mas_lessThanOrEqualTo(titleLbl.mas_right);
    make.bottom.mas_equalTo(priceLbl);
}];

声明式

声明式则是由开发使用语言描述UI页面长什么样子,之后全权交给引擎去做

  1. 对页面结构进行大的拆解。比如上面卡片分左右两大部分
  2. 选用合适的容器组件进行页面描述
  3. 针对拆解出来的每个部分重复上面的两步,直到无法拆解只能使用基本组件描述为止

比如上面的卡片可以进行如下的拆分

  1. 整体是一个Row容器,分为左右两大部分,左边是图片,右边是一个Column容器
  2. 右边Column容器又拆分为两大部分,上面是标题和描述,下面是价格。两部分按照space-between布局
  3. 上面的标题和描述作为一个整体,里面拆分成Column的两个组件
  4. 下面价格可以直接使用系统组件Text

ReactNative

代码语言:ts复制
<View
  style={{
    borderRadius: 8,
    marginHorizontal: 16,
    flexDirection: 'row',
    backgroundColor: 'white',
    overflow: 'hidden',
    height: 116,
  }}>
  <Image
    source={{
      uri: 'https://ke-image.ljcdn.com//110000-inspection//pc1_nBllrJgGj_1.jpg.280x210.jpg',
    }}
    style={{width: 107, backgroundColor: '#eee'}}
  />
  <View
    style={{
      marginVertical: 16,
      marginHorizontal: 12,
      flex: 1,
      justifyContent: 'space-between',
    }}>
    <View>
      <Text style={{fontSize: 14, color: '#222', fontWeight: '500'}}>
        万柳书院新一区 南北向满五唯一
      </Text>
      <Text style={{fontSize: 11, color: '#222', marginTop: 8}}>
        4室2厅/278.35㎡/南 北/万柳书院
      </Text>
    </View>
    <View
      style={{flexDirection: 'row', marginTop: 8, alignItems: 'flex-end'}}>
      <Text
        style={{
          fontSize: 17,
          color: '#E62222',
          fontWeight: 'bold',
        }}>
        4238万
      </Text>
      <Text style={{fontSize: 11, color: '#999', marginLeft: 6}}>
        155,445元/平
      </Text>
    </View>
  </View>
</View>

SwiftUI

代码语言:ts复制
HStack(spacing:0) {
    AsyncImage(url: URL(string: "https://ke-image.ljcdn.com//110000-inspection//pc1_nBllrJgGj_1.jpg.280x210.jpg"))
        .frame(width:107)
        .aspectRatio(contentMode: .fill)
        .clipped()

    VStack(alignment: .leading,
           spacing:0) {
        VStack(alignment: .leading,
               spacing:0) {
            Text("万柳书院新一区 南北向满五唯一")
                .lineLimit(1)
                .font(.system(size: 14))
                .foregroundColor(.black)
                .fontWeight(.bold)
            Text("4室2厅/278.35㎡/南 北/万柳书院")
                .lineLimit(1)
                .font(.system(size: 12))
                .foregroundColor(.black)
                .padding(.top, 8)
        }

        Spacer()

        HStack(alignment: .bottom,
               spacing:2) {
            Text("4238万")
                .font(.system(size: 14))
                .foregroundColor(.red)
                .fontWeight(.bold)
            Text("155,445元/平")
                .font(.system(size: 12))
                .foregroundColor(.secondary)
                .padding(.leading, 2)
        }
    }
           .padding(.vertical, 16)
           .padding(.horizontal, 12)

    Spacer()
}
.frame(height: 116)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 8))
.padding(.horizontal, 16)
}

ArkUI

代码语言:ts复制
  Row() {
    Row() {
      Image("https://ke-image.ljcdn.com//110000-inspection//pc1_nBllrJgGj_1.jpg.280x210.jpg")
        .width(107)
        .height("100%")
        .objectFit(ImageFit.Cover)
      Column() {
        Column() {
          Text("柳书院新一区 南北向满五唯一")
            .fontSize(16)
            .fontColor("#222")
            .maxLines(1)
          Text("4室2厅/278.35㎡/南 北/万柳书院")
            .fontSize(14)
            .fontColor("#222")
            .maxLines(1)
            .margin({ top: 8 })
        }
        .alignItems(HorizontalAlign.Start)

        Row() {
          Text("4238万")
            .fontSize(15)
            .fontColor("#E62222")
            .fontWeight(FontWeight.Bold)
          Text("155,445元/平")
            .fontSize(13)
            .fontColor("#222")
            .margin({ left: 2 })
        }
        .justifyContent(FlexAlign.Start)
        .alignItems(VerticalAlign.Bottom)
      }
      .width("100%")
      .height("100%")
      .padding({ top: 16, bottom: 16, left: 12, right: 12 })
      .alignItems(HorizontalAlign.Start)
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .borderRadius(8)
    .margin({ left: 16, right: 16 })
    .backgroundColor(Color.White)
    .justifyContent(FlexAlign.Start)
    .clip(true)
  }
  .height(116)
  .width("100%")

小结

  1. 从上面的例子可以看出来,声明式语法只需要我们描述UI长什么样就行。不需要做太多布局计算的工作,让我们少掉一些头发
  2. ArkUI和SwiftUI的语法最像,甚至它们的状态管理也很像,都是提供了状态绑定和监听机制来更新UI样式

声明式UI布局原理简述

Flutter中Widget的布局原理参考

Flutter中Widget的布局原理如下图所示。想了解更多Flutter的布局原理可以查看 深入理解 Flutter 布局约束

SwiftUI中的View布局原理参考

SwiftUI中的布局原理可以参考下图。想了解细节,可参考 SwiftUI 中布局的工作原理

小结

声明式布局想要布局子视图都会经历由上到下的一个过程,只有知道了子视图的大小之后才能根据对齐方式将子视图放置在准确的位置。

声明式布局几乎都是下面这个套路

  1. 父视图给子视图一个布局约束(作为Root的根视图默认是充满屏幕的,它给子视图的约束就是屏幕大小)
  2. 子视图渲染并将自身大小返回给父视图
  3. 父视图根据子视图的大小和设定的对齐方式计算要放置的位置
  4. 子视图的布局也遵循以上三步进行递归。整个过程是深度优先的

ArkUI

ArkUI官方链接

方舟开发框架(简称ArkUI)是鸿蒙开发的UI框架,提供如下两种开发范式,我们 只学声明式开发范式

  1. 基于ArkTS的声明式开发范式
  2. 兼容JS的类Web开发范式

整体架构图

我们使用ArkTS写完页面描述后,交给语言运行时进行语法解析,再之后由C 编写的后端引擎将UI转换为渲染指令交给渲染引擎绘制到屏幕上

ArkUI语法初见

  1. ArkTS对TypeScript语言进行扩展,提供值类型结构struct。
  2. struct定义自定义组件,必须搭配Component或者CustomDialog使用
  3. ArkUI中组件定义和状态管理都是通过装饰器来做的。TS中的装饰器主要有类装饰器、属性装饰器、方法装饰器以及参数装饰器四种
  4. 事件方法和属性方法只是方法的入参不一样,一个是基本值或者表达式值,一个是函数。
  5. 在TS中函数我们就把函数当成变量来用就行,只不过普通变量是存储一个类型的值,而函数用来存储一个输入到输出的转变过程
  6. 还记得我们上面说的描述UI嘛,在这里就在build函数中描述。框架会自动调用build,不需要我们手动调用

从代码到UI显示的整体渲染流程

ArkUI的渲染分为两大情况

从创建到显示(①~⑤)

① 通过devEco将源码编译成带类型标识的字节码文件,同时携带创建这个结构所需信息的指令流

② 通过跨语言调用生成C 层的Component树。这一步只是把ArkTS描述转变成了使用C 描述

③ 通过Component树生成Element树,Element是Component的实例,用于表示一个具体的组件节点。界面在运行时的树形结构就是通过Element树来维持的,同时自动更新的diff算法也是依赖Element树来减少复杂度的

④ 对于每个可显示的Element都会为其创建对应的RenderNode。RenderNode负责一个节点的显示信息,它形成的Render树维护着整个界面渲染需要用到的信息,包括位置、大小、绘制命令等。后续的布局、绘制都是在Render树上进行的

⑤ 实现真正的渲染并显示绘制结果

按钮点击到更新显示(⑥~⑪)

⑥ 点击事件传递到组件,组件的onClick事件方法被触发执行

⑦ 由于onClick事件方法中@State注解过的变量改变了,相应getter/setter函数会被触发

⑧ 状态管理模块定位出关联的UI组件

⑨ 状态管理模块更新相应的Element树的信息

⑩ 更新相应的UI组件的渲染信息

⑪ 界面显示,与⑤类似

盒子模型

上面我们说的布局原理,子视图上报给父视图自身大小的值是指 组件内容区的大小

ArkUI中常用布局容器

如何选择使用哪种布局

线性布局(Row/Column)

  1. 线性布局的子元素在线性方向上(水平方向和垂直方向)依次排列
  2. 线性布局容器包括 Row] 和 Column 。Column容器内子元素按照垂直方向排列,Row容器内子元素按照水平方向排列
主轴
  1. 线性布局容器在布局方向上的轴线,子元素默认沿主轴排列。
  2. Row容器主轴为横向,Column容器主轴为纵向。通过justifyContent属性设置子元素在容器主轴上的排列方式
  3. 默认相邻子元素是紧贴着的,也可以通过space设置子元素间的间距
Column容器内子元素在主轴上的排列

主轴方向:垂直向下

代码语言:ts复制
Column() {
...
}.justifyContent(FlexAlign.Start)
  1. justifyContent(FlexAlign.Start):元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐
  2. justifyContent(FlexAlign.Center):元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同
  3. justifyContent(FlexAlign.End):元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐
  4. justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐
  5. justifyContent(FlexAlign.SpaceAround):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半
  6. justifyContent(FlexAlign.SpaceEvenly):主轴方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样
Row容器内子元素在主轴上的排列

主轴方向:水平向右

代码语言:ts复制
Row() {
...
}.justifyContent(FlexAlign.Start)
  1. justifyContent(FlexAlign.Start):元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐
  2. justifyContent(FlexAlign.Center):元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同
  3. justifyContent(FlexAlign.End):元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐
  4. justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐
  5. justifyContent(FlexAlign.SpaceAround):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半
  6. justifyContent(FlexAlign.SpaceEvenly):主轴方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样
交叉轴
  1. 垂直于主轴方向的轴线。Row容器交叉轴为纵向,Column容器交叉轴为横向。
  2. 通过alignItems属性设置子元素在交叉轴(排列方向的垂直方向)上的对齐方式
  3. alignSelf属性用于控制单个子元素在容器交叉轴上的对齐方式,其优先级高于alignItems属性,如果设置了alignSelf属性,则在单个子元素上会覆盖alignItems属性
Column容器内子元素在水平方向上的排列
代码语言:ts复制
Column() {
...
}.alignItems(HorizontalAlign.Start)
  1. HorizontalAlign.Start:子元素在水平方向左对齐
  2. HorizontalAlign.Center:子元素在水平方向居中对齐
  3. HorizontalAlign.End:子元素在水平方向右对齐。
Row容器内子元素在垂直方向上的排列
代码语言:ts复制
Row() {
...
}.alignItems(VerticalAlign.Top)
  1. VerticalAlign.Top:子元素在垂直方向顶部对齐
  2. VerticalAlign.Center:子元素在垂直方向居中对齐
  3. VerticalAlign.Bottom:子元素在垂直方向底部对齐

层叠布局(Stack)

  1. 层叠布局主要用于实现基于Z轴的布局,容器中的子元素(子组件)依次入栈,后一个子元素覆盖前一个子元素,子元素可以叠加,也可以通过zIndex设置位置
  2. 可以通过 alignContent参数 实现位置的相对移动
代码语言:ts复制
Stack({ alignContent: Alignment.BottomStart })

弹性布局(Flex)

  1. Row和Column容器只支持单方向的布局。你可以把Flex理解为它俩的升级版,能更灵活的控制布局方向和子元素布局。
  2. 可以设置布局方向,是否自动换行等
弹性布局方向图

Flex({ direction: FlexDirection.Row })

  1. FlexDirection.Row(默认值):主轴为水平方向,子组件从起始端沿着水平方向开始排布
  2. FlexDirection.RowReverse:主轴为水平方向,子组件从终点端沿着FlexDirection. Row相反的方向开始排布
  3. FlexDirection.Column:主轴为垂直方向,子组件从起始端沿着垂直方向开始排布
  4. FlexDirection.ColumnReverse:主轴为垂直方向,子组件从终点端沿着FlexDirection. Column相反的方向开始排布

主轴为水平方向的Flex容器示意图

主轴对齐方式

通过justifyContent参数设置在主轴方向的对齐方式,和Row、Column的主轴对齐方式行为一样

交叉轴对齐方式

可以通过Flex组件的alignItems参数设置子组件在交叉轴的对齐方式,子组件默认使用Flex组件的对齐方式。但也可以通过alignSelf单独设置对齐方式

Flex({ alignItems: ItemAlign.Start })

ItemAlign.Auto:使用Flex容器中默认配置。

ItemAlign.Start:交叉轴方向首部对齐

ItemAlign.Center:交叉轴方向居中对齐

ItemAlign.End:交叉轴方向底部对齐

子组件通过 alignSelf 设置在父容器交叉轴的对齐格式,覆盖Flex布局容器中alignItems配置

相对布局(RelativeContainer)

  1. 相对布局可以让子元素指定兄弟元素或父容器作为锚点,基于锚点做位置布局
  2. 必须为RelativeContainer及其子元素设置ID,用于指定锚点信息。未设置ID的子元素不会显示
  3. RelativeContainer ID为“container”,其余子元素的ID通过id属性设置。
  4. 子元素通过 alignRules 指定相对布局规则

锚点的对齐位置示意图

一个示例

代码语言:ts复制
@Entry
@Component
struct Index {
  build() {
    Row() {
      RelativeContainer() {
        Row()
          .width(100)
          .height(100)
          .backgroundColor('#FF3333')
          .alignRules({
            top: { anchor: '__container__', align: VerticalAlign.Top },  //以父容器为锚点,竖直方向顶头对齐
            middle: { anchor: '__container__', align: HorizontalAlign.Center }  //以父容器为锚点,水平方向居中对齐
          })
          .id('row1')  //设置锚点为row1

        Row() {
          Image($r('app.media.icon'))
        }
        .height(100).width(100)
        .alignRules({
          top: { anchor: 'row1', align: VerticalAlign.Bottom },  //以row1组件为锚点,竖直方向低端对齐
          left: { anchor: 'row1', align: HorizontalAlign.Start }  //以row1组件为锚点,水平方向开头对齐
        })
        .id('row2')  //设置锚点为row2

        Row()
          .width(100)
          .height(100)
          .backgroundColor('#FFCC00')
          .alignRules({
            top: { anchor: 'row2', align: VerticalAlign.Top }
          })
          .id('row3')  //设置锚点为row3

        Row()
          .width(100)
          .height(100)
          .backgroundColor('#FF9966')
          .alignRules({
            top: { anchor: 'row2', align: VerticalAlign.Top },
            left: { anchor: 'row2', align: HorizontalAlign.End },
          })
          .id('row4')  //设置锚点为row4

        Row()
          .width(100)
          .height(100)
          .backgroundColor('#FF66FF')
          .alignRules({
            top: { anchor: 'row2', align: VerticalAlign.Bottom },
            middle: { anchor: 'row2', align: HorizontalAlign.Center }
          })
          .id('row5')  //设置锚点为row5
      }
      .width(300).height(300)
      .border({ width: 2, color: '#6699FF' })
    }
    .height('100%').margin({ left: 30 })
  }
}

创建列表(List)

  1. 列表容器是为了高效处理长列表的容器,能支持横向、竖向滚动,数据分组,分组头悬浮等功能
  2. 列表容器内的所有子元素必须是 ListItemGroup 或ListItem,我们实际的内容是在这俩容器内部的
  3. 创建列表子元素一般使用 ForEach 来减少开发量
List、ListItemGroup和ListItem组件关系
代码语言:ts复制
List() {
    ListItem() {
    }
    ListItem() {
    }
    ListItemGroup() {
    }
    LazyForEach(this.dataSource, item => {
        ListItem() {
            ...
        }
    })
}
// 设置垂直方向
.listDirection(Axis.Vertical)
// 粘性header
.sticky(StickyStyle.Header)
// 两列
.lanes(2)

创建网格(Grid/GridItem)

  1. 网格布局主要用于处理固定行列的UI,也支持动态调整。很类似iOS中的UICollectionView。
  2. Grid 容器的子组件一定是 GridItem
Grid,GridItem关系

容器内每一个条目对应一个GridItem组件

行列数量可配
  1. Grid组件提供了rowsTemplate和columnsTemplate属性用于设置网格布局行列数量与尺寸占比。
  2. rowsTemplate和columnsTemplate属性值是一个由多个空格和'数字 fr'间隔拼接的字符串,fr的个数即网格布局的行或列数,fr前面的数值大小,用于计算该行或列在网格布局宽度上的占比,最终决定该行或列的宽度
代码语言:ts复制
Grid() {
...
}
.rowsTemplate('1fr 1fr 1fr')
.columnsTemplate('1fr 2fr 1fr')
单个网格可以横跨多行或多列

通过设置GridItem的rowStart、rowEnd、columnStart和columnEnd可以实现单个网格横跨多行或多列的场景

代码语言:ts复制
Grid() {
    GridItem() {}
    GridItem() {}
    GridItem() {}
    .columnStart(1)
    .columnEnd(2)

    GridItem() {}
    .rowStart(1)
    .rowEnd(2)
    GridItem() {}
    GridItem() {}
    GridItem() {}
    
    GridItem() {}
    .columnStart(1)
    .columnEnd(3)
}
.rowsTemplate('1fr 1fr 1fr')
.columnsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(8)
.rowsGap(8)

其他常用布局容器和组件

  • 创建轮播(Swiper)实现轮播图功能
  • 栅格布局(GridRow/GridCol)和Grid布局类似,但是可以根据设置的分割点动态显示列数。特别适合做多设备适配布局
  • Badge 实现消息小红点和消息数功能
  • WaterFlow 实现瀑布流功能
  • Video 实现视频播放功能
  • TextTimer 实现倒计时显示功能
  • DataPanel 数据面板组件,使用占比图展示多个数据的占比情况
  • Gauge 以环形图表形式展示数据
  • Marquee 走马灯效果
  • PatternLock 图形密码锁组件
  • Rating 评分组件
  • Stepper 步骤导航器组件,主要用于引导介绍功能

写在最后