本项目参考codelab上的电子相册项目。该笔记为记录和收集开发过程中出现的疑问及知识点。

使用组件简介及用法

Swiper轮播图组件

Swiper是一个容器组件,如果自身尺寸没有被设置,它会根据子组件大小自动调整自身尺寸。如果开发者给Swiper设置了固定尺寸,那么在轮播过程中,Swiper的尺寸将一直保持设置的固定尺寸。如果未设置固定尺寸,Swiper会根据子组件大小自动调整自身尺寸。

SwiperController
  • SwiperController 是与 Swiper 组件配合使用的控制器,它提供了一组方法来控制 Swiper 组件的滑动行为。

  • 通过 SwiperController,你可以在代码中动态地控制 Swiper 组件,例如切换到特定的页面、触发下一页或上一页的滑动、控制自动播放等。

  • SwiperController 的实例通常作为组件的状态或属性,在 Swiper 组件的上下文中使用,以便在需要时调用其方法来改变 Swiper 的状态。

Scroller
  • Scroller 是一个用于控制滚动行为的控制器,它提供了一组方法来控制滚动视图的滚动位置和行为。

  • 通过 Scroller,你可以实现自定义的滚动逻辑,例如平滑滚动到特定位置、监听滚动事件、处理滚动动画等。

  • Scroller 的实例可以与各种滚动容器组件(如 ScrollViewList 组件)配合使用,以提供更丰富的滚动交互体验。

@ohos.hilog(HiLog日志打印)

导入模块
import hilog from '@ohos.hilog';
日志级别
  • DEBUG:详细的流程记录,有助于分析业务流程和定位问题。

  • INFO:记录业务关键流程节点,用于还原业务的主要运行过程。

  • WARN:记录较为严重的非预期情况,但对用户影响不大。

  • ERROR:记录影响功能正常运行或用户正常使用的错误。

  • FATAL:记录重大致命异常,表明应用即将崩溃,故障无法恢复。

日志打印

使用hilog中的不同方法来打印不同级别的日志。例如,打印一条IINFO级别的日志。

hilog.info(0x0001, "testTag", "This is a log message.");
日志格式化

hilog 支持格式化字符串,允许在日志中插入变量值。例如:

hilog.info(0x0001, "testTag", "Value: %{public}d", 123);

其中{public}字段为标记日志内容可见,同理还有{private}表示隐私内容,在设备上不显示具体值。

默认为{private}

检查日志可打印性

在打印日志之前,可以使用isLoggable方法检查当前环境是否允许打印指定级别的日志。

boolean canLog = hilog.isLoggable(0x0001, "testTag", hilog.LogLevel.INFO);

Grid组件(网格布局组件)

类似于LIst组件,但是多了设置行列数量与占比

参数:

  • rowsTemplate/columnsTemplate:设置行/列数,以及其在网格布局上的占比

  • (rowStart/End)/(columnStart/End):子组件所占行列数,这允许子组件横跨多行或多列。

在该布局下创建组件时可以使用ForEach和GridItem来进行组件的创建渲染。

开发步骤

确定常量——Constants

开发第一步需要我们设定好常量,例如轮换图列表,相册分类,还有一些尺寸相关的常量

要提前导入图片资源!!

//Constants.ets
export default class Constants {

  //轮换图列表
  static readonly BANNER_IMG_LIST: Array<Resource> = [
    $r('app.media.ic_food_0'),
    $r('app.media.ic_flower_0'),
    $r('app.media.ic_emoji_0'),
    $r('app.media.ic_dududa_0'),

  ]

  //食物相册
  static readonly FOOD_LIST: Array<Resource> = [
    $r('app.media.ic_food_0'),
    $r('app.media.ic_food_1'),
    $r('app.media.ic_food_2'),
    $r('app.media.ic_food_3'),
    $r('app.media.ic_food_4'),
    $r('app.media.ic_food_5')
  ]

  //嘟嘟哒嘟嘟哒!相册
  static readonly DUDUDA_LIST: Array<Resource> = [
    $r('app.media.ic_dududa_0'),
    $r('app.media.ic_dududa_1'),
    $r('app.media.ic_dududa_2'),
    $r('app.media.ic_dududa_3'),
    $r('app.media.ic_dududa_4'),
    $r('app.media.ic_dududa_5'),
    $r('app.media.ic_dududa_6'),
    $r('app.media.ic_dududa_7'),
    $r('app.media.ic_dududa_8')
  ]

