
#星光不负 码向未来# 鸿蒙6.0实战:打造一个“左滑上瘾”的电影清单 App 原创
@[toc]
本文目标
哈喽,各位未来的鸿蒙大佬们!欢迎来到我的学习笔记。今天我们要搞个大事——用 ArkTS 做一个酷炫的电影清单。
你以为只是个平平无奇的列表?No no no!我们要实现左滑操作(什么收藏、想看、分享),
还要能一滑到底直接删除!
这篇笔记非常适合像我一样刚上路的小白,咱们不求“精通”,只求“这玩意儿我好像搞明白了!”
准备好了吗?发车!
第一站:“装修”工具准备(导入依赖)
万丈高楼平地起,咱得先把“砖头”和“水泥”搬进来。这里我们导入了鸿蒙官方的 UI 组件,还有一个重量级嘉宾——HdsListItem
。看名字(Hds…)就知道,这可能是华为设计套件(UIDesignKit)里的“精装房”,能帮我们省不少事儿。
import { promptAction, SymbolGlyphModifier, TextModifier } from '@kit.ArkUI';
import { HdsListItem } from '@kit.UIDesignKit';
第二站:搭建“毛坯房”(组件主体)
@Entry
和 @Component
这俩“门牌号”挂上,咱的 HdsMovieListDemo
组件就正式开张了。
这里有两个核心成员:
@State dataSource
: 这位是我们的“数据大管家”,前面挂着@State
装饰器。你得记住,在 ArkTS 里,被@State
标记的变量一旦发生变化(比如你添加或删除了电影),UI 就会“嗖”地一下自动刷新。神奇吧!scroller
: 这位是“电梯管理员”,负责控制列表的滚动。
<!-- end list -->
@Entry
@Component
struct HdsMovieListDemo {
@State dataSource: LazyDataSource<MovieItem> = new LazyDataSource<MovieItem>();
private scroller: Scroller = new Scroller();
build() {
// ... 下面是 build 里的内容 ...
}
}
第三站:“精装修”开始(build
界面)
build()
方法就是我们的“装修图纸”。我们先用一个 Column
把所有东西竖着(垂直)放好。
build() {
Column() {
// 顶部标题
Text('我的观影清单')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#2c3e50')
.margin({ top: 16, bottom: 8, left: 16 })
// 简介文案
Text('左滑试试:收藏 / 想看 / 分享;滑到底可自动删除')
.fontSize(12)
.fontColor('#7f8c8d')
.margin({ left: 16, bottom: 12 })
// ... 列表马上就来 ...
}
.backgroundColor('#F7F9FC') // 给整个页面来个高级灰背景
.width('100%')
.height('100%')
}
第四站:上硬菜!“无限滚动”列表
来了来了,主角登场!List
就是列表本身。
注意看,我们用的是 LazyForEach
,而不是普通的 ForEach
。为啥要“偷懒”(Lazy)?
因为“偷懒”是美德!LazyForEach
非常聪明,它只会创建当前屏幕上看得见的列表项。当你滚动时,它会销毁滚出去的,创建刚要滚进来的。这样就算你有 10000 条电影数据,App 也不会卡成 PPT。
LazyForEach
需要两个参数:
this.dataSource
:我们的数据大管家。(item: MovieItem) => ...
:一个“模板”,告诉它每一项长啥样。(item: MovieItem) => \
${item.id}-${item.rev}``:这是重点!它为每一项生成一个唯一的 Key。
小白避坑指南:这个 Key 极其重要!我们后面修改了 item.rev
(revision,版本号),这个 Key 就会变,LazyForEach
就会知道:“哦!这个 item 被修改了,我必须刷新它!” 否则你改了数据,界面可能傻傻地不动。
List({ space: 10, scroller: this.scroller }) {
LazyForEach(this.dataSource, (item: MovieItem) => {
// ... HdsListItem 马上登场 ...
}, (item: MovieItem) => `${item.id}-${item.rev}`) // 关键的 Key!
}
.margin({ left: 12, right: 12, bottom: 12 })
.width('100%')
.layoutWeight(1) // 让列表撑满剩余空间
第五站:列表的“灵魂”—— HdsListItem
与左滑神技
现在我们来定义列表里每一项具体长啥样。还记得吗?我们请了“精装房”施工队 HdsListItem
。
5.1 列表项的“脸面”(textItem
)
hdsListItemCard
负责定义卡片长相。我们给它塞了 primaryText
(主标题:电影名+年份)和 secondaryText
(副标题:标语+状态)。
注意看副标题里的“三元运算符” (item.fav ? ' · 已收藏' : ''
),这是程序员的“黑话”,意思就是:如果 item.fav
是 true
,就显示“ · 已收藏”,否则就显示“空字符串”。
HdsListItem({
hdsListItemCard: {
textItem: {
primaryText: {
text: `${item.title}(${item.year})`,
modifier: new TextModifier().fontColor('#222').fontSize(16).fontWeight(FontWeight.Medium),
},
secondaryText: {
text: `${item.tagline}${item.fav ? ' · 已收藏' : ''}${item.wish ? ' · 想看' : ''}${item.shared ? ' · 已分享' : ''}`,
modifier: new TextModifier().fontColor('#666').fontSize(12)
}
}
},
// ... 激动人心的滑动操作来了 ...
})
5.2 “左滑三连” (swipeActionOptions
)
这才是精髓!swipeActionOptions
允许我们定义一个“图标数组”(icons)。
我们来看看第一个“收藏”(爱心)按钮:
icon
: 图标样式,根据item.fav
状态切换颜色和大小。backgroundColor
: 背景色,同样根据状态切换。onAction
: 核心中的核心! 当用户点击这个按钮时:item.fav = !item.fav;
:把收藏状态反转(true
变false
,false
变true
)。item.rev++;
:版本号+1。还记得上面那个 Key 吗?就是通知LazyForEach
刷新用的!this.dataSource.reload();
:通知数据大管家:“数据变了,快刷新!”promptAction.openToast(...)
:弹出一个“吐司”(Toast)提示用户。
后面两个“想看”(时钟)和“分享”(分享)按钮,逻辑一模一样,换汤不换药。
swipeActionOptions: {
icons: [
{
icon: new SymbolGlyphModifier($r('sys.symbol.heart')).fontColor([item.fav ? Color.White : Color.Orange]).fontSize(item.fav ? 18 : 16),
backgroundColor: item.fav ? Color.Orange : Color.Grey,
onAction: () => {
item.fav = !item.fav;
item.rev++;
this.dataSource.reload();
promptAction.openToast({ message: `${item.fav ? '已收藏' : '已取消收藏'}:${item.title}` , duration: 800 });
},
},
{
icon: new SymbolGlyphModifier($r('sys.symbol.clock')).fontColor([item.wish ? Color.White : Color.Green]).fontSize(item.wish ? 18 : 16),
backgroundColor: item.wish ? Color.Green : Color.Grey,
onAction: () => {
item.wish = !item.wish;
item.rev++;
this.dataSource.reload();
promptAction.openToast({ message: `${item.wish ? '加入想看' : '已取消想看'}:${item.title}`, duration: 800 });
},
},
{
icon: new SymbolGlyphModifier($r('sys.symbol.share')).fontColor([item.shared ? Color.White : Color.Blue]).fontSize(item.shared ? 18 : 16),
backgroundColor: item.shared ? Color.Blue : Color.Grey,
onAction: () => {
item.shared = !item.shared;
item.rev++;
this.dataSource.reload();
promptAction.openToast({ message: `${item.shared ? '准备分享' : '取消分享标记'}:${item.title}`, duration: 800 });
},
},
],
// ... 还没完,还有删除操作 ...
}
5.3 终极大招:滑动删除
HdsListItem
还贴心地提供了两种删除方式:
-
deleteIconOptions
:当你左滑(但没滑到底)时,出现的那个红色“删除”按钮。点击它,触发onAction
,我们调用this.dataSource.deleteItem(item)
来删除数据。
-
fullDeleteOptions
:当你“一路向左”滑到底时,直接触发onFullDeleteAction
。同样是删除数据。
我们还加了个 animateTo
动画,让删除看起来更丝滑。
deleteIconOptions: {
backgroundColor: Color.Red,
iconColor: Color.White,
onAction: () => {
promptAction.openToast({ message: `删除:${item.title}`, duration: 800 });
// Demo:演示删除
this.getUIContext()?.animateTo({ duration: 250 }, () => {
this.dataSource.deleteItem(item)
});
},
},
fullDeleteOptions: {
isFullDelete: true,
onFullDeleteAction: () => {
promptAction.openToast({ message: `滑动触发删除:${item.title}`, duration: 800 });
this.getUIContext()?.animateTo({ duration: 250 }, () => {
this.dataSource.deleteItem(item)
});
},
},
}
})
// LazyForEach 的 } 在这里
}
// List 的 } 在这里
// ...
}
// Column 的 } 在这里
}
// build() 的 } 在这里
第六站:“开业大酬宾”—— 填充初始数据
aboutToAppear()
是一个“生命周期”函数。你可以理解为:“当组件马上要显示在屏幕上时”,这个函数就会被调用。
我们在这里塞了 8 条“霸王别姬”、“大话西游”之类的经典电影数据,作为我们的“开业福利”。用 forEach
循环,一条条 pushItem
到 dataSource
里。
aboutToAppear() {
const sample: MovieItem[] = [
new MovieItem('1', '霸王别姬', 1993, '一段爱与执念的绝唱'),
new MovieItem('2', '大话西游', 1995, '一万年太久,只争朝夕'),
new MovieItem('3', '卧虎藏龙', 2000, '江湖决绝,剑气如虹'),
new MovieItem('4', '英雄', 2002, '天下与一人之间的抉择'),
new MovieItem('5', '一代宗师', 2013, '见自己,见天地,见众生'),
new MovieItem('6', '刺客聂隐娘', 2015, '无声之刃,心有余音'),
new MovieItem('7', '流浪地球', 2019, '带着地球去流浪'),
new MovieItem('8', '长安三万里', 2023, '大唐气象,诗酒年华'),
];
sample.forEach((m): void => this.dataSource.pushItem(m));
}
} // HdsMovieListDemo 组件的 } 在这里
第七站:定义“电影”的数据结构(MovieItem
类)
这个很简单,就是定义一下“什么才算一部电影”。它需要有 id
, title
(标题), year
(年份) 等等。
注意,fav
(收藏), wish
(想看), shared
(分享) 默认都是 false
。rev
(版本号) 默认是 0
,就是我们前面用来刷新UI的“小机关”。
class MovieItem {
public id: string;
public title: string;
public year: number;
public tagline: string;
public fav: boolean;
public wish: boolean;
public shared: boolean;
public rev: number;
constructor(id: string, title: string, year: number, tagline: string) {
this.id = id;
this.title = title;
this.year = year;
this.tagline = tagline;
this.fav = false;
this.wish = false;
this.shared = false;
this.rev = 0;
}
}
最终站:我们的“数据大管家” (LazyDataSource
)
这是我们自定义的一个类,它实现了鸿蒙要求的 IDataSource
接口(可以理解为一套“行业规范”)。
LazyForEach
之所以能工作,全靠这个类给它提供了标准化的方法,比如:
totalCount()
: 告诉列表总共有多少条数据。getData(index: number)
: 列表问:“我要显示第 5 条,数据是啥?” 这个方法就负责把第 5 条数据递过去。pushItem(item: T)
: 添加数据,并通知“监听者”(listeners
,也就是我们的List
):“嘿!来新数据了!”deleteItem(item: T)
: 删除数据,并通知“监听者”:“嘿!第 N 条数据没了!”registerDataChangeListener(listener: DataChangeListener)
:List
一上来就会调用这个,把自己“注册”成一个监听者。reload()
: 当我们改了(比如收藏)数据后,我们手动调用这个方法,它会遍历所有监听者,大喊一声:“数据变了,全部刷新!”
<!-- end list -->
export class LazyDataSource<T> implements IDataSource {
private elements: T[];
private listeners: Set<DataChangeListener>;
constructor(elements: T[] = []) {
this.elements = elements;
this.listeners = new Set();
}
totalCount(): number {
return this.elements.length;
}
getData(index: number): T {
return this.elements[index];
}
indexOf(item: T): number {
return this.elements.indexOf(item);
}
pinItem(item: T, index: number): void {
this.elements.splice(index, 1);
this.elements.unshift(item);
this.listeners.forEach(listener => listener.onDataReloaded());
}
pushItem(item: T) {
this.elements.push(item);
this.listeners.forEach(listener => listener.onDataAdd(this.elements.length - 1));
}
deleteItem(item: T): void {
const index = this.elements.indexOf(item);
if (index < 0) {
return;
}
this.elements.splice(index, 1);
this.listeners.forEach(listener => listener.onDataDelete(index));
}
deleteItemByIndex(index: number): void {
this.elements.splice(index, 1);
this.listeners.forEach(listener => listener.onDataDelete(index));
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listeners.add(listener);
}
unregisterDataChangeListener(listener: DataChangeListener): void {
this.listeners.delete(listener);
}
public reload(): void {
this.listeners.forEach((listener: DataChangeListener) => listener.onDataReloaded());
}
}
总结
呼!搞定!
通过这个 Demo,我们学到了:
- 如何使用
@State
和自定义数据源LazyDataSource
来管理“响应式”数据。 - 如何用
List
+LazyForEach
高效地显示长列表(以及那个rev
刷新小技巧!)。 - 如何白嫖
HdsListItem
组件,快速实现带左滑操作和滑动删除的酷炫列表。 aboutToAppear
生命周期钩子的妙用。
是不是感觉鸿蒙开发好像也挺简单的呀!
没错,跟着我学,肯定简单,赶紧操作起来吧!
