HarmonyOS应用开发03-基础组件-让我们来码出复杂酷炫的UI

本文聚焦HarmonyOS应用开发,介绍使用ArkTS语言开发基本组件的方法。涵盖Column&Row、List、Grid、Tabs、Swiper等组件,包括组件的主轴与交叉轴概念、对齐方式、间距设置、渲染方法、属性配置等内容,助力开发者实现复杂丰富的UI。

系列文章目录

HarmonyOS应用开发01-ArkTS基础知识

HarmonyOS应用开发02-程序框架UIAbility、启动模式与路由跳转

HarmonyOS应用开发03-基础组件-让我们来码出复杂酷炫的UI


文章目录


前言

想要实现一个列表,在前面是使用 ForEach 循环 StudentListItem.est 构建的,不会由滑动效果,数据源增加时,会发现现在这个“列表”显示不全,而且也是无法滑动的。对于数据源数量较多、需要分页加载的列表,我们使用 List去实现。
接下来学习使用HarmonyOS-ArkTS语言开发方式中的基本组件,这样我们就可以使用基本组件去实现较为复杂丰富的UI。


一、Column&Row组件的使用

参考链接:

Column组件的相关API参考:Column组件

Row组件的相关API参考:Row组件

Column表示沿垂直方向布局的容器。Row表示沿水平方向布局的容器。

1、主轴和交叉轴概念

在布局容器中,默认存在两根轴,分别是主轴和交叉轴,这两个轴始终是相互垂直的。不同的容器中主轴的方向不一样的。

  • 主轴:在Column容器中的子组件是按照从上到下的垂直方向布局的,其主轴的方向是垂直方向;在Row容器中的组件是按照从左到右的水平方向布局的,其主轴的方向是水平方向。
  • 交叉轴:与主轴垂直相交的轴线,如果主轴是垂直方向,则交叉轴就是水平方向;如果主轴是水平方向,则交叉轴是垂直方向。

Column容器主轴与交叉轴
Row容器主轴与交叉轴

2、主轴方向的对齐(justifyContent)

子组件在主轴方向上的对齐使用justifyContent属性来设置,其参数类型是FlexAlign。FlexAlign定义了以下几种类型:

(1)、Start:元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。

在这里插入图片描述

(2)、Center:元素在主轴方向中心对齐,第一个元素与行首的距离以及最后一个元素与行尾距离相同。

在这里插入图片描述

(3)、End:元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。

在这里插入图片描述

(4)、SpaceBetween:元素在主轴方向均匀分配弹性元素,相邻元素之间距离相同。 第一个元素与行首对齐,最后一个元素与行尾对齐。

在这里插入图片描述

(5)、SpaceAround:元素在主轴方向均匀分配弹性元素,相邻元素之间距离相同。 第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。

在这里插入图片描述

(6)、SpaceEvenly:元素在主轴方向等间距布局,无论是相邻元素还是边界元素到容器的间距都一样。

在这里插入图片描述

3、交叉轴方向的对齐(alignItems)

子组件在交叉轴方向上的对齐方式使用alignItems属性来设置。

(1)、Column容器的主轴是垂直方向,交叉轴是水平方向,其参数类型为HorizontalAlign(水平对齐),HorizontalAlign定义了以下几种类型:

  • Start:设置子组件在水平方向上按照起始端对齐。

在这里插入图片描述

  • Center(默认值):设置子组件在水平方向上居中对齐。

在这里插入图片描述

  • End:设置子组件在水平方向上按照末端对齐。

在这里插入图片描述

(2)、Row容器的主轴是水平方向,交叉轴是垂直方向,其参数类型为VerticalAlign(垂直对齐),VerticalAlign定义了以下几种类型:

  • Top:设置子组件在垂直方向上居顶部对齐。

在这里插入图片描述

  • Center(默认值):设置子组件在竖直方向上居中对齐。

在这里插入图片描述

  • Bottom:设置子组件在竖直方向上居底部对齐。

在这里插入图片描述

4、主轴方向的间距

Column和Row容器的接口都有一个可选参数space,表示子组件在主轴方向上的间距。