  //表情包相册
  static readonly EMOJI_LIST: Array<Resource> = [
    $r('app.media.ic_emoji_0'),
    $r('app.media.ic_emoji_1'),
    $r('app.media.ic_emoji_2'),
    $r('app.media.ic_emoji_3'),
    $r('app.media.ic_emoji_4'),
    $r('app.media.ic_emoji_5'),
  ]

  //花相册
  static readonly FLOWER_LIST: Array<Resource> = [
    $r('app.media.ic_flower_0'),
    $r('app.media.ic_flower_1')
  ]

  //索引页
  static readonly IMG_ARR: Resource[][] = [
    [...Constants.FOOD_LIST,...Constants.DUDUDA_LIST,...Constants.EMOJI_LIST,...Constants.FLOWER_LIST],
    [...Constants.DUDUDA_LIST,...Constants.EMOJI_LIST,...Constants.FLOWER_LIST],
    [...Constants.FOOD_LIST,...Constants.EMOJI_LIST,...Constants.FLOWER_LIST],
    [...Constants.FOOD_LIST,...Constants.DUDUDA_LIST,...Constants.FLOWER_LIST]
  ]

  //标题字体粗细
  static readonly TITLE_FONT_WEIGHT: number = 500

  //宽高比
  static readonly BANNER_ASPECT_RATIO: number = 1.5

  //动画持续时间
  static readonly BANNER_ANIMATE_DURATION: number = 300

  //共享延迟
  static readonly SHARE_TRANSITION_DELAY: number = 100

  //宽高比
  static readonly STACK_IMG_RATIO: number = 0.7

  //列表间隔
  static readonly LIST_ITEM_SPACE: number = 2

  //缓存大小
  static readonly CACHE_IMG_SIZE: number = 4

  //缓存列表
  static readonly CACHE_IMG_LIST: string[] = ['', '', '', '']
  //数组中四个元素都被初始化为一个空字符串

  //标题
  static readonly PAGE_TITLE: string = '电子相册'

  //页面路由参数
  static readonly PARAM_PHOTO_ARR_KEY: string = 'photoArr'

  //Grid 列布局模板
  static readonly GRID_COLUMNS_TEMPLATE: string = '1fr 1fr 1fr 1fr'
  //这个字符串定义了一个包含四列的Grd布局,创建四个等宽的列

  //索引页列布局模板
  static readonly INDEX_COLUMNS_TEMPLATE: string = '1fr 1fr'

  //百分比
  static readonly FULL_PERCENT: string = '100%'

  //图片百分比
  static readonly PHOTO_ITEM_PERCENT: string = '90%'

  //显示计数
  static readonly SHOW_COUNT: number = 8

  //标准宽度
  static readonly DEFAULT_WIDTH: number = 360

  //内边距
  static readonly PHOTO_ITEM_PADDING: number = 8

  //偏移量
  static readonly PHOTO_ITEM_OFFSET: number = 16

  //透明度偏移值
  static readonly ITEM_OPACITY_OFFSET: number = 0.2

  //双倍数字
  static readonly DOUBLE_NUMBER: number = 2

  //ListPage索引
  static readonly URL_LIST_PAGE: string = 'pages/ListPage'

  static readonly URL_DETAIL_LIST_PAGE: string = 'pages/DetailListPage'

  static readonly URL_DETAIL_PAGE: string = 'pages/DetailPage'

  //报错
  static readonly TAG_INDEX_PAGE: string = 'IndexPage push error '

  static readonly TAG_LIST_PAGE: string = 'ListPage push error '

  static readonly TAG_DETAIL_PAGE: string = 'DetailListPage push error '



}

编写日志工具——Logger

这里我们需要用到官方库里面的hilog

//Logger.ets
//日志记录工具
import hilog from '@ohos.hilog';

const LOGGER_PREFIX: string = 'Electronic Album'
//标识日志来源

//封装hilog中的日志记录功能
class Logger {
  private domain: number = 0   //服务域(0xFFFFF),用于区分不同的服务或者模块
  private prefix: string = ''   //标识日志标签(log tag)

  //日志格式
  private format: string = '%{public}s,%{public}s'

