【HarmonyOS开发实战】电子相册开发随笔(1)相册主页面,图片列表页面
Swiper是一个容器组件,如果自身尺寸没有被设置,它会根据子组件大小自动调整自身尺寸。如果开发者给Swiper设置了固定尺寸,那么在轮播过程中,Swiper的尺寸将一直保持设置的固定尺寸。如果未设置固定尺寸,Swiper会根据子组件大小自动调整自身尺寸。@Component@Builder //制定图片格式Column() {Image(img)
本项目参考codelab上的电子相册项目。该笔记为记录和收集开发过程中出现的疑问及知识点。
使用组件简介及用法
Swiper轮播图组件
Swiper是一个容器组件,如果自身尺寸没有被设置,它会根据子组件大小自动调整自身尺寸。如果开发者给Swiper设置了固定尺寸,那么在轮播过程中,Swiper的尺寸将一直保持设置的固定尺寸。如果未设置固定尺寸,Swiper会根据子组件大小自动调整自身尺寸。
SwiperController
-
SwiperController是与Swiper组件配合使用的控制器,它提供了一组方法来控制Swiper组件的滑动行为。 -
通过
SwiperController,你可以在代码中动态地控制Swiper组件,例如切换到特定的页面、触发下一页或上一页的滑动、控制自动播放等。 -
SwiperController的实例通常作为组件的状态或属性,在Swiper组件的上下文中使用,以便在需要时调用其方法来改变Swiper的状态。
Scroller
-
Scroller是一个用于控制滚动行为的控制器,它提供了一组方法来控制滚动视图的滚动位置和行为。 -
通过
Scroller,你可以实现自定义的滚动逻辑,例如平滑滚动到特定位置、监听滚动事件、处理滚动动画等。 -
Scroller的实例可以与各种滚动容器组件(如ScrollView或List组件)配合使用,以提供更丰富的滚动交互体验。
@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)
不管是什么规则,重点都是index和JSON.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导航栏布局。


总之就是这样的效果。
感觉这部分没啥可说的,先这样吧()
更多推荐



所有评论(0)