容器组件接口
ColumnColumn(value?:{space?: string
RowRow(value?:{space?: string

在这里插入图片描述

二、列表List组件与Grid网格组件

参考链接:

List组件的相关API参考:List组件
Grid组件的相关API参考:Grid组件
循环渲染(ForEach):循环渲染

在上文Demo中增加 getStudentList2 返回数据数量:

import { DataItemBean } from './DataItemBean';

export class DataModel {
  getStudentList2(): Array<DataItemBean> {
    let studentList: DataItemBean[] = [
      {
        "title": "丁程鑫",
        "image": "https://c-ssl.duitang.com/uploads/item/201805/06/20180506084619_2svWA.jpeg",
      },
      {
        "title": "贺峻霖",
        "image": "https://c-ssl.duitang.com/uploads/blog/202107/05/20210705215458_36c2b.jpg",
      },
      {
        "title": "肖战",
        "image": "https://c-ssl.duitang.com/uploads/blog/202112/17/20211217211133_85430.jpeg",
      },
      
      ...
      
      {
        "title": "丁程鑫2",
        "image": "https://c-ssl.duitang.com/uploads/item/201805/06/20180506084619_2svWA.jpeg",
      },
      {
        "title": "贺峻霖2",
        "image": "https://c-ssl.duitang.com/uploads/blog/202107/05/20210705215458_36c2b.jpg",
      },
      {
        "title": "肖战2",
        "image": "https://c-ssl.duitang.com/uploads/blog/202112/17/20211217211133_85430.jpeg",
      },
      
      ...
      
    ];
    return studentList;
  }
}

export default new DataModel();

再次运行APP,会发现数据充满整个屏幕,而且是不能进行滑动的,数据显示不全。要想可以滑动列表显示全部数据,这时就需要用到现行列表组件。常见的列表有线性列表(List列表)和网格布局(Grid列表)。

1、List组件

(1)、List是很常用的滚动类容器组件,一般和子组件ListItem一起使用,List列表中的每一个列表项对应一个ListItem组件;

(2)、使用ForEeach渲染列表

列表往往由多个列表项组成,所以我们需要在List组件中使用多个ListItem组件来构建列表,这就会导致代码的冗余。使用循环渲染(ForEach)遍历数组的方式构建列表,可以减少重复代码

List组件

对比代码:

StudentListPage.ets 代码中:

ForEach(this.studentList2, (item: DataItemBean) => {
     StudentListItem({ studentData: item })
}, (item: string) => JSON.stringify(item))

List组件:

// 列表
List({ space: 16 }) {
	ForEach(this.studentList2, (item: DataItemBean) => {
          ListItem() {
            StudentListItem({ studentData: item })
    }
   }, (item: string) => JSON.stringify(item))
}
.width('100%')
.height('50%')

运行代码,即可滑动显示所有数据。

(3)、设置列表分割线

List组件子组件ListItem之间默认是没有分割线的,部分场景子组件ListItem间需要设置分割线,这时候您可以使用List组件的divider属性。divider属性包含四个参数:

  • strokeWidth: 分割线的线宽。
  • color: 分割线的颜色。
  • startMargin:分割线距离列表侧边起始端的距离。
  • endMargin: 分割线距离列表侧边结束端的距离。
    在这里插入图片描述
代码实现:
List({ space: 16 }) {
   ForEach(this.studentList2, (item: DataItemBean) => {
       ListItem() {
          StudentListItem({ studentData: item })
       }
   }, (item: string) => JSON.stringify(item))
}
.width('100%')
.height('50%')
.divider({ strokeWidth: 3, color: Color.Gray, startMargin: 30, endMargin: 0 })

在这里插入图片描述

(4)、设置List排列方向

List组件里面的列表项默认是按垂直方向排列的,如果您想让列表沿水平方向排列,您可以将List组件的listDirection属性设置为Axis.Horizontal。

listDirection参数类型是Axis,定义了以下两种类型:

  • Vertical(默认值):子组件ListItem在List容器组件中呈纵向排列。listDirection(Axis.Vertical)
  • Horizontal:子组件ListItem在List容器组件中呈横向排列。listDirection(Axis.Horizontal)

在这里插入图片描述
List组件的相关API参考:List组件

2、Grid组件

(1)、Grid组件一般和子组件GridItem一起使用,Grid列表中的每一个条目对应一个GridItem组件。

(2)、使用ForEach渲染网格布局

使用ForEach渲染网格布局

  • columnsTemplate : 设置当前网格布局列的数量,不设置时默认1列。设置columnsTemplate的值为’1fr 1fr 1fr 1fr’,表示这个网格为4列,将Grid允许的宽分为4等分,每列占1份;
  • rowsTemplate :设置当前网格布局行的数量,不设置时默认1行。rowsTemplate的值为’1fr 1fr 1fr 1fr’,表示这个网格为4行,将Grid允许的高分为4等分,每行占1份。
  • columnsGap :设置列与列的间距。eg:使用columnsGap设置列间距为10vp.
  • rowsGap :设置行与行的间距。eg:使用rowsTemplate设置行间距也为10vp。

在这里插入图片描述

(3)、设置Grid排列方向

Grid组件使用 layoutDirection 设置布局的主轴方向。默认值:GridDirection.Row

注意:

1、网格布局如果使用了固定的行数和列数,则构建出的网格是不可滚动的。
2、有时候因为内容较多,需要通过滚动的方式来显示更多的内容,就需要一个可以滚动的网格布局。只需要设置rowsTemplate和columnsTemplate中的一个即可。

Grid组件的相关API参考:Grid组件

代码示例

(1)、在 StudentListItem.ets 代码中修改Item布局:

import router from '@ohos.router';
import CommonConstants from '../common/constants/CommonConstants';
import { DataItemBean } from '../viewmodel/DataItemBean';

@Component
export default struct StudentListItem {
  @State isChecked: boolean = false;

  private studentData?: DataItemBean;

  aboutToAppear() {
    console.log("DataItemBean", this.studentData.title)
    console.log("DataItemBean", this.studentData.image)
  }

  @Builder checkIcon(icon: Resource) {
    Image(icon)
      .objectFit(ImageFit.Contain)
      .width($r('app.float.checkbox_width'))
      .height($r('app.float.checkbox_height'))
      .margin($r('app.float.checkbox_margin'))
  }

  build() {
    Column() {
        Row() {
          Text(this.studentData.title)
            .fontColor(this.isChecked ? Color.Red : Color.Black)
            .fontSize(this.isChecked ? $r('app.float.item_checked_font_size') : $r('app.float.item_font_size'))
            .fontWeight(500)
            .opacity(this.isChecked ? 0.5 : 1.0)
            .decoration({ type: this.isChecked ? TextDecorationType.LineThrough : TextDecorationType.None })

          Blank()

          Image($r('app.media.ic_arrow_next'))
            .width('30vp')
            .height('30vp')
            .onClick(() => {
              // console.log('Next Click' + this.name);
              console.log('Next Click' + this.studentData.title);
              console.log('Next Click' + this.studentData.image);

              router.pushUrl({
                // url: 'pages/StudentDetailPage',
                url: CommonConstants.STUDENT_DETAIL_URL,
                params: {
                  // name: this.name,
                  studentData: this.studentData
                }
              }).catch((error) => {
                console.log('Next Click', 'IndexPage push error' + JSON.stringify(error));
              })
            })
        }
        .width('100%')
        .padding({ left: 10, right: 10 })

        if (this.isChecked) {
          this.checkIcon($r('app.media.ic_checked'))
        } else {
          this.checkIcon($r('app.media.ic_unchecked'))
        }

      }
      .borderRadius(10)
      .backgroundColor($r('app.color.start_window_background'))
      .width('100%')
      .height($r('app.float.grid_item_height'))
      .padding({ top: 10 })
      .justifyContent(FlexAlign.SpaceEvenly)
      .onClick(() => {
        this.isChecked = !this.isChecked;
      })
  }
}

(2)、在 StudentListPage.ets 代码中修改List组件为Grid组件:

import DataModel from '../viewmodel/DataModel';
import StudentListItem from '../view/StudentListItem';
import router from '@ohos.router';
import prompt from '@system.prompt';
import { DataItemBean } from '../viewmodel/DataItemBean';
// import DataItemBean from '../viewmodel/DataItemBean';

const TAG = '[StudentListPage]';

@Entry
@Component
export struct StudentListPage {
  // private studentList: Array<string> = [];
  private studentList2: Array<DataItemBean> = [];
  @State backMessage: string = '';
  @State isRowModel: boolean = true;

  // 调用router.back()方法,不会新建页面,返回的是原来的页面,在原来页面中@State声明的变量不会重复声明,
  // 以及也不会触发页面的aboutToAppear()生命周期回调,因此无法直接在变量声明以及页面的aboutToAppear()
  // 生命周期回调中接收和解析router.back()传递过来的自定义参数。
  onPageShow() {
    this.backMessage = router.getParams()?.['backMessage'];
    console.log(TAG, 'StudentDetailPage返回数据:StudentListPage => ' + this.backMessage)

    if (this.backMessage != undefined && this.backMessage != "") {
      this.showToast(this.backMessage)
    }
  }

  aboutToAppear() {
    // this.studentList = DataModel.getStudentList();
    this.studentList2 = DataModel.getStudentList2();
    // this.backMessage = router.getParams()?.['backMessage'];
  }

  showToast(message: string) {
    prompt.showToast({
      message: message
    })
  }

  build() {
    Navigation() {
      Row() {
        if (this.isRowModel) {
          Grid() {
            ForEach(this.studentList2, (item: DataItemBean) => {
              GridItem() {
                StudentListItem({ studentData: item, isRowModel: false })
              }
            }, (item: string) => JSON.stringify(item))
          }
          // .width('90%')
          .columnsTemplate('1fr 1fr 1fr')
          // .rowsTemplate('1fr 1fr 1fr')
          .columnsGap(10)
          .rowsGap(10)
          // .layoutDirection(GridDirection.Row)
      }
      .width('90%')
      // .margin({ left: 10, right: 10 })
    }
    .title('学生名单')
    .size({ width: '100%', height: '100%' })
    .titleMode(NavigationTitleMode.Mini)
    .hideBackButton(true)
    .menus(this.NavigationMenus())
    .backgroundColor($r('app.color.page_background'))
  }
}
效果图:

在这里插入图片描述

三、Tabs组件

参考链接:

Tabs组件的更多属性和参数的使用,可以参考API:Tabs
@Builder装饰器的使用,可以参考:@Builder

使用Tabs组件来实现类似Android开发中的BottomNavigationBar组件效果、TabIndicator效果;

在这里插入图片描述

1、属性介绍

(1)、通过 TabContent 的属性设 tabBarTabBar 的显示内容。使用通用属性width和height设置了Tabs组件的宽高,使用barWidth和barHeight设置了TabBar的宽度和高度

在这里插入图片描述

(2)、通过 barMode 设置TabBar布局模式

Tabs的布局模式有Fixed(默认)和Scrollable两种:
  • BarMode.Fixed:所有TabBar平均分配barWidth宽度(纵向时平均分配barHeight高度),页签不可滚动;

在这里插入图片描述

  • BarMode.Scrollable:每一个TabBar均使用实际布局宽度,超过总长度(横向Tabs的barWidth,纵向Tabs的barHeight)后可滑动。

在这里插入图片描述

(3)、设置TabBar位置和排列方向

使用Tabs组件接口中的参数barPosition设置页签位置。此外页签显示位置还与vertical属性相关联,vertical属性用于设置页签的排列方向,当vertical的属性值为false(默认值)时页签横向排列,为true时页签纵向排列。
barPosition的值可以设置为BarPosition.Start(默认值)和BarPosition.End:
  • BarPosition.Start

    vertical属性方法设置为false(默认值)时,页签位于容器顶部。
    vertical属性方法设置为true时,页签位于容器左侧。

  • BarPosition.End

    vertical属性方法设置为false时,页签位于容器底部。
    vertical属性方法设置为true时,页签位于容器右侧。

(4)、自定义TabBar样式

TabContent的tabBar属性除了支持string类型,还支持使用@Builder装饰器修饰的函数。您可以使用@Builder装饰器,构造一个生成自定义TabBar样式的函数,实现自定义的底部页签效果。
代码实现:
@State currentIndex: number = CommonConstants.STUDENT_LIST_TAB_INDEX

  // 设置Tabs控制器 Tabs组件的控制器,用于控制Tabs组件进行页签切换。不支持一个TabsController控制多个Tabs组件
  private tabsController: TabsController = new TabsController();

  // TabContent的tabBar属性除了支持string类型,还支持使用@Builder装饰器修饰的函数。
  // 可以使用@Builder装饰器,构造一个生成自定义TabBar样式的函数,实现上面的底部页签效果
  @Builder TabBuilder(title: string, index: number, selectImage: Resource, normalImage: Resource) {
    Column() {
      Image(this.currentIndex === index ? selectImage : normalImage)
        .width($r('app.float.mainPage_baseTab_size'))
        .height($r('app.float.mainPage_baseTab_size'))

      Text(title)
        .margin({ top: $r('app.float.mainPage_baseTab_top') })
        .fontSize(this.currentIndex === index ? $r('app.float.main_tab_selected_fontSize')
                                              : $r('app.float.main_tab_normal_fontSize'))
        .fontColor(this.currentIndex === index ? $r('app.color.mainPage_selected_color')
                                               : $r('app.color.mainPage_normal_color'))
    }
    .justifyContent(FlexAlign.Center)
    .height($r('app.float.mainPage_barHeight'))
    .width(CommonConstants.FULL_WIDTH)
    .onClick(() => {
      this.currentIndex = index;
      // 控制Tabs切换到指定页签
      this.tabsController.changeIndex(this.currentIndex);
    })
  }

2、代码实现

新建 MainPage.ets,初始化TabsController去设置Tabs组件;
import CommonConstants from '../common/constants/CommonConstants'
import GalleryPage from '../view/GalleryPage';
import { StudentListPage } from './StudentListPage';

@Entry
@Component
struct MainPage {
  @State currentIndex: number = CommonConstants.STUDENT_LIST_TAB_INDEX

  // 设置Tabs控制器 Tabs组件的控制器,用于控制Tabs组件进行页签切换。不支持一个TabsController控制多个Tabs组件
  private tabsController: TabsController = new TabsController();

  // TabContent的tabBar属性除了支持string类型,还支持使用@Builder装饰器修饰的函数。
  // 可以使用@Builder装饰器,构造一个生成自定义TabBar样式的函数,实现上面的底部页签效果
  @Builder TabBuilder(title: string, index: number, selectImage: Resource, normalImage: Resource) {
    Column() {
      Image(this.currentIndex === index ? selectImage : normalImage)
        .width($r('app.float.mainPage_baseTab_size'))
        .height($r('app.float.mainPage_baseTab_size'))

      Text(title)
        .margin({ top: $r('app.float.mainPage_baseTab_top') })
        .fontSize(this.currentIndex === index ? $r('app.float.main_tab_selected_fontSize')
                                              : $r('app.float.main_tab_normal_fontSize'))
        .fontColor(this.currentIndex === index ? $r('app.color.mainPage_selected_color')
                                               : $r('app.color.mainPage_normal_color'))
    }
    .justifyContent(FlexAlign.Center)
    .height($r('app.float.mainPage_barHeight'))
    .width(CommonConstants.FULL_WIDTH)
    .onClick(() => {
      this.currentIndex = index;
      // 控制Tabs切换到指定页签
      this.tabsController.changeIndex(this.currentIndex);
    })
  }

  build() {
    Tabs({
      barPosition: BarPosition.End,
      controller: this.tabsController,
    }) {
      TabContent() {
        StudentListPage()
      }
      .tabBar(
        this.TabBuilder(
          CommonConstants.STUDENT_LIST_TITLE, CommonConstants.STUDENT_LIST_TAB_INDEX,
          $r('app.media.ic_home_selected'), $r('app.media.ic_home_normal')
        ))

      TabContent() {
        GalleryPage()
      }
      .tabBar(
        this.TabBuilder(
          CommonConstants.PICTURE_TITLE, CommonConstants.PICTURE_TAB_INDEX,
          $r('app.media.ic_checked'), $r('app.media.ic_unchecked')
        ))
    }
    .vertical(true)
    .scrollable(true) // 设置为true时可以通过滑动页面进行页面切换,为false时不可滑动切换页面。默认值:true
    .width(CommonConstants.FULL_WIDTH) // 设置Tabs组件宽度
    .height(CommonConstants.FULL_HEIGHT) // 设置Tabs组件高度
    .backgroundColor(Color.White) // 设置Tabs组件背景颜色
    .barWidth(CommonConstants.FULL_WIDTH) // 设置TabBar宽度
    .barHeight($r('app.float.mainPage_barHeight')) // 设置TabBar高度
    .barMode(BarMode.Scrollable) // Tabs的布局模式有Fixed(默认)和Scrollable两种
    .onChange((index: number) => {
      // Tabs的布局模式有Fixed(默认)和Scrollable两种
      this.currentIndex = index;
    })
  }
}

其中在 GalleryPage().ets 中先放置一个Text组件显示页面;

@Component
export default struct GalleryPage {
  build() {
    Column() {
      Text('Gallery')
        .fontSize('22fp')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor($r('app.color.page_background'))
  }
}
实现效果:
底部导航栏效果:

设置 Tabs 组件的属性 barPosition: BarPosition.End ,并且设置 .vertical(false),就将页签位于容器底部,设置TabBar宽度为 100%,高度设置为 56vp

@Entry
@Component
struct MainPage {
  
  ...

  build() {
    Tabs({
      barPosition: BarPosition.End,
      controller: this.tabsController,
    }) {
      ...
    }
    .vertical(false)
    .scrollable(true) // 设置为true时可以通过滑动页面进行页面切换,为false时不可滑动切换页面。默认值:true
    .width(CommonConstants.FULL_WIDTH) // 设置Tabs组件宽度
    .height(CommonConstants.FULL_HEIGHT) // 设置Tabs组件高度
    .backgroundColor(Color.White) // 设置Tabs组件背景颜色
    .barWidth(CommonConstants.FULL_WIDTH) // 设置TabBar宽度
    .barHeight($r('app.float.mainPage_barHeight')) // 设置TabBar高度
    .barMode(BarMode.Fixed) // Tabs的布局模式有Fixed(默认)和Scrollable两种
    .onChange((index: number) => {
      // Tabs的布局模式有Fixed(默认)和Scrollable两种
      this.currentIndex = index;
    })
  }
}

在这里插入图片描述

侧边导航栏效果:

设置 Tabs 组件的属性 barPosition: BarPosition.End ,并且设置 .vertical(true),就将页签位于容器底部,设置TabBar宽度为 56vp,高度设置为100%

@Entry
@Component
struct MainPage {
  
  ...

  build() {
    Tabs({
      barPosition: BarPosition.End,
      controller: this.tabsController,
    }) {
      ...
    }
    .vertical(true)
    .scrollable(true) // 设置为true时可以通过滑动页面进行页面切换,为false时不可滑动切换页面。默认值:true
    .width(CommonConstants.FULL_WIDTH) // 设置Tabs组件宽度
    .height(CommonConstants.FULL_HEIGHT) // 设置Tabs组件高度
    .backgroundColor(Color.White) // 设置Tabs组件背景颜色
    .barWidth($r('app.float.mainPage_barHeight')) // 设置TabBar宽度 
    .barHeight(CommonConstants.FULL_WIDTH) // 设置TabBar高度
    .barMode(BarMode.Fixed) // Tabs的布局模式有Fixed(默认)和Scrollable两种
    .onChange((index: number) => {
      // Tabs的布局模式有Fixed(默认)和Scrollable两种
      this.currentIndex = index;
    })
  }
}

在这里插入图片描述

四、Swiper组件

参考链接:

Swiper轮播组件的相关API参考:Swiper组件

滑块视图容器,提供子组件滑动轮播显示的能力

1、属性介绍

(1)、控制器 SwiperController

给组件绑定一个控制器,用来控制组件翻页

  • showNext : 翻至下一页。翻页带动效切换过程,时长通过duration指定。
  • showPrevious : 翻至上一页。翻页带动效切换过程,时长通过duration指定。
  • finishAnimation : 停止播放动画。

(2)、autoPlay

子组件是否自动播放。默认值:false
loop为false时,自动轮播到最后一页时停止轮播。手势切换后不是最后一页时继续播放。

(3)、interval

使用自动播放时播放的时间间隔,单位为毫秒。默认值:3000

(4)、indicator

是否启用导航点指示器。默认值:true

(5)、loop

是否开启循环。默认值:true
设置为true时表示开启循环,在LazyForEach懒循环加载模式下,加载的组件数量建议大于5个。

(6)、duration

子组件切换的动画时长,单位为毫秒。默认值:400

(7)、vertical

是否为纵向滑动。默认值:false

(8)、itemSpace

设置子组件与子组件之间间隙。默认值:0
说明:不支持设置百分比。

(9)、disableSwipe

禁用组件滑动切换功能。默认值:false

(10)、indicatorStyle {}

设置导航点样式:

  • left: 设置导航点距离Swiper组件左边的距离。

  • top: 设置导航点距离Swiper组件顶部的距离。

  • right: 设置导航点距离Swiper组件右边的距离。

  • bottom: 设置导航点距离Swiper组件底部的距离。

  • size: 设置导航点的直径。不支持设置百分比。默认值:6vp。

  • mask: 设置是否显示导航点蒙层样式。

  • color: 设置导航点的颜色。

  • selectedColor: 设置选中的导航点的颜色。

2、事件 onChange

onChange(event: (index: number) => void)
* 当前显示的子组件索引变化时触发该事件,返回值为当前显示的子组件的索引值。
* 说明:Swiper组件结合LazyForEach使用时,不能在onChange事件里触发子页面UI的刷新。
* 返回值:index:number , 代表当前显示元素的索引。

3、代码示例

实现效果:在List组件、Grid组件上面增加Swiper轮播组件,整体滑动:
在这里插入图片描述
在这里插入图片描述

StudentListPage.ets 中修改:

使用@Builder构建Swiper自定义组件:
@Builder SwiperBuilder(studentList: Array<DataItemBean>) {
    Swiper(this.swiperController) {
      ForEach(studentList, (item: DataItemBean) => {
        Image(item.image)
          .borderRadius($r('app.float.home_swiper_borderRadius'))
          .width('100%')
          .height('240vp')
          .objectFit(ImageFit.Fill)
          .onClick(() => {
            router.pushUrl({
              // url: 'pages/StudentDetailPage',
              url: CommonConstants.STUDENT_DETAIL_URL,
              params: {
                studentData: item
              }
            }).catch((error) => {
              console.log('Next Click', 'IndexPage push error' + JSON.stringify(error));
            })
          })
      }, (item: DataItemBean) => JSON.stringify(item))
    }
    .autoPlay(true)
    .indicatorStyle({ mask: true, bottom: '10vp' })
    .margin({ top: $r('app.float.home_swiper_margin'), bottom: $r('app.float.home_swiper_margin') })
  }

(1)、List列表中修改:

嵌入 Scroll 列表中:

import DataModel from '../viewmodel/DataModel';
import StudentListItem from '../view/StudentListItem';
import router from '@ohos.router';
import prompt from '@system.prompt';
import { DataItemBean } from '../viewmodel/DataItemBean';
import CommonConstants from '../common/constants/CommonConstants';

const TAG = '[StudentListPage]';

@Entry
@Component
export struct StudentListPage {
  
  private studentList2: Array<DataItemBean> = [];

 ...

  // Swiper控制器
  private swiperController: SwiperController = new SwiperController();
  
  ...
  
  @Builder NavigationMenus() {
    Row() {
      Toggle({ type: ToggleType.Switch, isOn: true })
        .selectedColor(Color.Red)
        .switchPointColor(Color.White)
        .onChange((isOn: boolean) => {
          // this.isListModel = !this.isListModel
          this.isListModel = isOn
        })
    }
  }

  @Builder SwiperBuilder(studentList: Array<DataItemBean>) {
    Swiper(this.swiperController) {
      ForEach(studentList, (item: DataItemBean) => {
        Image(item.image)
          .borderRadius($r('app.float.home_swiper_borderRadius'))
          .width('100%')
          .height('240vp')
          .objectFit(ImageFit.Fill)
          .onClick(() => {
            router.pushUrl({
              // url: 'pages/StudentDetailPage',
              url: CommonConstants.STUDENT_DETAIL_URL,
              params: {
                studentData: item
              }
            }).catch((error) => {
              console.log('Next Click', 'IndexPage push error' + JSON.stringify(error));
            })
          })
      }, (item: DataItemBean) => JSON.stringify(item))
    }
    .autoPlay(true)
    .indicatorStyle({ mask: true, bottom: '10vp' })
    .margin({ top: $r('app.float.home_swiper_margin'), bottom: $r('app.float.home_swiper_margin') })
  }

  build() {
    Navigation() {
      Row() {
        Scroll() {
            Column() {
              // Swiper组件
              this.SwiperBuilder(this.studentList2)
              // 列表
              // List组件子组件ListItem之间默认是没有分割线的,部分场景子组件ListItem间需要设置分割线,
              // 这时候可以使用List组件的divider属性。divider属性包含四个参数:
              // 1、strokeWidth: 分割线的线宽。
              // 2、color: 分割线的颜色。
              // 3、startMargin:分割线距离列表侧边起始端的距离。
              // 4、endMargin: 分割线距离列表侧边结束端的距离
              List({ space: 16 }) {
                ForEach(this.studentList2, (item: DataItemBean) => {
                  ListItem() {
                    StudentListItem({ studentData: item, isRowModel: true })
                  }
                }, (item: string) => JSON.stringify(item))
              }
              // .width('90%')
              .divider({ strokeWidth: 1, color: Color.Gray, startMargin: 30, endMargin: 0 })
              // .listDirection(Axis.Horizontal)

              Text('---没有更多了---').fontSize('22vp').margin('30vp')
            }
          }
          .scrollBar(BarState.Off)
          .edgeEffect(EdgeEffect.Spring)
      }
      .width('90%')
    }
    .title('学生名单')
    .size({ width: '100%', height: '100%' })
    .titleMode(NavigationTitleMode.Mini)
    .hideBackButton(true)
    .menus(this.NavigationMenus())
    .backgroundColor($r('app.color.page_background'))
  }
}

(1)、Grid列表中修改:

嵌入 Scroll 列表中:

import DataModel from '../viewmodel/DataModel';
import StudentListItem from '../view/StudentListItem';
import router from '@ohos.router';
import prompt from '@system.prompt';
import { DataItemBean } from '../viewmodel/DataItemBean';
import CommonConstants from '../common/constants/CommonConstants';

const TAG = '[StudentListPage]';

@Entry
@Component
export struct StudentListPage {

  private studentList2: Array<DataItemBean> = [];

  ...

  // Swiper控制器
  private swiperController: SwiperController = new SwiperController();

  ...

  build() {
    Navigation() {
      Row() {
        Scroll() {
            Column() {
              // Swiper组件
              this.SwiperBuilder(this.studentList2)

              Grid() {
                ForEach(this.studentList2, (item: DataItemBean) => {
                  GridItem() {
                    StudentListItem({ studentData: item, isRowModel: false })
                  }
                }, (item: string) => JSON.stringify(item))
              }
              .columnsTemplate('1fr 1fr 1fr')
              .rowsTemplate('1fr 1fr 1fr 1fr 1fr')
              .columnsGap('10vp')
              .rowsGap('10vp')
              .height('640vp')
              // .layoutDirection(GridDirection.Row)

              Text('---没有更多了---').fontSize('22vp').margin('30vp')
            }
          }
          .scrollBar(BarState.Off)
          .edgeEffect(EdgeEffect.Spring)
      }
      .width('90%')
      // .margin({ left: 10, right: 10 })
    }
    .title('学生名单')
    .size({ width: '100%', height: '100%' })
    .titleMode(NavigationTitleMode.Mini)
    .hideBackButton(true)
    .menus(this.NavigationMenus())
    .backgroundColor($r('app.color.page_background'))
  }
}

StudentListPage.ets 完整代码:

import DataModel from '../viewmodel/DataModel';
import StudentListItem from '../view/StudentListItem';
import router from '@ohos.router';
import prompt from '@system.prompt';
import { DataItemBean } from '../viewmodel/DataItemBean';
import CommonConstants from '../common/constants/CommonConstants';
// import DataItemBean from '../viewmodel/DataItemBean';

const TAG = '[StudentListPage]';

@Entry
@Component
export struct StudentListPage {
  // private studentList: Array<string> = [];
  private studentList2: Array<DataItemBean> = [];

  // 返回上一层数据
  @State backMessage: string = '';

  // 是否是List组件模式
  @State isListModel: boolean = true;

  // Swiper控制器
  private swiperController: SwiperController = new SwiperController();

  // 调用router.back()方法,不会新建页面,返回的是原来的页面,在原来页面中@State声明的变量不会重复声明,
  // 以及也不会触发页面的aboutToAppear()生命周期回调,因此无法直接在变量声明以及页面的aboutToAppear()
  // 生命周期回调中接收和解析router.back()传递过来的自定义参数。
  onPageShow() {
    this.backMessage = router.getParams()?.['backMessage'];
    console.log(TAG, 'StudentDetailPage返回数据:StudentListPage => ' + this.backMessage)

    if (this.backMessage != undefined && this.backMessage != "") {
      this.showToast(this.backMessage)
    }
  }

  aboutToAppear() {
    // this.studentList = DataModel.getStudentList();
    this.studentList2 = DataModel.getStudentList2();
    // this.backMessage = router.getParams()?.['backMessage'];
  }

  showToast(message: string) {
    prompt.showToast({
      message: message
    })
  }

  @Builder NavigationMenus() {
    Row() {
      Toggle({ type: ToggleType.Switch, isOn: true })
        .selectedColor(Color.Red)
        .switchPointColor(Color.White)
        .onChange((isOn: boolean) => {
          // this.isListModel = !this.isListModel
          this.isListModel = isOn
        })
    }
  }

  @Builder SwiperBuilder(studentList: Array<DataItemBean>) {
    Swiper(this.swiperController) {
      ForEach(studentList, (item: DataItemBean) => {
        Image(item.image)
          .borderRadius($r('app.float.home_swiper_borderRadius'))
          .width('100%')
          .height('240vp')
          .objectFit(ImageFit.Fill)
          .onClick(() => {
            router.pushUrl({
              // url: 'pages/StudentDetailPage',
              url: CommonConstants.STUDENT_DETAIL_URL,
              params: {
                studentData: item
              }
            }).catch((error) => {
              console.log('Next Click', 'IndexPage push error' + JSON.stringify(error));
            })
          })
      }, (item: DataItemBean) => JSON.stringify(item))
    }
    .autoPlay(true)
    .indicatorStyle({ mask: true, bottom: '10vp' })
    .margin({ top: $r('app.float.home_swiper_margin'), bottom: $r('app.float.home_swiper_margin') })
  }

  build() {
    Navigation() {
      Row() {
        if (this.isListModel) {
          Scroll() {
            Column() {
              // Swiper组件
              this.SwiperBuilder(this.studentList2)
              // 列表
              // List组件子组件ListItem之间默认是没有分割线的,部分场景子组件ListItem间需要设置分割线,
              // 这时候可以使用List组件的divider属性。divider属性包含四个参数:
              // 1、strokeWidth: 分割线的线宽。
              // 2、color: 分割线的颜色。
              // 3、startMargin:分割线距离列表侧边起始端的距离。
              // 4、endMargin: 分割线距离列表侧边结束端的距离
              List({ space: 16 }) {
                ForEach(this.studentList2, (item: DataItemBean) => {
                  ListItem() {
                    StudentListItem({ studentData: item, isRowModel: true })
                  }
                }, (item: string) => JSON.stringify(item))
              }
              // .width('90%')
              .divider({ strokeWidth: 1, color: Color.Gray, startMargin: 30, endMargin: 0 })
              // .listDirection(Axis.Horizontal)

              Text('---没有更多了---').fontSize('22vp').margin('30vp')
            }
          }
          .scrollBar(BarState.Off)
          .edgeEffect(EdgeEffect.Spring)
        } else {
          Scroll() {
            Column() {
              // Swiper组件
              this.SwiperBuilder(this.studentList2)

              Grid() {
                ForEach(this.studentList2, (item: DataItemBean) => {
                  GridItem() {
                    StudentListItem({ studentData: item, isRowModel: false })
                  }
                }, (item: string) => JSON.stringify(item))
              }
              .columnsTemplate('1fr 1fr 1fr')
              .rowsTemplate('1fr 1fr 1fr 1fr 1fr')
              .columnsGap('10vp')
              .rowsGap('10vp')
              .height('640vp')
              // .layoutDirection(GridDirection.Row)

              Text('---没有更多了---').fontSize('22vp').margin('30vp')
            }
          }
          .scrollBar(BarState.Off)
          .edgeEffect(EdgeEffect.Spring)
        }
      }
      .width('90%')
      // .margin({ left: 10, right: 10 })
    }
    .title('学生名单')
    .size({ width: '100%', height: '100%' })
    .titleMode(NavigationTitleMode.Mini)
    .hideBackButton(true)
    .menus(this.NavigationMenus())
    .backgroundColor($r('app.color.page_background'))
  }
}



总结

本节梳理了HarmonyOS中使用ArkTs语言开发实现基本组件、容器组件、List组件、Grid组件、轮播组件Swiper以及页签切换组件Tabs等基本组件的使用,实现了 一个可以左右滑动切换视图的 带有顶部轮播Banner的 Star List ,并且通过 Toggle({ type: ToggleType.Switch, isOn: true }) 组件切换 List 视图与 Grid 列表视图功能。

现在我是公司的一名员工,到我分享我的学习心得了,以下是资料,帮我整理一下语言,看一下怎么分享,多写点,别写的一点点 UI 框架简介以及业界发展趋势 UI,即用户界面,主要包含视觉(比如图像、文字、动画等可视化内容)以及交互(比如按钮点击、列表滑动、图片缩放等用户操作)。UI框架,则是为开发UI而提供的基础设施,比如视图布局,UI组件,事件响应机制等。   从操作系统平台支持方式来看,UI框架一般可分为原生UI框架和跨平台UI框架两种。 1. 原生UI框架。一般是指操作系统自带的UI框架,典型的例子包括iOS的UI Kit,Android的View框架等。这些UI框架和操作系统深度绑定,一般只能运行在相应的操作系统上。功能,性能,开发调测等方面和相应的操作系统结合较好。 2. 跨平台UI框架。一般是指可以在不同的平台(OS)上运行的独立的UI框架。典型例子包括HTML5以及基于HTML5延伸来的前端框架React Native, 以及Google 的Flutter等。跨平台UI框架的目标是代只需一次编写,经过少量修改甚至不修改,可以部署到不同的操作系统平台上。当然,实现跨平台也是有代价的,由于不同平台存在差异性(比如UI的呈现方式差异,API差异等等),导致UI框架本身的架构实现,以及和不同平台的融合都有不小的挑战。   从编程方式上来看,UI框架一般可分为命令式UI框架和声明式UI框架两种: 1.命令式UI框架,过程导向——告诉“机器”具体步骤,命令“机器”按照指定步骤去做。比如Android原生UI框架(View框架)或iOS的UIKit,提供了一系列的API让开发者直接操控UI组件-比如定位到某个指定UI组件,进行属性变更等。这种方式的优点是开发者可以控制具体的实现路径,经验丰富的开发者能够写较为高效的实现。不过这种情况下,开发者需了解大量的API细节并指定好具体的执行路径,开发门槛较高。具体的实现效果上,也高度依赖开发者本身的开发技能。另外,由于和具体实现绑定较紧,在跨设备情况下,灵活性和扩展性相对有限。  2.声明式UI框架,结果导向—— 告诉“机器”你需要什么,“机器”负责怎么去做。比如Web前端框架Vue,或iOS的SwiftUI等,框架会根据声明式语法的描述,渲染相应的UI,同时结合相应编程模型,框架会根据数据的变化来自动更新相应的UI。 这种方式的优点是开发者只需描述好结果,相应的实现和优化由框架来处理。另外,由于结果描述和具体实现分离,实现方式相对灵活同时容易扩展。不过这种情况下,对框架的要求较高,需要框架有完备的直观的描述能力并能够针对相应的描述信息实现高效的处理。   UI框架是应用开发的核心组成部分。纵观业界UI框架,其主要发展趋势表现为: 1. 从命令式UI往声明式UI发展 比如iOS中的UIKit到SwiftUI, Android中的View到Jetpack Compose。这样可以实现更加直观便捷的UI开发。 2. UI框架和语言运行时深度融合 SwiftUI,Jetpack Compose, Flutter都利用了各自的语言特性——比如在UI描述方面,SwiftUI中的Swift语言,Jetpack Compose中的Kotlin语言都精简了UI描述语法;在性能方面, Swift通过引入轻量化结构体等语言特性更好的实现内存快速分配和释放,Flutter中Dart语言则在运行时专门针对小对象内存管理做相应优化等。 3.跨平台(OS)能力 跨平台(OS)能力可以让一套代复用到不同的OS上,主要是为了提升开发效率,降低开发成本。不过这里面也有一系列的挑战,比如运行在不同平台上的性能问题,能力和渲染效果的一致性问题等。业界在这方面也是不断的演进,主要有几种方式: 1)JS/Web方案。比如HTML5利用JS/Web的标准化生态,通过相应的Web引擎实现跨平台目标; 2)JS+Native混合方式。比如React Native、Weex等,结合JS桥接到原生UI组件的方式实现了一套应用代能够运行到不同OS上; 3)平台无关的UI自绘制能力+新的语言。比如Flutter,整个UI基于底层画布由框架层来绘制,同时结合Dart语言实现完整的UI框架。Flutter从设计之初就是将跨平台能力作为重要的竞争力去吸引更多的开发者; 另外,有趣的是,部分原生开发框架也开始往跨平台演进。比如,Android原生的开发框架Jetpack Compose也开始将跨OS支持作为其中的目标,计划将Compose拓展到桌面平台,比如Windows,MacOS等。 然而,随着智能设备的普及,多设备场景下,设备的形态差异(屏幕大小、分辨率,形状, 交互模式等),设备的能力差异(从百K级内存到G级内存设备等)以及应用需要在不同设备间协同,这些都对UI框架以及应用开发带来了新的挑战。   为什么要重新设计一个ACE UI框架 ACE全称是Ability Cross-platform Environment (元能力跨平台执行环境)。是华为设计的应用在HarmonyOS上的UI框架。ACE UI框架结合HarmonyOS的基础运行单元Ability,语言和运行时,以及各种平台(OS)能力API等共同构成HarmonyOS应用开发的基础,实现了跨设备分布式调度,以及原子化服务免安装等能力。 ACE提供两种开发语言以供不同开发者进行选择,分别为Java语言和JavaScript语言,其中Java仅支持在内存较大的设备上使用如大屏、手机、平板等设备使用,而JavaScript支持在百K级到G级设备上使用。 目前主流的UI框架都有各自的不足。另外,在多设备场景下,由于不同的设备形态以及设备能力的巨大差异,目前业界还没有任何一个UI框架能够较好的解决相应的问题。 而ACE UI框架的整体设计思路是: 1)建立分层机制,引入高效的UI基础后端,并能够与OS平台解耦,形成一致化的UI体验 2)通过多前端的方式扩展应用生态,并结合声明式UI,在开发效率上持续演进 3)框架层统一结合语言以及运行时,分布式,组件化设计等,围绕跨设备,进一步提升体验 ACE将应用的UI界面进行解析,通过创建后端具体UI组件、进行布局计算、资源加载等处理后生成具体绘制指令,并将绘制命令发送给渲染引擎,渲染引擎将绘制指令转换为具体屏幕像素,最终通过显示设备将应用程序转换为可见的界面效果展示给用户。 ACE UI框架的整体架构如下图所示,主要由前端框架层、桥接层、引擎层和平台抽象层四大部分组成,下面我们一一介绍。 1. 前端框架层 该层主要包括相应的开发范式(比如主流的类Web开发范式),组件/API,以及编程模型MVVM(Model-View-ViewModel)。其中Model是数据模型层,代表了从数据源读取到的数据;View是视图UI层,通过一定的形式把系统中的数据向用户呈现来;ViewModel: 视图模型层,是数据和视图之间的桥梁。它双向绑定了视图和数据,使得数据的变更能够及时在视图上呈现,用户在视图上的修改也能够及时传递给后台数据,从而实现数据驱动的UI自动变更。 开发范式可以扩展,来支持生态发展。不同的开发范式可以统一适配到底层的引擎层 2. 桥接层 该层主要是作为一个中间层,实现前端开发范式到底层引擎(包括UI后端,语言&运行时)的对接。 3. 引擎层 该层主要包含两部分:UI后端引擎和语言执行引擎。 1)由C++构建的UI后端引擎,包括UI组件、布局视图、动画事件、自绘制渲染管线和渲染引擎 。 在渲染方面,我们尽可能把这部分组件设计得小而灵活。这样的设计,为不同前端框架提供灵活的UI能力,这部分通过C++组件组合而成。通过底层组件的按需组合,布局计算和渲染并行化,并结合上层开发范式实现了视图变化最小化的局部更新机制,从而实现高效的UI渲染。 除此之外,引擎层还提供了组件的渲染管线、动画、主题、事件处理等基础能力。目前复用了Flutter引擎提供基础的图形渲染能力、字体管理、文字排版等能力,底层使用Skia或其他图形库实现,并通过OpenGL实现GPU硬件渲染加速。 在多设备UI适配方面,通过多种原子化布局能力(自动折行、隐藏、等比缩放等),多态UI控件(描述统一,表现形式多样),以及统一交互框架(不同的交互方式归一到统一的事件处理)来满足不同设备的形态差异化需求。 另外,引擎层也包含了能力扩展基础设施,来实现自定义组件以及系统API的能力扩展 2)语言&运行时执行引擎。 可根据需要切换到不同的运行时执行引擎,满足不同设备的能力差异化需求。 4. 平台抽象层 该层主要是通过平台抽象,将平台依赖聚焦到底层画布,通用线程以及事件机制等少数必要的接口上,为跨平台打造了相应的基础设施,并能够实现一致化UI渲染体验。 相应的,配套的开发者工具(HUAWEI DevEco Studio)结合ACE UI的跨平台渲染基础设施,以及自适应渲染,可做到和设备比较一致的渲染体验以及多设备上的UI实时预览。 另外,ACE UI框架还设计了可伸缩的架构,即前端框架、语言运行时、UI后端等都做了解耦,可以有不同的实现。这样就具备可部署到百K级内存的轻量级设备的能力,如下所示: 在ACE UI的轻量化实现中,通过前端框架核心下沉C++化,减小JS部分的内存占用,使用C++进行更为严格的内存分配与管理,并且采用更为轻量的JS引擎,UI部分采用轻量的UIKit并结合轻量图形引擎,达到内存非常轻量占用的目标。接口能力保证是全量能力的子集,这样可以保证轻量化设备上可执行的应用,可以在更高等级的设备上执行,而无需重新开发。这也就是采用ACE  JS开发范式的优势所在,采用统一的开发范式进行应用开发后,开发者无需关心具体运行时的前端框架、JS引擎与后端UI组件,根据运行平台不同,采用最佳的模块,保障了应用在不同平台都可具有最佳的运行性能。不过由于轻量级设备上的资源限制, 所支持的API 能力相对有限,但公共部分的API是完全共通的。 综上所述,ACE UI框架具备如下特点:     1)支持主流的语言生态 – JavaScript;     2)支持类Web开发范式, MVVM机制。并在架构上可支持多前端开发范式,进一步简化开发;     3)通过统一的UI后端,实现高性能以及跨平台一致化的渲染体验;     4)通过多态UI、原子化布局、统一交互,以及可伸缩的运行时设计,进一步降低不同设备形态下的UI开发门槛,并能够通过统一的开发范式,实现一套代跨设备部署(覆盖百K级到G级内存设备)。   ACE UI框架渲染流程解析 接下来我们通过一个手机侧ACE JS应用渲染流程的完整流程来介绍ACE UI框架的具体渲染技术。 1. 线程模型 ACE JS应用启动时会创建一系列线程,形成独立的线程模型,以实现高性能的渲染流程。 每个ACE JS应用的进程,包含唯一一个Platform线程和若干后台线程组成的异步任务线程池: Platform线程:当前平台的主线程,也就是应用的主线程,主要负责平台层的交互、应用生命周期以及窗口环境的创建 后台线程池:一系列后台任务,用于一些低优先级的可并行异步任务,如网络请求、Asset资源加载等。除此之外,每个实例还包括一系列专有线程 JS线程:JS前端框架的执行线程,应用的JS逻辑以及应用UI界面的解析构建都在该线程执行 UI线程:引擎的核心线程,组件树的构建以及整个渲染管线的核心逻辑都在该线程:包括渲染树的构建、布局、绘制以及动画调度 GPU线程:现代的渲染引擎,为了充分发挥硬件性能,都支持GPU硬件加速,在该线程上,会通过系统的窗口句柄,创建GPU加速的OpenGL环境,负责将整个渲染树的内容光栅化,直接将每一帧的内容渲染合成到该窗口的Surface上并送显 IO线程:主要为了异步的文件IO读写,同时该线程会创建一个离屏的GL环境,这个环境和 GPU线程的GL环境是同一个共享组,可以共享资源,图片资源解的内容可直接在该线程上传生成GPU纹理,实现更高效的图片渲染 每个线程的作用,在后续的渲染流程中还会进一步提到。 2. 前端脚本解析 ACE UI框架支持不同的开发范式,可以对接到不同的前端框架上。 以类Web开发范式为例,开发者开发的应用,通过开发工具链的编译,会生成引擎可执行的Bundle文件。应用启动时,会将Bundle文件在JS线程上进行加载,并且将该内容作为输入,供JS引擎进行解析执行,最终生成前端组件的结构化描述,并建立数据绑定关系。例如包含若干简单文本的应用会生成类似下图的树形结构,每个组件节点会包含该节点的属性及样式信息。 3. 渲染管线构建 如上图,前端框架的解析后,根据具体的组件规范定义向前端框架对接层请求创建ACE渲染引擎提供的组件。 前端框架对接层通过ACE引擎层提供的Component组件实现前端组件定义的能力。Component是一个由C++实现的UI组件的声明式描述,描述了UI组件的属性及样式,用于生成组件的实体元素。每一个前端组件会对接到一个Composed Component,表示一个组合型的UI组件,通过不同的子Component组合,构造前端对应的Composed组件。每个Composed组件是前后端对接的一个基础的更新单位。 以上面的前端组件树为例,每个节点会使用一组Composed组件进行组合描述,对应关系如下图,该对应关系只是一个示例,实际场景的对应关系可能会更复杂。 有了每个前端节点对应的Component,就形成了一个完成Page的描述结构,通知渲染管线挂载新的页面。 在Page挂载之前,渲染管线已经提前创建了几个关键的核心结构,Element树和Render树: Element树,Element是Component的实例,表示一个具体的组件节点,它形成的Element树负责维持界面在整个运行时的树形结构,方便计算局部更新算法。另外对于一些复杂的组件,在该数据结构上会实现一些对子组件逻辑上的管理。 Render树,对于每个可显示的Element都会为其创建对应的RenderNode,它负责一个节点的显示信息,它形成的Render树维护着整个界面的渲染需要用到的信息,包括它的位置、大小、绘制命令等,界面后续的布局、绘制都是在Render树上进行的。 当应用启动时,最初形成的Element树只有几个基础的几节点,一般包括root、overlay、stage,分别作用如下: RootElement:Element树的根节点,仅仅负责全局背景色的绘制 OverlayElement:一个全局的悬浮层容器,用于弹窗等全局绘制场景的管理 StageElement:一个Stack容器,作为全局的“舞台”,每个加载完成的页面都要挂载到这个“舞台”下,它管理应用的多个页面之间的转场动效等。 在Element树创建的过程中,也会同步的把Render树也创建起来,初始状态如下图: 当前端框架对接层通知渲染管线准备好了页面,在下一个帧同步信号(VSync)到来时,就会在渲染管线上进行页面的挂载,具体流程就是通过Component来实例化生成Element的过程,创建成功的Element同步创建对应的RenderNode:  如上图所示,目标要将整个Page的Component描述挂载到StageElement上,如果当前Stage下还未有任何Element节点,就会递归逐个节点生成Component对应的Element节点。对于组合类型的ComposedElement,则同时会把Element的引用记录到一个Composed Map中,方便后续更新时快速查找。对于可见类型的容器节点或渲染节点,则会创建对应的RenderNode,并挂在Render树上。 当生成了当前页面的Element树和Render树,页面渲染构建的完整过程就结束了。 4. 布局绘制机制 接下来就进入了布局和绘制的阶段,布局和绘制都是在Render树上进行的。每个RenderNode都会实现自己的布局算法和绘制方法。 布局 布局的过程就是通过各类布局的算法计算每个RenderNode在相对空间上的真实大小和位置。 如下图所示,当某个节点的内容发生变化时,就会标记自己为needLayout,并一直向上标记到布局边界(ReLayout Boundary),布局边界是重新布局的一个范围标记,一般情况下,如果一个节点的布局参数信息(LayoutParam)是强约束的,例如它布局期望的最大尺寸和最小尺寸是相同的,那么它就可以作为一个布局边界。布局是个深度优先遍历的过程。从布局边界开始,父节点自顶向下将LayoutParam传给子节点,子节点自底向上据此计算得到尺寸大小和位置。 对于每个节点来说,布局分为三个步骤: ① 当前节点递归调用子节点的layout方法,并传递布局的参数信息(LayoutParam),包含了布局期望的最大尺寸和最小尺寸等; ② 子节点根据布局参数信息,使用自己定义的布局算法来计算自己的尺寸大小; ③ 当前节点获取子节点布局后的大小,再根据自己的布局算法来计算每个子节点的位置信息,并将相对位置设置给子节点保存。 根据上述的流程,一次布局遍历完成后,每个节点的大小和位置就都计算来了,可以进行下一步的绘制。 绘制 同布局一样,绘制也是一个深度遍历的过程,遍历调用每个RenderNode的Paint方法,此时的绘制只是根据布局算来的大小和位置,在当前绘制的上下文记录每个节点的绘制命令。 为什么是记录命令,而不是直接绘制渲染呢?在现代的渲染引擎中,为了充分使用GPU硬件加速的能力,一般都会使用DisplayList的机制,绘制过程中仅仅将绘制的命令记录下来,在GPU渲染的时候统一转成OpenGL的指令执行,能最大限度的提高图形的处理效率。所以在上面提到的绘制上下文中,会提供一个可以记录绘制命令的画布(Canvas)。每一个独立的绘制上下文可以看作是一个图层。 为了提高性能,这里引入了图层(Layer)的概念。通常绘制会将渲染内容分为多个层进行加速。对于会频繁变化的内容,将其单独创建一个图层,那么这个独立图层的频繁刷新就不必导致其他内容重新绘制,从而达到提升性能并减少功耗的效果,同时还可以支持GPU缓存等优化。每个RenderNode都可以决定自己是否需要单独分层。 如下图所示,绘制流程会从需要绘制的节点中,挑选最近的且需要分层的节点开始,自顶向下的执行每个节点的Paint方法。 对每个节点,绘制分为四个步骤:     ① 如果当前节点需要分层,那么需要创建一个新的绘制上下文,并提供可以记录绘制命令的画布;     ② 在当前的画布上记录背景的绘制命令;     ③ 递归调用子节点的绘制方法,记录子节点的绘制命令;     ④ 在当前的画布上记录前景的绘制命令。 一次完整的绘制流程结束后,我们会得到一棵完整的Layer树,Layer树上包含了这一帧完整的绘制信息:包括每一层的位置、transform信息、Clip信息、以及每个元素的绘制命令。下一步就要经过光栅化和合成的过程,将这一帧的内容显示到界面。 5. 光栅化合成机制 在上面的绘制流程结束后,会通知GPU线程开始进行合成的流程。 如上图所示,UI线程(UI Thread)在渲染管线中的输是LayerTree,它相当于一个生产者,将生产的LayerTree添加到渲染队列中。GPU线程(GPU Thread)的合成器(Compositor)相当于消费者,每个新的渲染周期中,合成器会从渲染队列中获取一个LayerTree进行合成消费。 对于需要缓存的Layer,还要执行光栅化生成GPU纹理,所谓光栅化就是将Layer里面记录的命令进行回放,生成每个实体的像素的过程。像素是存储在纹理的图形内存中。 合成器会从系统的窗口中获取当前的Surface,将每个Layer生成的纹理进行合成,最终合成到当前Surface的图形内存(Graphic Buffer)中。这块内存中存储的就是当前帧的渲染结果内容。最终还需要将渲染结果提交到系统合成器中合成显示。系统的合成过程如下图所示: 当GPU线程的合成器完成一帧的合成后,会进行一次SwapBuffer的操作,将生成的Graphic Buffer提交到与系统合成器建立的帧缓冲队列(Buffer Queue)中。系统合成器会从各个生产端获取最新的内容进行最终的合成,以上图为例,系统合成器会将当前应用的内容和系统其它的显示内容,例如System UI的状态栏、导航栏,进行一次合成,最终写入到屏幕对应的帧缓冲区(Frame Buffer)中。液晶屏的驱动就会从缓冲区读取内容进行屏幕的刷新,最终将内容显示到屏幕上。 6. 局部更新机制 经过上面1~5的流程,完成了首次完整的渲染的流程,在后续的运行中,例如用户输入、动画、数据改变都有可能造成页面的刷新,如果只是部分元素发生了变化,并不需要全局的刷新,只需要启动局部更新即可。那么局部更新是怎么做到的?下面我们介绍一下局部 更新的流程。 以上图为例,JS在代中更新了数据,通过数据绑定模块会自动触发前端组件属性的更新,然后通过JS引擎异步发起更新属性的请求。前端组件会根据变更的属性,构建一组新的Composed的补丁(Patch),作为渲染管线更新的输入。 如上图所示,在下一个VSync到来时,渲染管线会在UI线程开始更新的流程。通过Composed补丁的Id,在ComposedMap中查询到对应的ComposedElement在Element树上的位置。通过补丁对Element树进行更新。以ComposedElement为起始,逐层进行对比,如果节点类型一致则直接更新对应属性和对应的RenderNode,如果不一致则重新创建新的Element和RenderNode。并将相关的RenderNode标记为needLayout和needRender。 如上图所示,根据标记需要重新布局和重新渲染的RenderNode,从最近的布局边界和绘制图层进行布局和绘制的流程,生成新的Layer树,只需要重新生成变更RenderNode对应的Layer即可。 如上图所示,接下来,根据刷新后的Layer树作为输入,在GPU线程进行光栅化和合成。对于已经缓存的Layer则不需要重新光栅化,合成器只需要将已缓存的Layer和未缓存或更新的Layer重新合成即可。最终经过系统合成器的合成,就会将新一帧的内容显示。 以上就是一个ACE JS应用的渲染及更新的流程。最后,通过两张流程图回顾一下整体的流程:     ACE UI框架目前的成熟度以及演进 截至目前,ACE UI框架已商用落地了华为运动手表,华为智能手表,华为智慧屏,华为手机,华为平板等一系列产品。使用场景包括日历、行、健身、实用工具等各类应用,手机-设备碰一碰全品类的应用,以及今年六月份发布的HarmonyOS中各类的服务卡片-图库、相机等。另外,在开发调测方面,开发者工具(HUAWEI DevEco Studio)中也集成了ACE UI框架,支持在PC端(MacOS,Windows)上的开发调测,实时预览(包括实时多设备预览,组件级预览,双向预览等),实现了在PC上和设备上一致的渲染体验。  未来,面向开发者的极简开发,面向消费者的流畅酷炫的体验,以及能够高效在不同设备不同平台上部署,ACE UI框架会继续沿着精简开发和高性能两个方面演进,结合语言更进一步简化开发范式,结合运行时在跨语言交互,类型优化等方面进一步增强性能体验,结合分布式能力将编程模型从MVVM演进到分布式MVVM(Distributed Model-View-ViewModel)等。采用类自然语言的声明式UI描述进行界面搭建,编程语言也进一步开放,未来考虑向TS进行演进,从动效、布局和性能方面进一步提升用户使用体验。 当然,应用生态还会涉及更多的方面,比如三方插件的繁荣,跨OS平台的扩展,更具创新的分布式体验等等。ACE UI框架还很年轻,期待和众多开发者一起,重点围绕着多设备组成的超级终端的新兴场景,不断打磨完善,共同构建领先的应用体验和生态!
09-23
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值