  //设立构造函数
  constructor(prefix: string = '',domain: number = 0xFF00) {
    this.prefix = prefix
    this.domain = domain
  }

  //定义格式化日志方法
  //这些方法接受可变参数args
  debug(...args: Object[]):void {
    hilog.debug(this.domain,this.prefix,this.format)
  }

  info(...args: Object[]):void {
    hilog.info(this.domain,this.prefix,this.format)
  }

  warn(...args: Object[]):void {
    hilog.warn(this.domain,this.prefix,this.format)
  }

  error(...args: Object[]):void {
    hilog.error(this.domain,this.prefix,this.format)
  }
}

export default new Logger(LOGGER_PREFIX)    //导出日志编辑工具

总之还是要会看日志啊

虽然不摆在明面上但还是要好好写的。

定义自定义组件——图片格式(PhotoItem)

import Constants from '../common/constants/Constants'

@Component
export default struct PhotoItem {
  photoArr: Array<Resource> = []

  @State currentIndex: number = 0

  private showCount: number = Constants.SHOW_COUNT / Constants.DOUBLE_NUMBER

  @Builder   //制定图片格式
  albumPicBuilder(img: Resource, index: number) {
    Column() {
      Image(img)
        .width(Constants.FULL_PERCENT)
        .height(Constants.FULL_PERCENT)
        .borderRadius($r('app.float.img_border_radius'))
        .opacity(1-(this.showCount-index-1)*Constants.ITEM_OPACITY_OFFSET)    //制定透明度按顺序递减
    }
    .padding((this.showCount-index-1) * Constants.PHOTO_ITEM_PADDING )         //制定内边距按顺序递减
    .offset({
      y: (this.showCount - index - 1) * (Constants.PHOTO_ITEM_OFFSET)          //制定位置偏移量按顺序递减
    })
    .height(Constants.PHOTO_ITEM_PERCENT)      //这个height是图片组件整体的高度
    .width(Constants.FULL_PERCENT)
  }

  build() {
    Stack({alignContent:Alignment.Top})        //设置堆叠模式
    {
      ForEach(Constants.CACHE_IMG_LIST,(img: string,index: number)=>{
        this.albumPicBuilder(this.photoArr[this.showCount- (index || 0) -1],index) //调用函数进行图片渲染
      },
        (item: string, index: number)=> JSON.stringify(item) + index)
      //这个回调函数用于生成一个唯一的键值,这个键值由图片资源的字符串表示和索引index组成。这个键值用于优化列表渲染性能,确保列表中的每个项都有一个唯一的标识符
    }
    .width(Constants.FULL_PERCENT)
    .height(Constants.FULL_PERCENT)
  }
}

为什么使用 JSON.stringify

使用 JSON.stringify 可以确保不同元素的键值是唯一的,因为如果数组中的元素是对象或数组,直接使用对象或数组作为键可能会导致问题,因为对象和数组的引用在 JavaScript 中是相等的,而不是它们的内容。通过将元素转换为 JSON 字符串,可以确保即使元素的内容不同,它们的键值也会不同。

设置堆叠模式和图片格式是在干什么?我怎么看不懂?

堆叠模式算是对图片展示的一种美化,具体效果如图

  • 由上到下透明度按规定值递减

  • 由上到下逐渐变小

  • 由上到下位置逐渐向下偏移。

所以之前我们在Constants文件中设置的常量就派上了用场,这些常量能够使图片组进行有序而简易的递减变化效果。配合堆叠组件就使图片组呈现一个铺展开的效果。

好用!

相册主页面——IndexPage

主页面包括轮播图(Swiper)部分和小相册预览(Grid布局)组成,其中在小相册预览部分用到了上面提到的PhotoItem图片格式

import router from '@ohos.router';
import Constants from '../common/constants/Constants'
import Logger from '../common/utils/Logger'
import PhotoItem from '../view/PhotoItem'
import { JSON } from '@kit.ArkTS';

//首页面
@Entry
@Component
struct IndexPage {

  swiperController: SwiperController = new SwiperController();
  scroller: Scroller = new Scroller();
  //定义Swiper组件所需控制器

  @State
  currentIndex: number = 0
  @State
  angle: number = 0


