未验证 提交 a640c277 编写于 作者: O openharmony_ci 提交者: Gitee

!17820 提交案例【不需要翻译】

Merge pull request !17820 from duangavin123/master
# 开发案例
## 案例介绍
本目录为社区开发者提供OpenHarmony开发典型案例,包括应用开发案例和设备开发案例,主要面向开发者在开发过程中经常使用的功能及其开发场景,提供对应的经验总结、指导教程等。
关于案例的引用规范和声明,请参考[案例库说明](operation-regulations.md)
## 案例目录
## 案例索引
我们通过目录中的Index.md对本目录以及三方网站的开发案例进行链接索引。
## 贡献案例
我们鼓励您积极参与案例贡献。您可以:
* 针对已有案例进行优化提升。
* 贡献全新的案例。
在参与贡献时,您需要注意:
- 您提交的内容、图片必须是原创内容,不得侵犯他人知识产权。
- 对于采纳的内容,OpenHarmony有权根据相关规范修改您提交的内容。
## 案例采纳/退出原则
### 采纳原则
* 案例完整,逻辑清晰。
* 案例具有实际指导意义。
* 案例阅读体验良好。
### 退出原则
* 案例内容陈旧,涉及不再推荐使用的技术方案或接口。
* 案例内容侵犯他人知识产权。
## 三方网站链接说明
本部分含有其他独立第三方网站的链接,该类链接仅供开发者进行开发参考。OpenHarmony开源项目不对链接网站的内容、服务、信息等提供任何明示和暗示的保证,此类链接也不视为OpenHarmony开源项目对链接网站的推荐或授权。您需自行独立判断您与链接网站的互动行为, OpenHarmony开源项目对此不承担任何责任。
## License
Creative Commons License version 4.0
### ArkUI
- [如何按字母分组展示联系人](how-to-group-contacts-with-alphabet.md)
- [如何实现列表项的新增和删除](how-to-add-delete-listitems.md)
- [如何通过显示动画实现书籍翻页动效](book-flip-animation.md)
- [如何为同一组件在不同场景下绑定不同的业务逻辑](how-to-bind-different-operations-for-one-component.md)
......
## 如何通过显示动画实现书籍翻页动效
### 场景介绍
翻页动效是应用开发中常见的动效场景,常见的如书籍翻页、日历翻页等。本文就为大家举例讲解如何通过ArkUI提供的显示动画接口[animateTo](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-explicit-animation.md/)实现书籍翻页的效果。
### 效果呈现
本例最终实现效果如下:
![翻页动效示例图](figures/book-flip-animation.gif)
### 环境要求
- IDE:DevEco Studio 3.1.1.301
- SDK:3.2.2.6(API9)
### 实现思路
如图,我们分上下两层、左右两侧建立4个文本组件(下文用A、B、C、D代称),左右两侧分别代表打开书籍的左右两面,上下两层堆叠放置。
当B沿旋转轴旋转180度覆盖在A上时,就体现为翻页效果。一个翻页动作的完成包括以下几步:
1、B沿旋转轴旋转180度。
2、B旋转时,D会在右侧显示出来,作为书籍的下一页,此时D承载的内容要变为下一页的内容。
3、B旋转到左侧后,A承载的内容变为B的内容。
4、由于A和B互为镜像,所以A显示为B的内容后,需要以A的中间为轴旋转180度。
5、B重新旋转到右边,其承载的内容变为下一页的内容。
***说明:C用来占位,不需要做动作。***
连续重复上述动作即可实现连续翻页动效。
![翻页动效](figures/book-flip-logic.png)
### 开发步骤
1. 创建文本组件
首先,我们看到动效中用到了4个文本组件,我们可以定义一个文本组件,然后对其进行重复调用。创建时我们为其添加[rotate](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-universal-attributes-transformation.md/)属性,用来控制组件的旋转。
由于各组件旋转的角度和旋转中心不同,需要父组件在调用时传入对应的参数,所以我们为对应变量添加[@Prop](https://docs.openharmony.cn/pages/v3.1/zh-cn/application-dev/ui/ts-component-states-prop.md/)装饰器,用来控制变量传递。具体代码如下:
```
@Component
struct BookCard{
// 为变量添加@Prop装饰器,用于接收父组件的动态传参
@Prop num:number
@Prop y_position:string
@Prop x_position:string
@Prop rotate_angle:number
build(){
Text(`${this.num}`)
.fontWeight(FontWeight.Bold)
.backgroundColor('#18183C')
.fontColor('white')
.fontSize(80)
.width('25%')
.height('30%')
.fontFamily('Monospace')
.textAlign(TextAlign.Center)
.borderRadius(20)
// 使用rotate属性控制旋转
.rotate({
x: 0,
y: 1,
z: 0,
angle: this.rotate_angle,
centerY: this.y_position,
centerX: this.x_position
})
}
}
```
2. 创建父组件框架
由于文本组件分为上下两层,所以我们在父组件中采用[Stack](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-stack.md/)组件进行层叠布局。同时使用[Divider](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-basic-components-divider.md/)组件作为书籍两个页面间的分隔线。具体代码如下:
```
@Entry
@Component
struct BookAnimation {
build(){
Stack(){
Row(){
// 组件C
BookCard()
// 组件D
BookCard()
}
Row(){
// 组件A
BookCard()
// 组件B
BookCard()
}
// 添加两个页面间的分隔线
Divider()
.strokeWidth(5)
.color('white')
.height('26%')
.vertical(true)
}
.width('100%')
.height('100%')
.backgroundColor('#A4AE77')
}
}
```
3. 添加翻页动效
最后我们通过以下几点来为静态的组件添加动效:
- 根据**实现思路**章节的分析,在父组件中定义对应的变量,并在调用子组件时分别传入子组件。
- 自定义book_animate函数,在其中使用animateTo方法添加动画效果,同时控制动画的时长,以及动画过程中各元素状态的改变。
- 在[aboutToAppear](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/ui/ui-ts-custom-component-lifecycle-callbacks.md/)方法中,使用[setInterval](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/apis/js-apis-timer.md/)方法重复调用book_animate函数,以实现连续翻页动效。
具体代码如下:
```
@Entry
@Component
struct BookAnimation {
// 父组件变量设置,注意使用@State做状态管理
@State rotate_angle1:number = 0
@State rotate_angle2:number = 0
@State rotate_angle3:number = 0
@State num_before: number = 0;
@State num: number = 1;
@State num_next: number = 0;
@State y_center1:string = '50%'
@State x_center1:string = '50%'
@State y_center2:string = '0%'
@State x_center2:string = '0%'
// 在UI显示前,传入各项变量的具体值
aboutToAppear() {
// 通过setInterval函数每秒调用一次动画效果,实现连续翻页
setInterval(() => {
this.book_animate()
}, 1000)//函数调用周期要大于每次动画持续的时长
}
private book_animate(){
// 通过animateTo方法为组件添加动效,动效时长要小于setInterval函数调用周期
animateTo({ duration:700,onFinish:()=>{
// 动画结束时,A显示的数字跟B显示的数字相等
this.num_before = this.num
// 动画结束时,A以中心线为轴旋转180度
this.rotate_angle3 = 180
// 动画结束时,B返回至初始状态
this.rotate_angle1 = 0
// 动画结束时,B显示的数字加1
this.num = (this.num + 1) % 10
}
},()=>{
// 动画开始,B的旋转角度变为180度
this.rotate_angle1 = 180
// 动画开始,D的数字加1
this.num_next = this.num+1
})
}
build() {
Stack(){
Row(){
// C组件的引用配置
BookCard({num:0,rotate_angle:this.rotate_angle2,
y_position:this.y_center2,x_position:this.x_center2})
// D组件的引用配置
BookCard({num:this.num_next,rotate_angle:this.rotate_angle2,
y_position:this.y_center2,x_position:this.x_center2})
}
Row(){
// A组件的引用配置
BookCard({num:this.num_before,rotate_angle:this.rotate_angle3,
y_position:this.y_center1,x_position:this.x_center1})
// B组件的引用配置
BookCard({num:this.num,rotate_angle:this.rotate_angle1,
y_position:this.y_center2,x_position:this.x_center2})
}
Divider().strokeWidth(5).color('white').height('26%').vertical(true)
}.width('100%').height('50%').backgroundColor('#A4AE77')
}
}
```
通过以上步骤我们就可以实现翻页动效了。
### 完整代码
示例完整代码如下:
```
@Component
struct BookCard{
@Prop num:number
@Prop y_position:string
@Prop x_position:string
@Prop rotate_angle:number
build(){
Text(`${this.num}`)
.fontWeight(FontWeight.Bold)
.backgroundColor('#18183C')
.fontColor('white')
.fontSize(80)
.width('25%')
.height('30%')
.fontFamily('Monospace')
.textAlign(TextAlign.Center)
.borderRadius(20)
.rotate({
x: 0,
y: 1,
z: 0,
angle: this.rotate_angle,
centerY: this.y_position,
centerX: this.x_position
})
}
}
@Entry
@Component
struct BookAnimation {
@State rotate_angle1:number = 0
@State rotate_angle2:number = 0
@State rotate_angle3:number = 0
@State num_before: number = 0;
@State num: number = 1;
@State num_next: number = 0;
@State y_center1:string = '50%'
@State x_center1:string = '50%'
@State y_center2:string = '0%'
@State x_center2:string = '0%'
aboutToAppear() {
setInterval(() => {
this.book_animate()
}, 1000)
}
private book_animate(){
animateTo({ duration:700,onFinish:()=>{
this.num_before = this.num
this.rotate_angle3 = 180
this.rotate_angle1 = 0
this.num = (this.num + 1) % 10
}
},()=>{
this.rotate_angle1 = 180
this.num_next = this.num+1
})
}
build() {
Stack(){
Row(){
BookCard({num:0,rotate_angle:this.rotate_angle2,y_position:this.y_center2,
x_position:this.x_center2})
BookCard({num:this.num_next,rotate_angle:this.rotate_angle2,y_position:this.y_center2,
x_position:this.x_center2})
}
Row(){
BookCard({num:this.num_before,rotate_angle:this.rotate_angle3,y_position:this.y_center1,
x_position:this.x_center1})
BookCard({num:this.num,rotate_angle:this.rotate_angle1,y_position:this.y_center2,
x_position:this.x_center2})
}
Divider().strokeWidth(5).color('white').height('26%').vertical(true)
}.width('100%').height('50%').backgroundColor('#A4AE77')
}
}
```
## 如何实现列表项的新增和删除
### 场景介绍
列表的编辑模式用途十分广泛,常见于待办事项管理、文件管理、备忘录的记录管理等应用场景。在列表的编辑模式下,新增和删除列表项是最基础的功能,其核心是对列表项对应的数据集合进行数据添加和删除。
下面以待办事项管理为例,介绍如何快速实现新增和删除列表项功能。
### 环境要求
- IDE:DevEco Studio 3.1 Beta1
- SDK:Ohos_sdk_public 3.2.11.9 (API Version 9 Release)
### 新增列表项
如下图所示,当用户点击添加按钮时,将弹出列表项选择界面,用户点击确定后,列表中新增对应项目。
**图17** 新增待办  
![新增列表](figures/add-item.gif)
#### 开发步骤
1. 定义列表项数据结构和初始化列表数据,构建列表整体布局和列表项。
以待办事项管理为例,首先定义待办事项的数据结构:
```ts
import util from '@ohos.util';
export class ToDo {
key: string = util.generateRandomUUID(true);
name: string;
constructor(name: string) {
this.name = name;
}
}
```
2. 然后,初始化待办事项列表和可选事项列表:
```ts
@State toDoData: ToDo[] = [];
private availableThings: string[] = ['读书', '运动', '旅游', '听音乐', '看电影', '唱歌'];
```
3. 构建UI界面。
初始界面包含“待办”和新增按钮“+”:
```ts
Text('待办')
.fontSize(36)
.margin({ left: 40})
Blank()
Text('+')
.fontWeight(FontWeight.Lighter)
.fontSize(40)
.margin({ right: 30 })
```
构建列表布局并通过ForEach循环渲染列表项:
```ts
List({ space: 10 }) {
ForEach(this.toDoData, (toDoItem) => {
ListItem() {
...
}
}, toDoItem => toDoItem.key)
}
```
4. 为新增按钮绑定点击事件,并在事件中通过TextPickerDialog.show添加新增列表项的逻辑:
```ts
Text('+')
.onClick(() => {
TextPickerDialog.show({
range: this.availableThings, // 将可选事项列表配置到选择对话框中
onAccept: (value: TextPickerResult) => {
this.toDoData.push(new ToDo(this.availableThings[value.index])); // 用户点击确认,将选择的数据添加到待办列表toDoData中
},
})
})
```
### 删除列表项
如下图所示,当用户长按列表项进入删除模式时,提供用户删除列表项选择的交互界面,用户勾选完成后点击删除按钮,列表中删除对应的项目。
**图18** 长按删除待办事项
![删除列表](figures/delete-item.gif)
#### 开发步骤
1. 列表的删除功能一般进入编辑模式后才可使用,所以需要提供编辑模式的入口。
以待办列表为例,通过LongPressGesture()监听列表项的长按事件,当用户长按列表项时,进入编辑模式。
```ts
// ToDoListItem.ets
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
...
}
.gesture(
GestureGroup(GestureMode.Exclusive,
LongPressGesture() // 监听长按事件
.onAction(() => {
if (!this.isEditMode) {
this.isEditMode = true; //进入编辑模式
this.selectedItems.push(this.toDoItem); // 记录长按时选中的列表项
}
})
)
)
```
2. 需要响应用户的选择交互,记录要删除的列表项数据。
在待办列表中,通过勾选框的勾选或取消勾选,响应用户勾选列表项变化,记录所有选择的列表项。
```ts
// ToDoListItem.ets
if (this.isEditMode) {
Checkbox()
.onChange((isSelected) => {
if (isSelected) {
this.selectedItems.push(this.toDoItem) // 勾选时,记录选中的列表项
} else {
let index = this.selectedItems.indexOf(this.toDoItem)
if (index !== -1) {
this.selectedItems.splice(index, 1) // 取消勾选时,则将此项从selectedItems中删除
}
}
})
...
}
```
3. 需要响应用户点击删除按钮事件,删除列表中对应的选项。
```ts
// ToDoList.ets
Button('删除')
.onClick(() => {
// 删除选中的列表项对应的toDoData数据
let leftData = this.toDoData.filter((item) => {
return this.selectedItems.find((selectedItem) => selectedItem !== item);
})
this.toDoData = leftData;
this.isEditMode = false;
})
...
```
### 完整示例代码
新增和删除列表项的实现共涉及三个文件,各文件完整代码如下:
1. 待办事项数据结构代码(ToDo.ets):
```ts
// ToDo.ets
import util from '@ohos.util';
export class ToDo {
key: string = util.generateRandomUUID(true)
name: string;
constructor(name: string) {
this.name = name;
}
}
```
2. 待办事项列表代码(ToDoList.ets):
```ts
// ToDoList.ets
import { ToDo } from '../model/ToDo';
import { ToDoListItem } from './ToDoListItem';
@Entry
@Component
struct ToDoList {
@State toDoData: ToDo[] = []
@Watch('onEditModeChange') @State isEditMode: boolean = false
@State selectedItems: ToDo[] = []
private availableThings: string[] = ["读书", "运动", "旅游", '听音乐', '看电影', '唱歌']
saveData(value: string) {
this.toDoData.push(new ToDo(value))
}
onEditModeChange() {
if (!this.isEditMode) {
this.selectedItems = []
}
}
build() {
Column() {
Row() {
if (this.isEditMode) {
Text('X')
.fontSize(20)
.onClick(() => {
this.isEditMode = false;
})
.margin({ left: 20, right: 20 })
Text('已选择' + this.selectedItems.length + '')
.fontSize(24)
} else {
Text('待办')
.fontSize(36)
.margin({ left: 40})
Blank()
Text('+')
.fontWeight(FontWeight.Lighter)
.fontSize(40)
.margin({ right: 30 })
.onClick(() => {
TextPickerDialog.show({
range: this.availableThings,
onAccept: (value: TextPickerResult) => {
this.toDoData.push(new ToDo(this.availableThings[value.index]))
console.info('to do data: ' + JSON.stringify(this.toDoData))
},
})
})
}
}
.height('12%')
.width('100%')
List({ initialIndex: 0, space: 10 }) {
ForEach(this.toDoData, toDoItem => {
ListItem() {
ToDoListItem({
isEditMode: $isEditMode,
toDoItem: toDoItem,
selectedItems: $selectedItems
})
}.padding({ left: 24, right: 24, bottom: 12 })
}, toDoItem => toDoItem.key)
}
.height('73%')
.listDirection(Axis.Vertical)
.edgeEffect(EdgeEffect.Spring)
if (this.isEditMode) {
Row() {
Button('删除')
.width('80%')
.onClick(() => {
let leftData = this.toDoData.filter((item) => {
return this.selectedItems.find((selectedItem) => selectedItem != item)
})
console.log('leftData: ' + leftData);
this.isEditMode = false;
this.toDoData = leftData;
})
.backgroundColor('#ffd75d5d')
}
.height('15%')
}
}
.backgroundColor('#fff1f3f5')
.width('100%')
.height('100%')
}
}
```
3. 待办事项代码(ToDoListItem.ets):
```ts
// ToDoListItem.ets
import { ToDo } from '../model/ToDo';
@Component
export struct ToDoListItem {
@Link isEditMode: boolean
@Link selectedItems: ToDo[]
private toDoItem: ToDo;
hasBeenSelected(): boolean {
return this.selectedItems.indexOf(this.toDoItem) != -1
}
build() {
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
Row({ space: 4 }) {
Circle()
.width(24)
.height(24)
.fill(Color.White)
.borderWidth(3)
.borderRadius(30)
.borderColor('#ffdcdfdf')
.margin({ right: 10 })
Text(`${this.toDoItem.name}`)
.maxLines(1)
.fontSize(24)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.padding({ left: 12 })
if (this.isEditMode) {
Checkbox()
.select(this.hasBeenSelected() ? true : false)
.onChange((isSelected) => {
if (isSelected) {
this.selectedItems.push(this.toDoItem)
} else {
let index = this.selectedItems.indexOf(this.toDoItem)
if (index != -1) {
this.selectedItems.splice(index, 1)
}
}
})
.width(24)
.height(24)
}
}
.width('100%')
.height(80)
.padding({
left: 16,
right: 12,
top: 4,
bottom: 4
})
.borderRadius(24)
.linearGradient({
direction: GradientDirection.Right,
colors: this.hasBeenSelected() ? [[0xffcdae, 0.0], [0xFfece2, 1.0]] : [[0xffffff, 0.0], [0xffffff, 1.0]]
})
.gesture(
GestureGroup(GestureMode.Exclusive,
LongPressGesture()
.onAction(() => {
if (!this.isEditMode) {
this.isEditMode = true
this.selectedItems.push(this.toDoItem)
}
})
)
)
}
}
```
\ No newline at end of file
## 如何为同一组件在不同场景下绑定不同的业务逻辑
### 场景介绍
我们在应用开发过程中经常遇到这样的场景:在不同的地方需要呈现同样的UI效果,为了简化处理,我们往往会把对应的UI元素封装成自定义组件,然后在用到的地方进行调用。但是,通常情况下,UI展示往往伴随着动态交互,而且在不同场景下需要做不同的交互处理。
*比如:A、B两个页面都有返回按钮,但是在A页面中点击返回按钮是返回上一页,在B页面点击返回按钮是直接退出当前应用。同样是点击返回按钮,但是业务处理逻辑是不同的。*
那么在不同场景下调用同一组件的时候如何实现不同的交互逻辑呢?这就引出了本文要讲的内容。
### 实现思路
要使同一组件实现不同效果,我们很容易就想到通过变量传参进行控制,而不是直接在程序中采用固定值,这样我们就可以根据不同场景传入不同参数从而实现不同效果。
我们通常使用传参的方式改变组件的属性,比如:大小、边框、背景色、字体等等,同样的,我们也可以通过传参的方式改变组件绑定的业务逻辑,只不过,此时我们传入的是方法,然后在对应方法中写入具体的业务逻辑。
接下来我们用一个简单的例子讲解如何实现。
### 开发示例
本示例将在一个页面中两次引用同一个按钮,然后为两次引用加入不同的处理逻辑,第一次引用中,点击按钮跳转到‘’Hello World!’‘页面;第二次引用中,点击按钮跳转到’‘Hello ArkTS!’‘页面。
#### 环境要求
- IDE:DevEco Studio 3.1 Beta1
- SDK:Ohos_sdk_public 3.2.11.9 (API Version 9 Release)
#### 开发步骤
1. 创建按钮组件
首先,让我们创建被引用的按钮组件。
这里需要注意的是,由于按钮要绑定不同的处理逻辑,所以我们在点击事件中不要写入固定的处理逻辑,而是传入一个自定义的空方法,该方法的逻辑在父组件中实现,然后传入。具体代码如下:
```
@Component
struct ChildComponent{
@State button_text:string = 'hi'
// 定义一个空函数
child_func:()=>void
build(){
Button(`${this.button_text}`)
.height(100)
.width(200)
.onClick(() => {
// 在组件的点击事件中绑定之前定义的空函数
this.child_func()
})
}
}
```
2. 在父组件中引用按钮组件
接下来,我们在父组件中引用两次第1步中创建的按钮组件。具体代码如下:
```
@Entry
@Component
struct FuncTransition{
build() {
Column(){
// 引用按钮组件
ChildComponent({button_text:'Hello World'})
.margin(50)
ChildComponent({button_text:'Hello ArkTS'})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
```
3. 在父组件中传入处理逻辑
以上两步已经把我们的页面框架搭好了,接下来就是给按钮组件传入处理逻辑了,这也是最重要的一步。
我们在第1步中为按钮组件的点击事件绑定了一个空函数,现在我们在父组件中创建一个带有具体处理逻辑的函数,并将其传入按钮组件中。在父组件中可以通过为同一函数传入不同参数来为两个按钮组件绑定不同逻辑,也可以通过不同函数来实现,本例中采用前者进行实现。具体代码如下:
```
import router from '@ohos.router'
@Entry
@Component
struct FuncTransition{
// 在父组件中创建逻辑处理函数,此处逻辑为页面跳转。
parent_func(page_url){
router.pushUrl({
url:page_url
})
}
build() {
Column(){
// 在第1处引用的子组件中传入父组件中定义的函数,此处为跳转到''Hello World!''页面
ChildComponent({child_func:()=>{this.parent_func('pages/HelloWorld')},button_text:'Hello World'})
.margin(50)
// 在第2处引用的子组件中传入父组件中定义的函数,此处为跳转到''Hello ArkTS!''页面
ChildComponent({child_func:()=>{this.parent_func('pages/HelloArkTS')},button_text:'Hello ArkTS'})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
```
通过以上步骤我们实现了在为同一按钮组件绑定不同的业务处理逻辑,接下来就让我们看下效果吧!
注意:文中的‘’Hello World!’‘页面和’‘Hello ArkTS!’‘页面不是本文说明的重点,所以本文中不做开发介绍。
![相同子组件不同业务逻辑](figures/different-operations-one-component.gif)
### 完整代码
示例完整代码如下:
```
import router from '@ohos.router'
@Entry
@Component
struct FuncTransition{
parent_func(page_url){
router.pushUrl({
url:page_url
})
}
build() {
Column(){
ChildComponent({child_func:()=>{this.parent_func('pages/HelloWorld')},button_text:'Hello World'})
.margin(50)
ChildComponent({child_func:()=>{this.parent_func('pages/HelloArkTS')},button_text:'Hello ArkTS'})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
@Component
struct ChildComponent{
@State button_text:string = 'hi'
child_func:()=>void
build(){
Button(`${this.button_text}`)
.height(100)
.width(200)
.onClick(() => {
this.child_func()
})
}
}
```
\ No newline at end of file
## 如何按字母分组展示联系人(仅UI)
### 场景说明
在通讯录中,需要将联系人按照姓氏的首字母进行分组排列,从而更方便联系人的查找;联系人列表右侧的字母导航可以随列表的滑动而定位到对应字母处;同时,也可以通过字母导航控制列表跳到指定联系人分组。
本例即为大家介绍如何通过实现上述场景。
### 效果呈现
本示例最终效果如下:
![contactlist](figures/contactlist.gif)
### 环境要求
- IDE:DevEco Studio 3.1 Beta1
- SDK:Ohos_sdk_public 3.2.11.9 (API Version 9 Release)
### 实现思路
本例涉及的四个关键特性及其实现方案如下:
- 联系人按字母分组展示:通过List组件显示联系人列表,通过ListItemGroup组件实现联系人分组。
- 联系人右侧呈现字母导航:使用AlphabetIndexer组件实现字母导航,同时通过Stack组件使字母导航浮在联系人列表右侧。
- 滑动联系人列表,右侧字母导航随之变动:通过List组件的onScrollIndex事件获取到联系人列表的滑动位置,并将该位置索引传递给字母导航的selected属性,作为字母导航的被选中项。
- 通过右侧字母导航控制联系人列表滑动到指定分组:通过字母导航的onSelected事件获取选中字母的索引,并将该索引传递给联系人列表的控制器,控制列表滑动到指定分组。
### 开发步骤
针对上述关键特性,具体实现步骤如下:
1、先通过Stack、List、ListItemGroup、AlphabetIndexer等关键组件将UI框架搭建起来。
先构建列表数据,其中Contact为联系人数据类
```ts
contactGroups: object[] = [
...
{
title: 'D',
contacts: [
new Contact('Donna', $r('app.media.contact6')),
new Contact('朵朵', $r('app.media.contact1')),
],
},
...
{
title: 'K',
contacts: [
new Contact('孔孔', $r('app.media.contact2')),
new Contact('康康', $r('app.media.contact3')),
],
},
{
title: 'L',
contacts: [
new Contact('Lisa', $r('app.media.contact4')),
new Contact('玲玲', $r('app.media.contact5')),
],
},
{
title: 'N',
contacts: [
new Contact('牛牛', $r('app.media.contact6')),
new Contact('Natasha', $r('app.media.contact1')),
],
},
...
]
```
有了列表数据后,我们来构建UI框架,关键代码如下:
```ts
@Entry
@Component
struct ContactList{
...
// 自定义组件groupHeader,作为ListItemGroup的头部组件,即A、B、C等字母列表项
@Builder groupHeader(titleLetter:string){
Text(titleLetter)
.fontSize(20)
.backgroundColor('#fff1f3f5')
.width('100%')
.padding(5)
}
// 创建字母列表作为字母导航的内容
private alphabets:string[] = [ 'A', 'B', 'D', 'G', 'K', 'L', 'N', 'X'];
build() {
Stack({alignContent:Alignment.End}){
List(){
// 循环渲染列表内容
ForEach(this.contactGroups,contactGroup=>{
// 采用ListItemGroup对联系人进行分组,将groupHeader作为ListItemGroup的头部组件
ListItemGroup({header:this.groupHeader(contactGroup.title)}){
ForEach(contactGroup.contacts,contact=>{
ListItem(){
Column(){
Row(){
Image(contact.icon)
...
Text(contact.name)
}
...
Divider().color('#fff1f3f5')
}
...
}
})
}
})
}
...
// 使用AlphabetIndexer组件实现右侧字母导航
AlphabetIndexer({arrayValue:this.alphabets,selected:0})
...
}
}
}
```
完成上述代码,我们的框架就搭建起来了,如图:
![contactframe](figures/contactframe.PNG)
2、接下来我们为UI框架添加逻辑控制。首先,通过List的onScrollIndex事件获取到列表滑动位置的索引,并将索引同步给右侧字母表的selected属性,从而在滑动联系人时,使右侧字母导航随之变动,关键代码如下:
```ts
...
// 创建动态变量,用于指定字母导航的选择项
@State selectedIndex:number = 0;
...
build() {
Stack({alignContent:Alignment.End}){
List({scroller:this.listScroller}){
ForEach(this.contactGroups,contactGroup=>{
ListItemGroup({header:this.groupHeader(contactGroup.title)}){
ForEach(contactGroup.contacts,contact=>{
ListItem(){
...
}
})
}
})
}
...
// 获取联系人列表滑动位置的索引,并将索引通过selectedIndex同步给右侧字母导航
.onScrollIndex((firstIndex:number)=>{
this.selectedIndex = firstIndex
})
AlphabetIndexer({arrayValue:this.alphabets,selected:0})
...
// 指定字母导航的选择项为selectedIndex,完成跟联系人列表的同步
.selected(this.selectedIndex)
...
}
}
```
至此,当我们滑动联系人列表时,就可以让右侧字母导航随之变动了。效果如下:
![listtonav](figures/listtonav.gif)
3、最后,我们通过AlphabetIndexer组件的onSelect事件获取到字母导航选择项的索引,然后通过List组件的scroller控制器控制联系人列表滑动到相同的索引处,从而实现通过右侧字母导航控制联系人列表滑动到指定分组。关键代码如下:
```ts
...
@State selectedIndex:number = 0;
// 创建List组件的scroller控制器:listScroller,用于控制联系人列表的滑动位置
private listScroller:Scroller = new Scroller()
...
build() {
Stack({alignContent:Alignment.End}){
// 将scroller控制器绑定到List组件
List({scroller:this.listScroller}){
ForEach(this.contactGroups,contactGroup=>{
ListItemGroup({header:this.groupHeader(contactGroup.title)}){
ForEach(contactGroup.contacts,contact=>{
ListItem(){
...
}
})
}
})
}
...
AlphabetIndexer({arrayValue:this.alphabets,selected:0})
...
// 获取字母导航中选中字母的索引值,并通过listScroller控制列表滑动到对应索引位置
.onSelect((index:number)=>{
this.listScroller.scrollToIndex(index)
})
...
}
}
```
至此,当我们在右侧字母导航选择某个字母时就可以控制联系人列表跳转到指定分组了,效果如下:
![navtolist](figures/navtolist.gif)
### 完整代码
通过上述步骤我们已经完成了整个示例的开发,现提供本示例的完整代码供大家参考:
联系人数据类代码:
```ts
// ListModel.ets
export default class Contact{
name:string;
icon:Resource;
constructor(name:string,icon:Resource) {
this.name = name
this.icon = icon
}
}
```
案例主代码:
```ts
// Contact.ets
import Contact from '../model/ListModel'
@Entry
@Component
struct ContactList{
// 联系人列表数据
contactGroups: object[] = [
{
title: 'A',
contacts: [
new Contact('艾薇而', $r('app.media.contact1')),
new Contact('安琪', $r('app.media.contact2')),
new Contact('Angela', $r('app.media.contact3')),
],
},
{
title: 'B',
contacts: [
new Contact('Bobe', $r('app.media.contact4')),
new Contact('勃勃', $r('app.media.contact5')),
],
},
{
title: 'D',
contacts: [
new Contact('Donna', $r('app.media.contact6')),
new Contact('朵朵', $r('app.media.contact1')),
],
},
{
title: 'G',
contacts: [
new Contact('Gavin', $r('app.media.contact4')),
new Contact('果味', $r('app.media.contact1')),
],
},
{
title: 'K',
contacts: [
new Contact('孔孔', $r('app.media.contact2')),
new Contact('康康', $r('app.media.contact3')),
],
},
{
title: 'L',
contacts: [
new Contact('Lisa', $r('app.media.contact4')),
new Contact('玲玲', $r('app.media.contact5')),
],
},
{
title: 'N',
contacts: [
new Contact('牛牛', $r('app.media.contact6')),
new Contact('Natasha', $r('app.media.contact1')),
],
},
{
title: 'X',
contacts: [
new Contact('小可爱', $r('app.media.contact2')),
new Contact('徐总是', $r('app.media.contact3')),
new Contact('璇璇', $r('app.media.contact3')),
new Contact('欣欣', $r('app.media.contact3')),
],
},
]
// 自定义组件groupHeader,作为ListItemGroup的头部组件,即A、B、C等字母列表项
@Builder groupHeader(titleLetter:string){
Text(titleLetter)
.fontSize(20)
.backgroundColor('#fff1f3f5')
.width('100%')
.padding(5)
}
// 创建字母列表作为字母导航的内容
private alphabets:string[] = [ 'A', 'B', 'D', 'G', 'K', 'L', 'N', 'X'];
// 创建动态变量,用于指定字母导航的选择项
@State selectedIndex:number = 0;
// 创建List组件的scroller控制器:listScroller,用于控制联系人列表的滑动位置
private listScroller:Scroller = new Scroller()
build() {
Stack({alignContent:Alignment.End}){
// 将scroller控制器绑定到List组件
List({scroller:this.listScroller}){
// 循环渲染列表内容
ForEach(this.contactGroups,contactGroup=>{
// 采用ListItemGroup对联系人进行分组,将groupHeader作为ListItemGroup的头部组件
ListItemGroup({header:this.groupHeader(contactGroup.title)}){
ForEach(contactGroup.contacts,contact=>{
ListItem(){
Column(){
Row(){
Image(contact.icon)
.width(35)
.height(35)
.margin(10)
Text(contact.name)
}
.width('100%')
Divider().color('#fff1f3f5')
}
.justifyContent(FlexAlign.Start)
}
})
}
})
}
.width('100%')
.height('100%')
.scrollBar(BarState.Auto)
// 获取联系人列表滑动位置的索引,并将索引通过selectedIndex同步给右侧字母导航
.onScrollIndex((firstIndex:number)=>{
this.selectedIndex = firstIndex
})
// 使用AlphabetIndexer组件实现右侧字母导航
AlphabetIndexer({arrayValue:this.alphabets,selected:0})
.margin({right:10})
.itemSize(25)
.font({size:15})
// 指定字母导航的选择项为selectedIndex,完成跟联系人列表的同步
.selected(this.selectedIndex)
// 获取选中字母的索引值,通过listScroller控制列表滑动到对应索引位置
.onSelect((index:number)=>{
this.listScroller.scrollToIndex(index)
})
}
}
}
```
### 参考
[创建列表](https://docs.openharmony.cn/pages/v3.2/zh-cn/application-dev/ui/arkts-layout-development-create-list.md/)
\ No newline at end of file
## 贡献案例
我们鼓励您积极参与案例贡献。您可以:
- 针对已有案例进行优化提升。
- 贡献全新的案例。
在参与贡献时,您需要注意:
- 您提交的内容、图片必须是原创内容,不得侵犯他人知识产权。
- 对于采纳的内容,OpenHarmony有权根据相关规范修改您提交的内容。
## 案例采纳/退出原则
### 采纳原则
- 案例完整,逻辑清晰。
- 案例具有实际指导意义。
- 案例阅读体验良好。
### 退出原则
- 案例内容陈旧,涉及不再推荐使用的技术方案或接口。
- 案例内容侵犯他人知识产权。
## 三方网站链接说明
本部分含有其他独立第三方网站的链接,该类链接仅供开发者进行开发参考。OpenHarmony开源项目不对链接网站的内容、服务、信息等提供任何明示和暗示的保证,此类链接也不视为OpenHarmony开源项目对链接网站的推荐或授权。您需自行独立判断您与链接网站的互动行为, OpenHarmony开源项目对此不承担任何责任。
## License
Creative Commons License version 4.0
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册