  build() {
    Column(){
      Row() {
      //相册标题
        Text($r('app.string.EntryAbility_label'))
          .fontSize($r('app.float.title_font_size'))
          .fontWeight(Constants.TITLE_FONT_WEIGHT)
      }
      .height($r('app.float.navi_bar_height'))
      .width(Constants.FULL_PERCENT)
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Start)
      .padding({
        left: $r('app.float.title_padding')
      })
      .margin({
        top: $r('app.float.grid_padding')
      })

      //设置首页轮播图
      Swiper(this.swiperController) {
        ForEach(Constants.BANNER_IMG_LIST,(item: Resource)=>{
          Row(){
            Image(item)
              .width(Constants.FULL_PERCENT)
              .height(Constants.FULL_PERCENT)
          }
          .width(Constants.FULL_PERCENT)
          .aspectRatio(Constants.BANNER_ASPECT_RATIO)
        },
          (item: Resource,index: number)=> JSON.stringify(item) +index)
      }
      //Swiper组件的属性
      .autoPlay(true)    //是否自动轮播
      .loop(true)        //是否循环
      .margin($r('app.float.grid_padding'))
      .borderRadius($r('app.float.img_border_radius'))
      .clip(true)        //超过组件的子组件将被剪辑(确保子组件不会超出容器的范围)
      .duration(Constants.BANNER_ASPECT_RATIO)     //组件切换的动画时长
      .indicator(false)       //是否显示导航点


      //网格布局
      Grid(){
        ForEach(Constants.IMG_ARR,(photoArr: Array<Resource>)=>{
          GridItem(){
            PhotoItem({photoArr})     //使用PhotoItem图片堆叠模式
          }
          .width(Constants.FULL_PERCENT)
          .aspectRatio(Constants.STACK_IMG_RATIO)     //设置宽高比
          .onClick(()=>{   //点击跳转页面
            router.pushUrl({
              url: Constants.URL_LIST_PAGE,
              params: {photoArr: photoArr}
            }).catch((error:Error) => {
            //日志工具打印错误 
              Logger.error(Constants.TAG_INDEX_PAGE, JSON.stringify(error));
            })
          })
        },
        //编写键生成函数,确保每个项都有一个唯一的标识符
          (item:Array<Resource>,index: number)=> JSON.stringify(item) + index)
      }
      //网格布局属性
      .columnsTemplate(Constants.INDEX_COLUMNS_TEMPLATE)  //网格布局的列(列数列宽)
      .columnsGap($r('app.float.grid_padding'))  //网格布局列/行间距
      .rowsGap($r('app.float.grid_padding'))  
      .padding({
        left: $r('app.float.grid_padding'),
        right: $r('app.float.grid_padding')
      })
      .width(Constants.FULL_PERCENT)
      .layoutWeight(1)       //自适应布局,括号内数字表示权重

    }
    .width(Constants.FULL_PERCENT)
    .height(Constants.FULL_PERCENT)
  }
}

上述代码中在进行ForEach循环渲染中出现了键生成函数,这部分必须要写吗?

在回答这个问题前,我们要先回顾一下ForEach的三个参数

  • 回调函数(必选):通常为一个函数,它定义了对集合中每个元素执行的操作。这个函数会被调用多次,每次传递集合中的一个元素作为参数。

  • 键生成函数(可选):这个函数用于为每个元素生成一个唯一的键(key)。这个键用于帮助框架识别哪些元素是新的,被移动的或被删除的,特别是在动态列表中,这有助于优化渲染性能。

  • 初始化函数或额外配置(可选):可以用于设置循环的初始状态或提供额外的上下文信息

因为键生成函数和初始化函数或额外配置为可选参数,所以在之前的学习中并没有多加注意。这里我们就先了解本篇笔记中提到的键生成函数。

键生成函数

在ForEach循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。键值的生成规则由开发者定义,可以使用默认规则或自定义规则

//默认规则
(item: T, index: number) => { return index + '__' + JSON.stringify(item) }
//本项目中
(item:Array<Resource>,index: number)=> JSON.stringify(item) + index)

不管是什么规则,重点都是indexJSON.stringify(item)

那么键值表示唯一性主要起到一个什么作用呢?

在ForEach首次渲染时,键生成函数通过键值生成规则为数据源的每个数组生成唯一键值,并创建相应的组件。

非首次渲染中,ForEach会检查新生成的键值是否在上次渲染中已经存在。若不存在则会创建一个新的组件,反之则不会创建新组件,而是直接渲染该键值所对应的组件。

比如:

@Entry
@Component
struct Parent {
  @State simpleList: Array<string> = ['one', 'two', 'two', 'three']; //这里有两个two,重复了

  build() {
    Row() {
      Column() {
        ForEach(this.simpleList, (item: string) => {
          ChildItem({ item: item })
        }, (item: string) => item)
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
    .backgroundColor(0xF1F3F5)
  }
}

@Component
struct ChildItem {
  @Prop item: string;

  build() {
    Text(this.item)
      .fontSize(50)
  }
}

这段代码中的键生成函数简单地使用数据元素的值作为键值,所以two的键值就是two,所以渲染到第二个two时,因为该键值已存在于上次渲染中,所以不会创建新组件,而是直接复用已有的组件。

而在常规模式下

(item:Array<Resource>,index: number)=> JSON.stringify(item) + index)

键值一般由`index`和`JSON.stringify(item)`组成,即使JSON值相同,只要索引值有区别就会创建组件,可以保证不漏组件。

其中`index`为数组中每个元素的索引,在ForEach循环每次迭代中,index都会发生变化(随数组中元素的位置而变化),从而使得不同数组中包含相同的图片资源时也能够创建组件(因为不同数组中元素index值不同)。虽然它看不见但是确实存在()

总之就是这个布局(标题+轮播图+网格展示小相册)

在上述代码中还有onClick事件,用于跳转到图片一览界面

//摘录自上文代码段
.onClick(()=>{
            router.pushUrl({
              url: Constants.URL_LIST_PAGE,
              params: {photoArr: photoArr}
            }).catch((error:Error) => {
              Logger.error(Constants.TAG_INDEX_PAGE, JSON.stringify(error));
            })
          })

router跳转组件,很常用,里面的参数url使用了之前写过的预设,而传递参数params则使用了对应组件的中的photoArr。

图片列表页面——ListPage

在相册主页面点击小相册即可跳转到对应相册的图片详情界面

因为在常量(Constants)部分二维数组内容实质为为相同数组的不同组合方式,所以点开相册仅有排列顺序的区别

import router from '@ohos.router';
import Constants from '../common/constants/Constants'
import Logger from  '../common/utils/Logger'
import { JSON } from '@kit.ArkTS';

@Entry
@Component
struct ListPage {
  photoArr: Array<Resource> = (router.getParams() as Record<string,Array<Resource>>)[`${Constants.PARAM_PHOTO_ARR_KEY}`]
  //获取页面路由参数,使用PARAM_PHOTO_ARR_KEY作为键来获取对应的值(其实就是主页面传来的photoArr参数)
  @StorageLink('selectedIndex') selectedIndex: number = 0
  //用于存储当前选中图片的索引,后面图片详情界面会使用。
  //@StoragrLink装饰器起到一个实现状态持久化的作用。


  build() {
    Navigation(){
      Grid(){
      //使用网格布局排列图片
        ForEach(this.photoArr,(img:Resource,index: number)=>{
          GridItem() {
            Image(img)
              .width(Constants.FULL_PERCENT)
              .height(Constants.FULL_PERCENT)
              .objectFit(ImageFit.Cover)     //图片自适应
              //点击事件,实现到图片详情页面的跳转
              .onClick(()=>{
                this.selectedIndex = index
                router.pushUrl({
                  url: Constants.URL_DETAIL_LIST_PAGE,
                  params: {
                    photoArr: this.photoArr
                  }
                }).catch((error: Error)=>{
                  Logger.error(Constants.TAG_LIST_PAGE,JSON.stringify(error))
                })
              })
          }
          .width(Constants.FULL_PERCENT)
          .aspectRatio(1)   //设置宽高比

        }, (item: Resource) => JSON.stringify(item))
      }
      .columnsTemplate(Constants.GRID_COLUMNS_TEMPLATE)
      .rowsGap(Constants.LIST_ITEM_SPACE)
      .layoutWeight(1)
    }
    //导航栏参数
    .title(Constants.PAGE_TITLE)
    .hideBackButton(false)
    .titleMode(NavigationTitleMode.Mini)
  }

} 

这部分布局的实现采用的Navigation导航栏布局。

总之就是这样的效果。

感觉这部分没啥可说的,先这样吧()

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