提交 fff61b3b 编写于 作者: Z zengyawen

update docs

Signed-off-by: Nzengyawen <zengyawen1@huawei.com>
上级 59470837
# UI开发 # UI开发
- [方舟开发框架(ArkUI)概述](arkui-overview.md) - [方舟开发框架概述](arkui-overview.md)
- 基于ArkTS的声明式开发范式 - UI开发(ArkTS声明式开发范式)
- [概述](ui-ts-overview.md) - [UI开发(ArkTS声明式开发范式)概述](arkts-ui-development-overview.md)
- [声明式UI开发指导](ui-ts-developing-intro.md) - 开发布局
- 声明式UI开发实例 - [布局概述](arkts-layout-development-overview.md)
- [创建简单视图](ui-ts-creating-simple-page.md) - 构建布局
- 构建完整实例 - [线性布局](arkts-layout-development-linear.md)
- [构建食物数据模型](ui-ts-building-data-model.md) - [层叠布局](arkts-layout-development-stack-layout.md)
- [构建食物列表List布局](ui-ts-building-category-list-layout.md) - [弹性布局](arkts-layout-development-flex-layout.md)
- [构建食物分类Grid布局](ui-ts-building-category-grid-layout.md) - [相对布局](arkts-layout-development-relative-layout.md)
- [页面跳转与数据传递](ui-ts-page-redirection-data-transmission.md) - [栅格布局](arkts-layout-development-grid-layout.md)
- 添加闪屏动画 - [媒体查询](arkts-layout-development-media-query.md)
- [绘制图像](ui-ts-drawing-feature.md) - [创建列表](arkts-layout-development-create-list.md)
- [添加动画效果](ui-ts-animation-feature.md) - [创建网格](arkts-layout-development-create-grid.md)
- [常用组件说明](ui-ts-components-intro.md) - [创建轮播](arkts-layout-development-create-looping.md)
- 常见布局开发指导 - [改善布局性能](arkts-layout-development-performance-boost.md)
- 自适应布局 - 添加组件
- [线性布局](ui-ts-layout-linear.md) - 添加常用组件
- [层叠布局](ui-ts-layout-stack.md) - [按钮](arkts-common-components-button.md)
- [弹性布局](ui-ts-layout-flex.md) - [单选框](arkts-common-components-radio-button.md)
- [网格布局](ui-ts-layout-grid.md) - [切换按钮](arkts-common-components-switch.md)
- 响应式布局 - [进度条](arkts-common-components-progress-indicator.md)
- [栅格布局](ui-ts-layout-grid-container-new.md) - [文本显示](arkts-common-components-text-display.md)
- [媒体查询](ui-ts-layout-mediaquery.md) - [文本输入](arkts-common-components-text-input.md)
- [自定义组件的生命周期](ui-ts-custom-component-lifecycle-callbacks.md) - [自定义弹窗](arkts-common-components-custom-dialog.md)
- [Web组件开发指导](ui-ts-components-web.md) - [视频播放](arkts-common-components-video-player.md)
- [性能提升的推荐方法](ui-ts-performance-improvement-recommendation.md) - [XComponent](arkts-common-components-xcomponent.md)
- 兼容JS的类Web开发范式 - 添加气泡和菜单
- [概述](ui-js-overview.md) - [气泡提示](arkts-popup-and-menu-components-popup.md)
- [菜单](arkts-popup-and-menu-components-menu.md)
- 设置页面路由和组件导航
- [页面路由](arkts-routing.md)
- 组件导航
- [Navigation](arkts-navigation-navigation.md)
- [Tabs](arkts-navigation-tabs.md)
- 显示图形
- [显示图片](arkts-graphics-display.md)
- [绘制几何图形](arkts-geometric-shape-drawing.md)
- [使用画布绘制自定义图形](arkts-drawing-customization-on-canvas.md)
- 使用动画
- [动画概述](arkts-animation-overview.md)
- 页面内的动画
- [布局更新动画](arkts-layout-update-animation.md)
- [组件内转场动画](arkts-transition-animation-within-component.md)
- [弹簧曲线动画](arkts-spring-animation.md)
- 页面间的动画
- [放大缩小视图](arkts-zoom-animation.md)
- [页面转场动画](arkts-page-transition-animation.md)
- 支持交互事件
- [交互事件概述](arkts-event-overview.md)
- 使用通用事件
- [触屏事件](arkts-common-events-touch-screen-event.md)
- [键鼠事件](arkts-common-events-device-input-event.md)
- [焦点事件(毕雪峰 00579046)](arkts-common-events-focus-event.md)
- 使用手势事件
- [绑定手势方法](arkts-gesture-events-binding.md)
- [单一手势](arkts-gesture-events-single-gesture.md)
- [组合手势](arkts-gesture-events-combined-gestures.md)
- [性能提升的推荐方法](arkts-performance-improvement-recommendation.md)
- UI开发(兼容JS的类Web开发范式)
- [UI开发(兼容JS的类Web开发范式)概述](ui-js-overview.md)
- 框架说明 - 框架说明
- [文件组织](js-framework-file.md) - [文件组织](js-framework-file.md)
- [js标签配置](js-framework-js-tag.md) - [js标签配置](js-framework-js-tag.md)
......
# 动画概述
动画的原理是在一个时间段内,多次改变UI外观,由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画。UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数,帧率越高则动画就会越流畅。
ArkUI中,产生动画的方式是改变属性值且指定动画参数。动画参数包含了如动画时长、变化规律(即曲线)等参数。当属性值发生变化后,按照动画参数,从原来的状态过渡到新的状态,即形成一个动画。
ArkUI提供的动画能力按照页面的分类方式,可分为页面内的动画和页面间的动画。如下图所示,页面内的动画指在一个页面内即可发生的动画,页面间的动画指两个页面跳转时才会发生的动画。
**图1** 按照页面分类的动画  
![zh-cn_image_0000001562700385](figures/zh-cn_image_0000001562700385.png)
如果按照基础能力分,可分为属性动画、显式动画、转场动画三部分。如下图所示。
**图2** 按照基础能力分类的动画  
![zh-cn_image_0000001562820753](figures/zh-cn_image_0000001562820753.png)
本文按照页面的分类方式,从使用场景出发,提供各种动画的使用方法和注意事项,使开发者快速学习动画。
# 按钮
Button是按钮组件,通常用于响应用户的点击操作,其类型包括胶囊按钮、圆形按钮、普通按钮。Button当做为容器使用时可以通过添加子组件实现包含文字、图片等元素的按钮。具体用法请参考[Button](../reference/arkui-ts/ts-basic-components-button.md)
## 创建按钮
Button通过调用接口来创建,接口调用有以下两种形式:
- 创建不包含子组件的按钮。
```ts
Button(label?: string, options?: { type?: ButtonType, stateEffect?: boolean })
```
该接口用于创建不包含子组件的按钮,其中label用来设置按钮文字,type用于设置Button类型,stateEffect属性设置Button是否开启点击效果。
```ts
Button('Ok', { type: ButtonType.Normal, stateEffect: true })
.borderRadius(8)
.backgroundColor(0x317aff)
.width(90)
.height(40)
```
![zh-cn_image_0000001562820757](figures/zh-cn_image_0000001562820757.png)
- 创建包含子组件的按钮。
```ts
Button(options?: {type?: ButtonType, stateEffect?: boolean})
```
该接口用于创建包含子组件的按钮,只支持包含一个子组件,子组件可以是[基础组件](../reference/arkui-ts/ts-basic-components-blank.md)或者[容器组件](../reference/arkui-ts/ts-container-ability-component.md)
```ts
Button({ type: ButtonType.Normal, stateEffect: true }) {
Row() {
Image($r('app.media.loading')).width(20).height(40).margin({ left: 12 })
Text('loading').fontSize(12).fontColor(0xffffff).margin({ left: 5, right: 12 })
}.alignItems(VerticalAlign.Center)
}.borderRadius(8).backgroundColor(0x317aff).width(90).height(40)
```
![zh-cn_image_0000001511421216](figures/zh-cn_image_0000001511421216.png)
## 设置按钮类型
Button有三种可选类型,分别为Capsule(胶囊类型)、Circle(圆形按钮)和Normal(普通按钮),通过type进行设置。
- 胶囊按钮(默认类型)
此类型按钮的圆角自动设置为高度的一半,不支持通过borderRadius属性重新设置圆角。
```ts
Button('Disable', { type: ButtonType.Capsule, stateEffect: false })
.backgroundColor(0x317aff)
.width(90)
.height(40)
```
![zh-cn_image_0000001511421208](figures/zh-cn_image_0000001511421208.png)
- 圆形按钮
此类型按钮为圆形,不支持通过borderRadius属性重新设置圆角。
```ts
Button('Circle', { type: ButtonType.Circle, stateEffect: false })
.backgroundColor(0x317aff)
.width(90)
.height(90)
```
![zh-cn_image_0000001511740428](figures/zh-cn_image_0000001511740428.png)
- 普通按钮
此类型的按钮默认圆角为0,支持通过borderRadius属性重新设置圆角。
```ts
Button('Ok', { type: ButtonType.Normal, stateEffect: true })
.borderRadius(8)
.backgroundColor(0x317aff)
.width(90)
.height(40)
```
![zh-cn_image_0000001563060641](figures/zh-cn_image_0000001563060641.png)
## 自定义样式
- 设置边框弧度。
一般使用通用属性来自定义按钮样式。例如通过borderRadius属性设置按钮的边框弧度。
```ts
Button('circle border', { type: ButtonType.Normal })
.borderRadius(20)
.height(40)
```
![zh-cn_image_0000001511900392](figures/zh-cn_image_0000001511900392.png)
- 设置文本样式。
通过添加文本样式设置按钮文本的展示样式。
```ts
Button('font style', { type: ButtonType.Normal })
.fontSize(20)
.fontColor(Color.Pink)
.fontWeight(800)
```
![zh-cn_image_0000001511580828](figures/zh-cn_image_0000001511580828.png)
- 设置背景颜色。
添加backgroundColor属性设置按钮的背景颜色。
```ts
Button('background color').backgroundColor(0xF55A42)
```
![zh-cn_image_0000001562940477](figures/zh-cn_image_0000001562940477.png)
- 用作功能型按钮。
为删除操作创建一个按钮。
```ts
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image($r('app.media.ic_public_delete_filled')).width(30).height(30)
}.width(55).height(55).margin({ left: 20 }).backgroundColor(0xF55A42)
```
![zh-cn_image_0000001511740436](figures/zh-cn_image_0000001511740436.png)
## 添加事件
Button组件通常用于触发某些操作,可以绑定onClick事件来响应点击操作后的自定义行为。
```ts
Button('Ok', { type: ButtonType.Normal, stateEffect: true })
.onClick(()=>{
console.info('Button onClick')
})
```
## 场景示例
- 用于启动操作。
可以用按钮启动任何用户界面元素,按钮会根据用户的操作触发相应的事件。例如,在List容器里通过点击按钮进行页面跳转。
```ts
// xxx.ets
import router from '@ohos.router';
@Entry
@Component
struct ButtonCase1 {
build() {
List({ space: 4 }) {
ListItem() {
Button("First").onClick(() => {
router.pushUrl({ url: 'pages/first_page' })
})
.width('100%')
}
ListItem() {
Button("Second").onClick(() => {
router.pushUrl({ url: 'pages/second_page' })
})
.width('100%')
}
ListItem() {
Button("Third").onClick(() => {
router.pushUrl({ url: 'pages/third_page' })
})
.width('100%')
}
}
.listDirection(Axis.Vertical)
.backgroundColor(0xDCDCDC).padding(20)
}
}
```
![zh-cn_image_0000001562700393](figures/zh-cn_image_0000001562700393.png)
- 用于表单的提交。
在用户登录/注册页面,使用按钮进行登录或注册操作。
```ts
// xxx.ets
@Entry
@Component
struct ButtonCase2 {
build() {
Column() {
TextInput({ placeholder: 'input your username' }).margin({ top: 20 })
TextInput({ placeholder: 'input your password' }).type(InputType.Password).margin({ top: 20 })
Button('Register').width(300).margin({ top: 20 })
.onClick(() => {
// 需要执行的操作
})
}.padding(20)
}
}
```
![zh-cn_image_0000001562940473](figures/zh-cn_image_0000001562940473.png)
- 悬浮按钮
在可以滑动的界面,滑动时按钮始终保持悬浮状态。
```ts
// xxx.ets
@Entry
@Component
struct HoverButtonExample {
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
build() {
Stack() {
List({ space: 20, initialIndex: 0 }) {
ForEach(this.arr, (item) => {
ListItem() {
Text('' + item)
.width('100%').height(100).fontSize(16)
.textAlign(TextAlign.Center).borderRadius(10).backgroundColor(0xFFFFFF)
}
}, item => item)
}.width('90%')
Button() {
Image($r('app.media.ic_public_add'))
.width(50)
.height(50)
}
.width(60)
.height(60)
.position({x: '80%', y: 600})
.shadow({radius: 10})
.onClick(() => {
// 需要执行的操作
})
}
.width('100%')
.height('100%')
.backgroundColor(0xDCDCDC)
.padding({ top: 5 })
}
}
```
![GIF](figures/GIF.gif)
# 自定义弹窗
自定义弹窗CustomDialog可用于广告、中奖、警告、软件更新等与用户交互响应操作。开发者可以通过CustomDialogController类显示自定义弹窗。具体用法请参考[自定义弹窗](../reference/arkui-ts/ts-methods-custom-dialog-box.md)
## 创建自定义弹窗
1. 使用\@CustomDialog装饰器装饰自定义弹窗。
2. \@CustomDialog装饰器用于装饰自定义弹框,此装饰器内进行自定义内容(也就是弹框内容)。
```ts
@CustomDialog
struct CustomDialogExample {
controller: CustomDialogController
build() {
Column() {
Text('我是内容')
.fontSize(20)
.margin({ top: 10, bottom: 10 })
}
}
}
```
3. 创建构造器,与装饰器呼应相连。
```ts
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({}),
})
```
4. 点击与onClick事件绑定的组件使弹窗弹出
```ts
Flex({justifyContent:FlexAlign.Center}){
Button('click me')
.onClick(() => {
this.dialogController.open()
})
}.width('100%')
```
![zh-cn_image_0000001562700493](figures/zh-cn_image_0000001562700493.png)
## 弹窗的交互
弹窗可用于数据交互,完成用户一系列响应操作。
1.\@CustomDialog装饰器内添加按钮操作,同时添加数据函数的创建。
```ts
@CustomDialog
struct CustomDialogExample {
controller: CustomDialogController
cancel: () => void
confirm: () => void
build() {
Column() {
Text('我是内容').fontSize(20).margin({ top: 10, bottom: 10 })
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Button('cancel')
.onClick(() => {
this.controller.close()
this.cancel()
}).backgroundColor(0xffffff).fontColor(Color.Black)
Button('confirm')
.onClick(() => {
this.controller.close()
this.confirm()
}).backgroundColor(0xffffff).fontColor(Color.Red)
}.margin({ bottom: 10 })
}
}
}
```
2. 页面内需要在构造器内进行接收,同时创建相应的函数操作。
```ts
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
cancel: this.onCancel,
confirm: this.onAccept,
}),
alignment: DialogAlignment.Default, // 可设置dialog的对齐方式,设定显示在底部或中间等,默认为底部显示
})
onCancel() {
console.info('Callback when the first button is clicked')
}
onAccept() {
console.info('Callback when the second button is clicked')
}
```
![zh-cn_image_0000001511421320](figures/zh-cn_image_0000001511421320.png)
# 进度条
Progress是进度条显示组件,显示内容通常为某次目标操作的当前进度。具体用法请参考[Progress](../reference/arkui-ts/ts-basic-components-progress.md)
## 创建进度条
Progress通过调用接口来创建,接口调用形式如下:
```ts
Progress(options: {value: number, total?: number, type?: ProgressType})
```
该接口用于创建type样式的进度条,其中value用于设置初始进度值,total用于设置进度总长度,type决定Progress样式。
```ts
Progress({ value: 24, total: 100, type: ProgressType.Linear }) // 创建一个进度总长为100,初始进度值为24的线性进度条
```
![create](figures/create.png)
## 设置进度条样式
Progress有5种可选类型,在创建时通过设置ProgressType枚举类型给type可选项指定Progress类型。其分别为:ProgressType.Linear(线性样式)、 ProgressType.Ring(环形无刻度样式)、ProgressType.ScaleRing(环形有刻度样式)、ProgressType.Eclipse(圆形样式)和ProgressType.Capsule(胶囊样式)。
- 线性样式进度条(默认类型)
>**说明:**
>
> 从API version9开始,组件高度大于宽度的时候自适应垂直显示,相等时仍然保持水平显示。
```ts
Progress({ value: 20, total: 100, type: ProgressType.Linear }).width(200).height(50)
Progress({ value: 20, total: 100, type: ProgressType.Linear }).width(50).height(200)
```
![zh-cn_image_0000001562700417](figures/zh-cn_image_0000001562700417.png)
- 环形无刻度样式进度条
```ts
// 从左往右,1号环形进度条,默认前景色为蓝色,默认strokeWidth进度条宽度为2.0vp
Progress({ value: 40, total: 150, type: ProgressType.Ring }).width(100).height(100)
// 从左往右,2号环形进度条
Progress({ value: 40, total: 150, type: ProgressType.Ring }).width(100).height(100)
.color(Color.Grey) // 进度条前景色为灰色
.style({ strokeWidth: 15}) // 设置strokeWidth进度条宽度为15.0vp
```
![progress_ring](figures/progress_ring.png)
- 环形有刻度样式进度条
```ts
Progress({ value: 20, total: 150, type: ProgressType.ScaleRing }).width(100).height(100)
.backgroundColor(Color.Black)
.style({ scaleCount: 20, scaleWidth: 5 }) // 设置环形有刻度进度条总刻度数为20,刻度宽度为5vp
Progress({ value: 20, total: 150, type: ProgressType.ScaleRing }).width(100).height(100)
.backgroundColor(Color.Black)
.style({ strokeWidth: 15, scaleCount: 20, scaleWidth: 5 }) // 设置环形有刻度进度条宽度15,总刻度数为20,刻度宽度为5vp
Progress({ value: 20, total: 150, type: ProgressType.ScaleRing }).width(100).height(100)
.backgroundColor(Color.Black)
.style({ strokeWidth: 15, scaleCount: 20, scaleWidth: 3 }) // 设置环形有刻度进度条宽度15,总刻度数为20,刻度宽度为3vp
```
![progress_scalering](figures/progress_scalering.png)
- 圆形样式进度条
```ts
// 从左往右,1号圆形进度条,默认前景色为蓝色
Progress({ value: 10, total: 150, type: ProgressType.Eclipse }).width(100).height(100)
// 从左往右,2号圆形进度条,指定前景色为灰色
Progress({ value: 20, total: 150, type: ProgressType.Eclipse }).color(Color.Grey).width(100).height(100)
```
![progress_circle](figures/progress_circle.png)
- 胶囊样式进度条
>**说明:**
>
>- 头尾两端圆弧处的进度展示效果与ProgressType.Eclipse样式相同;
>- 中段处的进度展示效果为矩形状长条,与ProgressType.Linear线性样式相似;
>
>- 组件高度大于宽度的时候自适应垂直显示。
```ts
Progress({ value: 10, total: 150, type: ProgressType.Capsule }).width(100).height(50)
Progress({ value: 20, total: 150, type: ProgressType.Capsule }).width(50).height(100).color(Color.Grey)
Progress({ value: 50, total: 150, type: ProgressType.Capsule }).width(50).height(100).backgroundColor(Color.Black)
```
![progress_captule](figures/progress_captule.png)
## 场景示例
更新当前进度值,如应用安装进度条。可通过点击Button增加progressValue,.value()属性将progressValue设置给Progress组件,进度条组件即会触发刷新,更新当前进度。
```ts
@Entry
@Component
struct ProgressCase1 {
@State progressValue: number = 0 // 设置进度条初始值为0
build() {
Column() {
Column() {
Progress({value:0, total:100, type:ProgressType.Capsule}).width(200).height(50)
.style({strokeWidth:50}).value(this.progressValue)
Row().width('100%').height(5)
Button("进度条+5")
.onClick(()=>{
this.progressValue += 5
if (this.progressValue > 100){
this.progressValue = 0
}
})
}
}.width('100%').height('100%')
}
}
```
![progress](figures/progress.gif)
# 单选框
Radio是单选框组件,通常用于提供相应的用户交互选择项,同一组的Radio中只有一个可以被选中。具体用法请参考[Radio](../reference/arkui-ts/ts-basic-components-radio.md)
## 创建单选框
Radio通过调用接口来创建,接口调用形式如下:
```ts
Radio(options: {value: string, group: string})
```
该接口用于创建一个单选框,其中value是单选框的名称,group是单选框的所属群组名称。checked属性可以设置单选框的状态,状态分别为false和true时,设置为true时表示单选框被选中。Radio仅支持选中和未选中两种样式,不支持自定义颜色和形状。
```ts
Radio({ value: 'Radio1', group: 'radioGroup' })
.checked(false)
Radio({ value: 'Radio2', group: 'radioGroup' })
.checked(true)
```
![zh-cn_image_0000001562820821](figures/zh-cn_image_0000001562820821.png)
## 添加事件
除支持[通用事件](../reference/arkui-ts/ts-universal-events-click.md)外,Radio通常用于选中后触发某些操作,可以绑定onChange事件来响应选中操作后的自定义行为。
```ts
Radio({ value: 'Radio1', group: 'radioGroup' })
.onChange((isChecked: boolean) => {
if(isChecked) {
//需要执行的操作
}
})
Radio({ value: 'Radio2', group: 'radioGroup' })
.onChange((isChecked: boolean) => {
if(isChecked) {
//需要执行的操作
}
})
```
## 场景示例
通过点击Radio切换声音模式。
```ts
// xxx.ets
import promptAction from '@ohos.promptAction';
@Entry
@Component
struct RadioExample {
build() {
Row() {
Column() {
Radio({ value: 'Radio1', group: 'radioGroup' }).checked(true)
.height(50)
.width(50)
.onChange((isChecked: boolean) => {
if(isChecked) {
// 切换为响铃模式
promptAction.showToast({ message: 'Ringing mode.' })
}
})
Text('Ringing')
}
Column() {
Radio({ value: 'Radio2', group: 'radioGroup' })
.height(50)
.width(50)
.onChange((isChecked: boolean) => {
if(isChecked) {
// 切换为振动模式
promptAction.showToast({ message: 'Vibration mode.' })
}
})
Text('Vibration')
}
Column() {
Radio({ value: 'Radio3', group: 'radioGroup' })
.height(50)
.width(50)
.onChange((isChecked: boolean) => {
if(isChecked) {
// 切换为静音模式
promptAction.showToast({ message: 'Silent mode.' })
}
})
Text('Silent')
}
}.height('100%').width('100%').justifyContent(FlexAlign.Center)
}
}
```
![zh-cn_image_0000001562700457](figures/zh-cn_image_0000001562700457.png)
# 切换按钮
Toggle组件提供状态按钮样式,勾选框样式及开关样式,一般用于两种状态之间的切换。具体用法请参考[Toggle](../reference/arkui-ts/ts-basic-components-toggle.md)
## 创建切换按钮
Toggle通过调用接口来创建,接口调用形式如下:
```ts
Toggle(options: { type: ToggleType, isOn?: boolean })
```
该接口用于创建切换按钮,其中ToggleType为开关类型,包括Button、Checkbox和Switch,isOn为切换按钮的状态,接口调用有以下两种形式:
- 创建不包含子组件的Toogle。
当ToggleType为Checkbox或者Switch时,用于创建不包含子组件的Toggle:
```ts
Toggle({ type: ToggleType.Checkbox, isOn: false })
Toggle({ type: ToggleType.Checkbox, isOn: true })
```
![zh-cn_image_0000001562940485](figures/zh-cn_image_0000001562940485.png)
```ts
Toggle({ type: ToggleType.Switch, isOn: false })
Toggle({ type: ToggleType.Switch, isOn: true })
```
![zh-cn_image_0000001511421228](figures/zh-cn_image_0000001511421228.png)
- 创建包含子组件的Toggle。
当ToggleType为Button时,如果子组件有文本设置,则相应的文本内容会显示在按钮内部。
```ts
Toggle({ type: ToggleType.Button, isOn: false }) {
Text('status button')
.fontColor('#182431')
.fontSize(12)
}.width(100)
Toggle({ type: ToggleType.Button, isOn: true }) {
Text('status button')
.fontColor('#182431')
.fontSize(12)
}.width(100)
```
![zh-cn_image_0000001511900404](figures/zh-cn_image_0000001511900404.png)
## 自定义样式
- 通过selectedColor属性设置Toggle打开选中后的背景颜色。
```ts
Toggle({ type: ToggleType.Button, isOn: true }) {
Text('status button')
.fontColor('#182431')
.fontSize(12)
}.width(100).selectedColor(Color.Pink)
Toggle({ type: ToggleType.Checkbox, isOn: true })
.selectedColor(Color.Pink)
Toggle({ type: ToggleType.Switch, isOn: true })
.selectedColor(Color.Pink)
```
![zh-cn_image_0000001563060657](figures/zh-cn_image_0000001563060657.png)
- 通过switchPointColor属性设置Switch类型的圆形滑块颜色,仅对type为ToggleType.Switch生效。
```ts
Toggle({ type: ToggleType.Switch, isOn: false })
.switchPointColor(Color.Pink)
Toggle({ type: ToggleType.Switch, isOn: true })
.switchPointColor(Color.Pink)
```
![zh-cn_image_0000001511421232](figures/zh-cn_image_0000001511421232.png)
## 添加事件
除支持通用事件外,Toggle通常用于选中和取消选中后触发某些操作,可以绑定onChange事件来响应操作后的自定义行为。
```ts
Toggle({ type: ToggleType.Switch, isOn: false })
.onChange((isOn: boolean) => {
if(isOn) {
// 需要执行的操作
}
})
```
## 场景示例
Toggle可用于切换蓝牙开关状态。
```ts
// xxx.ets
import prompt from '@ohos.promptAction';
@Entry
@Component
struct ToggleExample {
build() {
Column() {
Row() {
Text("Bluetooth Mode")
.height(50)
.fontSize(16)
}
Row() {
Text("Bluetooth")
.height(50)
.padding({left: 10})
.fontSize(16)
.textAlign(TextAlign.Start)
.backgroundColor(0xFFFFFF)
Toggle({ type: ToggleType.Switch })
.margin({left: 200, right: 10})
.onChange((isOn: boolean) => {
if(isOn) {
promptAction.showToast({ message: 'Bluetooth is on.' })
} else {
promptAction.showToast({ message: 'Bluetooth is off.' })
}
})
}
.backgroundColor(0xFFFFFF)
}
.padding(10)
.backgroundColor(0xDCDCDC)
.width('100%')
.height('100%')
}
}
```
![zh-cn_image_0000001511740448](figures/zh-cn_image_0000001511740448.png)
# 文本显示
Text是文本组件,通常用于展示用户的视图,如显示文章的文字。具体用法可参考[Text](../reference/arkui-ts/ts-basic-components-text.md)
## 创建文本
Text可通过以下两种方式来创建:
- string字符串
```ts
Text('我是一段文本')
```
![zh-cn_image_0000001563060685](figures/zh-cn_image_0000001563060685.png)
- 引用Resource资源
资源引用类型可以通过$r创建Resource类型对象,文件位置为/resources/base/element/string.json。
```ts
Text($r('app.string.module_desc'))
.baselineOffset(0)
.fontSize(30)
.border({ width: 1 })
.padding(10)
.width(300)
```
![zh-cn_image_0000001511580872](figures/zh-cn_image_0000001511580872.png)
## 添加子组件
[Span](../reference/arkui-ts/ts-basic-components-span.md)只能作为Text组件的子组件显示文本内容。可以在一个Text内添加多个Span来显示一段信息,例如产品说明书、承诺书等。
- 创建Span。
Span组件需要写到Text组件内,单独写Span组件不会显示信息,Text与Span同时配置文本内容内容时,Span内容覆盖Text内容。
```ts
Text('我是Text') {
Span('我是Span')
}
.padding(10)
.borderWidth(1)
```
![zh-cn_image_0000001562700441](figures/zh-cn_image_0000001562700441.png)
- 设置文本装饰线及颜色。
通过decoration设置文本装饰线及颜色。
```ts
Text() {
Span('我是Span1,').fontSize(16).fontColor(Color.Grey)
.decoration({ type: TextDecorationType.LineThrough, color: Color.Red })
Span('我是Span2').fontColor(Color.Blue).fontSize(16)
.fontStyle(FontStyle.Italic)
.decoration({ type: TextDecorationType.Underline, color: Color.Black })
Span(',我是Span3').fontSize(16).fontColor(Color.Grey)
.decoration({ type: TextDecorationType.Overline, color: Color.Green })
}
.borderWidth(1)
.padding(10)
```
![zh-cn_image_0000001562700437](figures/zh-cn_image_0000001562700437.png)
- 通过textCase设置文字一直保持大写或者小写状态。
```ts
Text() {
Span('I am Upper-span').fontSize(12)
.textCase(TextCase.UpperCase)
}
.borderWidth(1)
.padding(10)
```
![zh-cn_image_0000001562940525](figures/zh-cn_image_0000001562940525.png)
- 添加事件。
由于Span组件无尺寸信息,事件仅支持点击事件onClick。
```ts
Text() {
Span('I am Upper-span').fontSize(12)
.textCase(TextCase.UpperCase)
.onClick(()=>{
console.info('我是Span——onClick')
})
}
```
## 自定义文本样式
- 通过textAlign属性设置文本对齐样式。
```ts
Text('左对齐')
.width(300)
.textAlign(TextAlign.Start)
.border({ width: 1 })
.padding(10)
Text('中间对齐')
.width(300)
.textAlign(TextAlign.Center)
.border({ width: 1 })
.padding(10)
Text('右对齐')
.width(300)
.textAlign(TextAlign.End)
.border({ width: 1 })
.padding(10)
```
![zh-cn_image_0000001511421260](figures/zh-cn_image_0000001511421260.png)
- 通过textOverflow属性控制文本超长处理,textOverflow需配合maxLines一起使用(默认情况下文本自动折行)。
```ts
Text('This is the setting of textOverflow to Clip text content This is the setting of textOverflow to None text content. This is the setting of textOverflow to Clip text content This is the setting of textOverflow to None text content.')
.width(250)
.textOverflow({ overflow: TextOverflow.None })
.maxLines(1)
.fontSize(12)
.border({ width: 1 }).padding(10)
Text('我是超长文本,超出的部分显示省略号。I am an extra long text, with ellipses displayed for any excess。')
.width(250)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(1)
.fontSize(12)
.border({ width: 1 }).padding(10)
```
![zh-cn_image_0000001563060693](figures/zh-cn_image_0000001563060693.png)
![zh-cn_image_0000001563060701](figures/zh-cn_image_0000001563060701.png)
- 通过lineHeight属性设置文本行高。
```ts
Text('This is the text with the line height set. This is the text with the line height set.')
.width(300).fontSize(12).border({ width: 1 }).padding(10)
Text('This is the text with the line height set. This is the text with the line height set.')
.width(300).fontSize(12).border({ width: 1 }).padding(10)
.lineHeight(20)
```
![zh-cn_image_0000001511740480](figures/zh-cn_image_0000001511740480.png)
- 通过decoration属性设置文本装饰线样式及其颜色。
```ts
Text('This is the text')
.decoration({
type: TextDecorationType.LineThrough,
color: Color.Red
})
.borderWidth(1).padding(10).margin(5)
Text('This is the text')
.decoration({
type: TextDecorationType.Overline,
color: Color.Red
})
.borderWidth(1).padding(10).margin(5)
Text('This is the text')
.decoration({
type: TextDecorationType.Underline,
color: Color.Red
})
.borderWidth(1).padding(10).margin(5)
```
![zh-cn_image_0000001511580888](figures/zh-cn_image_0000001511580888.png)
- 通过baselineOffset属性设置文本基线的偏移量。
```ts
Text('This is the text content with baselineOffset 0.')
.baselineOffset(0)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
Text('This is the text content with baselineOffset 30.')
.baselineOffset(30)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
Text('This is the text content with baselineOffset -20.')
.baselineOffset(-20)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
```
![zh-cn_image_0000001562820789](figures/zh-cn_image_0000001562820789.png)
- 通过letterSpacing属性设置文本字符间距。
```ts
Text('This is the text content with letterSpacing 0.')
.letterSpacing(0)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
Text('This is the text content with letterSpacing 3.')
.letterSpacing(3)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
Text('This is the text content with letterSpacing -1.')
.letterSpacing(-1)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
```
![zh-cn_image_0000001562940513](figures/zh-cn_image_0000001562940513.png)
- 通过minFontSize与maxFontSize自适应字体大小,minFontSize设置文本最小显示字号,maxFontSize设置文本最大显示字号,minFontSize与maxFontSize必须搭配同时使用,以及需配合maxline或布局大小限制一起使用,单独设置不生效。
```ts
Text('我的最大字号为30,最小字号为5,宽度为250,maxLines为1')
.width(250)
.maxLines(1)
.maxFontSize(30)
.minFontSize(5)
.border({ width: 1 })
.padding(10)
.margin(5)
Text('我的最大字号为30,最小字号为5,宽度为250,maxLines为2')
.width(250)
.maxLines(2)
.maxFontSize(30)
.minFontSize(5)
.border({ width: 1 })
.padding(10)
.margin(5)
Text('我的最大字号为30,最小字号为15,宽度为250,高度为50')
.width(250)
.height(50)
.maxFontSize(30)
.minFontSize(15)
.border({ width: 1 })
.padding(10)
.margin(5)
Text('我的最大字号为30,最小字号为15,宽度为250,高度为100')
.width(250)
.height(100)
.maxFontSize(30)
.minFontSize(15)
.border({ width: 1 })
.padding(10)
.margin(5)
```
![zh-cn_image_0000001511740472](figures/zh-cn_image_0000001511740472.png)
- 通过textCase属性设置文本大小写。
```ts
Text('This is the text content with textCase set to Normal.')
.textCase(TextCase.Normal)
.padding(10)
.border({ width: 1 })
.padding(10)
.margin(5)
// 文本全小写展示
Text('This is the text content with textCase set to LowerCase.')
.textCase(TextCase.LowerCase)
.border({ width: 1 })
.padding(10)
.margin(5)
// 文本全大写展示
Text('This is the text content with textCase set to UpperCase.')
.textCase(TextCase.UpperCase)
.border({ width: 1 })
.padding(10)
.margin(5)
```
![zh-cn_image_0000001562940529](figures/zh-cn_image_0000001562940529.png)
- 通过copyOption属性设置文本是否可复制粘贴。
```ts
Text("这是一段可复制文本")
.fontSize(30)
.copyOption(CopyOptions.InApp)
```
![zh-cn_image_0000001511580868](figures/zh-cn_image_0000001511580868.png)
## 添加事件
Text组件可以添加通用事件,可以绑定onClick、onTouch等事件来响应操作。
```ts
Text('点我')
.onClick(()=>{
console.info('我是Text的点击响应事件');
})
```
## 场景示例
```ts
// xxx.ets
@Entry
@Component
struct TextExample {
build() {
Column() {
Row() {
Text("1").fontSize(14).fontColor(Color.Red).margin({ left: 10, right: 10 })
Text("我是热搜词条1")
.fontSize(12)
.fontColor(Color.Blue)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.fontWeight(300)
Text("")
.margin({ left: 6 })
.textAlign(TextAlign.Center)
.fontSize(10)
.fontColor(Color.White)
.fontWeight(600)
.backgroundColor(0x770100)
.borderRadius(5)
.width(15)
.height(14)
}.width('100%').margin(5)
Row() {
Text("2").fontSize(14).fontColor(Color.Red).margin({ left: 10, right: 10 })
Text("我是热搜词条2 我是热搜词条2 我是热搜词条2 我是热搜词条2 我是热搜词条2")
.fontSize(12)
.fontColor(Color.Blue)
.fontWeight(300)
.constraintSize({ maxWidth: 200 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text("")
.margin({ left: 6 })
.textAlign(TextAlign.Center)
.fontSize(10)
.fontColor(Color.White)
.fontWeight(600)
.backgroundColor(0xCC5500)
.borderRadius(5)
.width(15)
.height(14)
}.width('100%').margin(5)
Row() {
Text("3").fontSize(14).fontColor(Color.Orange).margin({ left: 10, right: 10 })
Text("我是热搜词条3")
.fontSize(12)
.fontColor(Color.Blue)
.fontWeight(300)
.maxLines(1)
.constraintSize({ maxWidth: 200 })
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text("")
.margin({ left: 6 })
.textAlign(TextAlign.Center)
.fontSize(10)
.fontColor(Color.White)
.fontWeight(600)
.backgroundColor(0xCC5500)
.borderRadius(5)
.width(15)
.height(14)
}.width('100%').margin(5)
Row() {
Text("4").fontSize(14).fontColor(Color.Grey).margin({ left: 10, right: 10 })
Text("我是热搜词条4 我是热搜词条4 我是热搜词条4 我是热搜词条4 我是热搜词条4")
.fontSize(12)
.fontColor(Color.Blue)
.fontWeight(300)
.constraintSize({ maxWidth: 200 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}.width('100%').margin(5)
}.width('100%')
}
}
```
![zh-cn_image_0000001562820805](figures/zh-cn_image_0000001562820805.png)
# 文本输入
TextInput、TextArea是输入框组件,通常用于响应用户的输入操作,比如评论区的输入、聊天框的输入、表格的输入等,也可以结合其它组件构建功能页面,例如登录注册页面。具体用法参考[TextInput](../reference/arkui-ts/ts-basic-components-textinput.md)[TextArea](../reference/arkui-ts/ts-basic-components-textarea.md)
## 创建输入框
TextInput为单行输入框、TextArea为多行输入框。通过以下接口来创建。
```ts
TextArea(value?:{placeholder?: ResourceStr, text?: ResourceStr, controller?: TextAreaController})
```
```ts
TextInput(value?:{placeholder?: ResourceStr, text?: ResourceStr, controller?: TextInputController})
```
- 单行输入框
```ts
TextInput()
```
![zh-cn_image_0000001511580844](figures/zh-cn_image_0000001511580844.png)
- 多行输入框
```ts
TextArea()
```
![zh-cn_image_0000001562940481](figures/zh-cn_image_0000001562940481.png)
多行输入框文字超出一行时会自动折行。
```ts
TextArea({text:"我是TextArea我是TextArea我是TextArea我是TextArea"}).width(300)
```
![zh-cn_image_0000001511580836](figures/zh-cn_image_0000001511580836.png)
## 设置输入框类型
TextInput有5种可选类型,分别为Normal基本输入模式、Password密码输入模式、Email邮箱地址输入模式、Number纯数字输入模式、PhoneNumber电话号码输入模式。通过type属性进行设置:
- 基本输入模式(默认类型)
```ts
TextInput()
.type(InputType.Normal)
```
![zh-cn_image_0000001562820765](figures/zh-cn_image_0000001562820765.png)
- 密码输入模式
```ts
TextInput()
.type(InputType.Password)
```
![zh-cn_image_0000001511580840](figures/zh-cn_image_0000001511580840.png)
## 自定义样式
- 设置无输入时的提示文本。
TextInput({placeholder:'我是提示文本'})
```ts
TextInput({placeholder:'我是提示文本'})
```
![zh-cn_image_0000001511900400](figures/zh-cn_image_0000001511900400.png)
- 设置输入框当前的文本内容。
```ts
TextInput({placeholder:'我是提示文本',text:'我是当前文本内容'})
```
![zh-cn_image_0000001562820761](figures/zh-cn_image_0000001562820761.png)
- 添加backgroundColor改变输入框的背景颜色。
```ts
TextInput({placeholder:'我是提示文本',text:'我是当前文本内容'})
.backgroundColor(Color.Pink)
```
![zh-cn_image_0000001511740444](figures/zh-cn_image_0000001511740444.png)
更丰富的样式可以结合[通用属性](../reference/arkui-ts/ts-universal-attributes-size.md)实现。
## 添加事件
文本框主要用于获取用户输入的信息,把信息处理成数据进行上传,绑定onChange事件可以获取输入框内改变的内容。用户也可以使用通用事件来进行相应的交互操作。
```ts
TextInput()
.onChange((value: string) => {
console.info(value);
})
.onFocus(() => {
console.info('获取焦点');
})
```
## 场景示例
用于表单的提交,在用户登录/注册页面,用户的登录或注册的输入操作。
```ts
@Entry
@Component
struct TextInputSample {
build() {
Column() {
TextInput({ placeholder: 'input your username' }).margin({ top: 20 })
.onSubmit((EnterKeyType)=>{
console.info(EnterKeyType+'输入法回车键的类型值')
})
TextInput({ placeholder: 'input your password' }).type(InputType.Password).margin({ top: 20 })
.onSubmit((EnterKeyType)=>{
console.info(EnterKeyType+'输入法回车键的类型值')
})
Button('Sign in').width(150).margin({ top: 20 })
}.padding(20)
}
}
```
![zh-cn_image_0000001563060653](figures/zh-cn_image_0000001563060653.png)
# 视频播放
Video组件用于播放视频文件并控制其播放状态,常用于为短视频应用和应用内部视频的列表页面。当视频完整出现时会自动播放,用户点击视频区域则会暂停播放,同时显示播放进度条,通过拖动播放进度条指定视频播放到具体位置。具体用法请参考[Video](../reference/arkui-ts/ts-media-components-video.md)
## 创建视频组件
Video通过调用接口来创建,接口调用形式如下:
```ts
Video(value: {src?: string | Resource, currentProgressRate?: number | string | PlaybackSpeed, previewUri?: string | PixelMap | Resource, controller?: VideoController})
```
该接口用于创建视频播放组件。其中,src指定视频播放源的路径,加载方式请参考[加载视频资源](#加载视频资源),currentProgressRate用于设置视频播放倍速,previewUri指定视频未播放时的预览图片路径,controller设置视频控制器,用于自定义控制视频。
## 加载视频资源
Video组件支持加载本地视频和网络视频。
### 加载本地视频
- 普通本地视频。
加载本地视频时,首先在本地rawfile目录指定对应的文件,如下图所示。
![zh-cn_image_0000001562700409](figures/zh-cn_image_0000001562700409.png)
再使用资源访问符$rawfile()引用视频资源。
```ts
@Component
export struct VideoPlayer{
private controller:VideoController;
private previewUris: Resource = $r ('app.media.preview');
private innerResource: Resource = $rawfile('videoTest.mp4');
build(){
Column() {
Video({
src: this.innerResource,
previewUri: this.previewUris,
controller: this.controller
})
}
}
}
```
- [Data Ability](../application-models/dataability-overview.md)提供的视频路径带有dataability://前缀,使用时确保对应视频资源存在即可。
```ts
@Component
export struct VideoPlayer{
private controller:VideoController;
private previewUris: Resource = $r ('app.media.preview');
private videosrc: string= 'dataability://device_id/com.domainname.dataability.videodata/video/10'
build(){
Column() {
Video({
src: this.videosrc,
previewUri: this.previewUris,
controller: this.controller
})
}
}
}
```
### 加载网络视频
加载网络视频时,需要申请权限ohos.permission.INTERNET,具体申请方式请参考[权限申请声明](../security/accesstoken-guidelines.md)。此时,Video的src属性为网络视频的链接。
```ts
@Component
export struct VideoPlayer{
private controller:VideoController;
private previewUris: Resource = $r ('app.media.preview');
private videosrc: string= 'https://www.example.com/example.mp4' // 使用时请替换为实际视频加载网址
build(){
Column() {
Video({
src: this.videosrc,
previewUri: this.previewUris,
controller: this.controller
})
}
}
}
```
## 添加属性
Video组件[属性](../reference/arkui-ts/ts-media-components-video.md#属性)主要用于设置视频的播放形式。例如设置视频播放是否静音、播放时是否显示控制条等。
```ts
@Component
export struct VideoPlayer {
private controller: VideoController;
build() {
Column() {
Video({
controller: this.controller
})
.muted(false) //设置是否静音
.controls(false) //设置是否显示默认控制条
.autoPlay(false) //设置是否自动播放
.loop(false) //设置是否循环播放
.objectFit(ImageFit.Contain) //设置视频适配模式
}
}
}
```
## 事件调用
Video组件回调事件主要为播放开始、暂停结束、播放失败、视频准备和操作进度条等事件,除此之外,Video组件也支持通用事件的调用,如点击、触摸等事件的调用。详细的事件请参考[事件说明](../reference/arkui-ts/ts-media-components-video.md#事件)
```ts
@Entry
@Component
struct VideoPlayer{
private controller:VideoController;
private previewUris: Resource = $r ('app.media.preview');
private innerResource: Resource = $rawfile('videoTest.mp4');
build(){
Column() {
Video({
src: this.innerResource,
previewUri: this.previewUris,
controller: this.controller
})
.onUpdate((event) => { //更新事件回调
console.info("Video update.");
})
.onPrepared((event) => { //准备事件回调
console.info("Video prepared.");
})
.onError(() => { //失败事件回调
console.info("Video error.");
})
}
}
}
```
## Video控制器使用
Video控制器主要用于控制视频的状态,包括播放、暂停、停止以及设置进度等,详细的使用请参考[VideoController使用说明](../reference/arkui-ts/ts-media-components-video.md#videocontroller)
- 默认控制器
默认的控制器支持视频的开始、暂停、进度调整、全屏显示四项基本功能。
```ts
@Entry
@Component
struct VideoGuide {
@State videoSrc: Resource = $rawfile('videoTest.mp4')
@State previewUri: string = 'common/videoIcon.png'
@State curRate: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X
build() {
Row() {
Column() {
Video({
src: this.videoSrc,
previewUri: this.previewUri,
currentProgressRate: this.curRate
})
}
.width('100%')
}
.height('100%')
}
}
```
- 自定义控制器
使用自定义的控制器,先将默认控制器关闭掉,之后可以使用button以及slider等组件进行自定义的控制与显示,适合自定义较强的场景下使用。
```ts
@Entry
@Component
struct VideoGuide {
@State videoSrc: Resource = $rawfile('videoTest.mp4')
@State previewUri: string = 'common/videoIcon.png'
@State curRate: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X
@State isAutoPlay: boolean = false
@State showControls: boolean = true
@State sliderStartTime: string = '';
@State currentTime: number = 0;
@State durationTime: number = 0;
@State durationStringTime: string ='';
controller: VideoController = new VideoController()
build() {
Row() {
Column() {
Video({
src: this.videoSrc,
previewUri: this.previewUri,
currentProgressRate: this.curRate,
controller: this.controller
}).controls(false).autoPlay(true)
.onPrepared((event)=>{
this.durationTime = event.duration
})
.onUpdate((event)=>{
this.currentTime =event.time
})
Row() {
Text(JSON.stringify(this.currentTime) + 's')
Slider({
value: this.currentTime,
min: 0,
max: this.durationTime
})
.onChange((value: number, mode: SliderChangeMode) => {
this.controller.setCurrentTime(value);
}).width("90%")
Text(JSON.stringify(this.durationTime) + 's')
}
.opacity(0.8)
.width("100%")
}
.width('100%')
}
.height('40%')
}
}
```
## 其他说明
Video组件已经封装好了视频播放的基础能力,开发者无需进行视频实例的创建,视频信息的设置获取,只需要设置数据源以及基础信息即可播放视频,相对扩展能力较弱。如果开发者想自定义视频播放,还请参考[媒体系统播放音视频](../media/video-playback.md)
# XComponent
[XComponent](../reference/arkui-ts/ts-basic-components-xcomponent.md)组件作为一种绘制组件,通常用于满足开发者较为复杂的自定义绘制需求,例如相机预览流的显示和游戏画面的绘制。
其可通过指定其type字段来实现不同的功能,主要有两个“surface”和“component”字段可供选择。
对于“surface”类型,开发者可将相关数据传入XComponent单独拥有的“surface”来渲染画面。
对于“component”类型则主要用于实现动态加载显示内容的目的。
## surface类型
XComponent设置为surface类型时通常用于EGL/OpenGLES和媒体数据写入,并将其显示在XComponent组件上。
设置为“surface“类型时XComponent组件可以和其他组件一起进行布局和渲染。
同时XComponent又拥有单独的“surface“,可以为开发者在native侧提供native window用来创建EGL/OpenGLES环境,进而使用标准的OpenGL ES开发。
除此之外,媒体相关应用(视频、相机等)也可以将相关数据写入XComponent所提供的surface,从而实现呈现相应画面。
## 使用EGL/OpenGLES渲染
### native侧代码开发要点
OpenHarmony的应用如果要通过js来桥接native,一般需要使用napi接口来处理js交互,XComponent同样不例外,具体使用请参考[Native API在应用工程中的使用指导](../napi/napi-guidelines.md)
Native侧处理js逻辑的文件类型为so:
- 每个模块对应一个so
- so的命名规则为 lib{模块名}.so
对于使用XComponent进行标准OpenGL ES开发的场景,CMAKELists.txt文件内容大致如下:
```
cmake_minimum_required(VERSION 3.4.1)
project(XComponent) # 项目名称
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
# 头文件查找路径
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include
)
# 编译目标so,SHARED表示动态库
add_library(nativerender SHARED
xxx.cpp
)
# 查找相关库 (包括OpenGL ES相关库和XComponent提供的ndk接口)
find_library( EGL-lib
EGL )
find_library( GLES-lib
GLESv3 )
find_library( libace-lib
ace_ndk.z )
# 编译so所需要的依赖
target_link_libraries(nativerender PUBLIC ${EGL-lib} ${GLES-lib} ${libace-lib} libace_napi.z.so libc++.a)
```
### Napi模块注册
```c++
static napi_value Init(napi_env env, napi_value exports)
{
// 定义暴露在模块上的方法
napi_property_descriptor desc[] ={
DECLARE_NAPI_FUNCTION("changeColor", PluginRender::NapiChangeColor),
};
// 通过此接口开发者可在exports上挂载native方法(即上面的PluginRender::NapiChangeColor),exports会通过js引擎绑定到js层的一个js对象
NAPI_CALL(env, napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc));
return exports;
}
static napi_module nativerenderModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init, // 指定加载对应模块时的回调函数
.nm_modname = "nativerender", // 指定模块名称,对于XComponent相关开发,这个名称必须和ArkTS侧XComponent中libraryname的值保持一致
.nm_priv = ((void*)0),
.reserved = { 0 },
};
extern "C" __attribute__((constructor)) void RegisterModule(void)
{
// 注册so模块
napi_module_register(&nativerenderModule);c
}
```
### 解析XComponent组件的NativeXComponent实例
NativeXComponent为XComponent提供了在native层的实例,可作为js层和native层XComponent绑定的桥梁。XComponent所提供的的NDK接口都依赖于该实例。具体NKD接口可参考[Native XComponent](../reference/native-apis/_o_h___native_x_component.md)
可以在模块被加载时的回调内(即[Napi模块注册](#napi模块注册)中的Init函数)解析获得NativeXComponent实例
```c++
{
// ...
napi_status status;
napi_value exportInstance = nullptr;
OH_NativeXComponent *nativeXComponent = nullptr;
// 用来解析出被wrap了NativeXComponent指针的属性
status = napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &exportInstance);
if (status != napi_ok) {
return false;
}
// 通过napi_unwrap接口,解析出NativeXComponent的实例指针
status = napi_unwrap(env, exportInstance, reinterpret_cast<void**>(&nativeXComponent));
// ...
}
```
### 注册XComponent事件回调
依赖[解析XComponent组件的NativeXComponent实例](#解析xcomponent组件的nativexcomponent实例)拿到的NativeXComponent指针,通过OH_NativeXComponent_RegisterCallback接口进行回调注册
```c++
{
...
OH_NativeXComponent *nativeXComponent = nullptr;
// 解析出NativeXComponent实例
OH_NativeXComponent_Callback callback;
callback->OnSurfaceCreated = OnSurfaceCreatedCB; // surface创建成功后触发,开发者可以从中获取native window的句柄
callback->OnSurfaceChanged = OnSurfaceChangedCB; // surface发生变化后触发,开发者可以从中获取native window的句柄以及XComponent的变更信息
callback->OnSurfaceDestroyed = OnSurfaceDestroyedCB; // surface销毁时触发,开发者可以在此释放资源
callback->DispatchTouchEvent = DispatchTouchEventCB; // XComponent的touch事件回调接口,开发者可以从中获得此次touch事件的信息
OH_NativeXComponent_RegisterCallback(nativeXComponent, callback);
...
}
```
### 创建EGL/OpenGLES环境
在注册的OnSurfaceCreated回调中开发者能拿到native window的句柄(其本质就是XComponent所单独拥有的surface),因此可以在这里创建应用自己的EGL/OpenGLES开发环境,由此开始具体渲染逻辑的开发。
```c++
EGLCore* eglCore_; // EGLCore为封装了OpenGL相关接口的类
uint64_t width_;
uint64_t height_;
void OnSurfaceCreatedCB(OH_NativeXComponent* component, void* window)
{
int32_t ret = OH_NativeXComponent_GetXComponentSize(component, window, &width_, &height_);
if (ret === OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
eglCore_->GLContextInit(window, width_, height_); // 初始化OpenGL环境
}
}
```
### ArkTS侧语法介绍
开发者在ArkTS侧使用如下代码即可用XComponent组件进行利用EGL/OpenGLES渲染的开发。
```ts
XComponent({ id: 'xcomponentId1', type: 'surface', libraryname: 'nativerender' })
.onLoad((context) => {})
.onDestroy(() => {})
```
- id : 与XComponent组件为一一对应关系,不可重复。通常开发者可以在native侧通过OH_NativeXComponent_GetXComponentId接口来获取对应的id从而绑定对应的XComponent。
- libraryname:加载模块的名称,必须与在native侧Napi模块注册时nm_modname的名字一致。
>**说明:**
>
> 应用加载模块实现跨语言调用有两种方式:
>
> 1. 使用NAPI的import方式加载:
>
> ```ts
> import nativerender from "libnativerender.so"
> ```
>
> 2. 使用XComponent组件加载,本质也是使用了NAPI机制来加载。
> 该加载方式和import加载方式的区别在于,在加载动态库是会将XComponent的NativeXComponent实例暴露到应用的native层中,从而让开发者可以使用XComponent的NDK接口。
- onLoad事件
- 触发时刻:XComponent准备好surface后触发。
- 参数context:其上面挂载了暴露在模块上的native方法,使用方法类似于利用 import context2 from "libnativerender.so" 直接加载模块后获得的context2实例。
- 时序:onLoad事件的触发和Surface相关,其和native侧的OnSurfaceCreated的时序如下图:
![图片2](figures/图片2.png)
- onDestroy事件
触发时刻:XComponent组件被销毁时触发与一般ArkUI的组件销毁时机一致,其和native侧的OnSurfaceDestroyed的时序如下图:
![图片3](figures/图片3.png)
### 媒体数据写入
XComponent所持有的Surface符合“生产者-消费者”模型
OpenHarmony上Camera、VideoPlayer等符合生产者设计的部件都可以将数据写入XComponent持有的surface并通过XComponent显示。
![图片1](figures/图片1.png)
开发者可通过绑定XComponentController获得对应XComponent的surfaceId(该id可以唯一确定一个surface),从而传给相应的部件接口。
```ts
@State surfaceId:string = "";
mXComponentController: XComponentController = new XComponentController();
XComponent({ id: '', type: 'surface', controller: this.mXComponentController })
.onLoad(() => {
this.surfaceId = this.mXComponentController.getXComponentSurfaceId()
})
```
具体部件接口可参考:[AVPlayer](../reference/apis/js-apis-media.md#avplayer9)[Camera](../reference/apis/js-apis-camera.md) 等。
### component类型
XComponent设置为component类型时通常用于在XComponent内部执行非UI逻辑以实现动态加载显示内容的目的。
>**说明:**
>
> type为"component"时,XComponent作为容器,子组件沿垂直方向布局:
>
> - 垂直方向上对齐格式:[FlexAlign](../reference/arkui-ts/ts-appendix-enums.md#flexalign).Start
>
> - 水平方向上对齐格式:[FlexAlign](../reference/arkui-ts/ts-appendix-enums.md#flexalign).Center
>
> 不支持所有的事件响应。
>
> 布局方式更改和事件响应均可通过挂载子组件来设置。
>
> 内部所写的非UI逻辑需要封装在一个或多个函数内。
### 场景示例
```ts
@Builder
function addText(label: string): void {
Text(label)
.fontSize(40)
}
@Entry
@Component
struct Index {
@State message: string = 'Hello XComponent'
@State messageCommon: string = 'Hello World'
build() {
Row() {
Column() {
XComponent({ id: 'xcomponentId-container', type: 'component' }) {
addText(this.message)
Divider()
.margin(4)
.strokeWidth(2)
.color('#F1F3F5')
.width("80%")
Column() {
Text(this.messageCommon)
.fontSize(30)
}
}
}
.width('100%')
}
.height('100%')
}
}
```
![zh-cn_image_0000001511900428](figures/zh-cn_image_0000001511900428.png)
# 键鼠事件
键鼠事件指键盘,鼠标外接设备的输入事件。
## 鼠标事件
支持的鼠标事件包含通过外设鼠标、触控板触发的事件。
鼠标事件可触发以下回调:
| 名称 | 描述 |
| ---------------------------------------- | ---------------------------------------- |
| onHover(event:&nbsp;(isHover:&nbsp;boolean)&nbsp;=&gt;&nbsp;void) | 鼠标进入或退出组件时触发该回调。<br/>isHover:表示鼠标是否悬浮在组件上,鼠标进入时为true,&nbsp;退出时为false。 |
| onMouse(event:&nbsp;(event?:&nbsp;MouseEvent)&nbsp;=&gt;&nbsp;void) | 当前组件被鼠标按键点击时或者鼠标在组件上悬浮移动时,触发该回调,event返回值包含触发事件时的时间戳、鼠标按键、动作、鼠标位置在整个屏幕上的坐标和相对于当前组件的坐标。 |
当组件绑定onHover回调时,可以通过[hoverEffect](../reference/arkui-ts/ts-universal-attributes-hover-effect.md)属性设置该组件的鼠标悬浮态显示效果。
**图1** 鼠标事件数据流  
![zh-cn_image_0000001511900504](figures/zh-cn_image_0000001511900504.png)
鼠标事件传递到ArkUI之后,会先判断鼠标事件是否是左键的按下/抬起/移动,然后做出不同响应:
- 是:鼠标事件先转换成相同位置的触摸事件,执行触摸事件的碰撞测试、手势判断和回调响应。接着去执行鼠标事件的碰撞测试和回调响应。
- 否:事件仅用于执行鼠标事件的碰撞测试和回调响应。
>**说明:**
>
>所有单指可响应的触摸事件/手势事件,均可通过鼠标左键来操作和响应。例如当我们需要开发单击Button跳转页面的功能、且需要支持手指点击和鼠标左键点击,那么只绑定一个点击事件(onClick)就可以实现该效果。若需要针对手指和鼠标左键的点击实现不一样的效果,可以在onClick回调中,使用回调参数中的source字段即可判断出当前触发事件的来源是手指还是鼠标。
### onHover
```ts
onHover(event: (isHover?: boolean) => void)
```
鼠标悬浮事件回调。参数isHover类型为boolean,表示鼠标进入组件或离开组件。该事件不支持自定义冒泡设置,默认父子冒泡。
若组件绑定了该接口,当鼠标指针从组件外部进入到该组件的瞬间会触发事件回调,参数isHover等于true;鼠标指针离开组件的瞬间也会触发该事件回调,参数isHover等于false。
>**说明:**
>
>事件冒泡:在一个树形结构中,当子节点处理完一个事件后,再将该事件交给它的父节点处理。
```ts
// xxx.ets
@Entry
@Component
struct MouseExample {
@State isHovered: boolean = false;
build() {
Column() {
Button(this.isHovered ? 'Hovered!' : 'Not Hover')
.width(200).height(100)
.backgroundColor(this.isHovered ? Color.Green : Color.Gray)
.onHover((isHover: boolean) => { // 使用onHover接口监听鼠标是否悬浮在Button组件上
this.isHovered = isHover;
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
```
该示例创建了一个Button组件,初始背景色为灰色,内容为“Not Hover”。示例中的Button组件绑定了onHover回调,在该回调中将this.isHovered变量置为回调参数:isHover。
当鼠标从Button外移动到Button内的瞬间,回调响应,isHover值等于true,isHovered的值变为true,将组件的背景色改成Color.Green,内容变为“Hovered!”。
当鼠标从Button内移动到Button外的瞬间,回调响应,isHover值等于false,又将组件变成了初始的样式。
![onHover](figures/onHover.gif)
### onMouse
```ts
onMouse(event: (event?: MouseEvent) => void)
```
鼠标事件回调。绑定该API的组件每当鼠标指针在该组件内产生行为(MouseAction)时,触发事件回调,参数为[MouseEvent](../reference/arkui-ts/ts-universal-mouse-key.md)对象,表示触发此次的鼠标事件。该事件支持自定义冒泡设置,默认父子冒泡。常见用于开发者自定义的鼠标行为逻辑处理。
开发者可以通过回调中的MouseEvent对象获取触发事件的坐标(screenX/screenY/x/y)、按键([MouseButton](../reference/arkui-ts/ts-appendix-enums.md#mousebutton))、行为([MouseAction](../reference/arkui-ts/ts-appendix-enums.md#mouseaction))、时间戳(timestamp)、交互组件的区域([EventTarget](../reference/arkui-ts/ts-universal-events-click.md))、事件来源([SourceType](../reference/arkui-ts/ts-gesture-settings.md))等。MouseEvent的回调函数stopPropagation用于设置当前事件是否阻止冒泡。
>**说明:**
>
>按键(MouseButton)的值:Left/Right/Middle/Back/Forward 均对应鼠标上的实体按键,当这些按键被按下或松开时触发这些按键的事件。None表示无按键,会出现在鼠标没有按键按下或松开的状态下,移动鼠标所触发的事件中。
```ts
// xxx.ets
@Entry
@Component
struct MouseExample {
@State isHovered: boolean = false;
@State buttonText: string = '';
@State columnText: string = '';
build() {
Column() {
Button(this.isHovered ? 'Hovered!' : 'Not Hover')
.width(200)
.height(100)
.backgroundColor(this.isHovered ? Color.Green : Color.Gray)
.onHover((isHover: boolean) => {
this.isHovered = isHover
})
.onMouse((event: MouseEvent) => { // 给Button组件设置onMouse回调
this.buttonText = 'Button onMouse:\n' + '' +
'button = ' + event.button + '\n' +
'action = ' + event.action + '\n' +
'x,y = (' + event.x + ',' + event.y + ')' + '\n' +
'screenXY=(' + event.screenX + ',' + event.screenY + ')';
})
Divider()
Text(this.buttonText).fontColor(Color.Green)
Divider()
Text(this.columnText).fontColor(Color.Red)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Red)
.onMouse((event: MouseEvent) => { // 给Column组件设置onMouse回调
this.columnText = 'Column onMouse:\n' + '' +
'button = ' + event.button + '\n' +
'action = ' + event.action + '\n' +
'x,y = (' + event.x + ',' + event.y + ')' + '\n' +
'screenXY=(' + event.screenX + ',' + event.screenY + ')';
})
}
}
```
在onHover示例的基础上,给Button绑定onMouse接口。在回调中,打印出鼠标事件的button/action等回调参数值。同时,在外层的Column容器上,也做相同的设置。整个过程可以分为以下两个动作:
1. 移动鼠标:当鼠标从Button外部移入Button的过程中,仅触发了Column的onMouse回调;当鼠标移入到Button内部后,由于onMouse事件默认是冒泡的,所以此时会同时响应Column的onMouse回调和Button的onMouse回调。此过程中,由于鼠标仅有移动动作没有点击动作,因此打印信息中的button均为0(MouseButton.None的枚举值)、action均为3(MouseAction.Move的枚举值)。
2. 点击鼠标:鼠标进入Button后进行了2次点击,分别是左键点击和右键点击。
左键点击时:button = 1(MouseButton.Left的枚举值),按下时 action = 1(MouseAction.Press的枚举值),抬起时 action = 2(MouseAction.Release的枚举值)。
右键点击时:button = 2(MouseButton.Right的枚举值),按下时 action = 1(MouseAction.Press的枚举值),抬起时 action = 2(MouseAction.Release的枚举值)。
![onMouse1](figures/onMouse1.gif)
如果需要阻止鼠标事件冒泡,可以通过调用stopPropagation()方法进行设置。
```ts
Button(this.isHovered ? 'Hovered!' : 'Not Hover')
.width(200)
.height(100)
.backgroundColor(this.isHovered ? Color.Green : Color.Gray)
.onHover((isHover: boolean) => {
this.isHovered = isHover;
})
.onMouse((event: MouseEvent) => {
event.stopPropagation(); // 在Button的onMouse事件中设置阻止冒泡
this.buttonText = 'Button onMouse:\n' + '' +
'button = ' + event.button + '\n' +
'action = ' + event.action + '\n' +
'x,y = (' + event.x + ',' + event.y + ')' + '\n' +
'screenXY=(' + event.screenX + ',' + event.screenY + ')';
})
```
在子组件(Button)的onMouse中,通过回调参数event调用stopPropagation回调方法(如下)即可阻止Button子组件的鼠标事件冒泡到父组件Column上。
```ts
event.stopPropagation()
```
效果是:当鼠标在Button组件上操作时,仅Button的onMouse回调会响应,Column的onMouse回调不会响应。
### hoverEffect
```ts
hoverEffect(value: HoverEffect)
```
鼠标悬浮态效果设置的通用属性。参数类型为HoverEffect,HoverEffect提供的Auto、Scale、Highlight效果均为固定效果,开发者无法自定义设置效果参数。
**表1** HoverEffect说明
| HoverEffect枚举值 | 效果说明 |
| -------------- | ---------------------------------------- |
| Auto | 组件默认提供的悬浮态效果,由各组件定义。 |
| Scale | 动画播放方式,鼠标悬浮时:组件大小从100%放大至105%,鼠标离开时:组件大小从105%缩小至100%。 |
| Highlight | 动画播放方式,鼠标悬浮时:组件背景色叠加一个5%透明度的白色,视觉效果是组件的原有背景色变暗,鼠标离开时:组件背景色恢复至原有样式。 |
| None | 禁用悬浮态效果 |
```ts
// xxx.ets
@Entry
@Component
struct HoverExample {
build() {
Column({ space: 10 }) {
Button('Auto')
.width(170).height(70)
Button('Scale')
.width(170).height(70)
.hoverEffect(HoverEffect.Scale)
Button('Highlight')
.width(170).height(70)
.hoverEffect(HoverEffect.Highlight)
Button('None')
.width(170).height(70)
.hoverEffect(HoverEffect.None)
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
```
![hoverEffect](figures/hoverEffect.gif)
Button默认的悬浮态效果就是缩放效果,因此Auto和Scale的效果一样,Highlight会使背板颜色变暗,None会禁用悬浮态效果。
## 按键事件
**图2** 按键事件数据流  
![zh-cn_image_0000001511580944](figures/zh-cn_image_0000001511580944.png)
按键事件由外设键盘等设备触发,经驱动和多模处理转换后发送给当前获焦的窗口。窗口获取到事件后,会先给输入法分发(输入法会消费按键用作输入),若输入法未消费该按键事件,才会将事件发给ArkUI框架。因此,当某输入框组件获焦,且打开了输入法,此时大部分按键事件均会被输入法消费,例如字母键会被输入法用来往输入框中输入对应字母字符、方向键会被输入法用来切换选中备选词。
按键事件到ArkUI框架之后,会先找到完整的父子节点获焦链。从叶子节点到根节点,逐一发送按键事件。
### onKeyEvent
```ts
onKeyEvent(event: (event?: KeyEvent) => void)
```
按键事件回调,当绑定该方法的组件处于[获焦状态](arkts-common-events-focus-event.md)下,外设键盘的按键事件会触发该API的回调响应,回调参数为[KeyEvent](../reference/arkui-ts/ts-universal-events-key.md),可由该参数获得当前按键事件的按键行为([KeyType](../reference/arkui-ts/ts-appendix-enums.md#keytype))、键码([keyCode](../reference/apis/js-apis-keycode.md))、按键英文名称(keyText)、事件来源设备类型([KeySource](../reference/arkui-ts/ts-appendix-enums.md#keysource))、事件来源设备id(deviceId)、元键按压状态(metaKey)、时间戳(timestamp)、阻止冒泡设置(stopPropagation)。
```ts
// xxx.ets
@Entry
@Component
struct KeyEventExample {
@State buttonText: string = '';
@State buttonType: string = '';
@State columnText: string = '';
@State columnType: string = '';
build() {
Column() {
Button('onKeyEvent')
.width(140).height(70)
.onKeyEvent((event: KeyEvent) => { // 给Button设置onKeyEvent事件
if (event.type === KeyType.Down) {
this.buttonType = 'Down';
}
if (event.type === KeyType.Up) {
this.buttonType = 'Up';
}
this.buttonText = 'Button: \n' +
'KeyType:' + this.buttonType + '\n' +
'KeyCode:' + event.keyCode + '\n' +
'KeyText:' + event.keyText;
})
Divider()
Text(this.buttonText).fontColor(Color.Green)
Divider()
Text(this.columnText).fontColor(Color.Red)
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
.onKeyEvent((event: KeyEvent) => { // 给父组件Column设置onKeyEvent事件
if (event.type === KeyType.Down) {
this.columnType = 'Down';
}
if (event.type === KeyType.Up) {
this.columnType = 'Up';
}
this.columnText = 'Column: \n' +
'KeyType:' + this.buttonType + '\n' +
'KeyCode:' + event.keyCode + '\n' +
'KeyText:' + event.keyText;
})
}
}
```
上述示例中给组件Button和其父容器Column绑定onKeyEvent。应用打开页面加载后,组件树上第一个可获焦的非容器组件自动获焦,该应用只有一个Button组件,因此该组件会自动获焦,由于Button是Column的子节点,Button获焦也同时意味着Column获焦。获焦机制见[焦点事件](arkts-common-events-focus-event.md)
![zh-cn_image_0000001511421324](figures/zh-cn_image_0000001511421324.gif)
打开应用后,依次在键盘上按这些按键:“空格、回车、左Ctrl、左Shift、字母A、字母Z”。
1. 由于onKeyEvent事件默认是冒泡的,所以Button和Column的onKeyEvent都可以响应。
2. 每个按键都有2次回调,分别对应KeyType.Down和KeyType.Up,表示按键被按下、然后抬起。
如果要阻止冒泡,即仅Button响应键盘事件,Column不响应,在Button的onKeyEvent回调中加入event.stopPropagation()方法即可,如下:
```ts
Button('onKeyEvent')
.width(140).height(70)
.onKeyEvent((event: KeyEvent) => {
// 通过stopPropagation阻止事件冒泡
event.stopPropagation();
if (event.type === KeyType.Down) {
this.buttonType = 'Down';
}
if (event.type === KeyType.Up) {
this.buttonType = 'Up';
}
this.buttonText = 'Button: \n' +
'KeyType:' + this.buttonType + '\n' +
'KeyCode:' + event.keyCode + '\n' +
'KeyText:' + event.keyText;
})
```
![zh-cn_image_0000001511900508](figures/zh-cn_image_0000001511900508.gif)
# 焦点事件(毕雪峰 00579046)
## 基本概念
- 焦点
指向当前应用界面上唯一的一个可交互元素,当用户使用键盘、电视遥控器、车机摇杆/旋钮等非指向性输入设备与应用程序进行间接交互时,基于焦点的导航和交互是重要的输入手段。
- 默认焦点
应用打开或切换页面后,若当前页上存在可获焦的组件,则树形结构的组件树中第一个可获焦的组件默认获得焦点。可以使用[自定义默认焦点](#自定义默认焦点)进行自定义指定。
- 获焦
指组件获得了焦点,同一时刻,应用中最多只有1个末端组件是获焦的,且此时它的所有祖宗组件(整个组件链)均是获焦的。当期望某个组件获焦,须确保该组件及其所有的祖宗节点均是可获焦的([focusable](#设置组件是否获焦)属性为true)。
- 失焦
指组件从获焦状态变成了非获焦状态,失去了焦点。组件失焦时,它的所有祖宗组件(失焦组件链)与新的获焦组件链不相同的节点都会失焦。
- 走焦
表示焦点在当前应用中转移的过程,走焦会带来原焦点组件的失焦和新焦点组件的获焦。应用中焦点发生变化的方式按行为可分为两类:
- 主动走焦:指开发者/用户主观的行为导致焦点移动,包含:外接键盘上按下TAB/方向键、使用[requestFocus](#focuscontrolrequestfocus)主动给指定组件申请焦点、组件[focusOnTouch](#focusontouch)属性为true后点击组件。
- 被动走焦:指组件焦点因其他操作被动的转移焦点,此特性为焦点系统默认行为,无法由开发者自由设定,例如当使用if-else语句将处于获焦的组件删除/将处于获焦的组件(或其父组件)置成不可获焦时、当页面切换时。
- 焦点态
获焦组件的样式,不同组件的焦点态样式大同小异,默认情况下焦点态不显示,仅使用外接键盘按下TAB键/方向键时才会触发焦点态样式出现。首次触发焦点态显示的TAB键/方向键不会触发走焦。当应用接收到点击事件时(包括手指触屏的按下事件和鼠标左键的按下事件),自动隐藏焦点态样式。焦点态样式由后端组件定义,开发者无法修改。
## 走焦规则
走焦规则是指用户使用“TAB键/SHIFT+TAB键/方向键”主动进行走焦,或焦点系统在执行被动走焦时的顺序规则。组件的走焦规则默认由走焦系统定义,由焦点所在的容器决定。
- 线性走焦:常见的容器有Flex、Row、Column、List,这些都是典型的单方向容器,组件在这些容器内的排列都是线性的,那么走焦规则也是线性的。走焦的方向和方向键的方向一致。
**图1** 线性走焦示意图  
![zh-cn_image_0000001562700537](figures/zh-cn_image_0000001562700537.png)
例如Row容器,使用方向键左右(←/→)即可将焦点在相邻的2个可获焦组件之间来回切换。
- 十字走焦:使用方向键上(↑)下(↓)左(←)右(→)可以使焦点在相邻的组件上切换。典型的是Grid容器,如下图:
**图2** Grid组件十字走焦示意图  
![zh-cn_image_0000001511740580](figures/zh-cn_image_0000001511740580.png)
>**说明:**
> - TAB/SHIFT+TAB键在以上两种走焦规则上的功能和方向键一致。TAB键等同于“先执行方向键右,若无法走焦,再执行方向键下”,SHIFT+TAB键等同于“先执行方向键左,若无法走焦,再执行方向键上”。
>
> - 触发走焦的按键是按下的事件(DOWN事件)。
>
> - 删除组件、设置组件无法获焦后,会使用线性走焦规则,自动先往被删除/Unfocusable组件的前置兄弟组件上走焦,无法走焦的话,再忘后置兄弟组件上走焦。
- tabIndex走焦:给组件设置[tabIndex](../reference/arkui-ts/ts-universal-attributes-focus.md)通用属性,自定义组件的TAB键/SHIFT+TAB键的走焦顺序。
- 区域走焦:给容器组件设置tabIndex通用属性,再结合[groupDefaultFocus](#groupdefaultfocus)通用属性,自定义容器区域的TAB键/SHIFT+TAB键的走焦顺序和默认获焦组件。
- 走焦至容器组件规则:当焦点走焦到容器(该容器没有配置groupDefaultFocus)上时,若该容器组件为首次获焦,则会先计算目标容器组件的子组件的区域位置,得到距离目标容器中心点最近的子组件,焦点会走到目标容器上的该子组件上。若该容器非首次获焦,焦点会自动走焦到上一次目标容器中获焦的子组件。
- 焦点交互:当某组件获焦时,该组件的固有点击任务或开发者绑定的onClick回调任务,会自动挂载到空格/回车按键上,当按下按键时,任务就和手指/鼠标点击一样被执行。
>**说明:**
>
>本文涉及到的焦点均为组件焦点,另外一个焦点的概念是:窗口焦点,指向当前获焦的窗口。当窗口失焦时,该窗口应用中的所有获焦组件全部失焦。
## 监听组件的焦点变化
```ts
onFocus(event: () => void)
```
获焦事件回调,绑定该API的组件获焦时,回调响应。
```ts
onBlur(event:() => void)
```
失焦事件回调,绑定该API的组件失焦时,回调响应。
onFocus和onBlur两个接口通常成对使用,来监听组件的焦点变化。
以下示例代码展示获焦/失焦回调的使用方法:
```ts
// xxx.ets
@Entry
@Component
struct FocusEventExample {
@State oneButtonColor: Color = Color.Gray;
@State twoButtonColor: Color = Color.Gray;
@State threeButtonColor: Color = Color.Gray;
build() {
Column({ space: 20 }) {
// 通过外接键盘的上下键可以让焦点在三个按钮间移动,按钮获焦时颜色变化,失焦时变回原背景色
Button('First Button')
.width(260)
.height(70)
.backgroundColor(this.oneButtonColor)
.fontColor(Color.Black)
// 监听第一个组件的获焦事件,获焦后改变颜色
.onFocus(() => {
this.oneButtonColor = Color.Green;
})
// 监听第一个组件的失焦事件,失焦后改变颜色
.onBlur(() => {
this.oneButtonColor = Color.Gray;
})
Button('Second Button')
.width(260)
.height(70)
.backgroundColor(this.twoButtonColor)
.fontColor(Color.Black)
// 监听第二个组件的获焦事件,获焦后改变颜色
.onFocus(() => {
this.twoButtonColor = Color.Green;
})
// 监听第二个组件的失焦事件,失焦后改变颜色
.onBlur(() => {
this.twoButtonColor = Color.Grey;
})
Button('Third Button')
.width(260)
.height(70)
.backgroundColor(this.threeButtonColor)
.fontColor(Color.Black)
// 监听第三个组件的获焦事件,获焦后改变颜色
.onFocus(() => {
this.threeButtonColor = Color.Green;
})
// 监听第三个组件的失焦事件,失焦后改变颜色
.onBlur(() => {
this.threeButtonColor = Color.Gray ;
})
}.width('100%').margin({ top: 20 })
}
}
```
![zh-cn_image_0000001511740584](figures/zh-cn_image_0000001511740584.gif)
上述示例包含以下4步:
1. 应用打开时,“First Button”默认获取焦点,onFocus回调响应,背景色变成绿色。
2. 按下TAB键(或方向键下↓),“First Button”显示焦点态样式:组件外围有一个蓝色的闭合框。不触发走焦,焦点仍然在“First Button”上。
3. 按下TAB键(或方向键下↓),触发走焦,“Second Button”获焦,onFocus回调响应,背景色变成绿色;“First Button”失焦、onBlur回调响应,背景色变回灰色。
4. 按下TAB键(或方向键下↓),触发走焦,“Third Button”获焦,onFocus回调响应,背景色变成绿色;“Second Button”失焦、onBlur回调响应,背景色变回灰色。
## 设置组件是否获焦
通过focusable接口设置组件是否可获焦:
```ts
focusable(value: boolean)
```
按照组件的获焦能力可大致分为三类:
- 默认可获焦的组件,通常是有交互行为的组件,例如Button、Checkbox,TextInput组件,此类组件无需设置任何属性,默认即可获焦。
- 有获焦能力,但默认不可获焦的组件,典型的是Text、Image组件,此类组件缺省情况下无法获焦,若需要使其获焦,可使用通用属性focusable(true)使能。
- 无获焦能力的组件,通常是无任何交互行为的展示类组件,例如Blank、Circle组件,此类组件即使使用focusable属性也无法使其可获焦。
>**说明:**
> - focusable为false表示组件不可获焦,同样可以使组件变成不可获焦的还有通用属性[enabled](../reference/arkui-ts/ts-universal-attributes-enable.md)。
>
> - 当某组件处于获焦状态时,将其的focusable属性或enabled属性设置为false,会自动使该组件失焦,然后焦点按照[走焦规则](#走焦规则)将焦点转移给其他组件。
**表1** 基础组件获焦能力
| 基础组件 | 是否有获焦能力 | focusable默认值 | 走焦规则 |
| ---------------------------------------- | ------- | ------------ | -------- |
| [AlphabetIndexer](../reference/arkui-ts/ts-container-alphabet-indexer.md) | 是 | true | 线性走焦 |
| [Blank](../reference/arkui-ts/ts-basic-components-blank.md) | 否 | false | / |
| [Button](../reference/arkui-ts/ts-basic-components-button.md) | 是 | true | / |
| [Checkbox](../reference/arkui-ts/ts-basic-components-checkbox.md) | 是 | true | / |
| [CheckboxGroup](../reference/arkui-ts/ts-basic-components-checkboxgroup.md) | 是 | true | / |
| [DataPanel](../reference/arkui-ts/ts-basic-components-datapanel.md) | 否 | false | / |
| [DatePicker](../reference/arkui-ts/ts-basic-components-datepicker.md) | 是 | true | 线性走焦 |
| [Divider](../reference/arkui-ts/ts-basic-components-divider.md) | 否 | false | / |
| [Formcomponent](../reference/arkui-ts/ts-basic-components-formcomponent.md) | 否 | false | / |
| [Gauge](../reference/arkui-ts/ts-basic-components-gauge.md) | 否 | false | / |
| [Image](../reference/arkui-ts/ts-basic-components-image.md) | 是 | false | / |
| [ImageAnimator](../reference/arkui-ts/ts-basic-components-imageanimator.md) | 是 | false | / |
| [LoadingProgress](../reference/arkui-ts/ts-basic-components-loadingprogress.md) | 否 | false | / |
| [Marquee](../reference/arkui-ts/ts-basic-components-marquee.md) | 否 | false | / |
| [Menu](../reference/arkui-ts/ts-basic-components-menu.md) | 是 | true | 线性走焦 |
| [MenuItem](../reference/arkui-ts/ts-basic-components-menuitem.md) | 是 | true | / |
| [MenuItemGroup](../reference/arkui-ts/ts-basic-components-menuitemgroup.md) | 是 | true | 线性走焦 |
| [Navigation](../reference/arkui-ts/ts-basic-components-navigation.md) | 否 | false | 组件自定义 |
| [NavRouter](../reference/arkui-ts/ts-basic-components-navrouter.md) | 否 | false | 跟随子容器 |
| [NavDestination](../reference/arkui-ts/ts-basic-components-navdestination.md) | 否 | false | 线性走焦 |
| [PatternLock](../reference/arkui-ts/ts-basic-components-patternlock.md) | 否 | false | / |
| [PluginComponent](../reference/arkui-ts/ts-basic-components-plugincomponent.md) | 否 | false | / |
| [Progress](../reference/arkui-ts/ts-basic-components-progress.md) | 否 | false | / |
| [QRCode](../reference/arkui-ts/ts-basic-components-qrcode.md) | 否 | false | / |
| [Radio](../reference/arkui-ts/ts-basic-components-radio.md) | 是 | true | / |
| [Rating](../reference/arkui-ts/ts-basic-components-rating.md) | 是 | true | / |
| [RemoteWindow](../reference/arkui-ts/ts-basic-components-remotewindow.md) | 否 | false | / |
| [RichText](../reference/arkui-ts/ts-basic-components-richtext.md) | 否 | false | / |
| [ScrollBar](../reference/arkui-ts/ts-basic-components-scrollbar.md) | 否 | false | / |
| [Search](../reference/arkui-ts/ts-basic-components-search.md) | 是 | true | / |
| [Select](../reference/arkui-ts/ts-basic-components-select.md) | 是 | true | 线性走焦 |
| [Slider](../reference/arkui-ts/ts-basic-components-slider.md) | 是 | true | / |
| [Span](../reference/arkui-ts/ts-basic-components-span.md) | 否 | false | / |
| [Stepper](../reference/arkui-ts/ts-basic-components-stepper.md) | 是 | true | / |
| [StepperItem](../reference/arkui-ts/ts-basic-components-stepperitem.md) | 是 | true | / |
| [Text](../reference/arkui-ts/ts-basic-components-text.md) | 是 | false | / |
| [TextArea](../reference/arkui-ts/ts-basic-components-textarea.md) | 是 | true | / |
| [TextClock](../reference/arkui-ts/ts-basic-components-textclock.md) | 否 | false | / |
| [TextInput](../reference/arkui-ts/ts-basic-components-textinput.md) | 是 | true | / |
| [TextPicker](../reference/arkui-ts/ts-basic-components-textpicker.md) | 是 | true | 线性走焦 |
| [TextTimer](../reference/arkui-ts/ts-basic-components-texttimer.md) | 否 | false | / |
| [TimePicker](../reference/arkui-ts/ts-basic-components-timepicker.md) | 是 | true | 线性走焦 |
| [Toggle](../reference/arkui-ts/ts-basic-components-toggle.md) | 是 | true | / |
| [Web](../reference/arkui-ts/ts-basic-components-web.md) | 是 | true | Web组件自定义 |
| [XComponent](../reference/arkui-ts/ts-basic-components-xcomponent.md) | 否 | false | / |
**表2** 容器组件获焦能力
| 容器组件 | 是否可获焦 | focusable默认值 | 走焦规则 |
| ---------------------------------------- | ----- | ------------ | -------- |
| [AbilityComponent](../reference/arkui-ts/ts-container-ability-component.md) | 否 | false | / |
| [Badge](../reference/arkui-ts/ts-container-badge.md) | 否 | false | / |
| [Column](../reference/arkui-ts/ts-container-column.md) | 是 | true | 线性走焦 |
| [ColumnSplit](../reference/arkui-ts/ts-container-columnsplit.md) | 是 | true | / |
| [Counter](../reference/arkui-ts/ts-container-counter.md) | 是 | true | 线性走焦 |
| [Flex](../reference/arkui-ts/ts-container-flex.md) | 是 | true | 线性走焦 |
| [GridCol](../reference/arkui-ts/ts-container-gridcol.md) | 是 | true | 容器组件自定义 |
| [GridRow](../reference/arkui-ts/ts-container-gridrow.md) | 是 | true | 容器组件自定义 |
| [Grid](../reference/arkui-ts/ts-container-grid.md) | 是 | true | 容器组件自定义 |
| [GridItem](../reference/arkui-ts/ts-container-griditem.md) | 是 | true | 跟随子组件 |
| [List](../reference/arkui-ts/ts-container-list.md) | 是 | true | 线性走焦 |
| [ListItem](../reference/arkui-ts/ts-container-listitem.md) | 是 | true | 跟随子组件 |
| [ListItemGroup](../reference/arkui-ts/ts-container-listitemgroup.md) | 是 | true | 跟随List组件 |
| [Navigator](../reference/arkui-ts/ts-container-navigator.md) | 否 | true | 容器组件自定义 |
| [Panel](../reference/arkui-ts/ts-container-panel.md) | 否 | true | 跟随子组件 |
| [Refresh](../reference/arkui-ts/ts-container-refresh.md) | 否 | false | / |
| [RelativeContainer](../reference/arkui-ts/ts-container-relativecontainer.md) | 否 | true | 容器组件自定义 |
| [Row](../reference/arkui-ts/ts-container-row.md) | 是 | true | 线性走焦 |
| [RowSplit](../reference/arkui-ts/ts-container-rowsplit.md) | 是 | true | / |
| [Scroll](../reference/arkui-ts/ts-container-scroll.md) | 是 | true | 线性走焦 |
| [SideBarContainer](../reference/arkui-ts/ts-container-sidebarcontainer.md) | 是 | true | 线性走焦 |
| [Stack](../reference/arkui-ts/ts-container-stack.md) | 是 | true | 线性走焦 |
| [Swiper](../reference/arkui-ts/ts-container-swiper.md) | 是 | true | 容器组件自定义 |
| [Tabs](../reference/arkui-ts/ts-container-tabs.md) | 是 | true | 容器组件自定义 |
| [TabContent](../reference/arkui-ts/ts-container-tabcontent.md) | 是 | true | 跟随子组件 |
**表3** 媒体组件获焦能力
| 媒体组件 | 是否可获焦 | focusable默认值 | 走焦规则 |
| ---------------------------------------- | ----- | ------------ | ---- |
| [Video](../reference/arkui-ts/ts-media-components-video.md) | 是 | true | / |
**表4** 画布组件获焦能力
| 画布组件 | 是否可获焦 | focusable默认值 | 走焦规则 |
| ---------------------------------------- | ----- | ------------ | ---- |
| [Canvas](../reference/arkui-ts/ts-components-canvas-canvas.md) | 否 | false | / |
以下示例为大家展示focusable接口的使用方法:
```ts
// xxx.ets
@Entry
@Component
struct FocusableExample {
@State textFocusable: boolean = true;
@State color1: Color = Color.Yellow;
@State color2: Color = Color.Yellow;
build() {
Column({ space: 5 }) {
Text('Default Text') // 第一个Text组件未设置focusable属性,默认不可获焦
.borderColor(this.color1)
.borderWidth(2)
.width(300)
.height(70)
.onFocus(() => {
this.color1 = Color.Blue;
})
.onBlur(() => {
this.color1 = Color.Yellow;
})
Divider()
Text('focusable: ' + this.textFocusable) // 第二个Text设置了focusable属性,初始值为true
.borderColor(this.color2)
.borderWidth(2)
.width(300)
.height(70)
.focusable(this.textFocusable)
.onFocus(() => {
this.color2 = Color.Blue;
})
.onBlur(() => {
this.color2 = Color.Yellow;
})
Divider()
Row() {
Button('Button1')
.width(140).height(70)
Button('Button2')
.width(160).height(70)
}
Divider()
Button('Button3')
.width(300).height(70)
Divider()
}.width('100%').justifyContent(FlexAlign.Center)
.onKeyEvent((e) => { // 绑定onKeyEvent,在该Column组件获焦时,按下'F'键,可将第二个Text的focusable置反
if (e.keyCode === 2022 && e.type === KeyType.Down) {
this.textFocusable = !this.textFocusable;
}
})
}
}
```
运行效果:
![zh-cn_image_0000001511900540](figures/zh-cn_image_0000001511900540.gif)
上述示例包含默认获焦和主动走焦两部分:
**默认获焦:**
- 根据默认焦点的说明,该应用打开后,默认第一个可获焦元素获焦:
- 第一个Text组件没有设置focusable(true)属性,该Text组件无法获焦。
- 第二个Text组件的focusable属性显式设置为true,说明该组件可获焦,那么默认焦点将置到它身上。
**主动走焦:**
按键盘F键,触发onKeyEvent,focusable置为false,Text组件变成不可获焦,焦点自动转移,按照被动走焦中的说明项,焦点会自动从Text组件先向上寻找下一个可获焦组件,由于上一个组件是一个不可获焦的Text,所以向下寻找下一个可获焦的组件,找到并使焦点转移到Row容器上,根据[走焦至容器规则](#走焦规则),计算Button1和Button2的位置,Button2比Button1更大,因此焦点会自动转移到Button2上。
## 自定义默认焦点
```ts
defaultFocus(value: boolean)
```
焦点系统在页面初次构建完成时,会搜索当前页下的所有组件,找到第一个绑定了defaultFocus(true)的组件,然后将该组件置为默认焦点,若无任何组件绑定defaultFocus(true),则将第一个找到的可获焦的组件置为默认焦点。
以如下应用为例,应用布局如下:
![zh-cn_image_0000001563060793](figures/zh-cn_image_0000001563060793.png)
以下是实现该应用的示例代码,且示例代码中没有设置defaultFocus:
```ts
// xxx.ets
import promptAction from '@ohos.promptAction';
class MyDataSource implements IDataSource {
private list: number[] = [];
private listener: DataChangeListener;
constructor(list: number[]) {
this.list = list;
}
totalCount(): number {
return this.list.length;
}
getData(index: number): any {
return this.list[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener;
}
unregisterDataChangeListener() {
}
}
@Entry
@Component
struct SwiperExample {
private swiperController: SwiperController = new SwiperController()
private data: MyDataSource = new MyDataSource([])
aboutToAppear(): void {
let list = []
for (let i = 1; i <= 4; i++) {
list.push(i.toString());
}
this.data = new MyDataSource(list);
}
build() {
Column({ space: 5 }) {
Swiper(this.swiperController) {
LazyForEach(this.data, (item: string) => {
Row({ space: 20 }) {
Column() {
Button('1').width(200).height(200)
.fontSize(40)
.backgroundColor('#dadbd9')
}
Column({ space: 20 }) {
Row({ space: 20 }) {
Button('2')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('3')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
Row({ space: 20 }) {
Button('4')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('5')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
Row({ space: 20 }) {
Button('6')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('7')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
}
}
.width(480)
.height(380)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor(Color.White)
}, item => item)
}
.cachedCount(2)
.index(0)
.interval(4000)
.indicator(true)
.loop(true)
.duration(1000)
.itemSpace(0)
.curve(Curve.Linear)
.onChange((index: number) => {
console.info(index.toString());
})
.margin({ left: 20, top: 20, right: 20 })
Row({ space: 40 }) {
Button('')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.swiperController.showPrevious();
})
Button('')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.swiperController.showNext();
})
}
.width(480)
.height(50)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor('#f7f6dc')
Row({ space: 40 }) {
Button('Cancel')
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140)
.height(50)
.backgroundColor('#dadbd9')
Button('OK')
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140)
.height(50)
.backgroundColor('#dadbd9')
.onClick(() => {
promptAction.showToast({ message: 'Button OK on clicked' });
})
}
.width(480)
.height(80)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor('#dff2e4')
.margin({ left: 20, bottom: 20, right: 20 })
}.backgroundColor('#f2f2f2')
.margin({ left: 50, top: 50, right: 20 })
}
}
```
当前应用上无任何defaultFocus设置,所以第一个可获焦的组件默认获取焦点,按下TAB键/方向键让获焦的组件显示焦点态样式:
![zh-cn_image_0000001511421360](figures/zh-cn_image_0000001511421360.gif)
假设开发者想让应用打开的时候,无需执行多余的切换焦点操作,直接点击按键的空格/回车键,就可以执行Button-OK的onClick回调操作,那么就可以给这个Button绑定defaultFocus(true),让它成为该页面上的默认焦点:
```ts
Button('OK')
.defaultFocus(true) // 设置Button-OK为defaultFocus
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140).height(50).backgroundColor('#dadbd9')
.onClick(() => {
promptAction.showToast({ message: 'Button OK on clicked' });
})
```
![zh-cn_image_0000001562940617](figures/zh-cn_image_0000001562940617.gif)
打开应用后按TAB键,Button-OK显示了焦点态,说明默认焦点变更到了Button-OK上。然后按下空格,响应了Button-OK的onClick事件。
## 自定义TAB键走焦顺序
```ts
tabIndex(index: number)
```
tabIndex用于设置自定义TAB键走焦顺序,默认值为0。使用“TAB/Shift+TAB键”走焦时(方向键不影响),系统会自动获取到所有配置了tabIndex大于0的组件,然后按照递增/递减排序进行走焦。
[defaultFocus](#自定义默认焦点)提供的示例为例,默认情况下的走焦顺序如下:
![zh-cn_image_0000001511421364](figures/zh-cn_image_0000001511421364.gif)
默认的走焦顺序从第一个获焦组件一路走到最后一个获焦组件,会经历Button1-&gt;Button4-&gt;Button5-&gt;Button7-&gt;左箭头-&gt;右箭头-&gt;ButtonOK。这种走焦队列比较完整,遍历了大部分的组件。但缺点是从第一个走到最后一个所经历的路径较长。
如果想实现快速的从第一个走到最后一个,又不想牺牲太多的遍历完整性,就可以使用tabIndex通用属性。
比如:开发者把白色的区域当为一个整体,黄色的区域当为一个整体,绿色的区域当为一个整体,实现Button1-&gt;左箭头-&gt;ButtonOK这种队列的走焦顺序,只需要在Button1、左箭头、ButtonOK这三个组件上依次增加tabIndex(1)、tabIndex(2)、tabIndex(3)。tabIndex的参数表示TAB走焦的顺序(从大于0的数字开始,从小到大排列)。
```ts
Button('1').width(200).height(200)
.fontSize(40)
.backgroundColor('#dadbd9')
.tabIndex(1) // Button-1设置为第一个tabIndex节点
```
```ts
Button('')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.swiperController.showPrevious();
})
.tabIndex(2) // Button-左箭头设置为第二个tabIndex节点
```
```ts
Button('OK')
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140).height(50).backgroundColor('#dadbd9')
.onClick(() => {
promptAction.showToast({ message: 'Button OK on clicked' });
})
.tabIndex(3) // Button-OK设置为第三个tabIndex节点
```
![zh-cn_image_0000001511580976](figures/zh-cn_image_0000001511580976.gif)
>**说明:**
> - 当焦点处于tabIndex(大于0)节点上时,TAB/ShiftTAB会优先在tabIndex(大于0)的队列中寻找后置/前置的节点,存在则走焦至相应的tabIndex节点。若不存在,则使用默认的走焦逻辑继续往后/往前走焦。
>
> - 当焦点处于tabIndex(等于0)节点上时,TAB/ShiftTAB使用默认的走焦逻辑走焦,走焦的过程中会跳过tabIndex(大于0)和tabIndex(小于0)的节点。
>
> - 当焦点处于tabIndex(小于0)节点上时,TAB/ShiftTAB无法走焦。
### groupDefaultFocus
```ts
groupDefaultFocus(value: boolean)
```
[自定义TAB键走焦顺序](#自定义tab键走焦顺序)中所展示的使用tabIndex完成快速走焦的能力有如下问题:
每个区域(白色/黄色/绿色三个区域)都设置了某个组件为tabIndex节点(白色-Button1、黄色-左箭头、绿色-ButtonOK),但这样设置之后,只能在这3个组件上按TAB/ShiftTab键走焦时会有快速走焦的效果。
解决方案是给每个区域的容器设置tabIndex,但是这样设置的问题是:第一次走焦到容器上时,获焦的子组件是默认的第一个可获焦组件,并不是自己想要的组件(Button1、左箭头、ButtonOK)。
这样便引入了groupDefaultFocus通用属性,参数:boolean,默认值:false。
用法需和tabIndex组合使用,使用tabIndex给区域(容器)绑定走焦顺序,然后给Button1、左箭头、ButtonOK绑定groupDefaultFocus(true),这样在首次走焦到目标区域(容器)上时,它的绑定了groupDefaultFocus(true)的子组件同时获得焦点。
```ts
// xxx.ets
import promptAction from '@ohos.promptAction';
class MyDataSource implements IDataSource {
private list: number[] = [];
private listener: DataChangeListener;
constructor(list: number[]) {
this.list = list;
}
totalCount(): number {
return this.list.length;
}
getData(index: number): any {
return this.list[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener;
}
unregisterDataChangeListener() {
}
}
@Entry
@Component
struct SwiperExample {
private swiperController: SwiperController = new SwiperController()
private data: MyDataSource = new MyDataSource([])
aboutToAppear(): void {
let list = []
for (let i = 1; i <= 4; i++) {
list.push(i.toString());
}
this.data = new MyDataSource(list);
}
build() {
Column({ space: 5 }) {
Swiper(this.swiperController) {
LazyForEach(this.data, (item: string) => {
Row({ space: 20 }) { // 设置该Row组件为tabIndex的第一个节点
Column() {
Button('1').width(200).height(200)
.fontSize(40)
.backgroundColor('#dadbd9')
.groupDefaultFocus(true) // 设置Button-1为第一个tabIndex的默认焦点
}
Column({ space: 20 }) {
Row({ space: 20 }) {
Button('2')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('3')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
Row({ space: 20 }) {
Button('4')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('5')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
Row({ space: 20 }) {
Button('6')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('7')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
}
}
.width(480)
.height(380)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor(Color.White)
.tabIndex(1)
}, item => item)
}
.cachedCount(2)
.index(0)
.interval(4000)
.indicator(true)
.loop(true)
.duration(1000)
.itemSpace(0)
.curve(Curve.Linear)
.onChange((index: number) => {
console.info(index.toString());
})
.margin({ left: 20, top: 20, right: 20 })
Row({ space: 40 }) { // 设置该Row组件为第二个tabIndex节点
Button('')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.swiperController.showPrevious();
})
.groupDefaultFocus(true) // 设置Button-左箭头为第二个tabIndex节点的默认焦点
Button('')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.swiperController.showNext();
})
}
.width(480)
.height(50)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor('#f7f6dc')
.tabIndex(2)
Row({ space: 40 }) { // 设置该Row组件为第三个tabIndex节点
Button('Cancel')
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140)
.height(50)
.backgroundColor('#dadbd9')
Button('OK')
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140)
.height(50)
.backgroundColor('#dadbd9')
.defaultFocus(true)
.onClick(() => {
promptAction.showToast({ message: 'Button OK on clicked' });
})
.groupDefaultFocus(true) // 设置Button-OK为第三个tabIndex节点的默认焦点
}
.width(480)
.height(80)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor('#dff2e4')
.margin({ left: 20, bottom: 20, right: 20 })
.tabIndex(3)
}.backgroundColor('#f2f2f2')
.margin({ left: 50, top: 50, right: 20 })
}
}
```
![zh-cn_image_0000001562700533](figures/zh-cn_image_0000001562700533.gif)
### focusOnTouch
```ts
focusOnTouch(value: boolean)
```
点击获焦能力,参数:boolean,默认值:false(输入类组件:TextInput、TextArea、Search、Web默认值是true)。
点击是指使用触屏或鼠标左键进行单击,默认为false的组件,例如Button,不绑定该API时,点击Button不会使其获焦,当给Button绑定focusOnTouch(true)时,点击Button会使Button立即获得焦点。
给容器绑定focusOnTouch(true)时,点击容器区域,会立即使容器的第一个可获焦组件获得焦点。
示例代码:
```ts
// requestFocus.ets
import promptAction from '@ohos.promptAction';
@Entry
@Component
struct RequestFocusExample {
@State idList: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'N']
build() {
Column({ space:20 }){
Button("id: " + this.idList[0] + " focusOnTouch(true) + focusable(false)")
.width(400).height(70).fontColor(Color.White).focusOnTouch(true)
.focusable(false)
Button("id: " + this.idList[1] + " default")
.width(400).height(70).fontColor(Color.White)
Button("id: " + this.idList[2] + " focusOnTouch(false)")
.width(400).height(70).fontColor(Color.White).focusOnTouch(false)
Button("id: " + this.idList[3] + " focusOnTouch(true)")
.width(400).height(70).fontColor(Color.White).focusOnTouch(true)
}.width('100%').margin({ top:20 })
}
}
```
![zh-cn_image_0000001511580980](figures/zh-cn_image_0000001511580980.gif)
解读:
Button-A虽然设置了focusOnTouch(true),但是同时也设置了focusable(false),该组件无法获焦,因此点击后也无法获焦;
Button-B不设置相关属性,点击后不会获焦;
Button-C设置了focusOnTouch(false),同Button-B,点击后也不会获焦;
Button-D设置了focusOnTouch(true),点击即可使其获焦;
>**说明:**
>
>由于焦点态的阐述的特性,焦点态在屏幕接收点击事件后会立即清除。因此该示例代码在每次点击后,需要再次按下TAB键使焦点态再次显示,才可知道当前焦点所在的组件。
### focusControl.requestFocus
```ts
focusControl.requestFocus(id: string)
```
主动申请焦点能力的全局方法,参数:string,参数表示被申请组件的id(通用属性id设置的字符串)。
使用方法为:在任意执行语句中调用该API,指定目标组件的id为方法参数,当程序执行到该语句时,会立即给指定的目标组件申请焦点。
代码示例:
```ts
// requestFocus.ets
import promptAction from '@ohos.promptAction';
@Entry
@Component
struct RequestFocusExample {
@State idList: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'N']
@State requestId: number = 0
build() {
Column({ space:20 }){
Row({space: 5}) {
Button("id: " + this.idList[0] + " focusable(false)")
.width(200).height(70).fontColor(Color.White)
.id(this.idList[0])
.focusable(false)
Button("id: " + this.idList[1])
.width(200).height(70).fontColor(Color.White)
.id(this.idList[1])
}
Row({space: 5}) {
Button("id: " + this.idList[2])
.width(200).height(70).fontColor(Color.White)
.id(this.idList[2])
Button("id: " + this.idList[3])
.width(200).height(70).fontColor(Color.White)
.id(this.idList[3])
}
Row({space: 5}) {
Button("id: " + this.idList[4])
.width(200).height(70).fontColor(Color.White)
.id(this.idList[4])
Button("id: " + this.idList[5])
.width(200).height(70).fontColor(Color.White)
.id(this.idList[5])
}
}.width('100%').margin({ top:20 })
.onKeyEvent((e) => {
if (e.keyCode >= 2017 && e.keyCode <= 2022) {
this.requestId = e.keyCode - 2017;
} else if (e.keyCode === 2030) {
this.requestId = 6;
} else {
return;
}
if (e.type !== KeyType.Down) {
return;
}
let res = focusControl.requestFocus(this.idList[this.requestId]);
if (res) {
promptAction.showToast({message: 'Request success'});
} else {
promptAction.showToast({message: 'Request failed'});
}
})
}
}
```
![zh-cn_image_0000001562820905](figures/zh-cn_image_0000001562820905.gif)
解读:页面中共6个Button组件,其中Button-A组件设置了focusable(false),表示其不可获焦,在外部容器的onKeyEvent中,监听按键事件,当按下A ~ F按键时,分别去申请Button A ~ F 的焦点,另外按下N键,是给当前页面上不存在的id的组件去申请焦点。
1. 按下TAB键,由于第一个组件Button-A设置了无法获焦,那么默认第二个组件Button-B获焦,Button-B展示焦点态样式;
2. 键盘上按下A键,申请Button-A的焦点,气泡显示Request failed,表示无法获取到焦点,焦点位置未改变;
3. 键盘上按下B键,申请Button-B的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置原本就在Button-B,位置未改变;
4. 键盘上按下C键,申请Button-C的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-B变更为Button-C;
5. 键盘上按下D键,申请Button-D的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-C变更为Button-D;
6. 键盘上按下E键,申请Button-E的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-D变更为Button-E;
7. 键盘上按下F键,申请Button-F的焦点,气泡显示Request success,表示获焦到了焦点,焦点位置从Button-E变更为Button-F;
8. 键盘上按下N键,申请未知组件的焦点,气泡显示Request failed,表示无法获取到焦点,焦点位置不变;
# 触屏事件
触屏事件指当手指/手写笔在组件上按下、滑动、抬起时触发的回调事件。包括[点击事件](#点击事件)[拖拽事件](#拖拽事件)[触摸事件](#触摸事件)
**图1 ** 触摸事件原理
![zh-cn_image_0000001562700461](figures/zh-cn_image_0000001562700461.png)
## 点击事件
点击事件是指通过手指或手写笔做出一次完整的按下和抬起动作。当发生点击事件时,会触发以下回调函数:
```ts
onClick(event: (event?: ClickEvent) => void)
```
event参数提供点击事件相对于窗口或组件的坐标位置,以及发生点击的事件源。
例如通过按钮的点击事件控制图片的显示和隐藏。
```ts
@Entry
@Component
struct IfElseTransition {
@State flag: boolean = true;
@State btnMsg: string = 'show';
build() {
Column() {
Button(this.btnMsg).width(80).height(30).margin(30)
.onClick(() => {
if (this.flag) {
this.btnMsg = 'hide';
} else {
this.btnMsg = 'show';
}
// 点击Button控制Image的显示和消失
this.flag = !this.flag;
})
if (this.flag) {
Image($r('app.media.icon')).width(200).height(200)
}
}.height('100%').width('100%')
}
}
```
## 拖拽事件
拖拽事件指手指/手写笔长按组件(&gt;=500ms),并拖拽到接收区域释放的事件。拖拽事件触发流程:
![zh-cn_image_0000001562820825](figures/zh-cn_image_0000001562820825.png)
拖拽事件的触发通过长按、拖动平移判定,手指平移的距离达到5vp即可触发拖拽事件。ArkUI支持应用内、跨应用的拖拽事件。
拖拽事件提供以下[接口](../reference/arkui-ts/ts-universal-events-drag-drop.md)
| 接口名称 | 描述 |
| ---------------------------------------- | ---------------------------------------- |
| onDragStart(event:&nbsp;(event?:&nbsp;DragEvent,&nbsp;extraParams?:&nbsp;string)&nbsp;=&gt;&nbsp;CustomBuilder&nbsp;\|&nbsp;DragItemInfo) | 拖拽启动接口。当前仅支持自定义pixelmap和自定义组件。 |
| onDragEnter(event:&nbsp;(event?:&nbsp;DragEvent,&nbsp;extraParams?:&nbsp;string)&nbsp;=&gt;&nbsp;void) | 拖拽进入组件接口。DragEvent定义拖拽发生位置,extraParmas表示用户自定义信息 |
| onDragLeave(event:&nbsp;(event?:&nbsp;DragEvent,&nbsp;extraParams?:&nbsp;string)&nbsp;=&gt;&nbsp;void) | 拖拽离开组件接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。 |
| onDragMove(event:&nbsp;(event?:&nbsp;DragEvent,&nbsp;extraParams?:&nbsp;string)&nbsp;=&gt;&nbsp;void) | 拖拽移动接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。 |
| onDrop(event:&nbsp;(event?:&nbsp;DragEvent,&nbsp;extraParams?:&nbsp;string)&nbsp;=&gt;&nbsp;void) | 拖拽释放组件接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。 |
如下是跨窗口拖拽,拖出窗口示例:
```ts
import image from '@ohos.multimedia.image';
@Entry
@Component
struct Index {
@State text: string = ''
@State bool1: boolean = false
@State bool2: boolean = false
@State visible: Visibility = Visibility.Visible
@State pixelMap: PixelMap = undefined
private pixelMapReader = undefined
aboutToAppear() {
console.info('begin to create pixmap has info message: ')
this.createPixelMap()
}
createPixelMap() {
let color = new ArrayBuffer(4 * 96 * 96);
var buffer = new Uint8Array(color);
for (var i = 0; i < buffer.length; i++) {
buffer[i] = (i + 1) % 255;
}
let opts = {
alphaType: 0,
editable: true,
pixelFormat: 4,
scaleMode: 1,
size: { height: 96, width: 96 }
}
const promise = image.createPixelMap(color, opts);
promise.then((data) => {
console.info('create pixmap has info message: ' + JSON.stringify(data))
this.pixelMap = data;
this.pixelMapReader = data;
})
}
@Builder pixelMapBuilder() {
Text('drag item')
.width('100%')
.height(100)
.fontSize(16)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text('App1')
.width('40%')
.height(80)
.fontSize(20)
.margin(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Pink)
.visibility(Visibility.Visible)
Text('Across Window Drag This')
.width('80%')
.height(80)
.fontSize(16)
.margin(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Pink)
.visibility(this.visible)
.onDragStart(() => { //启动跨窗口拖拽
console.info('Text onDrag start')
this.bool1 = true
this.text = 'TextDrag'
return { pixelMap: this.pixelMapReader, extraInfo: 'custom extra info.' }
})
.onDrop((event: DragEvent, extraParams: string) => {
console.info('Text onDragDrop, ')
this.visible = Visibility.None //拖动结束后,使源不可见
})
}
.width('100%')
.height('100%')
}
}
```
跨窗口拖拽,拖入示例:
```ts
@Entry
@Component
struct Index {
@State number: string[] = ['drag here']
@State text: string = ''
@State bool1: boolean = false
@State bool2: boolean = false
@State visible: Visibility = Visibility.Visible
@State visible2: Visibility = Visibility.None
scroller: Scroller = new Scroller()
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text('App2')
.width('40%')
.height(80)
.fontSize(20)
.margin(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Pink)
.visibility(Visibility.Visible)
List({ space: 20, initialIndex: 0 }) {
ForEach(this.number, (item) => {
ListItem() {
Text('' + item)
.width('100%')
.height(80)
.fontSize(16)
.borderRadius(10)
.textAlign(TextAlign.Center)
.backgroundColor(0xFFFFFF)
}
}, item => item)
ListItem() {
Text('Across Window Drag This')
.width('80%')
.height(80)
.fontSize(16)
.margin(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Pink)
.visibility(this.visible2)
}
}
.height('50%')
.width('90%')
.border({ width: 1 })
.divider({ strokeWidth: 2, color: 0xFFFFFF, startMargin: 20, endMargin: 20 })
.onDragEnter((event: DragEvent, extraParams: string) => { //拖拽进去组件
console.info('List onDragEnter, ' + extraParams)
})
.onDragMove((event: DragEvent, extraParams: string) => { //拖拽时移动
console.info('List onDragMove, ' + extraParams)
})
.onDragLeave((event: DragEvent, extraParams: string) => { //拖拽离开组件
console.info('List onDragLeave, ' + extraParams)
})
.onDrop((event: DragEvent, extraParams: string) => { //释放组件
console.info('List onDragDrop, ' + extraParams)
this.visible2 = Visibility.Visible //拖拽完成使拖入目标可见
})
}
.width('100%')
.height('100%')
}
}
```
## 触摸事件
当手指或手写笔在组件上触碰时,会触发不同动作所对应的事件响应,包括按下(Down)、滑动(Move)、抬起(Up)事件:
```ts
onTouch(event: (event?: TouchEvent) => void)
```
- event.type为TouchType.Down:表示手指按下。
- event.type为TouchType.Up:表示手指抬起。
- event.type为TouchType.Move:表示手指按住移动。
触摸事件可以同时多指触发,通过event参数可获取触发的手指位置、手指唯一标志、当前发生变化的手指和输入的设备源等信息。
```ts
// xxx.ets
@Entry
@Component
struct TouchExample {
@State text: string = '';
@State eventType: string = '';
build() {
Column() {
Button('Touch').height(40).width(100)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.eventType = 'Down';
}
if (event.type === TouchType.Up) {
this.eventType = 'Up';
}
if (event.type === TouchType.Move) {
this.eventType = 'Move';
}
this.text = 'TouchType:' + this.eventType + '\nDistance between touch point and touch element:\nx: '
+ event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nComponent globalPos:('
+ event.target.area.globalPosition.x + ',' + event.target.area.globalPosition.y + ')\nwidth:'
+ event.target.area.width + '\nheight:' + event.target.area.height
})
Button('Touch').height(50).width(200).margin(20)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.eventType = 'Down';
}
if (event.type === TouchType.Up) {
this.eventType = 'Up';
}
if (event.type === TouchType.Move) {
this.eventType = 'Move';
}
this.text = 'TouchType:' + this.eventType + '\nDistance between touch point and touch element:\nx: '
+ event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nComponent globalPos:('
+ event.target.area.globalPosition.x + ',' + event.target.area.globalPosition.y + ')\nwidth:'
+ event.target.area.width + '\nheight:' + event.target.area.height
})
Text(this.text)
}.width('100%').padding(30)
}
}
```
![zh-cn_image_0000001511900468](figures/zh-cn_image_0000001511900468.gif)
# 使用画布绘制自定义图形
Canvas提供画布组件,用于自定义绘制图形,开发者使用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象在Canvas组件上进行绘制,绘制对象可以是基础形状、文本、图片等。
## 使用画布组件绘制自定义图形
可以由以下三种形式在画布绘制自定义图形:
- 使用[CanvasRenderingContext2D对象](../reference/arkui-ts/ts-canvasrenderingcontext2d.md)在Canvas画布上绘制。
```ts
@Entry
@Component
struct CanvasExample1 {
//用来配置CanvasRenderingContext2D对象的参数,包括是否开启抗锯齿,true表明开启抗锯齿。
private settings: RenderingContextSettings = new RenderingContextSettings(true)
//用来创建CanvasRenderingContext2D对象,通过在canvas中调用CanvasRenderingContext2D对象来绘制。
private context: CanvasRenderingContext2D= new CanvasRenderingContext2D(this.settings)
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
//在canvas中调用CanvasRenderingContext2D对象。
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
//可以在这里绘制内容。
this.context.strokeRect(50, 50, 200, 150);
})
}
.width('100%')
.height('100%')
}
}
```
![2023022793003(1)](figures/2023022793003(1).jpg)
- 离屏绘制是指将需要绘制的内容先绘制在缓存区,再将其转换成图片,一次性绘制到Canvas上,加快了绘制速度。过程为:
1. 通过transferToImageBitmap方法将离屏画布最近渲染的图像创建为一个ImageBitmap对象。
2. 通过CanvasRenderingContext2D对象的transferFromImageBitmap方法显示给定的ImageBitmap对象。
具体使用参考[OffscreenCanvasRenderingContext2D对象](../reference/arkui-ts/ts-offscreencanvasrenderingcontext2d.md)。
```ts
@Entry
@Component
struct CanvasExample2 {
//用来配置CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象的参数,包括是否开启抗锯齿。true表明开启抗锯齿
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
//用来创建OffscreenCanvasRenderingContext2D对象,width为离屏画布的宽度,height为离屏画布的高度。通过在canvas中调用OffscreenCanvasRenderingContext2D对象来绘制。
private offContext: OffscreenCanvasRenderingContext2D = new OffscreenCanvasRenderingContext2D(600, 600, this.settings)
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
//可以在这里绘制内容
this.offContext.strokeRect(50, 50, 200, 150);
//将离屏绘值渲染的图像在普通画布上显示
let image = this.offContext.transferToImageBitmap();
this.context.transferFromImageBitmap(image);
})
}
.width('100%')
.height('100%')
}
}
```
![2023022793003(1)](figures/2023022793003(1).jpg)
>**说明:**
>
>在画布组件中,通过CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象在Canvas组件上进行绘制时调用的接口相同,另接口参数如无特别说明,单位均为vp。
- 在Canvas上加载Lottie动画时,需要先按照如下方式下载Lottie。
```ts
import lottie from '@ohos/lottie'
```
具体接口参考[Lottie](../reference/arkui-ts/ts-components-canvas-lottie.md),相关实例请参考[Lottie实例](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/Lottie)
>**说明:**
>
>在第一次使用Lottie之前,需要在Terminal窗口运行ohpm install \@ohos/lottieETS命令下载Lottie。
## 初始化画布组件
onReady(event: () =&gt; void)是Canvas组件初始化完成时的事件回调,调用该事件后,可获取Canvas组件的确定宽高,进一步使用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象调用相关API进行图形绘制。
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() => {
this.context.fillStyle = '#0097D4';
this.context.fillRect(50, 50, 100, 100);
})
```
![2023022793350(1)](figures/2023022793350(1).jpg)
## 画布组件绘制方式
在Canvas组件生命周期接口onReady()调用之后,开发者可以直接使用canvas组件进行绘制。或者可以脱离Canvas组件和onready生命周期,单独定义Path2d对象构造理想的路径,并在onready调用之后使用Canvas组件进行绘制。
- 通过CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象直接调用相关API进行绘制。
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
this.context.beginPath();
this.context.moveTo(50, 50);
this.context.lineTo(280, 160);
this.context.stroke();
})
```
![2023022793719(1)](figures/2023022793719(1).jpg)
- 先单独定义path2d对象构造理想的路径,再通过调用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象的stroke接口或者fill接口进行绘制,具体使用可以参考[Path2D对象](../reference/arkui-ts/ts-components-canvas-path2d.md)
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
let region = new Path2D();
region.arc(100, 75, 50, 0, 6.28);
this.context.stroke(region);
})
```
![2023022794031(1)](figures/2023022794031(1).jpg)
## 画布组件常用方法
OffscreenCanvasRenderingContext2D对象和CanvasRenderingContext2D对象提供了大量的属性和方法,可以用来绘制文本、图形,处理像素等,是Canvas组件的核心。常用接口有[fill](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#fill)(对封闭路径进行填充)、[clip](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#clip)(设置当前路径为剪切路径)[stroke](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#stroke)(进行边框绘制操作)等等,同时提供了[fillStyle](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#fillstyle)(指定绘制的填充色)、[globalAlpha](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#globalalpha)(设置透明度)与[strokeStyle](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#strokestyle)(设置描边的颜色)等属性修改绘制内容的样式。将通过以下几个方面简单介绍画布组件常见使用方法:
- 基础形状绘制。
可以通过[arc](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#arc)(绘制弧线路径)、 [ellipse](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#ellipse)(绘制一个椭圆)、[rect](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#rect)(创建矩形路径)等接口绘制基础形状。
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
//绘制矩形
this.context.beginPath();
this.context.rect(100, 50, 100, 100);
this.context.stroke();
//绘制圆形
this.context.beginPath();
this.context.arc(150, 250, 50, 0, 6.28);
this.context.stroke();
//绘制椭圆
this.context.beginPath();
this.context.ellipse(150, 450, 50, 100, Math.PI * 0.25, Math.PI * 0, Math.PI * 2);
this.context.stroke();
})
```
![2023022794521(1)](figures/2023022794521(1).jpg)
- 文本绘制。
可以通过[fillText](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#filltext)(绘制填充类文本)、[strokeText](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#stroketext)(绘制描边类文本)等接口进行文本绘制。
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
//绘制填充类文本
this.context.font = '50px sans-serif';
this.context.fillText("Hello World!", 50, 100);
//绘制描边类文本
this.context.font = '55px sans-serif';
this.context.strokeText("Hello World!", 50, 150);
})
```
![2023022795105(1)](figures/2023022795105(1).jpg)
- 绘制图片和图像像素信息处理。
可以通过[drawImage](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#drawimage)(图像绘制)、[putImageData](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#putimagedata)(使用[ImageData](../reference/arkui-ts/ts-components-canvas-imagedata.md)数据填充新的矩形区域)等接口绘制图片,通过[createImageData](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#createimagedata)(创建新的ImageData 对象)、[getPixelMap](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#getpixelmap)(以当前canvas指定区域内的像素创建[PixelMap](../reference/apis/js-apis-image.md#pixelmap7)对象)、[getImageData](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#getimagedata)(以当前canvas指定区域内的像素创建ImageData对象)等接口进行图像像素信息处理。
```ts
@Entry
@Component
struct GetImageData {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
private offContext: OffscreenCanvasRenderingContext2D = new OffscreenCanvasRenderingContext2D(600, 600, this.settings)
private img:ImageBitmap = new ImageBitmap("/common/images/1234.png")
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
// 使用drawImage接口将图片画在(0,0)为起点,宽高130的区域
this.offContext.drawImage(this.img,0,0,130,130);
// 使用getImageData接口,获得canvas组件区域中,(50,50)为起点,宽高130范围内的绘制内容
let imagedata = this.offContext.getImageData(50,50,130,130);
// 使用putImageData接口将得到的ImageData画在起点为(150, 150)的区域中
this.offContext.putImageData(imagedata,150,150);
// 将离屏绘制的内容画到canvas组件上
let image = this.offContext.transferToImageBitmap();
this.context.transferFromImageBitmap(image);
})
}
.width('100%')
.height('100%')
}
}
```
![drawimage](figures/drawimage.PNG)
- 其他方法。
Canvas中还提供其他类型的方法。渐变([CanvasGradient对象](../reference/arkui-ts/ts-components-canvas-canvasgradient.md))相关的方法:[createLinearGradient](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#createlineargradient)(创建一个线性渐变色)、[createRadialGradient](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#createradialgradient)(创建一个径向渐变色)等。
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
//创建一个径向渐变色的CanvasGradient对象
let grad = this.context.createRadialGradient(200,200,50, 200,200,200)
//为CanvasGradient对象设置渐变断点值,包括偏移和颜色
grad.addColorStop(0.0, '#E87361');
grad.addColorStop(0.5, '#FFFFF0');
grad.addColorStop(1.0, '#BDDB69');
//用CanvasGradient对象填充矩形
this.context.fillStyle = grad;
this.context.fillRect(0, 0, 400, 400);
})
```
![2023022700701(1)](figures/2023022700701(1).jpg)
## 场景示例
- 规则基础形状绘制:
```ts
@Entry
@Component
struct ClearRect {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
// 设定填充样式,填充颜色设为蓝色
this.context.fillStyle = '#0097D4';
// 以(50, 50)为左上顶点,画一个宽高200的矩形
this.context.fillRect(50,50,200,200);
// 以(70, 70)为左上顶点,清除宽150高100的区域
this.context.clearRect(70,70,150,100);
})
}
.width('100%')
.height('100%')
}
}
```
![2023022701120(1)](figures/2023022701120(1).jpg)
- 不规则图形绘制。
```ts
@Entry
@Component
struct Path2d {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
build() {
Row() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
// 使用Path2D的接口构造一个五边形
let path = new Path2D();
path.moveTo(150, 50);
path.lineTo(50, 150);
path.lineTo(100, 250);
path.lineTo(200, 250);
path.lineTo(250, 150);
path.closePath();
// 设定填充色为蓝色
this.context.fillStyle = '#0097D4';
// 使用填充的方式,将Path2D描述的五边形绘制在canvas组件内部
this.context.fill(path);
})
}
.width('100%')
}
.height('100%')
}
}
```
![2023032422159](figures/2023032422159.jpg)
\ No newline at end of file
# 概述
交互事件按照触发类型来分类,包括触屏事件、键鼠事件和焦点事件。
- [触屏事件](arkts-common-events-touch-screen-event.md):手指或手写笔在触屏上的单指或单笔操作。
- [键鼠事件](arkts-common-events-device-input-event.md):包括外设鼠标或触控板的操作事件和外设键盘的按键事件。
- 鼠标事件是指通过连接和使用外设鼠标/触控板操作时所响应的事件。
- 按键事件是指通过连接和使用外设键盘操作时所响应的事件。
- [焦点事件](arkts-common-events-focus-event.md):通过以上方式控制组件焦点的能力和响应的事件。
手势事件由绑定手势方法和绑定的手势组成,绑定的手势可以分为单一手势和组合手势两种类型,根据手势的复杂程度进行区分。
- [绑定手势方法](arkts-gesture-events-binding.md):用于在组件上绑定单一手势或组合手势,并声明所绑定的手势的响应优先级。
- [单一手势](arkts-gesture-events-single-gesture.md):手势的基本单元,是所有复杂手势的组成部分。
- [组合手势](arkts-gesture-events-combined-gestures.md):由多个单一手势组合而成,可以根据声明的类型将多个单一手势按照一定规则组合成组合手势,并进行使用。
# 绘制几何图形
绘制组件用于在页面绘制图形,Shape组件是绘制组件的父组件,父组件中会描述所有绘制组件均支持的通用属性。具体用法请参考[Shape](../reference/arkui-ts/ts-drawing-components-shape.md)
## 创建绘制组件
绘制组件可以由以下两种形式创建:
- 绘制组件使用Shape作为父组件,实现类似SVG的效果。接口调用为以下形式:
```ts
Shape(value?: PixelMap)
```
该接口用于创建带有父组件的绘制组件,其中value用于设置绘制目标,可将图形绘制在指定的PixelMap对象中,若未设置,则在当前绘制目标中进行绘制。
```ts
Shape() {
Rect().width(300).height(50)
}
```
- 绘制组件单独使用,用于在页面上绘制指定的图形。有7种绘制类型,分别为[Circle](../reference/arkui-ts/ts-drawing-components-circle.md)(圆形)、[Ellipse](../reference/arkui-ts/ts-drawing-components-ellipse.md)(椭圆形)、[Line](../reference/arkui-ts/ts-drawing-components-line.md)(直线)、[Polyine](../reference/arkui-ts/ts-drawing-components-polyline.md)(折线)、[Polygon](../reference/arkui-ts/ts-drawing-components-polygon.md)(多边形)、[Path](../reference/arkui-ts/ts-drawing-components-path.md)(路径)、[Rect](../reference/arkui-ts/ts-drawing-components-rect.md)(矩形)。以Circle的接口调用为例:
```ts
Circle(options?: {width?: string | number, height?: string | number}
```
该接口用于在页面绘制圆形,其中width用于设置圆形的宽度,height用于设置圆形的高度,圆形直径由宽高最小值确定。
```ts
Circle({ width: 150, height: 150 })
```
![创建2](figures/创建2.jpg)
## 形状视口viewport
```ts
viewPort{ x?: number | string, y?: number | string, width?: number | string, height?: number | string }
```
形状视口viewport指定用户空间中的一个矩形,该矩形映射到为关联的 SVG 元素建立的视区边界。viewport属性的值包含x、y、width和height四个可选参数,x 和 y 表示视区的左上角坐标,width和height表示其尺寸。
以下3个示例讲解Viewport具体用法:
- 通过形状视口对图形进行放大与缩小。
```ts
// 画一个宽高都为150的圆
Text('原始尺寸Circle组件')
Circle({width: 75, height: 75}).fill('#E87361')
Row({space:10}) {
Column() {
// 创建一个宽高都为150的shape组件,背景色为黄色,一个宽高都为75的viewport。用一个蓝色的矩形来填充viewport,在viewport中绘制一个直径为75的圆。
// 绘制结束,viewport会根据组件宽高放大两倍
Text('shape内放大的Circle组件')
Shape() {
Rect().width('100%').height('100%').fill('#0097D4')
Circle({width: 75, height: 75}).fill('#E87361')
}
.viewPort({x: 0, y: 0, width: 75, height: 75})
.width(150)
.height(150)
.backgroundColor('#F5DC62')
}
Column() {
// 创建一个宽高都为150的shape组件,背景色为黄色,一个宽高都为300的viewport。用一个绿色的矩形来填充viewport,在viewport中绘制一个直径为75的圆。
// 绘制结束,viewport会根据组件宽高缩小两倍。
Text('Shape内缩小的Circle组件')
Shape() {
Rect().width('100%').height('100%').fill('#BDDB69')
Circle({width: 75, height: 75}).fill('#E87361')
}
.viewPort({x: 0, y: 0, width: 300, height: 300})
.width(150)
.height(150)
.backgroundColor('#F5DC62')
}
}
```
![2023032401632](figures/2023032401632.jpg)
- 创建一个宽高都为300的shape组件,背景色为黄色,一个宽高都为300的viewport。用一个蓝色的矩形来填充viewport,在viewport中绘制一个半径为75的圆。
```ts
Shape() {
Rect().width("100%").height("100%").fill("#0097D4")
Circle({ width: 150, height: 150 }).fill("#E87361")
}
.viewPort({ x: 0, y: 0, width: 300, height: 300 })
.width(300)
.height(300)
.backgroundColor("#F5DC62")
```
![viewport(2)](figures/viewport(2).jpg)
- 创建一个宽高都为300的shape组件,背景色为黄色,创建一个宽高都为300的viewport。用一个蓝色的矩形来填充viewport,在viewport中绘制一个半径为75的圆,将viewport向右方和下方各平移150。
```ts
Shape() {
Rect().width("100%").height("100%").fill("#0097D4")
Circle({ width: 150, height: 150 }).fill("#E87361")
}
.viewPort({ x: -150, y: -150, width: 300, height: 300 })
.width(300)
.height(300)
.backgroundColor("#F5DC62")
```
![viewport(3)](figures/viewport(3).jpg)
## 自定义样式
绘制组件支持通过各种属性对组件样式进行更改。
- 通过fill可以设置组件填充区域颜色。
```ts
Path()
.width(100)
.height(100)
.commands('M150 0 L300 300 L0 300 Z')
.fill("#E87361")
```
![2023022792216(1)](figures/2023022792216(1).jpg)
- 通过stroke可以设置组件边框颜色。
```ts
Path()
.width(100)
.height(100)
.fillOpacity(0)
.commands('M150 0 L300 300 L0 300 Z')
.stroke(Color.Red)
```
![stroke](figures/stroke.jpg)
- 通过strokeOpacity可以设置边框透明度。
```ts
Path()
.width(100)
.height(100)
.fillOpacity(0)
.commands('M150 0 L300 300 L0 300 Z')
.stroke(Color.Red)
.strokeWidth(10)
.strokeOpacity(0.2)
```
![strokeopacity](figures/strokeopacity.jpg)
- 通过strokeLineJoin可以设置线条拐角绘制样式。拐角绘制样式分为Bevel(使用斜角连接路径段)、Miter(使用尖角连接路径段)、Round(使用圆角连接路径段)。
```ts
Polyline()
.width(100)
.height(100)
.fillOpacity(0)
.stroke(Color.Red)
.strokeWidth(8)
.points([[20, 0], [0, 100], [100, 90]])
// 设置折线拐角处为圆弧
.strokeLineJoin(LineJoinStyle.Round)
```
![strokeLineJoin](figures/strokeLineJoin.jpg)
- 通过strokeMiterLimit设置斜接长度与边框宽度比值的极限值。
斜接长度表示外边框外边交点到内边交点的距离,边框宽度即strokeWidth属性的值。strokeMiterLimit取值需大于等于1,且在strokeLineJoin属性取值LineJoinStyle.Miter时生效。
```ts
Polyline()
.width(100)
.height(100)
.fillOpacity(0)
.stroke(Color.Red)
.strokeWidth(10)
.points([[20, 0], [20, 100], [100, 100]])
// 设置折线拐角处为尖角
.strokeLineJoin(LineJoinStyle.Miter)
// 设置斜接长度与线宽的比值
.strokeMiterLimit(1/Math.sin(45))
Polyline()
.width(100)
.height(100)
.fillOpacity(0)
.stroke(Color.Red)
.strokeWidth(10)
.points([[20, 0], [20, 100], [100, 100]])
.strokeLineJoin(LineJoinStyle.Miter)
.strokeMiterLimit(1.42)
```
![2023032405917](figures/2023032405917.jpg)
- 通过antiAlias设置是否开启抗锯齿,默认值为true(开启抗锯齿)。
```ts
//开启抗锯齿
Circle()
.width(150)
.height(200)
.fillOpacity(0)
.strokeWidth(5)
.stroke(Color.Black)
```
![无标题](figures/无标题.png)
```ts
//关闭抗锯齿
Circle()
.width(150)
.height(200)
.fillOpacity(0)
.strokeWidth(5)
.stroke(Color.Black)
.antiAlias(false)
```
![2023032411518](figures/2023032411518.jpg)
## 场景示例
- 在Shape的(-80, -5)点绘制一个封闭路径,填充颜色0x317AF7,线条宽度10,边框颜色红色,拐角样式锐角(默认值)。
```ts
@Entry
@Component
struct ShapeExample {
build() {
Column({ space: 10 }) {
Shape() {
Path().width(200).height(60).commands('M0 0 L400 0 L400 150 Z')
}
.viewPort({ x: -80, y: -5, width: 500, height: 300 })
.fill(0x317AF7)
.stroke(Color.Red)
.strokeWidth(3)
.strokeLineJoin(LineJoinStyle.Miter)
.strokeMiterLimit(5)
}.width('100%').margin({ top: 15 })
}
}
```
![场景1](figures/场景1.jpg)
- 绘制一个直径为150的圆,和一个直径为150、线条为红色虚线的圆环(宽高设置不一致时以短边为直径)。
```ts
@Entry
@Component
struct CircleExample {
build() {
Column({ space: 10 }) {
//绘制一个直径为150的圆
Circle({ width: 150, height: 150 })
//绘制一个直径为150、线条为红色虚线的圆环
Circle()
.width(150)
.height(200)
.fillOpacity(0)
.strokeWidth(3)
.stroke(Color.Red)
.strokeDashArray([1, 2])
}.width('100%')
}
}
```
![场景2](figures/场景2.jpg)
# 绑定手势方法
通过给各个组件绑定不同的手势事件,并设计事件的响应方式,当手势识别成功时,ArkUI框架将通过事件回调通知组件手势识别的结果。
## gesture(常规手势绑定方法)
```ts
.gesture(gesture: GestureType, mask?: GestureMask)
```
gesture为通用的一种手势绑定方法,可以将手势绑定到对应的组件上。
例如,可以将点击手势TapGesture通过gesture手势绑定方法绑定到Text组件上。
```ts
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
Text('Gesture').fontSize(28)
// 采用gesture手势绑定方法绑定TapGesture
.gesture(
TapGesture()
.onAction(() => {
console.info('TapGesture is onAction');
}))
}
.height(200)
.width(250)
}
}
```
## priorityGesture(带优先级的手势绑定方法)
```ts
.priorityGesture(gesture: GestureType, mask?: GestureMask)
```
priorityGesture是带优先级的手势绑定方法,可以在组件上绑定优先识别的手势。
在默认情况下,当父组件和子组件使用gesture绑定同类型的手势时,子组件优先识别通过gesture绑定的手势。当父组件使用priorityGesture绑定与子组件同类型的手势时,父组件优先识别通过priorityGesture绑定的手势。
例如,当父组件Column和子组件Text同时绑定TapGesture手势时,父组件以带优先级手势priorityGesture的形式进行绑定时,优先响应父组件绑定的TapGesture。
```ts
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
Text('Gesture').fontSize(28)
.gesture(
TapGesture()
.onAction(() => {
console.info('Text TapGesture is onAction');
}))
}
.height(200)
.width(250)
// 设置为priorityGesture时,点击文本区域会忽略Text组件的TapGesture手势事件,优先响应父组件Column的TapGesture手势事件
.priorityGesture(
TapGesture()
.onAction(() => {
console.info('Column TapGesture is onAction');
}), GestureMask.IgnoreInternal)
}
}
```
## parallelGesture(并行手势绑定方法)
```ts
.parallelGesture(gesture: GestureType, mask?: GestureMask)
```
parallelGesture是并行的手势绑定方法,可以在父子组件上绑定可以同时响应的相同手势。
在默认情况下,手势事件为非冒泡事件,当父子组件绑定相同的手势时,父子组件绑定的手势事件会发生竞争,最多只有一个组件的手势事件能够获得响应。而当父组件绑定了并行手势parallelGesture时,父子组件相同的手势事件都可以触发,实现类似冒泡效果。
```ts
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
Text('Gesture').fontSize(28)
.gesture(
TapGesture()
.onAction(() => {
console.info('Text TapGesture is onAction');
}))
}
.height(200)
.width(250)
// 设置为parallelGesture时,点击文本区域会同时响应父组件Column和子组件Text的TapGesture手势事件
.parallelGesture(
TapGesture()
.onAction(() => {
console.info('Column TapGesture is onAction');
}), GestureMask.IgnoreInternal)
}
}
```
>**说明:**
>
>当父组件和子组件同时绑定单击手势事件和双击手势事件时,父组件和子组件均只响应单击手势事件。
# 组合手势
组合手势由多种单一手势组合而成,通过在GestureGroup中使用不同的GestureMode来声明该组合手势的类型,支持[连续识别](#连续识别)[并行识别](#并行识别)[互斥识别](#互斥识别)三种类型。
```ts
GestureGroup(mode:GestureMode, ...gesture:GestureType[])
```
- mode:必选参数,为GestureMode枚举类。用于声明该组合手势的类型。
- gesture:必选参数,为由多个手势组合而成的数组。用于声明组合成该组合手势的各个手势。
## 连续识别
连续识别组合手势对应的GestureMode为Sequence。连续识别组合手势将按照手势的注册顺序识别手势,直到所有的手势识别成功。当连续识别组合手势中有一个手势识别失败时,所有的手势识别失败。
以一个由长按手势和拖动手势组合而成的连续手势为例:
在一个Column组件上绑定了translate属性,通过修改该属性可以设置组件的位置移动。然后在该组件上绑定LongPressGesture和PanGesture组合而成的Sequence组合手势。当触发LongPressGesture时,更新显示的数字。当长按后进行拖动时,根据拖动手势的回调函数,实现组件的拖动。
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State offsetX: number = 0;
@State offsetY: number = 0;
@State count: number = 0;
@State positionX: number = 0;
@State positionY: number = 0;
@State borderStyles: BorderStyle = BorderStyle.Solid
build() {
Column() {
Text('sequence gesture\n' + 'LongPress onAction:' + this.count + '\nPanGesture offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY)
.fontSize(28)
}
// 绑定translate属性可以实现组件的位置移动
.translate({ x: this.offsetX, y: this.offsetY, z: 0 })
.height(250)
.width(300)
//以下组合手势为顺序识别,当长按手势事件未正常触发时不会触发拖动手势事件
.gesture(
// 声明该组合手势的类型为Sequence类型
GestureGroup(GestureMode.Sequence,
// 该组合手势第一个触发的手势为长按手势,且长按手势可多次响应
LongPressGesture({ repeat: true })
// 当长按手势识别成功,增加Text组件上显示的count次数
.onAction((event: GestureEvent) => {
if (event.repeat) {
this.count++;
}
console.info('LongPress onAction');
})
.onActionEnd(() => {
console.info('LongPress end');
}),
// 当长按之后进行拖动,PanGesture手势被触发
PanGesture()
.onActionStart(() => {
this.borderStyles = BorderStyle.Dashed;
console.info('pan start');
})
// 当该手势被触发时,根据回调获得拖动的距离,修改该组件的位移距离从而实现组件的移动
.onActionUpdate((event: GestureEvent) => {
this.offsetX = this.positionX + event.offsetX;
this.offsetY = this.positionY + event.offsetY;
console.info('pan update');
})
.onActionEnd(() => {
this.positionX = this.offsetX;
this.positionY = this.offsetY;
this.borderStyles = BorderStyle.Solid;
})
)
)
}
}
```
![sequence](figures/sequence.gif)
>**说明:**
>
>拖拽事件是一种典型的连续识别组合手势事件,由长按手势事件和滑动手势事件组合而成。只有先长按达到长按手势事件预设置的时间后进行滑动才会触发拖拽事件。如果长按事件未达到或者长按后未进行滑动,拖拽事件均识别失败。
## 并行识别
并行识别组合手势对应的GestureMode为Parallel。并行识别组合手势中注册的手势将同时进行识别,直到所有手势识别结束。并行识别手势组合中的手势进行识别时互不影响。
以在一个Column组件上绑定点击手势和双击手势组成的并行识别手势为例,由于单击手势和双击手势是并行识别,因此两个手势可以同时进行识别,二者互不干涉。
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State count1: number = 0;
@State count2: number = 0;
build() {
Column() {
Text('parallel gesture\n' + 'tapGesture count is 1:' + this.count1 + '\ntapGesture count is 2:' + this.count2 + '\n')
.fontSize(28)
}
.height(200)
.width(250)
// 以下组合手势为并行并别,单击手势识别成功后,若在规定时间内再次点击,双击手势也会识别成功
.gesture(
GestureGroup(GestureMode.Parallel,
TapGesture({ count: 1 })
.onAction(() => {
this.count1++;
}),
TapGesture({ count: 2 })
.onAction(() => {
this.count2++;
})
)
)
}
}
```
![parallel](figures/parallel.gif)
>**说明:**
>
>当由单击手势和双击手势组成一个并行识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。
>
>当只有单次点击时,单击手势识别成功,双击手势识别失败。
>
>当有两次点击时,若两次点击相距时间在规定时间内(默认规定时间为300毫秒),触发两次单击事件和一次双击事件。
>
>当有两次点击时,若两次点击相距时间超出规定时间,触发两次单击事件不触发双击事件。
## 互斥识别
互斥识别组合手势对应的GestureMode为Exclusive。互斥识别组合手势中注册的手势将同时进行识别,若有一个手势识别成功,则结束手势识别,其他所有手势识别失败。
以在一个Column组件上绑定单击手势和双击手势组合而成的互斥识别组合手势为例,由于单击手势只需要一次点击即可触发而双击手势需要两次,每次的点击事件均被单击手势消费而不能积累成双击手势,所以双击手势无法触发。
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State count1: number = 0;
@State count2: number = 0;
build() {
Column() {
Text('parallel gesture\n' + 'tapGesture count is 1:' + this.count1 + '\ntapGesture count is 2:' + this.count2 + '\n')
.fontSize(28)
}
.height(200)
.width(250)
//以下组合手势为互斥并别,单击手势识别成功后,双击手势会识别失败
.gesture(
GestureGroup(GestureMode.Exclusive,
TapGesture({ count: 1 })
.onAction(() => {
this.count1++;
}),
TapGesture({ count: 2 })
.onAction(() => {
this.count2++;
})
)
)
}
}
```
![exclusive](figures/exclusive.gif)
>**说明:**
>
>当由单击手势和双击手势组成一个互斥识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。
>
>当只有单次点击时,单击手势识别成功,双击手势识别失败。
>
>当有两次点击时,单击手势在第一次点击时即宣告识别成功,此时双击手势已经失败。即使在规定时间内进行了第二次点击,双击手势事件也不会进行响应,此时会触发单击手势事件的第二次识别成功。
# 单一手势
## 点击手势(TapGesture)
```ts
TapGesture(value?:{count?:number; fingers?:number})
```
点击手势支持单次点击和多次点击,拥有两个可选参数:
- count:非必填参数,声明该点击手势识别的连续点击次数。默认值为1,若设置小于1的非法值会被转化为默认值。如果配置多次点击,上一次抬起和下一次按下的超时时间为300毫秒。
- fingers:非必填参数,用于声明触发点击的手指数量,最小值为1,最大值为10,默认值为1。当配置多指时,若第一根手指按下300毫秒内未有足够的手指数按下则手势识别失败。当实际点击手指数超过配置值时,手势识别失败。
以在Text组件上绑定双击手势(count值为2的点击手势)为例:
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State value: string = "";
build() {
Column() {
Text('Click twice').fontSize(28)
.gesture(
// 绑定count为2的TapGesture
TapGesture({ count: 2 })
.onAction((event: GestureEvent) => {
this.value = JSON.stringify(event.fingerList[0]);
}))
Text(this.value)
}
.height(200)
.width(250)
.padding(20)
.border({ width: 3 })
.margin(30)
}
}
```
![tap](figures/tap.gif)
## 长按手势(LongPressGesture)
```ts
LongPressGesture(value?:{fingers?:number; repeat?:boolean; duration?:number})
```
长按手势用于触发长按手势事件,触发长按手势的最少手指数量为1,最短长按事件为500毫秒,拥有三个可选参数:
- fingers:非必选参数,用于声明触发长按手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。
- repeat:非必选参数,用于声明是否连续触发事件回调,默认值为false。
- duration:非必选参数,用于声明触发长按所需的最短时间,单位为毫秒,默认值为500。
以在Text组件上绑定可以重复触发的长按手势为例:
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State count: number = 0;
build() {
Column() {
Text('LongPress OnAction:' + this.count).fontSize(28)
.gesture(
// 绑定可以重复触发的LongPressGesture
LongPressGesture({ repeat: true })
.onAction((event: GestureEvent) => {
if (event.repeat) {
this.count++;
}
})
.onActionEnd(() => {
this.count = 0;
})
)
}
.height(200)
.width(250)
.padding(20)
.border({ width: 3 })
.margin(30)
}
}
```
![longPress](figures/longPress.gif)
## 拖动手势(PanGesture)
```ts
PanGestureOptions(value?:{ fingers?:number; direction?:PanDirection; distance?:number})
```
拖动手势用于触发拖动手势事件,滑动达到最小滑动距离(默认值为5vp)时拖动手势识别成功,拥有三个可选参数:
- fingers:非必选参数,用于声明触发拖动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。
- direction:非必选参数,用于声明触发拖动的手势方向,此枚举值支持逻辑与(&amp;)和逻辑或(|)运算。默认值为Pandirection.All。
- distance:非必选参数,用于声明触发拖动的最小拖动识别距离,单位为vp,默认值为5。
以在Text组件上绑定拖动手势为例,可以通过在拖动手势的回调函数中修改组件的布局位置信息来实现组件的拖动:
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State offsetX: number = 0;
@State offsetY: number = 0;
@State positionX: number = 0;
@State positionY: number = 0;
build() {
Column() {
Text('PanGesture Offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY)
.fontSize(28)
.height(200)
.width(300)
.padding(20)
.border({ width: 3 })
// 在组件上绑定布局位置信息
.translate({ x: this.offsetX, y: this.offsetY, z: 0 })
.gesture(
// 绑定拖动手势
PanGesture()
.onActionStart((event: GestureEvent) => {
console.info('Pan start');
})
// 当触发拖动手势时,根据回调函数修改组件的布局位置信息
.onActionUpdate((event: GestureEvent) => {
this.offsetX = this.positionX + event.offsetX;
this.offsetY = this.positionY + event.offsetY;
})
.onActionEnd(() => {
this.positionX = this.offsetX;
this.positionY = this.offsetY;
})
)
}
.height(200)
.width(250)
}
}
```
![pan](figures/pan.gif)
>**说明:**
>
>大部分可滑动组件,如List、Grid、Scroll、Tab等组件是通过PanGesture实现滑动,在组件内部的子组件绑定[拖动手势(PanGesture)](#拖动手势pangesture)或者[滑动手势(SwipeGesture)](#滑动手势swipegesture)会导致手势竞争。
>
>当在子组件绑定PanGesture时,在子组件区域进行滑动仅触发子组件的PanGesture。如果需要父组件响应,需要通过修改手势绑定方法或者子组件向父组件传递消息进行实现,或者通过修改父子组件的PanGesture参数distance使得拖动更灵敏。当子组件绑定SwipeGesture时,由于PanGesture和SwipeGesture触发条件不同,需要修改PanGesture和SwipeGesture的参数以达到所需效果。
## 捏合手势(PinchGesture)
```ts
PinchGesture(value?:{fingers?:number; distance?:number})
```
捏合手势用于触发捏合手势事件,触发捏合手势的最少手指数量为2指,最大为5指,最小识别距离为3vp,拥有两个可选参数:
- fingers:非必选参数,用于声明触发捏合手势所需要的最少手指数量,最小值为2,最大值为5,默认值为2。
- distance:非必选参数,用于声明触发捏合手势的最小距离,单位为vp,默认值为3。
以在Column组件上绑定三指捏合手势为例,可以通过在捏合手势的函数回调中获取缩放比例,实现对组件的缩小或放大:
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State scaleValue: number = 1;
@State pinchValue: number = 1;
@State pinchX: number = 0;
@State pinchY: number = 0;
build() {
Column() {
Column() {
Text('PinchGesture scale:\n' + this.scaleValue)
Text('PinchGesture center:\n(' + this.pinchX + ',' + this.pinchY + ')')
}
.height(200)
.width(300)
.border({ width: 3 })
.margin({ top: 100 })
// 在组件上绑定缩放比例,可以通过修改缩放比例来实现组件的缩小或者放大
.scale({ x: this.scaleValue, y: this.scaleValue, z: 1 })
.gesture(
// 在组件上绑定三指触发的捏合手势
PinchGesture({ fingers: 3 })
.onActionStart((event: GestureEvent) => {
console.info('Pinch start');
})
// 当捏合手势触发时,可以通过回调函数获取缩放比例,从而修改组件的缩放比例
.onActionUpdate((event: GestureEvent) => {
this.scaleValue = this.pinchValue * event.scale;
this.pinchX = event.pinchCenterX;
this.pinchY = event.pinchCenterY;
})
.onActionEnd(() => {
this.pinchValue = this.scaleValue;
console.info('Pinch end');
})
)
}
}
}
```
![pinch](figures/pinch.png)
## 旋转手势(RotationGesture)
```ts
RotationGesture(value?:{fingers?:number; angle?:number})
```
旋转手势用于触发旋转手势事件,触发旋转手势的最少手指数量为2指,最大为5指,最小改变度数为1度,拥有两个可选参数:
- fingers:非必选参数,用于声明触发旋转手势所需要的最少手指数量,最小值为2,最大值为5,默认值为2。
- angle:非必选参数,用于声明触发旋转手势的最小改变度数,单位为deg,默认值为1。
以在Text组件上绑定旋转手势实现组件的旋转为例,可以通过在旋转手势的回调函数中获取旋转角度,从而实现组件的旋转:
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State angle: number = 0;
@State rotateValue: number = 0;
build() {
Column() {
Text('RotationGesture angle:' + this.angle).fontSize(28)
// 在组件上绑定旋转布局,可以通过修改旋转角度来实现组件的旋转
.rotate({ angle: this.angle })
.gesture(
RotationGesture()
.onActionStart((event: GestureEvent) => {
console.info('RotationGesture is onActionStart');
})
// 当旋转手势生效时,通过旋转手势的回调函数获取旋转角度,从而修改组件的旋转角度
.onActionUpdate((event: GestureEvent) => {
this.angle = this.rotateValue + event.angle;
console.info('RotationGesture is onActionEnd');
})
// 当旋转结束抬手时,固定组件在旋转结束时的角度
.onActionEnd(() => {
this.rotateValue = this.angle;
console.info('RotationGesture is onActionEnd');
})
.onActionCancel(() => {
console.info('RotationGesture is onActionCancel');
})
)
}
.height(200)
.width(250)
}
}
```
![rotation](figures/rotation.png)
## 滑动手势(SwipeGesture)
```ts
SwipeGesture(value?:{fingers?:number; direction?:SwipeDirection; speed?:number})
```
滑动手势用于触发滑动事件,当滑动速度大于100vp/s时可以识别成功,拥有三个可选参数:
- fingers:非必选参数,用于声明触发滑动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。
- direction:非必选参数,用于声明触发滑动手势的方向,此枚举值支持逻辑与(&amp;)和逻辑或(|)运算。默认值为SwipeDirection.All。
- speed:非必选参数,用于声明触发滑动的最小滑动识别速度,单位为vp/s,默认值为100。
以在Column组件上绑定滑动手势实现组件的旋转为例:
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State rotateAngle: number = 0;
@State speed: number = 1;
build() {
Column() {
Column() {
Text("SwipeGesture speed\n" + this.speed)
Text("SwipeGesture angle\n" + this.rotateAngle)
}
.border({ width: 3 })
.width(300)
.height(200)
.margin(100)
// 在Column组件上绑定旋转,通过滑动手势的滑动速度和角度修改旋转的角度
.rotate({ angle: this.rotateAngle })
.gesture(
// 绑定滑动手势且限制仅在竖直方向滑动时触发
SwipeGesture({ direction: SwipeDirection.Vertical })
// 当滑动手势触发时,获取滑动的速度和角度,实现对组件的布局参数的修改
.onAction((event: GestureEvent) => {
this.speed = event.speed;
this.rotateAngle = event.angle;
})
)
}
}
}
```
![swipe](figures/swipe.gif)
>**说明:**
>
>当SwipeGesture和PanGesture同时绑定时,若二者是以默认方式或者互斥方式进行绑定时,会发生竞争。SwipeGesture的触发条件为滑动速度达到100vp/s,PanGesture的触发条件为滑动距离达到5vp,先达到触发条件的手势触发。可以通过修改SwipeGesture和PanGesture的参数以达到不同的效果。
# 显示图片
开发者经常需要在应用中显示一些图片,例如:按钮中的logo、网络图片、本地图片等。在应用中显示图片需要使用Image组件实现,Image支持多种图片格式,包括png、jpg、bmp、svg和gif,具体用法请参考[Image](../reference/arkui-ts/ts-basic-components-image.md)组件。
Image通过调用接口来创建,接口调用形式如下:
```ts
Image(src: string | Resource | media.PixelMap)
```
该接口通过图片数据源获取图片,支持本地图片和网络图片的渲染展示。其中,src是图片的数据源,加载方式请参考[加载图片资源](#加载图片资源)
## 加载图片资源
Image支持加载存档图、多媒体像素图两种类型。
### 存档图类型数据源
存档图类型的数据源可以分为本地资源、网络资源、Resource资源、媒体库datashare资源和base64。
- 本地资源
创建文件夹,将本地图片放入ets文件夹下的任意位置。
Image组件引入本地图片路径,即可显示图片(根目录为ets文件夹)。
```ts
Image('images/view.jpg')
.width(200)
```
- 网络资源
引入网络图片需申请权限ohos.permission.INTERNET,具体申请方式请参考[权限申请声明](../security/accesstoken-guidelines.md)。此时,Image组件的src参数为网络图片的链接。
```ts
Image('https://www.example.com/example.JPG') // 实际使用时请替换为真实地址
```
- Resource资源
使用资源格式可以跨包/跨模块引入图片,resources文件夹下的图片都可以通过$r资源接口读 取到并转换到Resource格式。
**图1** resouces  
![image-resource](figures/image-resource.jpg)
调用方式:
```
Image($r('app.media.icon'))
```
还可以将图片放在rawfile文件夹下。
**图2** rawfile  
![image-rawfile](figures/image-rawfile.jpg)
调用方式:
```
Image($rawfile('snap'))
```
- 媒体库datashare
支持datashare://路径前缀的字符串,用于访问通过媒体库提供的图片路径。
1. 调用接口获取图库的照片url。
```ts
import picker from '@ohos.file.picker';
@Entry
@Component
struct Index {
private imgDatas: string[] = [];
// 获取照片url集
async getAllImg() {
let photoPicker = new picker.PhotoViewPicker();
let result = new Array<string>();
try {
let PhotoSelectOptions = new picker.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 5;
let photoPicker = new picker.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult) => {
result = PhotoSelectResult.photoUris;
}).catch((err) => {
console.error(`PhotoViewPicker.select failed with. Code: ${err.code}, message: ${err.message}`);
});
} catch (err) {
console.error(`PhotoViewPicker failed with. Code: ${err.code}, message: ${err.message}`); }
return result;
}
// aboutToAppear中调用上述函数,获取图库的所有图片url,存在imgDatas中
async aboutToAppear() {
this.imgDatas = await this.getAllImg();
}
// 使用imgDatas的url加载图片。
build() {
Grid() {
ForEach(this.imgDatas, item => {
GridItem() {
Image(item)
.width(200)
}
}, item => JSON.stringify(item))
}
}
}
```
2. 从媒体库获取的url格式通常如下。
```ts
Image('datashare:///media/5')
.width(200)
```
- base64
路径格式为data:image/[png|jpeg|bmp|webp];base64,[base64 data],其中[base64 data]为Base64字符串数据。
Base64格式字符串可用于存储图片的像素数据,在网页上使用较为广泛。
### 多媒体像素图
PixelMap是图片解码后的像素图,具体用法请参考[图片开发指导](../media/image.md)。以下示例将加载的网络图片返回的数据解码成PixelMap格式,再显示在Image组件上,
1. 创建PixelMap状态变量。
```ts
@State image: PixelMap = undefined;
```
2. 引用多媒体。
请求网络图片请求,解码编码PixelMap。
1. 引用网络权限与媒体库权限。
```ts
import http from '@ohos.net.http';
import ResponseCode from '@ohos.net.http';
import image from '@ohos.multimedia.image';
```
2. 填写网络图片地址。
```ts
http.createHttp().request("https://www.example.com/xxx.png",
(error, data) => {
if (error){
console.error(`http reqeust failed with. Code: ${error.code}, message: ${error.message}`);
} else {
}
}
)
```
3. 将网络地址成功返回的数据,编码转码成pixelMap的图片格式。
```ts
let code = data.responseCode;
if(ResponseCode.ResponseCode.OK === code) {
let imageSource = image.createImageSource(data.result);
let options = {
alphaType: 0, // 透明度
editable: false, // 是否可编辑
pixelFormat: 3, // 像素格式
scaleMode: 1, // 缩略值
size: {height: 100, width: 100}
} // 创建图片大小
imageSource.createPixelMap(options).then((pixelMap) => {
this.image = pixelMap
})
```
4. 显示图片。
```ts
Button("获取网络图片")
.onClick(() => {
this.httpRequest()
})
Image(this.image).height(100).width(100)
```
## 显示矢量图
Image组件可显示矢量图(svg格式的图片),支持的svg标签为:svg、rect、circle、ellipse、path、line、polyline、polygon和animate。
svg格式的图片可以使用fillColor属性改变图片的绘制颜色。
```ts
Image($r('app.media.cloud')).width(50)
.fillColor(Color.Blue)
```
**图3** 原始图片  
![屏幕截图_20230223_141141](figures/屏幕截图_20230223_141141.png)
**图4** 设置绘制颜色后的svg图片  
![屏幕截图_20230223_141404](figures/屏幕截图_20230223_141404.png)
## 添加属性
给Image组件设置属性可以使图片显示更灵活,达到一些自定义的效果。以下是几个常用属性的使用示例,完整属性信息详见[Image](../reference/arkui-ts/ts-basic-components-image.md)
### 设置图片缩放类型
通过objectFit属性使图片缩放到高度和宽度确定的框内。
```ts
@Entry
@Component
struct MyComponent {
scroller: Scroller = new Scroller()
build() {
Scroll(this.scroller) {
Row() {
Image($r('app.media.img_2')).width(200).height(150)
.border({ width: 1 })
.objectFit(ImageFit.Contain).margin(15) // 保持宽高比进行缩小或者放大,使得图片完全显示在显示边界内。
.overlay('Contain', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.ic_img_2')).width(200).height(150)
.border({ width: 1 })
.objectFit(ImageFit.Cover).margin(15)
// 保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界。
.overlay('Cover', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2')).width(200).height(150)
.border({ width: 1 })
// 自适应显示。
.objectFit(ImageFit.Auto).margin(15)
.overlay('Auto', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}
Row() {
Image($r('app.media.img_2')).width(200).height(150)
.border({ width: 1 })
.objectFit(ImageFit.Fill).margin(15)
// 不保持宽高比进行放大缩小,使得图片充满显示边界。
.overlay('Fill', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2')).width(200).height(150)
.border({ width: 1 })
// 保持宽高比显示,图片缩小或者保持不变。
.objectFit(ImageFit.ScaleDown).margin(15)
.overlay('ScaleDown', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2')).width(200).height(150)
.border({ width: 1 })
// 保持原有尺寸显示。
.objectFit(ImageFit.None).margin(15)
.overlay('None', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}
}
}
}
```
![zh-cn_image_0000001511421240](figures/zh-cn_image_0000001511421240.png)
### 同步加载图片
一般情况下,图片加载流程会异步进行,以避免阻塞主线程,影响UI交互。但是特定情况下,图片刷新时会出现闪烁,这时可以使用syncLoad属性,使图片同步加载,从而避免出现闪烁。不建议图片加载较长时间时使用,会导致页面无法响应。
```ts
Image($r('app.media.icon'))
.syncLoad(true)
```
## 事件调用
通过在Image组件上绑定onComplete事件,图片加载成功后可以获取图片的必要信息。如果图片加载失败,也可以通过绑定onError回调来获得结果。
```ts
@Entry
@Component
struct MyComponent {
@State widthValue: number = 0
@State heightValue: number = 0
@State componentWidth: number = 0
@State componentHeight: number = 0
build() {
Column() {
Row() {
Image($r('app.media.ic_img_2'))
.width(200)
.height(150)
.margin(15)
.onComplete((msg: {
width: number,
height: number,
componentWidth: number,
componentHeight: number
}) => {
this.widthValue = msg.width
this.heightValue = msg.height
this.componentWidth = msg.componentWidth
this.componentHeight = msg.componentHeight
})
// 图片获取失败,打印结果
.onError(() => {
console.info('load image fail')
})
.overlay('\nwidth: ' + String(this.widthValue) + ', height: ' + String(this.heightValue) + '\ncomponentWidth: ' + String(this.componentWidth) + '\ncomponentHeight: ' + String(this.componentHeight), {
align: Alignment.Bottom,
offset: { x: 0, y: 60 }
})
}
}
}
}
```
![zh-cn_image_0000001511740460](figures/zh-cn_image_0000001511740460.png)
# 创建网格
## 概述
网格布局是由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。网格布局具有较强的页面均分能力,子组件占比控制能力,是一种重要自适应布局,其使用场景有九宫格图片展示、日历、计算器等。
ArkUI提供了[Grid](../reference/arkui-ts/ts-container-grid.md)容器组件和子组件[GridItem](../reference/arkui-ts/ts-container-griditem.md),用于构建网格布局。Grid用于设置网格布局相关参数,GridItem定义子组件相关特征。Grid组件支持使用条件渲染、循环渲染、[懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)等方式生成子组件。
## 布局与约束
Grid组件为网格容器,其中容器内个条目对应一个GridItem组件,如下图所示。
**图1** Grid与GridItem组件关系
![zh-cn_image_0000001511900472](figures/zh-cn_image_0000001511900472.png)
>**说明:**
>
>Grid的子组件必须是GridItem组件。
网格布局是一种二维布局。Grid组件支持自定义行列数和每行每列尺寸占比、设置子组件横跨几行或者几列,同时提供了垂直和水平布局能力。当网格容器组件尺寸发生变化时,所有子组件以及间距会等比例调整,从而实现网格布局的自适应能力。根据Grid的这些布局能力,可以构建出不同样式的网格布局,如下图所示。
**图2** 网格布局  
![zh-cn_image_0000001562700473](figures/zh-cn_image_0000001562700473.png)
如果Grid组件设置了宽高属性,则其尺寸为设置值。如果没有设置宽高属性,Grid组件的尺寸默认适应其父组件的尺寸。
Grid组件根据行列数量与占比属性的设置,可以分为三种布局情况:
- 行、列数量与占比同时设置:Grid只展示固定行列数的元素,其余元素不展示,且Grid不可滚动。
- 只设置行、列数量与占比中的一个:元素按照设置的方向进行排布,超出的元素可通过滚动的方式展示。
- 行列数量与占比都不设置:元素在布局方向上排布,其行列数由布局方向、单个网格的宽高等多个属性共同决定。超出行列容纳范围的元素不展示,且Grid不可滚动。
>**说明:**
>
>推荐优先采用确定的行或列数量占比方式进行布局。
## 设置排列方式
### 设置行列数量与占比
通过设置行列数量与尺寸占比可以确定网格布局的整体排列方式。Grid组件提供了rowsTemplate和columnsTemplate属性用于设置网格布局行列数量与尺寸占比。
rowsTemplate和columnsTemplate属性值是一个由多个空格和'数字+fr'间隔拼接的字符串,fr的个数即网格布局的行或列数,fr前面的数值大小,用于计算该行或列在网格布局宽度上的占比,最终决定该行或列宽度。
**图3** 行列数量占比示例
![zh-cn_image_0000001562820833](figures/zh-cn_image_0000001562820833.png)
如上图所示,构建的是一个三行三列的的网格布局,其在垂直方向上分为三等份,每行占一份;在水平方向上分为四等份,第一列占一份,第二列占两份,第三列占一份。
只要将rowsTemplate的值为'1fr 1fr 1fr',同时将columnsTemplate的值为'1fr 2fr 1fr',即可实现上述网格布局。
```ts
Grid() {
...
}
.rowsTemplate('1fr 1fr 1fr')
.columnsTemplate('1fr 2fr 1fr')
```
>**说明:**
>
>当Grid组件设置了rowsTemplate或columnsTemplate时,Grid的layoutDirection、maxCount、minCount、cellLength属性不生效,属性说明可参考[Grid-属性](../reference/arkui-ts/ts-container-grid.md#属性)。
### 设置子组件所占行列数
除了大小相同的等比例网格布局,由不同大小的网格组成不均匀分布的网格布局场景在实际应用中十分常见,如下图所示。在Grid组件中,通过设置GridItem的rowStart、rowEnd、columnStart和columnEnd可以实现如图所示的单个网格横跨多行或多列的场景。
**图4** 不均匀网格布局
![zh-cn_image_0000001511900480](figures/zh-cn_image_0000001511900480.png)
例如计算器的按键布局就是常见的不均匀网格布局场景。如下图,计算器中的按键“0”和“=”,按键“0”横跨第一、二两列,按键“=”横跨第五、六两行。使用Grid构建的网格布局,其行列标号从1开始,依次编号。
**图5** 计算器  
![zh-cn_image_0000001511421292](figures/zh-cn_image_0000001511421292.png)
在单个网格单元中,rowStart和rowEnd属性表示指定当前元素起始行号和终点行号,columnStart和columnEnd属性表示指定当前元素的起始列号和终点列号。
所以“0”按键横跨第一列和第二列,只要将“0”对应GridItem的columnStart和columnEnd设为1和2,将“=”对应GridItem的的rowStart和rowEnd设为5和6即可。
```ts
GridItem() {
Text(key)
...
}
.columnStart(1)
.columnEnd(2)
```
“=”按键横跨第五行和第六行,只要将将“=”对应GridItem的的rowStart和rowEnd设为5和6即可。
```ts
GridItem() {
Text(key)
...
}
.rowStart(5)
.rowEnd(6)
```
### 设置主轴方向
使用Grid构建网格布局时,若没有设置行列数量与占比,可以通过layoutDirection可以设置网格布局的主轴方向,决定子组件的排列方式。此时可以结合minCount和maxCount属性来约束主轴方向上的网格数量。
**图6** 主轴方向示意图
![zh-cn_image_0000001562700469](figures/zh-cn_image_0000001562700469.png)
当前layoutDirection设置为Row时,先从左到右排列,排满一行再排一下一行。当前layoutDirection设置为Column时,先从上到下排列,排满一列再排一下一列,如上图所示。此时,将maxCount属性设为3,表示主轴方向上最大显示的网格单元数量为3。
```ts
Grid() {
...
}
.maxCount(3)
.layoutDirection(GridDirection.Row)
```
>**说明:**
>
>- layoutDirection属性仅在不设置rowsTemplate和columnsTemplate时生效,此时元素在layoutDirection方向上排列。
>- 仅设置rowsTemplate时,Grid主轴为水平方向,交叉轴为垂直方向。
>- 仅设置columnsTemplate时,Grid主轴为垂直方向,交叉轴为水平方向。
## 在网格布局中显示数据
网格布局采用二维布局的方式组织其内部元素,如下图所示。
**图7** 通用办公服务  
![zh-cn_image_0000001563060729](figures/zh-cn_image_0000001563060729.png)
Grid组件可以通过二维布局的方式显示一组GridItem子组件。
```ts
Grid() {
GridItem() {
Text('会议')
...
}
GridItem() {
Text('签到')
...
}
GridItem() {
Text('投票')
...
}
GridItem() {
Text('打印')
...
}
}
.rowsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
```
对于内容结构相似的多个GridItem,通常更推荐使用ForEach语句中嵌套GridItem的形式,来减少重复代码。
```ts
@Component
struct OfficeService {
@State services: Array<string> = ['会议', '投票', '签到', '打印']
...
build() {
Column() {
Grid() {
ForEach(this.services, service => {
GridItem() {
Text(service)
...
}
}, service => service)
}
.rowsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
...
}
...
}
}
```
## 设置行列间距
在两个网格单元之间的网格横向间距称为行间距,网格纵向间距称为列间距,如下图所示。
**图8** 网格的行列间距  
![zh-cn_image_0000001511580908](figures/zh-cn_image_0000001511580908.png)
通过Grid的rowsGap和columnsGap可以设置网格布局的行列间距。在图5所示的计算器中,行间距为15vp,列间距为10vp。
```ts
Grid() {
...
}
.columnsGap(10)
.rowsGap(15)
```
## 构建可滚动的网格布局
可滚动的网格布局常用在文件管理、购物或视频列表等页面中,如下图所示。在设置Grid的行列数量与占比时,如果仅设置行、列数量与占比中的一个,即仅设置rowsTemplate或仅设置columnsTemplate属性,网格单元按照设置的方向排列,超出Grid显示区域后,Grid拥有可滚动能力。
**图9** 横向可滚动网格布局
![zh-cn_image_0000001511740512](figures/zh-cn_image_0000001511740512.gif)
如果设置的是columnsTemplate,Grid的滚动方向为垂直方向;如果设置的是rowsTemplate,Grid的滚动方向为水平方向。
如上图所示的横向可滚动网格布局,只要设置rowsTemplate属性的值且不设置columnsTemplate属性,当内容超出Grid组件宽度时,Grid可横向滚动进行内容展示。
```ts
@Component
struct Shopping {
@State services: Array<string> = ['直播', '进口', ...]
...
build() {
Column({ space: 5 }) {
Grid() {
ForEach(this.services, (service: string, index) => {
GridItem() {
...
}
.width('25%')
}, service => service)
}
.rowsTemplate('1fr 1fr') // 只设置rowsTemplate属性,当内容超出Grid区域时,可水平滚动。
.rowsGap(15)
...
}
...
}
}
```
## 控制滚动位置
与新闻列表的返回顶部场景类似,控制滚动位置功能在网格布局中也很常用,例如下图所示日历的翻页功能。
**图10** 日历翻页  
![zh-cn_image_0000001562940549](figures/zh-cn_image_0000001562940549.gif)
Grid组件初始化时,可以绑定一个[Scroller](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-scroll.md/#scroller)对象,用于进行滚动控制,例如通过Scroller对象的[scrollPage](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-scroll.md/#scrollpage)方法进行翻页。
```ts
private scroller: Scroller = new Scroller()
```
在日历页面中,用户在点击“下一页”按钮时,应用响应点击事件,通过指定scrollPage方法的参数next为true,滚动到下一页。
```ts
Column({ space: 5 }) {
Grid(this.scroller) {
...
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
...
Row({space: 20}) {
Button('上一页')
.onClick(() => {
this.scroller.scrollPage({
next: false
})
})
Button('下一页')
.onClick(() => {
this.scroller.scrollPage({
next: true
})
})
}
}
...
```
## 性能优化
[长列表的处理](arkts-layout-development-create-list.md#长列表的处理)类似,[循环渲染](../quick-start/arkts-rendering-control-foreach.md)适用于数据量较小的布局场景,当构建具有大量网格项的可滚动网格布局时,推荐使用[数据懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)方式实现按需迭代加载数据,从而提升列表性能。
关于按需加载优化的具体实现可参考[数据懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)章节中的示例。
当使用懒加载方式渲染网格时,为了更好的滚动体验,减少滑动时出现白块,Grid组件中也可通过cachedCount属性设置GridItem的预加载数量,只在懒加载LazyForEach中生效。
设置预加载数量后,会在Grid显示区域前后各缓存cachedCount\*列数个GridItem,超出显示和缓存范围的GridItem会被释放。
```ts
Grid() {
LazyForEach(this.dataSource, item => {
GridItem() {
...
}
})
}
.cachedCount(3)
```
>**说明:**
>
>cachedCount的增加会增大UI的CPU、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。
## 相关实例
如需详细了解网格布局的实现,请参考以下示例:
- [分布式计算器](https://gitee.com/openharmony/applications_app_samples/tree/master/Preset/EtsDistributedCalc)
# 创建列表
## 概述
列表是一种复杂的容器,当列表项达到一定数量,内容超过屏幕大小时,可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集,例如图片和文本。在列表中显示数据集合是许多应用程序中的常见要求(如通讯录、音乐列表、购物清单等)。
使用列表可以轻松高效地显示结构化、可滚动的信息。通过在[List](../reference/arkui-ts/ts-container-list.md/)组件中按垂直或者水平方向线性排列子组件[ListItemGroup](../reference/arkui-ts/ts-container-listitemgroup.md/)[ListItem](../reference/arkui-ts/ts-container-listitem.md),为列表中的行或列提供单个视图,或使用[循环渲染](../quick-start/arkts-rendering-control-foreach.md)迭代一组行或列,或混合任意数量的单个视图和ForEach结构,构建一个列表。List组件支持使用条件渲染、循环渲染、懒加载等[渲染控制](../quick-start/arkts-rendering-control-overview.md)方式生成子组件。
## 布局与约束
列表作为一种容器,会自动按其滚动方向排列子组件,向列表中添加组件或从列表中移除组件会重新排列子组件。
如下图所示,在垂直列表中,List按垂直方向自动排列ListItemGroup或ListItem。
ListItemGroup用于列表数据的分组展示,其子组件也是ListItem。ListItem表示单个列表项,可以包含单个子组件。
**图1** List、ListItemGroup和ListItem组件关系  
![zh-cn_image_0000001562940589](figures/zh-cn_image_0000001562940589.png)
>**说明:**
>
>List的子组件必须是ListItemGroup或ListItem,ListItem和ListItemGroup必须配合List来使用。
### 布局
List除了提供垂直和水平布局能力、超出屏幕时可以滚动的自适应[延伸能力](../key-features/multi-device-app-dev/adaptive-layout.md/#%E5%BB%B6%E4%BC%B8%E8%83%BD%E5%8A%9B)之外,还提供了自适应交叉轴方向上排列个数的布局能力。
利用垂直布局能力可以构建单列或者多列垂直滚动列表,如下图所示。
**图2** 垂直滚动列表(左:单列;右:多列)  
![zh-cn_image_0000001511580940](figures/zh-cn_image_0000001511580940.png)
利用水平布局能力可以是构建单行或多行水平滚动列表,如下图所示。
**图3** 水平滚动列表(左:单行;右:多行)  
![zh-cn_image_0000001511421344](figures/zh-cn_image_0000001511421344.png)
### 约束
列表的主轴方向是指子组件列的排列方向,也是列表的滚动方向。垂直于主轴的轴称为交叉轴,其方向与主轴方向相互垂直。
如下图所示,垂直列表的主轴是垂直方向,交叉轴是水平方向;水平列表的主轴是水平方向,交叉轴是水平方向。
**图4** 列表的主轴与交叉轴  
![zh-cn_image_0000001562940581](figures/zh-cn_image_0000001562940581.png)
如果List组件主轴或交叉轴方向设置了尺寸,则其对应方向上的尺寸为设置值。
如果List组件主轴方向没有设置尺寸,当List子组件主轴方向总尺寸小于List的父组件尺寸时,List主轴方向尺寸自动适应子组件的总尺寸。
如下图所示,一个垂直列表B没有设置高度时,其父组件A高度为200vp,若其所有子组件C的高度总和为150vp,则此时列表B的高度为150vp。
**图5** 列表主轴高度约束示例1(**A**: List的父组件; **B**: List组件; **C**: List的所有子组件)  
![zh-cn_image_0000001511580956](figures/zh-cn_image_0000001511580956.png)
如果子组件主轴方向总尺寸超过List父组件尺寸时,List主轴方向尺寸适应List的父组件尺寸。
如下图所示,同样是没有设置高度的垂直列表B,其父组件A高度为200vp,若其所有子组件C的高度总和为300vp,则此时列表B的高度为200vp。
**图6** 列表主轴高度约束示例2(**A**: List的父组件; **B**: List组件; **C**: List的所有子组件)  
![zh-cn_image_0000001511740548](figures/zh-cn_image_0000001511740548.png)
List组件交叉轴方向在没有设置尺寸时,其尺寸默认自适应父组件尺寸。
## 开发布局
### 设置主轴方向
List组件主轴默认是垂直方向,即默认情况下不需要手动设置List方向,就可以构建一个垂直滚动列表。
若是水平滚动列表场景,将List的listDirection属性设置为Axis.Horizontal即可实现。listDirection默认为Axis.Vertical,即主轴默认是垂直方向。
```ts
List() {
...
}
.listDirection(Axis.Horizontal)
```
### 设置交叉轴布局
List组件的交叉轴布局可以通过lanes和alignListItem属性进行设置,lanes属性用于确定交叉轴排列的列表项数量,alignListItem用于设置子组件在交叉轴方向的对齐方式。
List组件的lanes属性通常用于在不同尺寸的设备自适应构建不同行数或列数的列表,即一次开发、多端部署的场景,例如[歌单列表](../key-features/multi-device-app-dev/music-album-page.md#%E6%AD%8C%E5%8D%95%E5%88%97%E8%A1%A8)。lanes属性的取值类型是"number | [LengthConstrain](../reference/arkui-ts/ts-types.md/#lengthconstrain)",即整数或者LengthConstrain类型。以垂直列表为例,如果将lanes属性设为2,表示构建的是一个两列的垂直列表,如图2中右图所示。lanes的默认值为1,即默认情况下,垂直列表的列数是1。
```ts
List() {
...
}
.lanes(2)
```
当其取值为LengthConstrain类型时,表示会根据LengthConstrain与List组件的尺寸自适应决定行或列数。
```ts
List() {
...
}
.lanes({ minLength: 200, maxLength: 300 })
```
例如,假设在垂直列表中设置了lanes的值为{ minLength: 200, maxLength: 300 }。此时,
- 当List组件宽度为300vp时,由于minLength为200vp,此时列表为一列。
- 当List组件宽度变化至400vp时,符合两倍的minLength,则此时列表自适应为两列。
>**说明:**
>
>当lanes为LengthConstrain类型时,仅用于计算当前列表的行或列数,不影响列表项本身的尺寸。
同样以垂直列表为例,当alignListItem属性设置为ListItemAlign.Center表示列表项在水平方向上居中对齐。alignListItem的默认值是ListItemAlign.Start,即列表项在列表交叉轴方向上默认按首部对齐。
```ts
List() {
...
}
.alignListItem(ListItemAlign.Center)
```
## 在列表中显示数据
列表视图垂直或水平显示项目集合,在行或列超出屏幕时提供滚动功能,使其适合显示大型数据集合。在最简单的列表形式中,List静态地创建其列表项ListItem的内容。
**图7** 城市列表  
![zh-cn_image_0000001563060761](figures/zh-cn_image_0000001563060761.png)
```ts
@Component
struct CityList {
build() {
List() {
ListItem() {
Text('北京').fontSize(24)
}
ListItem() {
Text('杭州').fontSize(24)
}
ListItem() {
Text('上海').fontSize(24)
}
}
.backgroundColor('#FFF1F3F5')
.alignListItem(ListItemAlign.Center)
}
}
```
由于在ListItem中只能有一个根节点组件,不支持以平铺形式使用多个组件。因此,若列表项是由多个组件元素组成的,则需要将这多个元素组合到一个容器组件内或组成一个自定义组件。
**图8** 联系人列表项示例  
![zh-cn_image_0000001511421328](figures/zh-cn_image_0000001511421328.png)
如上图所示,联系人列表的列表项中,每个联系人都有头像和名称。此时,需要将Image和Text封装到一个Row容器内。
```ts
List() {
ListItem() {
Row() {
Image($r('app.media.iconE'))
.width(40)
.height(40)
.margin(10)
Text('小明')
.fontSize(20)
}
}
ListItem() {
Row() {
Image($r('app.media.iconF'))
.width(40)
.height(40)
.margin(10)
Text('小红')
.fontSize(20)
}
}
}
```
## 迭代列表内容
通常更常见的是,应用通过数据集合动态地创建列表。使用[循环渲染](../quick-start/arkts-rendering-control-foreach.md)可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件,降低代码复杂度。
ArkTS通过[ForEach](../quick-start/arkts-rendering-control-foreach.md)提供了组件的循环渲染能力。以简单形式的联系人列表为例,将联系人名称和头像数据以Contact类结构存储到contacts数组,使用ForEach中嵌套ListItem的形式来代替多个平铺的、内容相似的ListItem,从而减少重复代码。
```ts
import util from '@ohos.util';
class Contact {
key: string = util.generateRandomUUID(true);
name: string;
icon: Resource;
constructor(name: string, icon: Resource) {
this.name = name;
this.icon = icon;
}
}
@Entry
@Component
struct SimpleContacts {
private contacts = [
new Contact('小明', $r("app.media.iconA")),
new Contact('小红', $r("app.media.iconB")),
...
]
build() {
List() {
ForEach(this.contacts, (item: Contact) => {
ListItem() {
Row() {
Image(item.icon)
.width(40)
.height(40)
.margin(10)
Text(item.name).fontSize(20)
}
.width('100%')
.justifyContent(FlexAlign.Start)
}
}, item => item.key)
}
.width('100%')
}
}
```
在List组件中,ForEach除了可以用来循环渲染ListItem,也可以用来循环渲染ListItemGroup。ListItemGroup的循环渲染详细使用请参见[支持分组列表](#支持分组列表)
## 自定义列表样式
### 设置内容间距
在初始化列表时,如需在列表项之间添加间距,可以使用space参数。例如,在每个列表项之间沿主轴方向添加10vp的间距:
```ts
List({ space: 10 }) {
...
}
```
### 添加分隔线
分隔线用来将界面元素隔开,使单个元素更加容易识别。如下图所示,当列表项左边有图标(如蓝牙图标),由于图标本身就能很好的区分,此时分隔线从图标之后开始显示即可。
**图9** 设置列表分隔线样式  
![zh-cn_image_0000001511580960](figures/zh-cn_image_0000001511580960.png)
List提供了divider属性用于给列表项之间添加分隔线。在设置divider属性时,可以通过strokeWidth和color属性设置分隔线的粗细和颜色。
startMargin和endMargin属性分别用于设置分隔线距离列表侧边起始端的距离和距离列表侧边结束端的距离。
```ts
List() {
...
}
.divider({
strokeWidth: 1,
startMargin: 60,
endMargin: 10,
color: '#ffe9f0f0'
})
```
此示例表示从距离列表侧边起始端60vp开始到距离结束端10vp的位置,画一条粗细为1vp的分割线,可以实现图8设置列表分隔线的样式。
>**说明:**
>
>1. 分隔线的宽度会使ListItem之间存在一定间隔,当List设置的内容间距小于分隔线宽度时,ListItem之间的间隔会使用分隔线的宽度。
>
>2. 当List存在多列时,分割线的startMargin和endMargin作用于每一列上。
>
>3. List组件的分隔线画在两个ListItem之间,第一个ListItem上方和最后一个ListItem下方不会绘制分隔线。
### 添加滚动条
当列表项高度(宽度)超出屏幕高度(宽度)时,列表可以沿垂直(水平)方向滚动。在页面内容很多时,若用户需快速定位,可拖拽滚动条,如下图所示。
**图10** 列表的滚动条 
![zh-cn_image_0000001511740544](figures/zh-cn_image_0000001511740544.gif)
在使用List组件时,可通过scrollBar属性控制列表滚动条的显示。scrollBar的取值类型为[BarState](../reference/arkui-ts/ts-appendix-enums.md/#barstate),当取值为BarState.Auto表示按需显示滚动条。此时,当触摸到滚动条区域时显示控件,可上下拖拽滚动条快速浏览内容,拖拽时会变粗。若不进行任何操作,2秒后滚动条自动消失。
```ts
List() {
...
}
.scrollBar(BarState.Auto)
```
## 支持分组列表
在列表中支持数据的分组展示,可以使列表显示结构清晰,查找方便,从而提高使用效率。分组列表在实际应用中十分常见,如下图所示联系人列表。
**图11** 联系人分组列表 
![zh-cn_image_0000001511580948](figures/zh-cn_image_0000001511580948.png)
在List组件中使用ListItemGroup对项目进行分组,可以构建二维列表。
在List组件中可以直接使用一个或者多个ListItemGroup组件,ListItemGroup的宽度默认充满List组件。在初始化ListItemGroup时,可通过header参数设置列表分组的头部组件。
```ts
@Component
struct ContactsList {
...
@Builder itemHead(text: string) {
// 列表分组的头部组件,对应联系人分组A、B等位置的组件
Text(text)
.fontSize(20)
.backgroundColor('#fff1f3f5')
.width('100%')
.padding(5)
}
build() {
List() {
ListItemGroup({ header: this.itemHead('A') }) {
// 循环渲染分组A的ListItem
...
}
...
ListItemGroup({ header: this.itemHead('B') }) {
// 循环渲染分组B的ListItem
...
}
...
}
}
}
```
如果多个ListItemGroup结构类似,可以将多个分组的数据组成数组,然后使用ForEach对多个分组进行循环渲染。例如在联系人列表中,将每个分组的联系人数据contacts(可参考[迭代列表内容](#迭代列表内容)章节)和对应分组的标题title数据进行组合,定义为数组contactsGroups。
```ts
contactsGroups: object[] = [
{
title: 'A',
contacts: [
new Contact('艾佳', $r('app.media.iconA')),
new Contact('安安', $r('app.media.iconB')),
new Contact('Angela', $r('app.media.iconC')),
],
},
{
title: 'B',
contacts: [
new Contact('白叶', $r('app.media.iconD')),
new Contact('伯明', $r('app.media.iconE')),
],
},
...
]
```
然后在ForEach中对contactsGroups进行循环渲染,即可实现多个分组的联系人列表。
```ts
List() {
// 循环渲染ListItemGroup,contactsGroups为多个分组联系人contacts和标题title的数据集合
ForEach(this.contactsGroups, item => {
ListItemGroup({ header: this.itemHead(item.title) }) {
// 循环渲染ListItem
ForEach(item.contacts, (contact) => {
ListItem() {
...
}
}, item => item.key)
}
...
})
}
```
## 添加粘性标题
粘性标题是一种常见的标题模式,常用于定位字母列表的头部元素。如下图所示,在联系人列表中滚动A部分时,B部分开始的头部元素始终处于A的下方。而在开始滚动B部分时,B的头部会固定在屏幕顶部,直到所有B的项均完成滚动后,才被后面的头部替代。
粘性标题不仅有助于阐明列表中数据的表示形式和用途,还可以帮助用户在大量信息中进行数据定位,从而避免用户在标题所在的表的顶部与感兴趣区域之间反复滚动。
**图12** 粘性标题  
![zh-cn_image_0000001511740552](figures/zh-cn_image_0000001511740552.gif)
List组件的sticky属性配合ListItemGroup组件使用,用于设置ListItemGroup中的头部组件是否呈现吸顶效果或者尾部组件是否呈现吸底效果。
通过给List组件设置sticky属性为StickyStyle.Header,即可实现列表的粘性标题效果。如果需要支持吸底效果,可以通过footer参数初始化ListItemGroup的底部组件,并将sticky属性设置为StickyStyle.Footer。
```ts
@Component
struct ContactsList {
// 定义分组联系人数据集合contactsGroups数组
...
@Builder itemHead(text: string) {
// 列表分组的头部组件,对应联系人分组A、B等位置的组件
Text(text)
.fontSize(20)
.backgroundColor('#fff1f3f5')
.width('100%')
.padding(5)
}
build() {
List() {
// 循环渲染ListItemGroup,contactsGroups为多个分组联系人contacts和标题title的数据集合
ForEach(this.contactsGroups, item => {
ListItemGroup({ header: this.itemHead(item.title) }) {
// 循环渲染ListItem
ForEach(item.contacts, (contact) => {
ListItem() {
...
}
}, item => item.key)
}
...
})
}
.sticky(StickyStyle.Header) // 设置吸顶,实现粘性标题效果
}
}
```
## 控制滚动位置
控制滚动位置在实际应用中十分常见,例如当新闻页列表项数量庞大,用户滚动列表到一定位置时,希望快速滚动到列表底部或返回列表顶部。此时,可以通过控制滚动位置来实现列表的快速定位,如下图所示。
**图13** 返回列表顶部  
![zh-cn_image_0000001511900520](figures/zh-cn_image_0000001511900520.gif)
List组件初始化时,可以通过scroller参数绑定一个[Scroller](../reference/arkui-ts/ts-container-scroll.md/#scroller)对象,进行列表的滚动控制。例如,用户在新闻应用中,点击新闻页面底部的返回顶部按钮时,就可以通过Scroller对象的scrollToIndex方法使列表滚动到指定的列表项索引位置。
首先,需要创建一个Scroller的对象listScroller。
```ts
private listScroller: Scroller = new Scroller();
```
然后,通过将listScroller用于初始化List组件的scroller参数,完成listScroller与列表的绑定。在需要跳转的位置指定scrollToIndex的参数为0,表示返回列表顶部。
```ts
Stack({ alignContent: Alignment.BottomEnd }) {
// 将listScroller用于初始化List组件的scroller参数,完成listScroller与列表的绑定。
List({ space: 20, scroller: this.listScroller }) {
...
}
...
Button() {
...
}
.onClick(() => {
// 点击按钮时,指定跳转位置,返回列表顶部
this.listScroller.scrollToIndex(0)
})
...
}
```
## 响应滚动位置
许多应用需要监听列表的滚动位置变化并作出响应。例如,在联系人列表滚动时,如果跨越了不同字母开头的分组,则侧边字母索引栏也需要更新到对应的字母位置。
除了字母索引之外,滚动列表结合多级分类索引在应用开发过程中也很常见,例如购物应用的商品分类页面,多级分类也需要监听列表的滚动位置。
**图14** 字母索引响应联系人列表滚动  
![zh-cn_image_0000001563060769](figures/zh-cn_image_0000001563060769.gif)
如上图所示,当联系人列表从A滚动到B时,右侧索引栏也需要同步从选中A状态变成选中B状态。此场景可以通过监听List组件的onScrollIndex事件来实现,右侧索引栏需要使用字母表索引组件[AlphabetIndexer](../reference/arkui-ts/ts-container-alphabet-indexer.md/)
在列表滚动时,根据列表此时所在的索引值位置firstIndex,重新计算字母索引栏对应字母的位置selectedIndex。由于AlphabetIndexer组件通过selected属性设置了选中项索引值,当selectedIndex变化时会触发AlphabetIndexer组件重新渲染,从而显示为选中对应字母的状态。
```ts
...
const alphabets = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
@Entry
@Component
struct ContactsList {
@State selectedIndex: number = 0;
private listScroller: Scroller = new Scroller();
...
build() {
Stack({ alignContent: Alignment.End }) {
List({ scroller: this.listScroller }) {
...
}
.onScrollIndex((firstIndex: number) => {
// 根据列表滚动到的索引值,重新计算对应联系人索引栏的位置this.selectedIndex
...
})
...
// 字母表索引组件
AlphabetIndexer({ arrayValue: alphabets, selected: 0 })
.selected(this.selectedIndex)
...
}
}
}
```
>**说明:**
>
>计算索引值时,ListItemGroup作为一个整体占一个索引值,不计算ListItemGroup内部ListItem的索引值。
## 响应列表项侧滑
侧滑菜单在许多应用中都很常见。例如,通讯类应用通常会给消息列表提供侧滑删除功能,即用户可以通过向左侧滑列表的某一项,再点击删除按钮删除消息,如下图所示。
**图15** 侧滑删除列表项  
![zh-cn_image_0000001563060773](figures/zh-cn_image_0000001563060773.gif)
ListItem的swipeAction属性可用于实现列表项的左右滑动功能。swipeAction属性方法初始化时有必填参数SwipeActionOptions,其中,start参数表示设置列表项右滑时起始端滑出的组件,end参数表示设置列表项左滑时尾端滑出的组件。
在消息列表中,end参数表示设置ListItem左滑时尾端划出自定义组件,即删除按钮。在初始化end方法时,将滑动列表项的索引传入删除按钮组件,当用户点击删除按钮时,可以根据索引值来删除列表项对应的数据,从而实现侧滑删除功能。
```ts
@Entry
@Component
struct MessageList {
@State messages: object[] = [
// 初始化消息列表数据
...
];
@Builder itemEnd(index: number) {
// 侧滑后尾端出现的组件
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_public_delete_filled'))
.width(20)
.height(20)
}
.onClick(() => {
this.messages.splice(index, 1);
})
...
}
build() {
...
List() {
ForEach(this.messages, (item, index) => {
ListItem() {
...
}
.swipeAction({ end: this.itemEnd.bind(this, index) }) // 设置侧滑属性
}, item => item.id.toString())
}
...
}
}
```
## 给列表项添加标记
添加标记是一种无干扰性且直观的方法,用于显示通知或将注意力集中到应用内的某个区域。例如,当消息列表接收到新消息时,通常对应的联系人头像的右上方会出现标记,提示有若干条未读消息,如下图所示。
**图16** 给列表项添加标记  
![zh-cn_image_0000001511580952](figures/zh-cn_image_0000001511580952.png)
在ListItem中使用[Badge](../reference/arkui-ts/ts-container-badge.md/)组件可实现给列表项添加标记功能。Badge是可以附加在单个组件上用于信息标记的容器组件。
在消息列表中,若希望在联系人头像右上角添加标记,可在实现消息列表项ListItem的联系人头像时,将头像Image组件作为Badge的子组件。
在Badge组件中,count和position参数用于设置需要展示的消息数量和提示点显示位置,还可以通过style参数灵活设置标记的样式。
```ts
Badge({
count: 1,
position: BadgePosition.RightTop,
style: { badgeSize: 16, badgeColor: '#FA2A2D' }
}) {
// Image组件实现消息联系人头像
...
}
...
```
## 下拉刷新与上拉加载
页面的下拉刷新与上拉加载功能在移动应用中十分常见,例如,新闻页面的内容刷新和加载。这两种操作的原理都是通过响应用户的[触摸事件](../reference/arkui-ts/ts-universal-events-touch.md/),在顶部或者底部显示一个刷新或加载视图,完成后再将此视图隐藏。
以下拉刷新为例,其实现主要分成三步:
1. 监听手指按下事件,记录其初始位置的值。
2. 监听手指按压移动事件,记录并计算当前移动的位置与初始值的差值,大于0表示向下移动,同时设置一个允许移动的最大值。
3. 监听手指抬起事件,若此时移动达到最大值,则触发数据加载并显示刷新视图,加载完成后将此视图隐藏。
下拉刷新与上拉加载的具体实现可参考Codelab:[新闻数据加载](https://gitee.com/openharmony/codelabs/tree/master/NetworkManagement/NewsDataArkTS)。若开发者希望快速实现此功能,也可使用三方组件[PullToRefresh](https://gitee.com/openharmony-sig/PullToRefresh)
## 编辑列表
列表的编辑模式用途十分广泛,常见于待办事项管理、文件管理、备忘录的记录管理等应用场景。在列表的编辑模式下,新增和删除列表项是最基础的功能,其核心是对列表项对应的数据集合进行数据添加和删除。
下面以待办事项管理为例,介绍如何快速实现新增和删除列表项功能。
### 新增列表项
如下图所示,当用户点击添加按钮时,提供用户新增列表项内容选择或填写的交互界面,用户点击确定后,列表中新增对应的项目。
**图17** 新增待办  
![zh-cn_image_0000001511740556](figures/zh-cn_image_0000001511740556.gif)
添加列表项功能实现主要流程如下:
1. 定义列表项数据结构和初始化列表数据,构建列表整体布局和列表项。
以待办事项管理为例,首先定义待办数据结构:
```ts
import util from '@ohos.util';
export class ToDo {
key: string = util.generateRandomUUID(true);
name: string;
constructor(name: string) {
this.name = name;
}
}
```
然后,初始化待办列表数据和可选事项:
```ts
@State toDoData: ToDo[] = [];
private availableThings: string[] = ['读书', '运动', '旅游', '听音乐', '看电影', '唱歌'];
```
最后,构建列表布局和列表项:
```ts
List({ space: 10 }) {
ForEach(this.toDoData, (toDoItem) => {
ListItem() {
...
}
}, toDoItem => toDoItem.key)
}
```
2. 提供新增列表项入口,即给新增按钮添加点击事件。
3. 响应用户确定新增事件,更新列表数据。
待办事项管理示例的步骤2和步骤3功能实现如下:
```ts
Text('+')
.onClick(() => {
TextPickerDialog.show({
range: this.availableThings,
onAccept: (value: TextPickerResult) => {
this.toDoData.push(new ToDo(this.availableThings[value.index])); // 新增列表项数据toDoData
},
})
})
```
### 删除列表项
如下图所示,当用户长按列表项进入删除模式时,提供用户删除列表项选择的交互界面,用户勾选完成后点击删除按钮,列表中删除对应的项目。
**图18** 长按删除待办事项  
![zh-cn_image_0000001562820877](figures/zh-cn_image_0000001562820877.gif)
删除列表项功能实现主要流程如下:
1. 列表的删除功能一般进入编辑模式后才可使用,所以需要提供编辑模式的入口。
以待办列表为例,通过监听列表项的长按事件,当用户长按列表项时,进入编辑模式。
```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;
})
...
```
## 长列表的处理
[循环渲染](../quick-start/arkts-rendering-control-foreach.md)适用于短列表,当构建具有大量列表项的长列表时,如果直接采用循环渲染方式,会一次性加载所有的列表元素,会导致页面启动时间过长,影响用户体验。因此,推荐使用[数据懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)(LazyForEach)方式实现按需迭代加载数据,从而提升列表性能。
关于长列表按需加载优化的具体实现可参考[数据懒加载](../quick-start/arkts-rendering-control-lazyforeach.md)章节中的示例。
当使用懒加载方式渲染列表时,为了更好的列表滚动体验,减少列表滑动时出现白块,List组件提供了cachedCount参数用于设置列表项缓存数,只在懒加载LazyForEach中生效。
```ts
List() {
LazyForEach(this.dataSource, item => {
ListItem() {
...
}
})
}.cachedCount(3)
```
以垂直列表为例:
- 若懒加载是用于ListItem,当列表为单列模式时,会在List显示的ListItem前后各缓存cachedCount个ListItem;若是多列模式下,会在List显示的ListItem前后各缓存cachedCount\*列数个ListItem。
- 若懒加载是用于ListItemGroup,无论单列模式还是多列模式,都是在List显示的ListItem前后各缓存cachedCount个ListItemGroup。
>**说明:**
>
>1. cachedCount的增加会增大UI的CPU、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。
>
>2. 列表使用数据懒加载时,除了显示区域的列表项和前后缓存的列表项,其他列表项会被销毁。
## 相关实例
如需详细了解ArkUI中列表的创建与使用,请参考以下示例:
- [List组件的使用之商品列表](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/List)
- [新闻数据加载](https://gitee.com/openharmony/codelabs/tree/master/NetworkManagement/NewsDataArkTS)
- [音乐专辑页](../key-features/multi-device-app-dev/music-album-page.md)
[../reference/arkui-ts/ts-container-listitem.md]:
\ No newline at end of file
# 创建轮播
[Swiper](../reference/arkui-ts/ts-container-swiper.md)组件提供滑动轮播显示的能力。Swiper本身是一个容器组件,当设置了多个子组件后,可以对这些子组件进行轮播显示。通常,在一些应用首页显示推荐的内容时,需要用到轮播显示的能力。
## 布局与约束
Swiper作为一个容器组件,在自身尺寸属性未被设置时,会自动根据子组件的大小设置自身的尺寸。如果开发者对Swiper组件设置了固定的尺寸,则在轮播显示过程中均以该尺寸生效;否则,在轮播过程中,会根据子组件的大小自动调整自身的尺寸。
## 循环播放
通过loop属性控制是否循环播放,该属性默认值为true。
当loop为true时,在显示第一页或最后一页时,可以继续往前切换到前一页或者往后切换到后一页。如果loop为false,则在第一页或最后一页时,无法继续向前或者向后切换页面。
loop为true:
```ts
...
private swiperController: SwiperController = new SwiperController()
...
Swiper(this.swiperController) {
Text("0")
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width('90%')
.height('100%')
.backgroundColor(Color.Blue)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.loop(true)
```
![loop_true](figures/loop_true.gif)
loop为false:
```ts
Swiper(this.swiperController) {
Text("0")
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width('90%')
.height('100%')
.backgroundColor(Color.Blue)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.loop(false)
```
![loop_false](figures/loop_false.gif)
## 自动轮播
Swiper通过设置autoPlay属性,控制是否自动轮播子组件。该属性默认值为false。
autoPlay为true时,会自动切换播放子组件,子组件与子组件之间的播放间隔通过interval属性设置。interval属性默认值为3000,单位毫秒。
autoPlay为true:
```ts
Swiper(this.swiperController) {
Text("0")
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width('90%')
.height('100%')
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.loop(true)
.autoPlay(true)
.interval(1000)
```
![autoPlay](figures/autoPlay.gif)
## 导航点样式
Swiper提供了默认的导航点样式,导航点默认显示在Swiper下方居中位置,开发者也可以通过indicatorStyle属性自定义导航点的位置和样式。
通过indicatorStyle属性,开发者可以设置导航点相对于Swiper组件上下左右四个方位的位置,同时也可以设置每个导航点的尺寸、颜色、蒙层和被选中导航点的颜色。
导航点使用默认样式:
```ts
Swiper(this.swiperController) {
Text("0")
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width('90%')
.height('100%')
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
}
```
![indicator](figures/indicator.PNG)
自定义导航点样式(示例:导航点直径设为30VP,左边距为0,导航点颜色设为红色):
```ts
Swiper(this.swiperController) {
Text("0")
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width('90%')
.height('100%')
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.indicatorStyle({
size: 30,
left: 0,
color: Color.Red
})
```
![ind](figures/ind.PNG)
## 页面切换方式
Swiper支持三种页面切换方式:手指滑动、点击导航点和通过控制器。
通过控制器切换页面:
```ts
@Entry
@Component
struct SwiperDemo {
private swiperController: SwiperController = new SwiperController();
build() {
Column({ space: 5 }) {
Swiper(this.swiperController) {
Text("0")
.width(250)
.height(250)
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width(250)
.height(250)
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width(250)
.height(250)
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.indicator(true)
Row({ space: 12 }) {
Button('showNext')
.onClick(() => {
this.swiperController.showNext(); // 通过controller切换到后一页
})
Button('showPrevious')
.onClick(() => {
this.swiperController.showPrevious(); // 通过controller切换到前一页
})
}.margin(5)
}.width('100%')
.margin({ top: 5 })
}
}
```
![controll](figures/controll.gif)
## 轮播方向
Swiper支持水平和垂直方向上进行轮播,主要通过vertical属性控制。
当vertical为true时,表示在垂直方向上进行轮播;为false时,表示在水平方向上进行轮播。vertical默认值为false。
设置水平方向上轮播:
```ts
Swiper(this.swiperController) {
...
}
.indicator(true)
.vertical(false)
```
![截图2](figures/截图2.PNG)
设置垂直方向轮播:
```ts
Swiper(this.swiperController) {
...
}
.indicator(true)
.vertical(true)
```
![截图3](figures/截图3.PNG)
## 每页显示多个子页面
Swiper支持在一个页面内同时显示多个子组件,通过[displayCount](../reference/arkui-ts/ts-container-swiper.md#%E5%B1%9E%E6%80%A7)属性设置。
设置一个页面内显示两个子组件:
```ts
Swiper(this.swiperController) {
Text("0")
.width(250)
.height(250)
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width(250)
.height(250)
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width(250)
.height(250)
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("3")
.width(250)
.height(250)
.backgroundColor(Color.Blue)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.indicator(true)
.displayCount(2)
```
![two](figures/two.PNG)
# 弹性布局
## 概述
弹性布局([Flex](../reference/arkui-ts/ts-container-flex.md))提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。容器默认存在主轴与交叉轴,子元素默认沿主轴排列,子元素在主轴方向的尺寸称为主轴尺寸,在交叉轴方向的尺寸称为交叉轴尺寸。弹性布局在开发场景中用例特别多,比如页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等等。
**图1** 主轴为水平方向的Flex容器示意图  
![flex-layout](figures/flex-layout.png)
## 基本概念
- 主轴:Flex组件布局方向的轴线,子元素默认沿着主轴排列。主轴开始的位置称为主轴起始端,结束位置称为主轴终点端。
- 交叉轴:垂直于主轴方向的轴线。交叉轴起始的位置称为交叉轴首部,结束位置称为交叉轴尾部。
## 布局方向
在弹性布局中,容器的子元素可以按照任意方向排列。通过设置参数direction,可以决定主轴的方向,从而控制子组件的排列方向。
**图2** 弹性布局方向图  
![flex-layout-direction](figures/flex-layout-direction.png)
- FlexDirection.Row(默认值):主轴为水平方向,子组件从起始端沿着水平方向开始排布。
```ts
Flex({ direction: FlexDirection.Row }) {
Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562820817](figures/zh-cn_image_0000001562820817.png)
- FlexDirection.RowReverse:主轴为水平方向,子组件从终点端沿着FlexDirection. Row相反的方向开始排布。
```ts
Flex({ direction: FlexDirection.RowReverse }) {
Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511900464](figures/zh-cn_image_0000001511900464.png)
- FlexDirection.Column:主轴为垂直方向,子组件从起始端沿着垂直方向开始排布。
```ts
Flex({ direction: FlexDirection.Column }) {
Text('1').width('100%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('100%').height(50).backgroundColor(0xD2B48C)
Text('3').width('100%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511580884](figures/zh-cn_image_0000001511580884.png)
- FlexDirection.ColumnReverse:主轴为垂直方向,子组件从终点端沿着FlexDirection. Column相反的方向开始排布。
```ts
Flex({ direction: FlexDirection.ColumnReverse }) {
Text('1').width('100%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('100%').height(50).backgroundColor(0xD2B48C)
Text('3').width('100%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562940541](figures/zh-cn_image_0000001562940541.png)
## 布局换行
弹性布局分为单行布局和多行布局。默认情况下,Flex容器中的子元素都排在一条线(又称“轴线”)上。wrap属性控制当子元素主轴尺寸之和大于容器主轴尺寸时,Flex是单行布局还是多行布局。在多行布局时,通过交叉轴方向,确认新行堆叠方向。
- FlexWrap. NoWrap(默认值):不换行。如果子组件的宽度总和大于父元素的宽度,则子组件会被压缩宽度。
```ts
Flex({ wrap: FlexWrap.NoWrap }) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562700425](figures/zh-cn_image_0000001562700425.png)
- FlexWrap. Wrap:换行,每一行子组件按照主轴方向排列。
```ts
Flex({ wrap: FlexWrap.Wrap }) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xD2B48C)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511740468](figures/zh-cn_image_0000001511740468.png)
- FlexWrap. WrapReverse:换行,每一行子组件按照主轴反方向排列。
```ts
Flex({ wrap: FlexWrap.WrapReverse}) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562940521](figures/zh-cn_image_0000001562940521.png)
## 主轴对齐方式
通过justifyContent参数设置在主轴方向的对齐方式。
![flex-spindle-alignment](figures/flex-spindle-alignment.png)
- FlexAlign.Start(默认值):子组件在主轴方向起始端对齐, 第一个子组件与父元素边沿对齐,其他元素与前一个元素对齐。
```ts
Flex({ justifyContent: FlexAlign.Start }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511421280](figures/zh-cn_image_0000001511421280.png)
- FlexAlign.Center:子组件在主轴方向居中对齐。
```ts
Flex({ justifyContent: FlexAlign.Center }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001563060681](figures/zh-cn_image_0000001563060681.png)
- FlexAlign.End:子组件在主轴方向终点端对齐, 最后一个子组件与父元素边沿对齐,其他元素与后一个元素对齐。
```ts
Flex({ justifyContent: FlexAlign.End }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562820809](figures/zh-cn_image_0000001562820809.png)
- FlexAlign.SpaceBetween:Flex主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件和最后一个子组件与父元素边沿对齐。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511421288](figures/zh-cn_image_0000001511421288.png)
- FlexAlign.SpaceAround:Flex主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件到主轴起始端的距离和最后一个子组件到主轴终点端的距离是相邻元素之间距离的一半。
```ts
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511900436](figures/zh-cn_image_0000001511900436.png)
- FlexAlign.SpaceEvenly:Flex主轴方向元素等间距布局,相邻子组件之间的间距、第一个子组件与主轴起始端的间距、最后一个子组件到主轴终点端的间距均相等。
```ts
Flex({ justifyContent: FlexAlign.SpaceEvenly }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001563060713](figures/zh-cn_image_0000001563060713.png)
## 交叉轴对齐方式
容器和子元素都可以设置交叉轴对齐方式,且子元素设置的对齐方式优先级较高。
### 容器组件设置交叉轴对齐
可以通过Flex组件的alignItems参数设置子组件在交叉轴的对齐方式。
- ItemAlign.Auto:使用Flex容器中默认配置。
```ts
Flex({ alignItems: ItemAlign.Auto }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001563060677](figures/zh-cn_image_0000001563060677.png)
- ItemAlign.Start:交叉轴方向首部对齐。
```ts
Flex({ alignItems: ItemAlign.Start }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562700453](figures/zh-cn_image_0000001562700453.png)
- ItemAlign.Center:交叉轴方向居中对齐。
```ts
Flex({ alignItems: ItemAlign.Center }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511740484](figures/zh-cn_image_0000001511740484.png)
- ItemAlign.End:交叉轴方向底部对齐。
```ts
Flex({ alignItems: ItemAlign.End }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511580876](figures/zh-cn_image_0000001511580876.png)
- ItemAlign.Stretch:交叉轴方向拉伸填充,在未设置尺寸时,拉伸到容器尺寸。
```ts
Flex({ alignItems: ItemAlign.Stretch }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511421252](figures/zh-cn_image_0000001511421252.png)
- ItemAlign. Baseline:交叉轴方向文本基线对齐。
```ts
Flex({ alignItems: ItemAlign.Baseline }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511900440](figures/zh-cn_image_0000001511900440.png)
### 子组件设置交叉轴对齐
子组件的[alignSelf](../reference/arkui-ts/ts-universal-attributes-flex-layout.md)属性也可以设置子组件在父容器交叉轴的对齐格式,且会覆盖Flex布局容器中alignItems配置。如下例所示:
```ts
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) { // 容器组件设置子组件居中
Text('alignSelf Start').width('25%').height(80)
.alignSelf(ItemAlign.Start)
.backgroundColor(0xF5DEB3)
Text('alignSelf Baseline')
.alignSelf(ItemAlign.Baseline)
.width('25%')
.height(80)
.backgroundColor(0xD2B48C)
Text('alignSelf Baseline').width('25%').height(100)
.backgroundColor(0xF5DEB3)
.alignSelf(ItemAlign.Baseline)
Text('no alignSelf').width('25%').height(100)
.backgroundColor(0xD2B48C)
Text('no alignSelf').width('25%').height(100)
.backgroundColor(0xF5DEB3)
}.width('90%').height(220).backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562940533](figures/zh-cn_image_0000001562940533.png)
上例中,Flex容器中alignItems设置交叉轴子组件的对齐方式为居中,子组件自身设置了alignSelf属性的情况,覆盖父组件的alignItems值,表现为alignSelf的定义。
### 内容对齐
可以通过[alignContent](../reference/arkui-ts/ts-container-flex.md)参数设置子组件各行在交叉轴剩余空间内的对齐方式,只在多行的flex布局中生效,可选值有:
- FlexAlign.Start:子组件各行与交叉轴起点对齐。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Start }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511900460](figures/zh-cn_image_0000001511900460.png)
- FlexAlign.Center:子组件各行在交叉轴方向居中对齐。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Center }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511421256](figures/zh-cn_image_0000001511421256.png)
- FlexAlign.End:子组件各行与交叉轴终点对齐。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.End }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562820801](figures/zh-cn_image_0000001562820801.png)
- FlexAlign.SpaceBetween:子组件各行与交叉轴两端对齐,各行间垂直间距平均分布。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceBetween }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511900448](figures/zh-cn_image_0000001511900448.png)
- FlexAlign.SpaceAround:子组件各行间距相等,是元素首尾行与交叉轴两端距离的两倍。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceAround }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562700445](figures/zh-cn_image_0000001562700445.png)
- FlexAlign.SpaceEvenly: 子组件各行间距,子组件首尾行与交叉轴两端距离都相等。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceEvenly }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001511580864](figures/zh-cn_image_0000001511580864.png)
## 自适应拉伸
在弹性布局父组件尺寸不够大的时候,通过子组件的下面几个属性设置其在父容器的占比,达到自适应布局能力。
- flexBasis:设置子组件在父容器主轴方向上的基准尺寸。如果设置了该值,则子项占用的空间为设置的值;如果没设置该属性,那子项的空间为width/height的值。
```ts
Flex() {
Text('flexBasis("auto")')
.flexBasis('auto') // 未设置width以及flexBasis值为auto,内容自身宽松
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexBasis("auto")'+' width("40%")')
.width('40%')
.flexBasis('auto') //设置width以及flexBasis值auto,使用width的值
.height(100)
.backgroundColor(0xD2B48C)
Text('flexBasis(100)') // 未设置width以及flexBasis值为100,宽度为100vp
.flexBasis(100)
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexBasis(100)')
.flexBasis(100)
.width(200) // flexBasis值为100,覆盖width的设置值,宽度为100vp
.height(100)
.backgroundColor(0xD2B48C)
}.width('90%').height(120).padding(10).backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562940505](figures/zh-cn_image_0000001562940505.png)
- flexGrow:设置父容器的剩余空间分配给此属性所在组件的比例。用于“瓜分”父组件的剩余空间。
```ts
Flex() {
Text('flexGrow(1)')
.flexGrow(2)
.width(100)
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexGrow(2)')
.flexGrow(2)
.width(100)
.height(100)
.backgroundColor(0xD2B48C)
Text('no flexGrow')
.width(100)
.height(100)
.backgroundColor(0xF5DEB3)
}.width(400).height(120).padding(10).backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562700449](figures/zh-cn_image_0000001562700449.png)
父容器宽度400vp,三个子组件原始宽度为100vp,总和300vp,剩余空间100vp根据flexGrow值的占比分配给子组件,未设置flexGrow的子组件不参与“瓜分”。
第一个元素以及第二个元素以2:3分配剩下的100vp。第一个元素为100vp+100vp2/5=140vp,第二个元素为100vp+100vp3/5=160vp。
- flexShrink: 当父容器空间不足时,子组件的压缩比例。
```ts
Flex({ direction: FlexDirection.Row }) {
Text('flexShrink(3)')
.flexShrink(3)
.width(200)
.height(100)
.backgroundColor(0xF5DEB3)
Text('no flexShrink')
.width(200)
.height(100)
.backgroundColor(0xD2B48C)
Text('flexShrink(2)')
.flexShrink(2)
.width(200)
.height(100)
.backgroundColor(0xF5DEB3)
}.width(400).height(120).padding(10).backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001562820813](figures/zh-cn_image_0000001562820813.png)
## 相关实例
使用弹性布局,可以实现子组件沿水平方向排列,两端对齐,子组件间距平分,竖直方向上子组件居中的效果。
```ts
@Entry
@Component
struct FlexExample {
build() {
Column() {
Column({ space: 5 }) {
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
Text('1').width('30%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('30%').height(50).backgroundColor(0xD2B48C)
Text('3').width('30%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.backgroundColor(0xAFEEEE)
}.width('100%').margin({ top: 5 })
}.width('100%')
}
}
```
![zh-cn_image_0000001511900452](figures/zh-cn_image_0000001511900452.png)
# 栅格布局 # 栅格布局
栅格系统作为一种辅助布局的定位工具,在平面设计和网站设计都起到了很好的作用,对移动设备的界面设计有较好的借鉴作用。总结栅格系统对于移动设备的优势主要有:
1. 给布局提供一种可循的规律,解决多尺寸多设备的动态布局问题。 ## 概述
2. 给系统提供一种统一的定位标注,保证各模块各设备的布局一致性。
3. 给应用提供一种灵活的间距调整方法,满足特殊场景布局调整的可能性。
推荐使用栅格组件[GridRow](../reference/arkui-ts/ts-container-gridrow.md)[GridCol](../reference/arkui-ts/ts-container-gridcol.md)来实现栅格布局效果, 栅格布局是一种通用的辅助定位工具,对移动设备的界面设计有较好的借鉴作用。主要优势包括:
相对于目前已废弃的[GridContainer](../reference/arkui-ts/ts-container-gridcontainer.md)组件,GridRow和GridCol提供了更灵活、更全面的栅格系统实现方案。GridRow为栅格容器组件,只能与栅格子组件GridCol在栅格布局场景中使用。
1. 提供可循的规律:栅格布局可以为布局提供规律性的结构,解决多尺寸多设备的动态布局问题。通过将页面划分为等宽的列数和行数,可以方便地对页面元素进行定位和排版。
## 栅格容器GridRow 2. 统一的定位标注:栅格布局可以为系统提供一种统一的定位标注,保证不同设备上各个模块的布局一致性。这可以减少设计和开发的复杂度,提高工作效率。
3. 灵活的间距调整方法:栅格布局可以提供一种灵活的间距调整方法,满足特殊场景布局调整的需求。通过调整列与列之间和行与行之间的间距,可以控制整个页面的排版效果。
4. 自动换行和自适应:栅格布局可以完成一对多布局的自动换行和自适应。当页面元素的数量超出了一行或一列的容量时,他们会自动换到下一行或下一列,并且在不同的设备上自适应排版,使得页面布局更加灵活和适应性强。
[GridRow](../reference/arkui-ts/ts-container-gridrow.md)为栅格容器组件,需与栅格子组件[GridCol](../reference/arkui-ts/ts-container-gridcol.md)在栅格布局场景中联合使用。
栅格容器有columns、gutter、direction、breakpoints四个属性。
- columns: 栅格布局的主要定位工具,设置栅格布局的总列数。
- gutter: 设置元素之间的距离,决定内容间的紧密程度。
- direction: 设置栅格子组件在栅格容器中的排列方向。
- breakpoints:以设备宽度为基准,将应用宽度分成了几个不同的区间,即不同的断点。开发者可根据需要在不同的区间下实现不同的页面布局效果。
## 栅格容器GridRow
首先通过设置断点,得到一系列断点区间;然后,借助栅格组件能力监听应用窗口大小的变化,判断应用当前处于哪个断点区间,最后调整应用的布局。
### 栅格系统断点 ### 栅格系统断点
栅格系统以设备的水平宽度(屏幕密度像素值,单位vp)作为断点依据,定义设备的宽度类型,形成了一套断点规则。开发者可根据需求在不同的断点区间实现不同的页面布局效果。 栅格系统以设备的水平宽度(屏幕密度像素值,单位vp)作为断点依据,定义设备的宽度类型,形成了一套断点规则。开发者可根据需求在不同的断点区间实现不同的页面布局效果。
栅格系统默认断点将设备宽度分为xs、sm、md、lg四类,尺寸范围如下: 栅格系统默认断点将设备宽度分为xs、sm、md、lg四类,尺寸范围如下:
| 断点名称 | 取值范围(vp)| | 断点名称 | 取值范围(vp) | 设备描述 |
| --------| ------ | | ---- | --------------- | --------- |
| xs | [0, 320) | | xs | [0,&nbsp;320) | 最小宽度类型设备。 |
| sm | [320, 520) | | sm | [320,&nbsp;520) | 小宽度类型设备。 |
| md | [520, 840) | | md | [520,&nbsp;840) | 中等宽度类型设备。 |
| lg | [840, +∞) | | lg | [840,&nbsp;+∞) | 大宽度类型设备。 |
在GridRow新栅格组件中,允许开发者使用breakpoints自定义修改断点的取值范围,最多支持6个断点,除了默认的四个断点外, 在GridRow栅格组件中,允许开发者使用breakpoints自定义修改断点的取值范围,最多支持6个断点,除了默认的四个断点外,还可以启用xl,xxl两个断点,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的布局设置。
还可以启用xl,xxl两个断点,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的布局设置。
| 断点名称 | 设备描述 |
| 断点名称 | 设备描述 | | ---- | --------- |
| ----- | ---------------------------------------- | | xs | 最小宽度类型设备。 |
| xs | 最小宽度类型设备。 | | sm | 小宽度类型设备。 |
| sm | 小宽度类型设备。 | | md | 中等宽度类型设备。 |
| md | 中等宽度类型设备。 | | lg | 大宽度类型设备。 |
| lg | 大宽度类型设备。 | | xl | 特大宽度类型设备。 |
| xl | 特大宽度类型设备。 | | xxl | 超大宽度类型设备。 |
| xxl | 超大宽度类型设备。 |
- 针对断点位置,开发者根据实际使用场景,通过一个单调递增数组设置。由于breakpoints最多支持六个断点,单调递增数组长度最大为5。 - 针对断点位置,开发者根据实际使用场景,通过一个单调递增数组设置。由于breakpoints最多支持六个断点,单调递增数组长度最大为5。
```ts ```ts
breakpoints: {value: ["100vp", "200vp"]} breakpoints: {value: ['100vp', '200vp']}
``` ```
表示启用xs、sm、md共3个断点,小于100vp为xs,100vp-200vp为sm,大于200vp为md。 表示启用xs、sm、md共3个断点,小于100vp为xs,100vp-200vp为sm,大于200vp为md。
```ts ```ts
breakpoints: {value: ["320vp", "520vp", "840vp", "1080vp"]} breakpoints: {value: ['320vp', '520vp', '840vp', '1080vp']}
``` ```
表示启用xs、sm、md、lg、xl共5个断点,小于320vp为xs,320vp-520vp为sm,520vp-840vp为md,840vp-1080vp为lg,大于1080vp为xl。 表示启用xs、sm、md、lg、xl共5个断点,小于320vp为xs,320vp-520vp为sm,520vp-840vp为md,840vp-1080vp为lg,大于1080vp为xl。
- 栅格系统通过监听窗口或容器的尺寸变化进行断点,通过reference设置断点切换参考物。 考虑到应用可能以非全屏窗口的形式显示,以应用窗口宽度为参照物更为通用。 - 栅格系统通过监听窗口或容器的尺寸变化进行断点,通过reference设置断点切换参考物。 考虑到应用可能以非全屏窗口的形式显示,以应用窗口宽度为参照物更为通用。
下例中,使用栅格的默认列数12列,通过断点设置将应用宽度分成六个区间,在各区间中,每个栅格子元素占用的列数均不同。效果如图: 例如,使用栅格的默认列数12列,通过断点设置将应用宽度分成六个区间,在各区间中,每个栅格子元素占用的列数均不同。
```ts
```ts
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow({ GridRow({
breakpoints: { breakpoints: {
value: ['200vp', '300vp', '400vp', '500vp', '600vp'], value: ['200vp', '300vp', '400vp', '500vp', '600vp'],
reference: BreakpointsReference.WindowSize reference: BreakpointsReference.WindowSize
} }
}) { }) {
ForEach(this.bgColors, (color, index) => { ForEach(this.bgColors, (color, index) => {
GridCol({ GridCol({
span: { span: {
xs: 2, xs: 2,
sm: 3, sm: 3,
md: 4, md: 4,
lg: 6, lg: 6,
xl: 8, xl: 8,
xxl: 12 xxl: 12
} }
}) { }) {
Row() { Row() {
Text(`${index}`) Text(`${index}`)
}.width("100%").height("50vp") }.width("100%").height('50vp')
}.backgroundColor(color) }.backgroundColor(color)
}) })
} }
``` ```
![](figures/breakpoints.gif) ![zh-cn_image_0000001511421272](figures/zh-cn_image_0000001511421272.gif)
### 布局的总列数
### 栅格布局的总列数
GridRow中通过columns设置栅格布局的总列数。 GridRow中通过columns设置栅格布局的总列数。
- columns默认值为12,当未设置columns时,在任何断点下,栅格布局被分成12列。 - columns默认值为12,即在未设置columns时,任何断点下,栅格布局被分成12列。
```ts
```ts
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow() { GridRow() {
ForEach(this.bgColors, (item, index) => { ForEach(this.bgColors, (item, index) => {
GridCol() { GridCol() {
Row() { Row() {
Text(`${index + 1}`) Text(`${index + 1}`)
}.width("100%").height("50") }.width('100%').height('50')
}.backgroundColor(item) }.backgroundColor(item)
}) })
} }
``` ```
![](figures/columns1.png) ![zh-cn_image_0000001563060709](figures/zh-cn_image_0000001563060709.png)
- 当columns为自定义值,栅格布局在任何尺寸设备下都被分为columns列。下面分别设置栅格布局列数为4和8,子元素默认占一列,效果如下:
- 当columns类型为number时,栅格布局在任何尺寸设备下都被分为columns列。下面分别设置栅格布局列数为4和8,子元素默认占一列,效果如下:
```ts ```ts
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
@State currentBp: string = 'unknown';
...
Row() { Row() {
GridRow({ columns: 4 }) { GridRow({ columns: 4 }) {
ForEach(this.bgColors, (item, index) => { ForEach(this.bgColors, (item, index) => {
GridCol() { GridCol() {
Row() { Row() {
Text(`${index + 1}`) Text(`${index + 1}`)
}.width("100%").height("50") }.width('100%').height('50')
}.backgroundColor(item) }.backgroundColor(item)
}) })
} }
.width("100%").height("100%") .width('100%').height('100%')
.onBreakpointChange((breakpoint) => { .onBreakpointChange((breakpoint) => {
this.currentBp = breakpoint this.currentBp = breakpoint
}) })
...@@ -134,18 +143,18 @@ GridRow中通过columns设置栅格布局的总列数。 ...@@ -134,18 +143,18 @@ GridRow中通过columns设置栅格布局的总列数。
.height(160) .height(160)
.border({ color: Color.Blue, width: 2 }) .border({ color: Color.Blue, width: 2 })
.width('90%') .width('90%')
Row() { Row() {
GridRow({ columns: 8 }) { GridRow({ columns: 8 }) {
ForEach(this.bgColors, (item, index) => { ForEach(this.bgColors, (item, index) => {
GridCol() { GridCol() {
Row() { Row() {
Text(`${index + 1}`) Text(`${index + 1}`)
}.width("100%").height("50") }.width('100%').height('50')
}.backgroundColor(item) }.backgroundColor(item)
}) })
} }
.width("100%").height("100%") .width('100%').height('100%')
.onBreakpointChange((breakpoint) => { .onBreakpointChange((breakpoint) => {
this.currentBp = breakpoint this.currentBp = breakpoint
}) })
...@@ -154,77 +163,82 @@ GridRow中通过columns设置栅格布局的总列数。 ...@@ -154,77 +163,82 @@ GridRow中通过columns设置栅格布局的总列数。
.border({ color: Color.Blue, width: 2 }) .border({ color: Color.Blue, width: 2 })
.width('90%') .width('90%')
``` ```
![](figures/columns2.png)
![zh-cn_image_0000001511421268](figures/zh-cn_image_0000001511421268.png)
- 当columns类型为GridRowColumnOption时,支持下面六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的总列数设置,各个尺寸下数值可不同。 - 当columns类型为GridRowColumnOption时,支持下面六种不同尺寸(xs, sm, md, lg, xl, xxl)设备的总列数设置,各个尺寸下数值可不同。
```ts ```ts
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown]
GridRow({ columns: { sm: 4, md: 8 }, breakpoints: { value: ['200vp', '300vp', '400vp', '500vp', '600vp'] } }) { GridRow({ columns: { sm: 4, md: 8 }, breakpoints: { value: ['200vp', '300vp', '400vp', '500vp', '600vp'] } }) {
ForEach(this.bgColors, (item, index) => { ForEach(this.bgColors, (item, index) => {
GridCol() { GridCol() {
Row() { Row() {
Text(`${index + 1}`) Text(`${index + 1}`)
}.width("100%").height("50") }.width('100%').height('50')
}.backgroundColor(item) }.backgroundColor(item)
}) })
} }
``` ```
![](figures/columns3.gif)
如上,若只设置sm, md的栅格总列数,则较小的尺寸使用默认columns值12,较大的尺寸使用前一个尺寸的columns。这里只设置sm:8, md:10,则较小尺寸的xs:12,较大尺寸的参照md的设置,lg:10, xl:10, xxl:10。 ![zh-cn_image_0000001563060689](figures/zh-cn_image_0000001563060689.gif)
### 栅格子组件间距 若只设置sm, md的栅格总列数,则较小的尺寸使用默认columns值12,较大的尺寸使用前一个尺寸的columns。这里只设置sm:8, md:10,则较小尺寸的xs:12,较大尺寸的参照md的设置,lg:10, xl:10, xxl:10
GridRow中通过gutter设置子元素在水平和垂直方向的间距。
- 当gutter类型为number时,同时设置栅格子组件间水平和垂直方向边距且相等。下例中,设置子组件水平与垂直方向距离相邻元素的间距为10。 ### 排列方向
栅格布局中,可以通过设置GridRow的direction属性来指定栅格子组件在栅格容器中的排列方向。该属性可以设置为GridRowDirection.Row(从左往右排列)或GridRowDirection.RowReverse(从右往左排列),以满足不同的布局需求。通过合理的direction属性设置,可以使得页面布局更加灵活和符合设计要求。
- 子组件默认从左往右排列。
```ts ```ts
GridRow({ gutter: 10 }){} GridRow({ direction: GridRowDirection.Row }){}
``` ```
![](figures/gutter1.png) ![zh-cn_image_0000001511740488](figures/zh-cn_image_0000001511740488.png)
- 子组件从右往左排列。
- 当gutter类型为GutterOption时,单独设置栅格子组件水平垂直边距,x属性为水平方向间距,y为垂直方向间距。
```ts ```ts
GridRow({ gutter: { x: 20, y: 50 } }){} GridRow({ direction: GridRowDirection.RowReverse }){}
``` ```
![](figures/gutter2.png) ![zh-cn_image_0000001562940517](figures/zh-cn_image_0000001562940517.png)
### 子组件间距
### 排列方向 GridRow中通过gutter属性设置子元素在水平和垂直方向的间距。
通过GridRow的direction属性设置栅格子组件在栅格容器中的排列方向 - 当gutter类型为number时,同时设置栅格子组件间水平和垂直方向边距且相等。下例中,设置子组件水平与垂直方向距离相邻元素的间距为10
- 子组件默认从左往右排列。
```ts ```ts
GridRow({ direction: GridRowDirection.Row }){} GridRow({ gutter: 10 }){}
``` ```
![](figures/direction1.png)
- 子组件从右往左排列。 ![zh-cn_image_0000001511740476](figures/zh-cn_image_0000001511740476.png)
- 当gutter类型为GutterOption时,单独设置栅格子组件水平垂直边距,x属性为水平方向间距,y为垂直方向间距。
```ts ```ts
GridRow({ direction: GridRowDirection.RowReverse }){} GridRow({ gutter: { x: 20, y: 50 } }){}
``` ```
![](figures/direction2.png) ![zh-cn_image_0000001511900456](figures/zh-cn_image_0000001511900456.png)
## 子组件GridCol
## 栅格子组件GridCol GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设置属性两种方式,设置span(占用列数),offset(偏移列数),order(元素序号)的值。
GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设置属性两种方式,设置span,offset,order的值 - 设置span
- span的设置
```ts ```ts
GridCol({ span: 2 }){} GridCol({ span: 2 }){}
...@@ -233,7 +247,8 @@ GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设 ...@@ -233,7 +247,8 @@ GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设
GridCol(){}.span({ xs: 1, sm: 2, md: 3, lg: 4 }) GridCol(){}.span({ xs: 1, sm: 2, md: 3, lg: 4 })
``` ```
- offset的设置 - 设置offset。
```ts ```ts
GridCol({ offset: 2 }){} GridCol({ offset: 2 }){}
...@@ -242,7 +257,8 @@ GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设 ...@@ -242,7 +257,8 @@ GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设
GridCol(){}.offset({ xs: 1, sm: 2, md: 3, lg: 4 }) GridCol(){}.offset({ xs: 1, sm: 2, md: 3, lg: 4 })
``` ```
- order的设置 - 设置order。
```ts ```ts
GridCol({ order: 2 }){} GridCol({ order: 2 }){}
...@@ -251,7 +267,6 @@ GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设 ...@@ -251,7 +267,6 @@ GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设
GridCol(){}.order({ xs: 1, sm: 2, md: 3, lg: 4 }) GridCol(){}.order({ xs: 1, sm: 2, md: 3, lg: 4 })
``` ```
下面使用传参的方式演示各属性的使用。
### span ### span
...@@ -259,139 +274,208 @@ GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设 ...@@ -259,139 +274,208 @@ GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设
- 当类型为number时,子组件在所有尺寸设备下占用的列数相同。 - 当类型为number时,子组件在所有尺寸设备下占用的列数相同。
```ts ```ts
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow({ columns: 8 }) { GridRow({ columns: 8 }) {
ForEach(this.bgColors, (color, index) => { ForEach(this.bgColors, (color, index) => {
GridCol({ span: 2 }) { GridCol({ span: 2 }) {
Row() { Row() {
Text(`${index}`) Text(`${index}`)
}.width("100%").height("50vp") }.width('100%').height('50vp')
} }
.backgroundColor(color) .backgroundColor(color)
}) })
} }
``` ```
![](figures/span1.png) ![zh-cn_image_0000001511421264](figures/zh-cn_image_0000001511421264.png)
- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。 - 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。
```ts ```ts
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow({ columns: 8 }) { GridRow({ columns: 8 }) {
ForEach(this.bgColors, (color, index) => { ForEach(this.bgColors, (color, index) => {
GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }) { GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }) {
Row() { Row() {
Text(`${index}`) Text(`${index}`)
}.width("100%").height("50vp") }.width('100%').height('50vp')
} }
.backgroundColor(color) .backgroundColor(color)
}) })
} }
``` ```
![](figures/span2.gif) ![zh-cn_image_0000001511740492](figures/zh-cn_image_0000001511740492.gif)
### offset ### offset
栅格子组件相对于前一个子组件的偏移列数,默认为0。 栅格子组件相对于前一个子组件的偏移列数,默认为0。
- 当类型为number时,子组件偏移相同列数。 - 当类型为number时,子组件偏移相同列数。
```ts ```ts
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow() { GridRow() {
ForEach(this.bgColors, (color, index) => { ForEach(this.bgColors, (color, index) => {
GridCol({ offset: 2 }) { GridCol({ offset: 2 }) {
Row() { Row() {
Text("" + index) Text('' + index)
}.width("100%").height("50vp") }.width('100%').height('50vp')
} }
.backgroundColor(color) .backgroundColor(color)
}) })
} }
``` ```
![](figures/offset1.png) ![zh-cn_image_0000001563060705](figures/zh-cn_image_0000001563060705.png)
栅格默认分成12列,每一个子组件默认占1列,偏移2列,每个子组件及间距共占3列,一行放四个子组件。 栅格默认分成12列,每一个子组件默认占1列,偏移2列,每个子组件及间距共占3列,一行放四个子组件。
- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。 - 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件所占列数设置,各个尺寸下数值可不同。
```ts ```ts
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow() { GridRow() {
ForEach(this.bgColors, (color, index) => { ForEach(this.bgColors, (color, index) => {
GridCol({ offset: { xs: 1, sm: 2, md: 3, lg: 4 } }) { GridCol({ offset: { xs: 1, sm: 2, md: 3, lg: 4 } }) {
Row() { Row() {
Text("" + index) Text('' + index)
}.width("100%").height("50vp") }.width('100%').height('50vp')
} }
.backgroundColor(color) .backgroundColor(color)
}) })
} }
``` ```
![](figures/offset2.gif) ![zh-cn_image_0000001562700433](figures/zh-cn_image_0000001562700433.gif)
### order ### order
栅格子组件的序号,决定子组件排列次序。当子组件不设置order或者设置相同的order, 子组件按照代码顺序展示。当子组件设置不同的order时,order较小的组件在前,较大的在后。 栅格子组件的序号,决定子组件排列次序。当子组件不设置order或者设置相同的order, 子组件按照代码顺序展示。当子组件设置不同的order时,order较小的组件在前,较大的在后。
当子组件部分设置order,部分不设置order时,未设置order的子组件依次排序靠前,设置了order的子组件按照数值从小到大排列。
当子组件部分设置order,部分不设置order时,未设置order的子组件依次排序靠前,设置了order的子组件按照数值从小到大排列。
- 当类型为number时,子组件在任何尺寸下排序次序一致。 - 当类型为number时,子组件在任何尺寸下排序次序一致。
```ts ```ts
GridRow() { GridRow() {
GridCol({ order: 5 }) { GridCol({ order: 4 }) {
Row() { Row() {
Text("1") Text('1')
}.width("100%").height("50vp") }.width('100%').height('50vp')
}.backgroundColor(Color.Red) }.backgroundColor(Color.Red)
GridCol({ order: 4 }) { GridCol({ order: 3 }) {
Row() { Row() {
Text("2") Text('2')
}.width("100%").height("50vp") }.width('100%').height('50vp')
}.backgroundColor(Color.Orange) }.backgroundColor(Color.Orange)
GridCol({ order: 3 }) { GridCol({ order: 2 }) {
Row() { Row() {
Text("3") Text('3')
}.width("100%").height("50vp") }.width('100%').height('50vp')
}.backgroundColor(Color.Yellow) }.backgroundColor(Color.Yellow)
GridCol({ order: 2 }) { GridCol({ order: 1 }) {
Row() { Row() {
Text("4") Text('4')
}.width("100%").height("50vp") }.width('100%').height('50vp')
}.backgroundColor(Color.Green) }.backgroundColor(Color.Green)
} }
``` ```
![](figures/order1.png) ![zh-cn_image_0000001511580892](figures/zh-cn_image_0000001511580892.png)
- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件排序次序设置。
- 当类型为GridColColumnOption时,支持六种不同尺寸(xs, sm, md, lg, xl, xxl)设备中子组件排序次序设置。在xs设备中,子组件排列顺序为1234;sm为2341,md为3412,lg为2431。
```ts ```ts
GridRow() { GridRow() {
GridCol({ order: { xs:1, sm:5, md:3, lg:7}}) { GridCol({ order: { xs:1, sm:5, md:3, lg:7}}) {
Row() { Row() {
Text("1") Text('1')
}.width("100%").height("50vp") }.width('100%').height('50vp')
}.backgroundColor(Color.Red) }.backgroundColor(Color.Red)
GridCol({ order: { xs:2, sm:2, md:6, lg:1} }) { GridCol({ order: { xs:2, sm:2, md:6, lg:1} }) {
Row() { Row() {
Text("2") Text('2')
}.width("100%").height("50vp") }.width('100%').height('50vp')
}.backgroundColor(Color.Orange) }.backgroundColor(Color.Orange)
GridCol({ order: { xs:3, sm:3, md:1, lg:6} }) { GridCol({ order: { xs:3, sm:3, md:1, lg:6} }) {
Row() { Row() {
Text("3") Text('3')
}.width("100%").height("50vp") }.width('100%').height('50vp')
}.backgroundColor(Color.Yellow) }.backgroundColor(Color.Yellow)
GridCol({ order: { xs:4, sm:4, md:2, lg:5} }) { GridCol({ order: { xs:4, sm:4, md:2, lg:5} }) {
Row() { Row() {
Text("4") Text('4')
}.width("100%").height("50vp") }.width('100%').height('50vp')
}.backgroundColor(Color.Green) }.backgroundColor(Color.Green)
} }
``` ```
![](figures/order2.gif) ![zh-cn_image_0000001511900444](figures/zh-cn_image_0000001511900444.gif)
## 栅格组件的嵌套使用
栅格组件也可以嵌套使用,完成一些复杂的布局。
以下示例中,栅格把整个空间分为12份。第一层GridRow嵌套GridCol,分为中间大区域以及“footer”区域。第二层GridRow嵌套GridCol,分为“left”和“right”区域。子组件空间按照上一层父组件的空间划分,粉色的区域是屏幕空间的12列,绿色和蓝色的区域是父组件GridCol的12列,依次进行空间的划分。
```ts
@Entry
@Component
struct GridRowExample {
build() {
GridRow() {
GridCol({ span: { sm: 12 } }) {
GridRow() {
GridCol({ span: { sm: 2 } }) {
Row() {
Text('left').fontSize(24)
}
.justifyContent(FlexAlign.Center)
.height('90%')
}.backgroundColor('#ff41dbaa')
GridCol({ span: { sm: 10 } }) {
Row() {
Text('right').fontSize(24)
}
.justifyContent(FlexAlign.Center)
.height('90%')
}.backgroundColor('#ff4168db')
}
.backgroundColor('#19000000')
.height('100%')
}
GridCol({ span: { sm: 12 } }) {
Row() {
Text('footer').width('100%').textAlign(TextAlign.Center)
}.width('100%').height('10%').backgroundColor(Color.Pink)
}
}.width('100%').height(300)
}
}
```
![zh-cn_image_0000001563060697](figures/zh-cn_image_0000001563060697.png)
综上所述,栅格组件提供了丰富的自定义能力,功能异常灵活和强大。只需要明确栅格在不同断点下的Columns、Margin、Gutter及span等参数,即可确定最终布局,无需关心具体的设备类型及设备状态(如横竖屏)等。
# 线性布局
## 概述
线性布局(LinearLayout)是开发中最常用的布局,通过线性容器[Row](../reference/arkui-ts/ts-container-row.md)[Column](../reference/arkui-ts/ts-container-column.md)构建。线性布局是其他布局的基础,其子元素在线性方向上(水平方向和垂直方向)依次排列。线性布局的排列方向由所选容器组件决定,Column容器内子元素按照垂直方向排列,Row容器内子元素按照水平方向排列。根据不同的排列方向,开发者可选择使用Row或Column容器创建线性布局。
**图1** Column容器内子元素排列示意图  
![arrangement-child-elements-column](figures/arrangement-child-elements-column.png)
**图2** Row容器内子元素排列示意图  
![arrangement-child-elements-row](figures/arrangement-child-elements-row.png)
## 基本概念
- 布局容器:具有布局能力的容器组件,可以承载其他元素作为其子元素,布局容器会对其子元素进行尺寸计算和布局排列。
- 布局子元素:布局容器内部的元素。
- 主轴:线性布局容器在布局方向上的轴线,子元素默认沿主轴排列。Row容器主轴为纵向,Column容器主轴为横向。
- 交叉轴:垂直于主轴方向的轴线。Row容器交叉轴为横向,Column容器交叉轴为纵向。
- 间距:布局子元素的纵向间距。
## 布局子元素在排列方向上的间距
在布局容器内,可以通过space属性设置排列方向上子元素的间距,使各子元素在排列方向上有等间距效果。
### Column容器内排列方向上的间距
**图3** Column容器内排列方向的间距图  
![arrangement-direction-column](figures/arrangement-direction-column.png)
```ts
Column({ space: 20 }) {
Text('space: 20').fontSize(15).fontColor(Color.Gray).width('90%')
Row().width('90%').height(50).backgroundColor(0xF5DEB3)
Row().width('90%').height(50).backgroundColor(0xD2B48C)
Row().width('90%').height(50).backgroundColor(0xF5DEB3)
}.width('100%')
```
![arrangement-direction-column-sample](figures/arrangement-direction-column-sample.png)
### Row容器内排列方向上的间距
**图4** Row容器内排列方向的间距图  
![arrangement-direction-row](figures/arrangement-direction-row.png)
```ts
Row({ space: 35 }) {
Text('space: 35').fontSize(15).fontColor(Color.Gray)
Row().width('10%').height(150).backgroundColor(0xF5DEB3)
Row().width('10%').height(150).backgroundColor(0xD2B48C)
Row().width('10%').height(150).backgroundColor(0xF5DEB3)
}.width('90%')
```
![zh-cn_image_0000001562700509](figures/zh-cn_image_0000001562700509.png)
## 布局子元素在交叉轴上的对齐方式
在布局容器内,可以通过alignItems属性设置子元素在交叉轴(排列方向的垂直方向)上的对齐方式。且在各类尺寸屏幕中,表现一致。其中,交叉轴为垂直方向时,取值为[VerticalAlign类型](../reference/arkui-ts/ts-appendix-enums.md#verticalalign),水平方向取值为[HorizontalAlign](../reference/arkui-ts/ts-appendix-enums.md#horizontalalign)
alignSelf属性用于控制单个子元素在容器主轴上的对齐方式,其优先级高于alignItems属性,如果设置了alignSelf属性,则在单个子元素上会覆盖alignItems属性。
### Column容器内子元素在水平方向上的排列
**图5** Column容器内子元素在水平方向上的排列图  
![horizontal-arrangement-child-column](figures/horizontal-arrangement-child-column.png)
- HorizontalAlign.Start:子元素在水平方向左对齐。
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').alignItems(HorizontalAlign.Start).backgroundColor('rgb(242,242,242)')
```
![zh-cn_image_0000001511580964](figures/zh-cn_image_0000001511580964.png)
- HorizontalAlign.Center:子元素在水平方向居中对齐。
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').alignItems(HorizontalAlign.Center).backgroundColor('rgb(242,242,242)')
```
![zh-cn_image_0000001562820897](figures/zh-cn_image_0000001562820897.png)
- HorizontalAlign.End:子元素在水平方向右对齐。
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').alignItems(HorizontalAlign.End).backgroundColor('rgb(242,242,242)')
```
![zh-cn_image_0000001511421348](figures/zh-cn_image_0000001511421348.png)
### Row容器内子元素在垂直方向上的排列
**图6** Row容器内子元素在垂直方向上的排列图  
![horizontal-arrangement-child-row](figures/horizontal-arrangement-child-row.png)
- VerticalAlign.Top:子元素在垂直方向顶部对齐。
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).alignItems(VerticalAlign.Top).backgroundColor('rgb(242,242,242)')
```
![zh-cn_image_0000001563060765](figures/zh-cn_image_0000001563060765.png)
- VerticalAlign.Center:子元素在垂直方向居中对齐。
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).alignItems(VerticalAlign.Center).backgroundColor('rgb(242,242,242)')
```
![zh-cn_image_0000001562700505](figures/zh-cn_image_0000001562700505.png)
- VerticalAlign.Bottom:子元素在垂直方向底部对齐。
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).alignItems(VerticalAlign.Bottom).backgroundColor('rgb(242,242,242)')
```
![zh-cn_image_0000001563060781](figures/zh-cn_image_0000001563060781.png)
## 布局子元素在主轴上的排列方式
在布局容器内,可以通过justifyContent属性设置子元素在容器主轴上的排列方式。可以从主轴起始位置开始排布,也可以从主轴结束位置开始排布,或者均匀分割主轴的空间。
### Column容器内子元素在主轴上的排列
**图7** Column容器内子元素在主轴上的排列图 
![vertial-arrangement-child-column](figures/vertial-arrangement-child-column.png)
- justifyContent(FlexAlign.Start):元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Start)
```
![zh-cn_image_0000001562700501](figures/zh-cn_image_0000001562700501.png)
- justifyContent(FlexAlign.Center):元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Center)
```
![zh-cn_image_0000001562700517](figures/zh-cn_image_0000001562700517.png)
- justifyContent(FlexAlign.End):元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.End)
```
![zh-cn_image_0000001562940585](figures/zh-cn_image_0000001562940585.png)
- justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceBetween)
```
![zh-cn_image_0000001511900532](figures/zh-cn_image_0000001511900532.png)
- justifyContent(FlexAlign.SpaceAround):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceAround)
```
![zh-cn_image_0000001562700525](figures/zh-cn_image_0000001562700525.png)
- justifyContent(FlexAlign.SpaceEvenly):主轴方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceEvenly)
```
![zh-cn_image_0000001563060785](figures/zh-cn_image_0000001563060785.png)
### Row容器内子元素在主轴上的排列
**图8** Row容器内子元素在主轴上的排列图  
![vertial-arrangement-child-row](figures/vertial-arrangement-child-row.png)
- justifyContent(FlexAlign.Start):元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Start)
```
![zh-cn_image_0000001511421356](figures/zh-cn_image_0000001511421356.png)
- justifyContent(FlexAlign.Center):元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Center)
```
![zh-cn_image_0000001511900516](figures/zh-cn_image_0000001511900516.png)
- justifyContent(FlexAlign.End):元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.End)
```
![zh-cn_image_0000001562940601](figures/zh-cn_image_0000001562940601.png)
- justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceBetween)
```
![zh-cn_image_0000001562700521](figures/zh-cn_image_0000001562700521.png)
- justifyContent(FlexAlign.SpaceAround):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceAround)
```
![zh-cn_image_0000001562820893](figures/zh-cn_image_0000001562820893.png)
- justifyContent(FlexAlign.SpaceEvenly):主轴方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceEvenly)
```
![zh-cn_image_0000001511421352](figures/zh-cn_image_0000001511421352.png)
## 自适应拉伸
在线性布局下,常用空白填充组件[Blank](../reference/arkui-ts/ts-basic-components-blank.md),在容器主轴方向自动填充空白空间,达到自适应拉伸效果。Row和Column作为容器,只需要添加宽高为百分比,当屏幕宽高发生变化时,会产生自适应效果。
```ts
@Entry
@Component
struct BlankExample {
build() {
Column() {
Row() {
Text('Bluetooth').fontSize(18)
Blank()
Toggle({ type: ToggleType.Switch, isOn: true })
}.backgroundColor(0xFFFFFF).borderRadius(15).padding({ left: 12 }).width('100%')
}.backgroundColor(0xEFEFEF).padding(20).width('100%')
}
}
```
**图9** 竖屏  
![zh-cn_image_0000001562820881](figures/zh-cn_image_0000001562820881.png)
**图10** 横屏  
![zh-cn_image_0000001511421332](figures/zh-cn_image_0000001511421332.png)
## 自适应缩放
自适应缩放是指子组件随容器尺寸的变化而按照预设的比例自动调整尺寸,适应各种不同大小的设备。在线性布局中,可以使用以下两种方法实现自适应缩放。
- 父容器尺寸确定时,使用layoutWeight属性设置子组件和兄弟元素在主轴上的权重,忽略元素本身尺寸设置,使它们在任意尺寸的设备下自适应占满剩余空间。
```ts
@Entry
@Component
struct layoutWeightExample {
build() {
Column() {
Text('1:2:3').width('100%')
Row() {
Column() {
Text('layoutWeight(1)')
.textAlign(TextAlign.Center)
}.layoutWeight(2).backgroundColor(0xF5DEB3).height('100%')
Column() {
Text('layoutWeight(2)')
.textAlign(TextAlign.Center)
}.layoutWeight(4).backgroundColor(0xD2B48C).height('100%')
Column() {
Text('layoutWeight(6)')
.textAlign(TextAlign.Center)
}.layoutWeight(6).backgroundColor(0xF5DEB3).height('100%')
}.backgroundColor(0xffd306).height('30%')
Text('2:5:3').width('100%')
Row() {
Column() {
Text('layoutWeight(2)')
.textAlign(TextAlign.Center)
}.layoutWeight(2).backgroundColor(0xF5DEB3).height('100%')
Column() {
Text('layoutWeight(5)')
.textAlign(TextAlign.Center)
}.layoutWeight(5).backgroundColor(0xD2B48C).height('100%')
Column() {
Text('layoutWeight(3)')
.textAlign(TextAlign.Center)
}.layoutWeight(3).backgroundColor(0xF5DEB3).height('100%')
}.backgroundColor(0xffd306).height('30%')
}
}
}
```
**图11** 横屏  
![zh-cn_image_0000001511421336](figures/zh-cn_image_0000001511421336.png)
**图12** 竖屏  
![zh-cn_image_0000001511580968](figures/zh-cn_image_0000001511580968.png)
- 父容器尺寸确定时,使用百分比设置子组件和兄弟元素的宽度,使他们在任意尺寸的设备下保持固定的自适应占比。
```ts
@Entry
@Component
struct WidthExample {
build() {
Column() {
Row() {
Column() {
Text('left width 20%')
.textAlign(TextAlign.Center)
}.width('20%').backgroundColor(0xF5DEB3).height('100%')
Column() {
Text('center width 50%')
.textAlign(TextAlign.Center)
}.width('50%').backgroundColor(0xD2B48C).height('100%')
Column() {
Text('right width 30%')
.textAlign(TextAlign.Center)
}.width('30%').backgroundColor(0xF5DEB3).height('100%')
}.backgroundColor(0xffd306).height('30%')
}
}
}
```
**图13** 横屏  
![zh-cn_image_0000001563060777](figures/zh-cn_image_0000001563060777.png)
**图14** 竖屏  
![zh-cn_image_0000001511740564](figures/zh-cn_image_0000001511740564.png)
## 自适应延伸
自适应延伸是指在不同尺寸设备下,当页面的内容超出屏幕大小而无法完全显示时,可以通过滚动条进行拖动展示。这种方法适用于线性布局中内容无法一屏展示的场景。通常有以下两种实现方式。
- [在List中添加滚动条](arkts-layout-development-create-list.md#添加滚动条):当List子项过多一屏放不下时,可以将每一项子元素放置在不同的组件中,通过滚动条进行拖动展示。可以通过scrollBar属性设置滚动条的常驻状态,edgeEffect属性设置拖动到内容最末端的回弹效果。
- 使用Scroll组件:在线性布局中,开发者可以进行竖向或者横向的布局。当一屏无法完全显示时,可以在Column或Row组件的外层包裹一个可滚动的容器组件Scroll来实现可滑动的线性布局。
竖向布局中使用Scroll组件:
```ts
@Entry
@Component
struct ScrollExample {
scroller: Scroller = new Scroller();
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
build() {
Scroll(this.scroller) {
Column() {
ForEach(this.arr, (item) => {
Text(item.toString())
.width('90%')
.height(150)
.backgroundColor(0xFFFFFF)
.borderRadius(15)
.fontSize(16)
.textAlign(TextAlign.Center)
.margin({ top: 10 })
}, item => item)
}.width('100%')
}
.backgroundColor(0xDCDCDC)
.scrollable(ScrollDirection.Vertical) // 滚动方向纵向
.scrollBar(BarState.On) // 滚动条常驻显示
.scrollBarColor(Color.Gray) // 滚动条颜色
.scrollBarWidth(10) // 滚动条宽度
.edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹
}
}
```
![zh-cn_image_0000001511900524](figures/zh-cn_image_0000001511900524.gif)
横向布局中使用Scroll组件:
```ts
@Entry
@Component
struct ScrollExample {
scroller: Scroller = new Scroller();
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
build() {
Scroll(this.scroller) {
Row() {
ForEach(this.arr, (item) => {
Text(item.toString())
.height('90%')
.width(150)
.backgroundColor(0xFFFFFF)
.borderRadius(15)
.fontSize(16)
.textAlign(TextAlign.Center)
.margin({ left: 10 })
})
}.height('100%')
}
.backgroundColor(0xDCDCDC)
.scrollable(ScrollDirection.Horizontal) // 滚动方向横向
.scrollBar(BarState.On) // 滚动条常驻显示
.scrollBarColor(Color.Gray) // 滚动条颜色
.scrollBarWidth(10) // 滚动条宽度
.edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹
}
}
```
![zh-cn_image_0000001562940609](figures/zh-cn_image_0000001562940609.gif)
# 媒体查询
## 概述
[媒体查询](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/apis/js-apis-mediaquery.md/)作为响应式设计的核心,在移动设备上应用十分广泛。媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。媒体查询常用于下面两种场景:
1. 针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。
2. 当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。
## 引入与使用流程
媒体查询通过mediaquery模块接口,设置查询条件并绑定回调函数,在对应的条件的回调函数里更改页面布局或者实现业务逻辑,实现页面的响应式设计。具体步骤如下:
首先导入媒体查询模块。
```ts
import mediaquery from '@ohos.mediaquery';
```
通过matchMediaSync接口设置媒体查询条件,保存返回的条件监听句柄listener。例如监听横屏事件:
```ts
let listener = mediaquery.matchMediaSync('(orientation: landscape)');
```
给条件监听句柄listener绑定回调函数onPortrait,当listener检测设备状态变化时执行回调函数。在回调函数内,根据不同设备状态更改页面布局或者实现业务逻辑。
```ts
onPortrait(mediaQueryResult) {
if (mediaQueryResult.matches) {
// do something here
} else {
// do something here
}
}
listener.on('change', onPortrait);
```
## 媒体查询条件
媒体查询条件由媒体类型、逻辑操作符、媒体特征组成,其中媒体类型可省略,逻辑操作符用于连接不同媒体类型与媒体特征,其中,媒体特征要使用“()”包裹且可以有多个。具体规则如下:
### 语法规则
语法规则包括[媒体类型(media-type)](#媒体类型media-type)[媒体逻辑操作(media-logic-operations)](#媒体逻辑操作media-logic-operations)[媒体特征(media-feature)](#媒体特征media-feature)
```ts
[media-type] [media-logic-operations] [(media-feature)]
```
例如:
- screen and (round-screen: true) :表示当设备屏幕是圆形时条件成立。
- (max-height: 800) :表示当高度小于等于800时条件成立。
- (height &lt;= 800) :表示当高度小于等于800时条件成立。
- screen and (device-type: tv) or (resolution &lt; 2) :表示包含多个媒体特征的多条件复杂语句查询,当设备类型为tv或设备分辨率小于2时条件成立。
### 媒体类型(media-type)
| **类型** | **说明** |
| ------ | -------------- |
| screen | 按屏幕相关参数进行媒体查询。 |
### 媒体逻辑操作(media-logic-operations)
媒体逻辑操作符:and、or、not、only用于构成复杂媒体查询,也可以通过comma(, )将其组合起来,详细解释说明如下表。
**表1** 媒体逻辑操作符
| 类型 | 说明 |
| -------------- | ---------------------------------------- |
| and | 将多个媒体特征(Media&nbsp;Feature)以“与”的方式连接成一个媒体查询,只有当所有媒体特征都为true,查询条件成立。另外,它还可以将媒体类型和媒体功能结合起来。例如:screen&nbsp;and&nbsp;(device-type:&nbsp;wearable)&nbsp;and&nbsp;(max-height:&nbsp;600)&nbsp;表示当设备类型是智能穿戴且应用的最大高度小于等于600个像素单位时成立。 |
| or | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。例如:screen&nbsp;and&nbsp;(max-height:&nbsp;1000)&nbsp;or&nbsp;(round-screen:&nbsp;true)&nbsp;表示当应用高度小于等于1000个像素单位或者设备屏幕是圆形时,条件成立。 |
| not | 取反媒体查询结果,媒体查询结果不成立时返回true,否则返回false。例如:not&nbsp;screen&nbsp;and&nbsp;(min-height:&nbsp;50)&nbsp;and&nbsp;(max-height:&nbsp;600)&nbsp;表示当应用高度小于50个像素单位或者大于600个像素单位时成立。<br/>使用not运算符时必须指定媒体类型。 |
| only | 当整个表达式都匹配时,才会应用选择的样式,可以应用在防止某些较早的版本的浏览器上产生歧义的场景。一些较早版本的浏览器对于同时包含了媒体类型和媒体特征的语句会产生歧义,比如:screen&nbsp;and&nbsp;(min-height:&nbsp;50)。老版本浏览器会将这句话理解成screen,从而导致仅仅匹配到媒体类型(screen),就应用了指定样式,使用only可以很好地规避这种情况。<br/>使用only时必须指定媒体类型。 |
| comma(,&nbsp;) | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。其效果等同于or运算符。例如:screen&nbsp;and&nbsp;(min-height:&nbsp;1000),&nbsp;(round-screen:&nbsp;true)&nbsp;表示当应用高度大于等于1000个像素单位或者设备屏幕是圆形时,条件成立。 |
媒体范围操作符包括&lt;=,&gt;=,&lt;&gt;,详细解释说明如下表。
**表2** 媒体逻辑范围操作符
| 类型 | 说明 |
| ----- | ---------------------------------------- |
| &lt;= | 小于等于,例如:screen&nbsp;and&nbsp;(height&nbsp;&lt;=&nbsp;50)。 |
| &gt;= | 大于等于,例如:screen&nbsp;and&nbsp;(height&nbsp;&gt;=&nbsp;600)。 |
| &lt; | 小于,例如:screen&nbsp;and&nbsp;(height&nbsp;&lt;&nbsp;50)。 |
| &gt; | 大于,例如:screen&nbsp;and&nbsp;(height&nbsp;&gt;&nbsp;600)。 |
### 媒体特征(media-feature)
媒体特征包括应用显示区域的宽高、设备分辨率以及设备的宽高等属性,详细说明如下表。
**表3** 媒体特征说明表
| 类型 | 说明 |
| ----------------- | ---------------------------------------- |
| height | 应用页面显示区域的高度。 |
| min-height | 应用页面显示区域的最小高度。 |
| max-height | 应用页面显示区域的最大高度。 |
| width | 应用页面显示区域的宽度。 |
| min-width | 应用页面显示区域的最小宽度。 |
| max-width | 应用页面显示区域的最大宽度。 |
| resolution | 设备的分辨率,支持dpi,dppx和dpcm单位。其中:<br/>-&nbsp;dpi表示每英寸中物理像素个数,1dpi&nbsp;&nbsp;0.39dpcm;<br/>-&nbsp;dpcm表示每厘米上的物理像素个数,1dpcm&nbsp;&nbsp;2.54dpi;<br/>-&nbsp;dppx表示每个px中的物理像素数(此单位按96px&nbsp;=&nbsp;1英寸为基准,与页面中的px单位计算方式不同),1dppx&nbsp;=&nbsp;96dpi。 |
| min-resolution | 设备的最小分辨率。 |
| max-resolution | 设备的最大分辨率。 |
| orientation | 屏幕的方向。<br/>可选值:<br/>-&nbsp;&nbsp;orientation:&nbsp;&nbsp;portrait(设备竖屏);<br/>-&nbsp;&nbsp;orientation:&nbsp;&nbsp;landscape(设备横屏)。 |
| device-height | 设备的高度。 |
| min-device-height | 设备的最小高度。 |
| max-device-height | 设备的最大高度。 |
| device-width | 设备的宽度。 |
| device-type | 设备的类型。<br/>可选值:default、tablet。 |
| min-device-width | 设备的最小宽度。 |
| max-device-width | 设备的最大宽度。 |
| round-screen | 屏幕类型,圆形屏幕为true,非圆形屏幕为false。 |
| dark-mode | 系统为深色模式时为true,否则为false。 |
## 场景示例
下例中使用媒体查询,实现屏幕横竖屏切换时,给页面文本应用添加不同的内容和样式。
Stage模型下的示例:
```ts
import mediaquery from '@ohos.mediaquery';
import window from '@ohos.window';
import common from '@ohos.app.ability.common';
let portraitFunc = null;
@Entry
@Component
struct MediaQueryExample {
@State color: string = '#DB7093';
@State text: string = 'Portrait';
// 当设备横屏时条件成立
listener = mediaquery.matchMediaSync('(orientation: landscape)');
// 当满足媒体查询条件时,触发回调
onPortrait(mediaQueryResult) {
if (mediaQueryResult.matches) { // 若设备为横屏状态,更改相应的页面布局
this.color = '#FFD700';
this.text = 'Landscape';
} else {
this.color = '#DB7093';
this.text = 'Portrait';
}
}
aboutToAppear() {
// 绑定当前应用实例
portraitFunc = this.onPortrait.bind(this);
// 绑定回调函数
this.listener.on('change', portraitFunc);
}
// 改变设备横竖屏状态函数
private changeOrientation(isLandscape: boolean) {
// 获取UIAbility实例的上下文信息
let context = getContext(this) as common.UIAbilityContext;
// 调用该接口手动改变设备横竖屏状态
window.getLastWindow(context).then((lastWindow) => {
lastWindow.setPreferredOrientation(isLandscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT)
});
}
build() {
Column({ space: 50 }) {
Text(this.text).fontSize(50).fontColor(this.color)
Text('Landscape').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange)
.onClick(() => {
this.changeOrientation(true);
})
Text('Portrait').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange)
.onClick(() => {
this.changeOrientation(false);
})
}
.width('100%').height('100%')
}
}
```
FA模型下的示例:
```ts
import mediaquery from '@ohos.mediaquery';
import featureAbility from '@ohos.ability.featureAbility';
let portraitFunc = null;
@Entry
@Component
struct MediaQueryExample {
@State color: string = '#DB7093';
@State text: string = 'Portrait';
listener = mediaquery.matchMediaSync('(orientation: landscape)'); // 当设备横屏时条件成立
onPortrait(mediaQueryResult) { // 当满足媒体查询条件时,触发回调
if (mediaQueryResult.matches) { // 若设备为横屏状态,更改相应的页面布局
this.color = '#FFD700';
this.text = 'Landscape';
} else {
this.color = '#DB7093';
this.text = 'Portrait';
}
}
aboutToAppear() {
portraitFunc = this.onPortrait.bind(this); // 绑定当前应用实例
this.listener.on('change', portraitFunc); //绑定回调函数
}
build() {
Column({ space: 50 }) {
Text(this.text).fontSize(50).fontColor(this.color)
Text('Landscape').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange)
.onClick(() => {
let context = featureAbility.getContext();
context.setDisplayOrientation(0); //调用该接口手动改变设备横竖屏状态
})
Text('Portrait').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange)
.onClick(() => {
let context = featureAbility.getContext();
context.setDisplayOrientation(1); //调用该接口手动改变设备横竖屏状态
})
}
.width('100%').height('100%')
}
}
```
**图1** 竖屏  
![portralit](figures/portralit.jpg)
**图2** 横屏  
![landscape](figures/landscape.jpg)
## 相关实例
基于媒体查询,可参考以下实例:
[媒体查询](https://gitee.com/openharmony/applications_app_samples/tree/master/ETSUI/MediaQuery):使用媒体查询,完成在不同设备上显示不同的界面效果。
# 布局概述
组件按照布局的要求依次排列,构成应用的页面。在声明式UI中,所有的页面都是由自定义组件构成,开发者可以根据自己的需求,选择合适的布局进行页面开发。
布局指用特定的组件或者属性来管理用户页面所放置UI组件的大小和位置。在实际的开发过程中,需要遵守以下流程保证整体的布局效果:
- 确定页面的布局结构。
- 分析页面中的元素构成。
- 选用适合的布局容器组件或属性控制页面中各个元素的位置和大小约束。
## 布局结构
布局的结构通常是分层级的,代表了用户界面中的整体架构。一个常见的页面结构如下所示:
**图1** 常见页面结构图  
![common-page-structure](figures/common-page-structure.png)
为实现上述效果,开发者需要在页面中声明对应的元素。其中,Page表示页面的根节点,Column/Row等元素为系统组件。针对不同的页面结构,ArkUI提供了不同的布局组件来帮助开发者实现对应布局的效果,例如Row用于实现线性布局。
## 布局元素的组成
布局相关的容器组件可形成对应的布局效果。例如,List组件可构成线性布局。
**图2** 布局元素组成图  
![layout-element-omposition](figures/layout-element-omposition.png)
- 组件区域(蓝区方块):组件区域表明组件的大小,width、height属性设置该区域的大小。
- 组件内容区(黄色方块):组件区域大小减去组件的Padding值,组件内容区大小会作为组件内容(或者子组件)进行大小测算时的布局测算限制。
- 组件内容(绿色方块):组件内容本身占用的大小,比如文本内容占用的大小。组件内容和组件内容区不一定匹配,比如设置了固定的width和height,此时组件内容区大小就是设置的width和height减去Padding值,但文本内容则是通过文本布局引擎测算后得到的大小,可能出现文本真实大小小于设置的组件内容区大小。当组件内容和组件内容区大小不一致时,align属性生效,定义组件内容在组件内容区的对齐方式,如居中对齐。
- 组件布局边界(虚线部分):组件通过Margin属性设置外边距时,组件布局边界就是组件区域加上Margin的大小。
## 如何选择布局
声明式UI提供了以下8种常见布局,开发者可根据实际应用场景选择合适的布局进行页面开发。
| 布局 | 应用场景 |
| ---------------------------------------- | ---------------------------------------- |
| [线性布局](arkts-layout-development-linear.md)(Row、Column) | 如果布局内子元素为复数个,且能够以某种方式线性排列时优先考虑此布局。 |
| [层叠布局](arkts-layout-development-stack-layout.md)(Stack) | 组件需要有堆叠效果时优先考虑此布局,层叠布局的堆叠效果不会占用或影响其他同容器内子组件的布局空间。例如[Panel](../reference/arkui-ts/ts-container-panel.md)作为子组件弹出时将其他组件覆盖更为合理,则优先考虑在外层使用堆叠布局。 |
| [弹性布局](arkts-layout-development-flex-layout.md)(Flex) | 弹性布局是与线性布局类似的布局方式。区别在于弹性布局默认能够使子组件压缩或拉伸。在子组件需要计算拉伸或压缩比例时优先使用此布局,可使得多个容器内子组件能有更好的视觉上的填充容器效果。 |
| [相对布局](arkts-layout-development-relative-layout.md)(RelativeContainer) | 相对布局是在二维空间中的布局方式,不需要遵循线性布局的规则,布局方式更为自由。通过在子组件上设置锚点规则(AlignRules)使子组件能够将自己在横轴、纵轴中的位置与容器或容器内其他子组件的位置对齐。设置的锚点规则可以天然支持子元素压缩、拉伸,堆叠或形成多行效果。在页面元素分布复杂或通过线性布局会使容器嵌套层数过深时推荐使用。 |
| [栅格布局](arkts-layout-development-grid-layout.md)(GridRow、GridCol) | 栅格是多设备场景下通用的辅助定位工具,通过将空间分割为有规律的栅格。栅格不同于网格布局固定的空间划分,可以实现不同设备下不同的布局,空间划分更随心所欲,从而显著降低适配不同屏幕尺寸的设计及开发成本,使得整体设计和开发流程更有秩序和节奏感,同时也保证多设备上应用显示的协调性和一致性,提升用户体验。推荐内容相同但布局不同时使用。 |
| [媒体查询](arkts-layout-development-media-query.md)\@ohos.mediaquery) | 媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。例如根据设备和应用的不同属性信息设计不同的布局,以及屏幕发生动态改变时更新应用的页面布局。 |
| [列表](arkts-layout-development-create-list.md)(List) | 使用列表可以轻松高效地显示结构化、可滚动的信息。在ArkUI中,列表具有垂直和水平布局能力和自适应交叉轴方向上排列个数的布局能力,超出屏幕时可以滚动。列表适合用于呈现同类数据类型或数据类型集,例如图片和文本。 |
| [网格](arkts-layout-development-create-grid.md)(Grid) | 网格布局具有较强的页面均分能力,子组件占比控制能力,是一种重要自适应布局。网格布局可以控制元素所占的网格数量、设置子组件横跨几行或者几列,当网格容器尺寸发生变化时,所有子组件以及间距等比例调整。推荐在需要按照固定比例或者均匀分配空间的布局场景下使用,例如计算器、相册、日历等。 |
| [轮播](arkts-layout-development-create-looping.md)(Swiper) | 轮播组件通常用于实现广告轮播、图片预览、可滚动应用等。 |
## 布局位置
position、offset等属性影响了布局容器相对于自身或其他组件的位置。
| 定位能力 | 使用场景 | 实现方式 |
| ---- | ---------------------------------------- | ---------------------------------------- |
| 绝对定位 | 对于不同尺寸的设备,使用绝对定位的适应性会比较差,在屏幕的适配上有缺陷。 | 使用[position](../reference/arkui-ts/ts-universal-attributes-location.md)实现绝对定位,设置元素左上角相对于父容器左上角偏移位置。在布局容器中,设置该属性不影响父容器布局,仅在绘制时进行位置调整。 |
| 相对定位 | 相对定位不脱离文档流,即原位置依然保留,不影响元素本身的特性,仅相对于原位置进行偏移。 | 使用[offset](../reference/arkui-ts/ts-universal-attributes-location.md)可以实现相对定位,设置元素相对于自身的偏移量。设置该属性,不影响父容器布局,仅在绘制时进行位置调整。 |
## 对子元素的约束
| 对子元素的约束能力 | 使用场景 | 实现方式 |
| --------- | ---------------------------------------- | ---------------------------------------- |
| 拉伸 | 容器组件尺寸发生变化时,增加或减小的空间全部分配给容器组件内指定区域。 | [flexGrow](../reference/arkui-ts/ts-universal-attributes-flex-layout.md)[flexShrink](../reference/arkui-ts/ts-universal-attributes-flex-layout.md)属性:<br/>1.&nbsp;flexGrow基于父容器的剩余空间分配来控制组件拉伸。<br/>2.&nbsp;flexShrink设置父容器的压缩尺寸来控制组件拉伸。 |
| 缩放 | 子组件的宽高按照预设的比例,随容器组件发生变化,且变化过程中子组件的宽高比不变。 | [aspectRatio](../reference/arkui-ts/ts-universal-attributes-layout-constraints.md)属性指定当前组件的宽高比来控制缩放,公式为:aspectRatio=width/height。 |
| 占比 | 占比能力是指子组件的宽高按照预设的比例,随父容器组件发生变化。 | 基于通用属性的两种实现方式:<br/>1.&nbsp;将子组件的宽高设置为父组件宽高的百分比。<br/>2.&nbsp;[layoutWeight](../reference/arkui-ts/ts-universal-attributes-size.md)属性,使得子元素自适应占满剩余空间。 |
| 隐藏 | 隐藏能力是指容器组件内的子组件,按照其预设的显示优先级,随容器组件尺寸变化显示或隐藏,其中相同显示优先级的子组件同时显示或隐藏。 | 通过[displayPriority](../reference/arkui-ts/ts-universal-attributes-layout-constraints.md)属性来控制页面的显示和隐藏。 |
# 改善布局性能
Flex为采用弹性布局的容器。容器内部的所有子元素,会自动参与弹性布局。子元素默认沿主轴排列,子元素在主轴方向的尺寸称为主轴尺寸。
在单行布局场景下,子组件的主轴尺寸长度和可能存在不等于容器主轴尺寸长度的情况,部分子组件会被布局两次来填充容器,即需要二次布局,导致布局效率下降。
## 场景一
所有子组件未设置DisplayPriority属性(或DisplayPriority设置为默认值1)和LayoutWeight属性(或LayoutWeight设置为默认值0)时,所有子组件先按序布局一次。
- 第一次布局子组件主轴尺寸长度总和等于容器主轴尺寸长度,不需要二次布局。
![layout-performance-1](figures/layout-performance-1.png)
- 第一次布局子组件主轴尺寸长度总和小于容器主轴尺寸长度,且包含设置有效的flexGrow属性的子组件,设置有效的flexGrow属性的子组件会触发二次布局,拉伸布局填满容器。
![layout-performace-2](figures/layout-performace-2.gif)
- 第一次布局子组件主轴尺寸长度总和大于容器主轴尺寸长度,且包含设置有效的flexShrink属性(flex子组件默认值为1,为有效值)的子组件,设置有效的flexShrink属性的子组件会触发二次布局,压缩布局填满容器。
![layout-performace-3](figures/layout-performace-3.gif)
## 场景二
子组件存在设置DisplayPriority属性,不存在设置LayoutWeight属性。
根据DisplayPriority从大到小顺序,布局每组同DisplayPriority值的子组件,直到子组件主轴尺寸长度总和最大且不超过容器主轴尺寸长度,舍弃未布局的低优先级DisplayPriority(可能存在一组临界DisplayPriority值的子组件布局但未使用的情况)。
- 第一次布局子组件主轴尺寸长度总和等于容器主轴尺寸长度,不需要二次布局。
![layout-performance-4](figures/layout-performance-4.png)
- 第一次布局子组件主轴尺寸长度总和小于容器主轴尺寸长度,且包含设置有效的flexGrow属性的子组件,设置有效的flexGrow属性的子组件会触发二次布局,拉伸布局填满容器。
![layout-performace-5](figures/layout-performace-5.gif)
## 场景三
子组件中存在设置LayoutWeight属性。
根据DisplayPriority从大到小顺序,布局每组DisplayPriority值相同且不设置LayoutWeight属性的子组件,直到子组件主轴尺寸长度总和最大且不超过容器主轴尺寸长度,舍弃未布局的低优先级DisplayPriority,可能存在一组临界DisplayPriority值的子组件布局但未使用的情况。
剩余空间按设置LayoutWeight属性的子组件的LayoutWeight比例填满容器。
- 两次遍历都只布局一次组件,不会触发二次布局。
![layout-performace-6](figures/layout-performace-6.gif)
## 如何优化Flex的布局性能
- 使用Column/Row代替Flex。
- 大小不需要变更的子组件主动设置flexShrink属性值为0。
- 优先使用LayoutWeight属性替代flexGrow属性和flexShrink属性。
- 子组件主轴长度分配设置为最常用场景的布局结果,使子组件主轴长度总和等于Flex容器主轴长度。
# 相对布局
## 概述
[RelativeContainer](../reference/arkui-ts/ts-container-relativecontainer.md)为采用相对布局的容器,支持容器内部的子元素设置相对位置关系。子元素支持指定兄弟元素作为锚点,也支持指定父容器作为锚点,基于锚点做相对位置布局。下图是一个RelativeContainer的概念图,图中的虚线表示位置的依赖关系。
**图1** 相对布局示意图  
![relative-layout](figures/relative-layout.png)
子元素并不完全是上图中的依赖关系。比如,Item4可以以Item2为依赖锚点,也可以以RelativeContainer父容器为依赖锚点。
## 基本概念
- 锚点:通过锚点设置当前元素基于哪个元素确定位置。
- 对齐方式:通过对齐方式,设置当前元素是基于锚点的上中下对齐,还是基于锚点的左中右对齐。
## 设置依赖关系
### 锚点设置
锚点设置是指设置子元素相对于父元素或兄弟元素的位置依赖关系。在水平方向上,可以设置left、middle、right的锚点。在竖直方向上,可以设置top、center、bottom的锚点。为了明确定义锚点,必须为RelativeContainer及其子元素设置ID,用于指定锚点信息。ID默认为“container”,其余子元素的ID通过id属性设置。未设置ID的子元素在RelativeContainer中不会显示。
>**说明:**
>
>在使用锚点时要注意子元素的相对位置关系,避免出现错位或遮挡的情况。
- RelativeContainer父组件为锚点,__container__代表父容器的id。
```ts
RelativeContainer() {
Row()
// 添加其他属性
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})
.id("row1")
Row()
...
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
right: { anchor: '__container__', align: HorizontalAlign.End }
})
.id("row2")
}
...
```
![zh-cn_image_0000001562820901](figures/zh-cn_image_0000001562820901.png)
- 以子元素为锚点。
```ts
RelativeContainer() {
...
top: { anchor: 'row1', align: VerticalAlign.Bottom },
...
}
.width(300).height(300)
.margin({ left: 20 })
.border({ width: 2, color: '#6699FF' })
```
![zh-cn_image_0000001562940613](figures/zh-cn_image_0000001562940613.png)
### 设置相对于锚点的对齐位置
设置了锚点之后,可以通过align设置相对于锚点的对齐位置。
在水平方向上,对齐位置可以设置为HorizontalAlign.Start、HorizontalAlign.Center、HorizontalAlign.End。
![alignment-relative-anchor-horizontal](figures/alignment-relative-anchor-horizontal.png)
在竖直方向上,对齐位置可以设置为VerticalAlign.Top、VerticalAlign.Center、VerticalAlign.Bottom。
![alignment-relative-anchor-vertical](figures/alignment-relative-anchor-vertical.png)
## 场景实例
相对布局内的子元素相对灵活,只要在RelativeContainer容器内,均可以通过alignRules进行相应相应的位置移动。
```ts
@Entry
@Component
struct Index {
build() {
Row() {
RelativeContainer() {
Row()
.width(100)
.height(100)
.backgroundColor('#FF3333')
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top }, //以父容器为锚点,竖直方向顶头对齐
middle: { anchor: '__container__', align: HorizontalAlign.Center } //以父容器为锚点,水平方向居中对齐
})
.id('row1') //设置锚点为row1
Row() {
Image($r('app.media.icon'))
}
.height(100).width(100)
.alignRules({
top: { anchor: 'row1', align: VerticalAlign.Bottom }, //以row1组件为锚点,竖直方向低端对齐
left: { anchor: 'row1', align: HorizontalAlign.Start } //以row1组件为锚点,水平方向开头对齐
})
.id('row2') //设置锚点为row2
Row()
.width(100)
.height(100)
.backgroundColor('#FFCC00')
.alignRules({
top: { anchor: 'row2', align: VerticalAlign.Top }
})
.id('row3') //设置锚点为row3
Row()
.width(100)
.height(100)
.backgroundColor('#FF9966')
.alignRules({
top: { anchor: 'row2', align: VerticalAlign.Top },
left: { anchor: 'row2', align: HorizontalAlign.End },
})
.id('row4') //设置锚点为row4
Row()
.width(100)
.height(100)
.backgroundColor('#FF66FF')
.alignRules({
top: { anchor: 'row2', align: VerticalAlign.Bottom },
middle: { anchor: 'row2', align: HorizontalAlign.Center }
})
.id('row5') //设置锚点为row5
}
.width(300).height(300)
.border({ width: 2, color: '#6699FF' })
}
.height('100%').margin({ left: 30 })
}
}
```
![zh-cn_image_0000001562700529](figures/zh-cn_image_0000001562700529.png)
# 层叠布局
## 概述
层叠布局(StackLayout)用于在屏幕上预留一块区域来显示组件中的元素,提供元素可以重叠的布局。层叠布局通过[Stack](https://docs.openharmony.cn/pages/v3.2Beta/zh-cn/application-dev/reference/arkui-ts/ts-container-stack.md/)容器组件实现位置的固定定位与层叠,容器中的子元素(子组件)依次入栈,后一个子元素覆盖前一个子元素,子元素可以叠加,也可以设置位置。
层叠布局具有较强的页面层叠、位置定位能力,其使用场景有广告、卡片层叠效果等。
如图1,Stack作为容器,容器内的子元素(子组件)的顺序为Item1-&gt;Item2-&gt;Item3。
**图1** 层叠布局  
![stack-layout](figures/stack-layout.png)
## 开发布局
Stack组件为容器组件,容器内可包含各种子组件。其中的子组件根据自己的大小默认进行居中堆叠。子元素被约束在Stack下,进行自己的样式定义以及排列。
```ts
Column(){
Stack({ }) {
Column(){}.width('90%').height('100%').backgroundColor('#ff58b87c')
Text('text').width('60%').height('60%').backgroundColor('#ffc3f6aa')
Button('button').width('30%').height('30%').backgroundColor('#ff8ff3eb').fontColor('#000')
}.width('100%').height(150).margin({ top: 50 })
}
```
![stack-layout-sample](figures/stack-layout-sample.png)
## 对齐方式
Stack组件通过[alignContent参数](../reference/arkui-ts/ts-appendix-enums.md#alignment)实现位置的相对移动。如图2所示,支持九种对齐方式。
**图2** Stack容器内元素的对齐方式  
![zh-cn_image_0000001562940621](figures/zh-cn_image_0000001562940621.png)
## Z序控制
Stack容器中兄弟组件显示层级关系可以通过[Z序控制](../reference/arkui-ts/ts-universal-attributes-z-order.md)的zIndex属性改变。zIndex值越大,显示层级越高,即zIndex值大的组件会覆盖在zIndex值小的组件上方。
在层叠布局中,如果后面子元素尺寸大于前面子元素尺寸,则前面子元素完全隐藏。
```ts
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text('Stack子元素1').textAlign(TextAlign.End).fontSize(20)
}.width(100).height(100).backgroundColor(0xffd306)
Column() {
Text('Stack子元素2').fontSize(20)
}.width(150).height(150).backgroundColor(Color.Pink)
Column() {
Text('Stack子元素3').fontSize(20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0)
```
![zh-cn_image_0000001511900544](figures/zh-cn_image_0000001511900544.png)
下图中,最后的子元素3的尺寸大于前面的所有子元素,所以,前面两个元素完全隐藏。改变子元素1,子元素2的zIndex属性后,可以将元素展示出来。
```ts
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text('Stack子元素1').fontSize(20)
}.width(100).height(100).backgroundColor(0xffd306).zIndex(2)
Column() {
Text('Stack子元素2').fontSize(20)
}.width(150).height(150).backgroundColor(Color.Pink).zIndex(1)
Column() {
Text('Stack子元素3').fontSize(20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0)
```
![zh-cn_image_0000001563060797](figures/zh-cn_image_0000001563060797.png)
## 场景示例
使用层叠布局快速搭建页面显示模型。
```ts
@Entry
@Component
struct StackSample {
private arr: string[] = ['APP1', 'APP2', 'APP3', 'APP4', 'APP5', 'APP6', 'APP7', 'APP8'];
build() {
Stack({ alignContent: Alignment.Bottom }) {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.arr, (item) => {
Text(item)
.width(100)
.height(100)
.fontSize(16)
.margin(10)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}, item => item)
}.width('100%').height('100%')
Flex({ justifyContent: FlexAlign.SpaceAround, alignItems: ItemAlign.Center }) {
Text('联系人').fontSize(16)
Text('设置').fontSize(16)
Text('短信').fontSize(16)
}
.width('50%')
.height(50)
.backgroundColor('#16302e2e')
.margin({ bottom: 15 })
.borderRadius(15)
}.width('100%').height('100%').backgroundColor('#CFD0CF')
}
}
```
![zh-cn_image_0000001511421368](figures/zh-cn_image_0000001511421368.png)
# 布局更新动画
[属性动画](../reference/arkui-ts/ts-animatorproperty.md)(animation)和[显式动画](../reference/arkui-ts/ts-explicit-animation.md)(animateTo)是ArkUI提供的最基础和常用的动画功能。在布局属性(如[尺寸属性](../reference/arkui-ts/ts-universal-attributes-size.md)[位置属性](../reference/arkui-ts/ts-universal-attributes-location.md))发生变化时,可以通过属性动画或显式动画,按照动画参数过渡到新的布局参数状态。
| 动画类型 | 特点 | 适用场景 |
| ---- | ---------------------------------------- | -------- |
| 属性动画 | 动画设置简单,属性变化时自动触发动画。 | 较简单的动画场景 |
| 显式动画 | 闭包内的变化均会触发动画,包括由数据变化引起的组件的增删、组件属性的变化等,可以做较为复杂的动画。 | 较复杂的动画场景 |
## 使用显式动画产生布局更新动画
显式动画的接口为:
```ts
animateTo(value: AnimateParam, event: () => void): void
```
第一个参数指定动画参数,第二个参数为动画的闭包函数。
以下是使用显式动画产生布局更新动画的示例。示例中,当Column组件的alignItems属性改变后,其子组件的布局位置结果发生变化。只要该属性是在animateTo的闭包函数中修改的,那么由其引起的所有变化都会按照animateTo的动画参数执行动画过渡到终点值。
```ts
@Entry
@Component
struct LayoutChange {
// 用于控制Column的alignItems属性
@State itemAlign: HorizontalAlign = HorizontalAlign.Start;
allAlign: HorizontalAlign[] = [HorizontalAlign.Start, HorizontalAlign.Center, HorizontalAlign.End];
alignIndex: number = 0;
build() {
Column() {
Column({ space: 10 }) {
Button("1").width(100).height(50)
Button("2").width(100).height(50)
Button("3").width(100).height(50)
}
.margin(20)
.alignItems(this.itemAlign)
.borderWidth(2)
.width("90%")
.height(200)
Button("click").onClick(() => {
// 动画时长为1000ms,曲线为EaseInOut
animateTo({ duration: 1000, curve: Curve.EaseInOut }, () => {
this.alignIndex = (this.alignIndex + 1) % this.allAlign.length;
// 在闭包函数中修改this.itemAlign参数,使Column容器内部孩子的布局方式变化,使用动画过渡到新位置
this.itemAlign = this.allAlign[this.alignIndex];
});
})
}
.width("100%")
.height("100%")
}
}
```
![layoutChange1](figures/layoutChange1.gif)
除直接改变布局方式外,也可直接修改组件的宽、高、位置。
```ts
@Entry
@Component
struct LayoutChange2 {
@State myWidth: number = 100;
@State myHeight: number = 50;
// 标志位,true和false分别对应一组myWidth、myHeight值
@State flag: boolean = false;
build() {
Column({ space: 10 }) {
Button("text")
.type(ButtonType.Normal)
.width(this.myWidth)
.height(this.myHeight)
.margin(20)
Button("area: click me")
.fontSize(12)
.margin(20)
.onClick(() => {
animateTo({ duration: 1000, curve: Curve.Ease }, () => {
// 动画闭包中根据标志位改变控制第一个Button宽高的状态变量,使第一个Button做宽高动画
if (this.flag) {
this.myWidth = 100;
this.myHeight = 50;
} else {
this.myWidth = 200;
this.myHeight = 100;
}
this.flag = !this.flag;
});
})
}
.width("100%")
.height("100%")
}
}
```
在第二个Button的点击事件中,使用animateTo函数,在闭包中修改this.myWidth和this.myHeight状态变量,而这两个状态变量分别为第一个Button的宽、高属性值,所以第一个Button做了宽高动画。效果如下图。
![layoutChange2_animateTo](figures/layoutChange2_animateTo.gif)
与此同时,第二个Button也产生了一个位置动画。这是由于第一个Button的宽高变化后,引起了Column内部其他组件的布局结果也发生了变化,第二个Button的布局发生变化也是由于闭包内改变第一个Button的宽高造成的。
如果不希望第二个Button有动画效果,有两种方式可以实现。一种是给做第一个Button外面再加一个容器,使其动画前后的大小都在容器的范围内,这样第二个Button的位置不会被第一个Button的位置所影响。修改后的核心代码如下。
```ts
Column({ space: 10 }) {
Column() {
// Button放在足够大的容器内,使其不影响更外层的组件位置
Button("text")
.type(ButtonType.Normal)
.width(this.myWidth)
.height(this.myHeight)
}
.margin(20)
.width(200)
.height(100)
Button("area: click me")
.fontSize(12)
.onClick(() => {
animateTo({ duration: 1000, curve: Curve.Ease }, () => {
// 动画闭包中根据标志位改变控制第一个Button宽高的状态变量,使第一个Button做宽高动画
if (this.flag) {
this.myWidth = 100;
this.myHeight = 50;
} else {
this.myWidth = 200;
this.myHeight = 100;
}
this.flag = !this.flag;
});
})
}
.width("100%")
.height("100%")
```
![layoutChange2_animateTo_change](figures/layoutChange2_animateTo_change.gif)
另一种方式是给第二个Button添加布局约束,如position的位置约束,使其位置不被第一个Button的宽高影响。核心代码如下:
```ts
Column({ space: 10 }) {
Button("text")
.type(ButtonType.Normal)
.width(this.myWidth)
.height(this.myHeight)
.margin(20)
Button("area: click me")
.fontSize(12)
// 配置position属性固定,使自己的布局位置不被第一个Button的宽高影响
.position({ x: "30%", y: 200 })
.onClick(() => {
animateTo({ duration: 1000, curve: Curve.Ease }, () => {
// 动画闭包中根据标志位改变控制第一个Button宽高的状态变量,使第一个Button做宽高动画
if (this.flag) {
this.myWidth = 100;
this.myHeight = 50;
} else {
this.myWidth = 200;
this.myHeight = 100;
}
this.flag = !this.flag;
});
})
}
.width("100%")
.height("100%")
```
## 使用属性动画产生布局更新动画
显式动画把要执行动画的属性的修改放在闭包函数中触发动画,而属性动画则无需使用闭包,把animation属性加在要做属性动画的组件的属性后即可。
属性动画的接口为:
```ts
animation(value: AnimateParam)
```
其入参为动画参数。想要组件随某个属性值的变化而产生动画,此属性需要加在animation属性之前。有的属性变化不希望通过animation产生属性动画,可以放在animation之后。上面显式动画的示例很容易改为用属性动画实现。例如:
```ts
@Entry
@Component
struct LayoutChange2 {
@State myWidth: number = 100;
@State myHeight: number = 50;
@State flag: boolean = false;
@State myColor: Color = Color.Blue;
build() {
Column({ space: 10 }) {
Button("text")
.type(ButtonType.Normal)
.width(this.myWidth)
.height(this.myHeight)
// animation只对其上面的type、width、height属性生效,时长为1000ms,曲线为Ease
.animation({ duration: 1000, curve: Curve.Ease })
.backgroundColor(this.myColor)
.margin(20)
// animation对下面的backgroundColor、margin属性不生效
Button("area: click me")
.fontSize(12)
.onClick(() => {
// 改变属性值,配置了属性动画的属性会进行动画过渡
if (this.flag) {
this.myWidth = 100;
this.myHeight = 50;
this.myColor = Color.Blue;
} else {
this.myWidth = 200;
this.myHeight = 100;
this.myColor = Color.Pink;
}
this.flag = !this.flag;
})
}
}
}
```
上述示例中,第一个button上的animation属性,只对写在animation之前的type、width、height属性生效,而对写在animation之后的backgroundColor、margin属性无效。运行结果是width、height属性会按照animation的动画参数执行动画,而backgroundColor会直接跳变,不会产生动画。效果如下图:
![size-change-animation](figures/size-change-animation.gif)
>**说明:**
>
> 1. 使用属性动画时,会按照指定的属性动画参数执行动画。每个组件可为自己的属性配置不同参数的属性动画。
>
> 2. 显式动画会对动画闭包前后造成的所有界面差异执行动画,且使用同一动画参数,适用于统一执行的场景。此外,显式动画也可以用于一些非属性变量造成的动画,如if/else的条件,ForEach使用的数组元素的删减。
>
> 3. 如果一个属性配置了属性动画,且在显式动画闭包中改变该属性值,属性动画优先生效,会使用属性动画的动画参数。
# Navigation
[Navigation](../reference/arkui-ts/ts-basic-components-navigation.md)组件一般作为页面的根容器,包括单页面、分栏和自适应三种显示模式。同时,Navigation提供了属性来设置页面的标题栏、工具栏、导航栏等。
Navigation组件的页面包含主页和内容页。主页由标题栏、内容区和工具栏组成,可在内容区中使用[NavRouter](../reference/arkui-ts/ts-basic-components-navrouter.md)子组件实现导航栏功能。内容页主要显示[NavDestination](../reference/arkui-ts/ts-basic-components-navdestination.md)子组件中的内容。
NavRouter是和Navigation搭配使用的特殊子组件,默认提供点击响应处理,不需要开发者自定义点击事件逻辑。NavRouter有且仅有两个根节点,第二个根节点是NavDestination。NavDestination是和NavRouter搭配使用的特殊子组件,用于显示Navigation组件的内容页。当开发者点击NavRouter组件时,会跳转到对应的NavDestination内容区。
## 设置页面显示模式
Navigation组件通过mode属性设置页面的显示模式。
- 自适应模式
Navigation组件默认为自适应模式,此时mode属性为NavigationMode.Auto。自适应模式下,当设备宽度大于520vp时,Navigation组件采用分栏模式,反之采用单页面模式。
```
Navigation() {
...
}
.mode(NavigationMode.Auto)
```
- 单页面模式
**图1** 单页面布局示意图  
![zh-cn_image_0000001511740532](figures/zh-cn_image_0000001511740532.png)
将mode属性设置为NavigationMode.Stack,Navigation组件即可设置为单页面显示模式。
```ts
Navigation() {
...
}
.mode(NavigationMode.Stack)
```
![单页面1](figures/单页面1.jpg)
- 分栏模式
**图2** 分栏布局示意图
![zh-cn_image_0000001562820845](figures/zh-cn_image_0000001562820845.png)
将mode属性设置为NavigationMode.Split,Navigation组件即可设置为分栏显示模式。
```ts
@Entry
@Component
struct NavigationExample {
private arr: number[] = [1, 2, 3];
build() {
Column() {
Navigation() {
TextInput({ placeholder: 'search...' })
.width("90%")
.height(40)
.backgroundColor('#FFFFFF')
List({ space: 12 }) {
ForEach(this.arr, (item) => {
ListItem() {
NavRouter() {
Text("NavRouter" + item)
.width("100%")
.height(72)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.fontSize(16)
.fontWeight(500)
.textAlign(TextAlign.Center)
NavDestination() {
Text("NavDestinationContent" + item)
}
.title("NavDestinationTitle" + item)
}
}
}, item => item)
}
.width("90%")
.margin({ top: 12 })
}
.title("主标题")
.mode(NavigationMode.Split)
.menus([
{value: "", icon: "./image/ic_public_search.svg", action: ()=> {}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}}
])
.toolBar({items: [
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=> {}},
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=> {}},
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=> {}}
]})
}
.height('100%')
.width('100%')
.backgroundColor('#F1F3F5')
}
}
```
![分栏](figures/分栏.jpg)
## 设置标题栏模式
标题栏在界面顶部,用于呈现界面名称和操作入口,Navigation组件通过titleMode属性设置标题栏模式。
- Mini模式
普通型标题栏,用于一级页面不需要突出标题的场景。
**图3** Mini模式标题栏  
![mini](figures/mini.jpg)
```ts
Navigation() {
...
}
.titleMode(NavigationTitleMode.Mini)
```
- Full模式
强调型标题栏,用于一级页面需要突出标题的场景。
**图4** Full模式标题栏  
![free1](figures/free1.jpg)
```ts
Navigation() {
...
}
.titleMode(NavigationTitleMode.Full)
```
## 设置菜单栏
菜单栏位于Navigation组件的右上角,开发者可以通过menus属性进行设置。menus支持Array&lt;[NavigationMenuItem](../reference/arkui-ts/ts-basic-components-navigation.md#navigationmenuitem%E7%B1%BB%E5%9E%8B%E8%AF%B4%E6%98%8E)&gt;和CustomBuilder两种参数类型。使用Array&lt;NavigationMenuItem&gt;类型时,竖屏最多支持显示3个图标,横屏最多支持显示5个图标,多余的图标会被放入自动生成的更多图标。
**图5** 设置了3个图标的菜单栏  
![菜单栏2](figures/菜单栏2.jpg)
```ts
Navigation() {
...
}
.menus([{value: "", icon: "./image/ic_public_search.svg", action: ()=>{}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=>{}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=>{}}])
```
**图6** 设置了4个图标的菜单栏  
![菜单栏](figures/菜单栏.jpg)
```ts
Navigation() {
...
}
.menus([{value: "", icon: "./image/ic_public_search.svg", action: ()=>{}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=>{}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=>{}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=>{}}])
```
## 设置工具栏
工具栏位于Navigation组件的底部,开发者可以通过toolBar属性进行设置。
**图7** 工具栏  
![free3](figures/free3.jpg)
```ts
Navigation() {
...
}
.toolBar({items:[
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=>{}},
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=>{}},
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=>{}}]})
```
# Tabs
当页面信息较多时,为了让用户能够聚焦于当前显示的内容,需要对页面内容进行分类,提高页面空间利用率。[Tabs](../reference/arkui-ts/ts-container-tabs.md)组件可以在一个页面内快速实现视图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量。
## 基本布局
Tabs组件的页面组成包含两个部分,分别是TabContent和TabBar。TabContent是内容页,TabBar是导航页签栏,页面结构如下图所示,根据不同的导航类型,布局会有区别,可以分为底部导航、顶部导航、侧边导航,其导航栏分别位于底部、顶部和侧边。
**图1** Tabs组件布局示意图
![tabs-layout](figures/tabs-layout.png)
>**说明:**
>
> - TabContent组件不支持设置通用宽度属性,其宽度默认撑满Tabs父组件。
>
> - TabContent组件不支持设置通用高度属性,其高度由Tabs父组件高度与TabBar组件高度决定。
Tabs使用花括号包裹TabContent,如图2,其中TabContent显示相应的内容页。
**图2** Tabs与TabContent使用  
![tabs-tabscontent](figures/tabs-tabscontent.png)
每一个TabContent对应的内容需要有一个页签,可以通过TabContent的tabBar属性进行配置。在如下TabContent组件上设置属性tabBar,可以设置其对应页签中的内容,tabBar作为内容的页签。
```ts
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')
```
设置多个内容时,需在Tabs内按照顺序放置。
```ts
Tabs() {
TabContent() {
Text('首页的内容').fontSize(30)
}
.tabBar('首页')
TabContent() {
Text('推荐的内容').fontSize(30)
}
.tabBar('推荐')
TabContent() {
Text('发现的内容').fontSize(30)
}
.tabBar('发现')
TabContent() {
Text('我的内容').fontSize(30)
}
.tabBar("我的")
}
```
## 底部导航
底部导航是应用中最常见的一种导航方式。底部导航位于应用一级页面的底部,用户打开应用,能够分清整个应用的功能分类,以及页签对应的内容,并且其位于底部更加方便用户单手操作。底部导航一般作为应用的主导航形式存在,其作用是将用户关心的内容按照功能进行分类,迎合用户使用习惯,方便在不同模块间的内容切换。
**图3** 底部导航栏  
![底部导航](figures/底部导航.gif)
导航栏位置使用Tabs的参数barPosition进行设置,默认情况下,导航栏位于顶部,参数默认值为Start。设置为底部导航需要在Tabs传递参数,设置barPosition为End。
```ts
Tabs({ barPosition: BarPosition.End }) {
// TabContent的内容:首页、发现、推荐、我的
...
}
```
## 顶部导航
当内容分类较多,用户对不同内容的浏览概率相差不大,需要经常快速切换时,一般采用顶部导航模式进行设计,作为对底部导航内容的进一步划分,常见一些资讯类应用对内容的分类为关注、视频、数码,或者主题应用中对主题进行进一步划分为图片、视频、字体等。
**图4** 顶部导航栏  
![顶部导航](figures/顶部导航.gif)
Tabs组件默认的barPosition参数为Start,即顶部导航模式。
```ts
Tabs({ barPosition: BarPosition.Start }) {
// TabContent的内容:关注、视频、游戏、数码、科技、体育、影视
...
}
```
## 侧边导航
侧边导航是应用较为少见的一种导航模式,更多适用于横屏界面,用于对应用进行导航操作,由于用户的视觉习惯是从左到右,侧边导航栏默认为左侧侧边栏。
**图5** 侧边导航栏  
![侧边导航](figures/侧边导航.png)
实现侧边导航栏需要设置Tabs的属性vertical为true。在底部导航和顶部导航实现中,其默认值为false,表明内容页和导航栏垂直方向排列。
```ts
Tabs({ barPosition: BarPosition.Start }) {
// TabContent的内容:首页、发现、推荐、我的
...
}
.vertical(true)
.barWidth(100)
.barHeight(200)
```
>**说明:**
>
> - vertical为true时,tabbar宽度会默认撑满屏幕的宽度,需要设置barWidth为合适值。
>
> - vertical为true时,tabbar的高度会默认实际内容高度,需要设置barHeight为合适值。
## 限制导航栏的滑动切换
默认情况下,导航栏都支持滑动切换,在一些内容信息量需要进行多级分类的页面,如支持底部导航+顶部导航组合的情况下,底部导航栏的滑动效果与顶部导航出现冲突,此时需要限制底部导航的滑动,避免引起不好的用户体验。
**图6** 限制底部导航栏滑动  
![限制导航](figures/限制导航.gif)
控制滑动切换的属性为scrollable,默认值为true,表示可以滑动,若要限制滑动切换页签则需要设置为false。
```ts
Tabs({ barPosition: BarPosition.End }) {
TabContent(){
Column(){
Tabs(){
// 顶部导航栏内容
...
}
}
.backgroundColor('#ff08a8f1')
.width('100%')
}
.tabBar('首页')
// 其他TabContent内容:发现、推荐、我的
...
}
.scrollable(false)
```
## 固定导航栏
当内容分类较为固定且不具有拓展性时,例如底部导航内容分类一般固定,分类数量一般在3-5个,此时使用固定导航栏。固定导航栏不可滚动,无法被拖拽滚动,内容均分tabBar的宽度。
**图7** 固定导航栏 
![固定导航](figures/固定导航.gif)
Tabs的属性barMode是控制导航栏是否可以滚动,默认值为Fixed。
```ts
Tabs({ barPosition: BarPosition.End }) {
// TabContent的内容:首页、发现、推荐、我的
...
}
.barMode(BarMode.Fixed)
```
## 滚动导航栏
滚动导航栏可以用于顶部导航栏或者侧边导航栏的设置,内容分类较多,屏幕宽度无法容纳所有分类页签的情况下,需要使用可滚动的导航栏,支持用户点击和滑动来加载隐藏的页签内容。
**图8** 可滚动导航栏  
![滚动导航](figures/滚动导航.gif)
滚动导航栏需要设置Tabs组件的barMode属性,默认情况下其值为Fixed,表示为固定导航栏,设置为Scrollable即可设置为可滚动导航栏。
```ts
Tabs({ barPosition: BarPosition.Start }) {
// TabContent的内容:关注、视频、游戏、数码、科技、体育、影视、人文、艺术、自然、军事
...
}
.barMode(BarMode.Scrollable)
```
## 自定义导航栏
对于底部导航栏,一般作为应用主页面功能区分,为了更好的用户体验,会组合文字以及对应语义图标表示页签内容,这种情况下,需要自定义导航页签的样式。
**图9** 自定义导航栏图  
![custom-navigation-bar](figures/custom-navigation-bar.png)
系统默认情况下采用了下划线标志当前活跃的页签,而自定义导航栏需要自行实现相应的样式,用于区分当前活跃页签和未活跃页签。
设置自定义导航栏需要使用tabBar的参数,以其支持的CustomBuilder的方式传入自定义的函数组件样式。例如这里声明TabBuilder的自定义函数组件,传入参数包括页签文字title,对应位置index,以及选中状态和未选中状态的图片资源。通过当前活跃的currentIndex和页签对应的targetIndex匹配与否,决定UI显示的样式。
```ts
@Builder TabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
Column() {
Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
.size({ width: 25, height: 25 })
Text(title)
.fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
```
在TabContent对应tabBar属性中传入自定义函数组件,并传递相应的参数。
```ts
TabContent() {
Column(){
Text('我的内容')
}
.width('100%')
.height('100%')
.backgroundColor('#007DFF')
}
.tabBar(this.TabBuilder('我的', 0, $r('app.media.mine_selected'), $r('app.media.mine_normal')))
```
## 切换至指定页签
在不使用自定义导航栏时,系统默认的Tabs会实现切换逻辑。在使用了自定义导航栏后,切换页签的逻辑需要手动实现。即用户点击对应页签时,屏幕需要显示相应的内容页。
**图10** 使用自定义导航栏实现切换指定页签  
![切换指定页签](figures/切换指定页签.gif)
切换指定页签需要使用TabsController,TabsController是Tabs组件的控制器,用于控制Tabs组件进行页签切换。通过TabsController的changeIndex方法来实现跳转至指定索引值对应的TabContent内容。
```ts
private tabsController : TabsController = new TabsController()
@State currentIndex:number = 0;
@Builder TabBuilder(title: string, targetIndex: number) {
Column() {
Text(title)
.fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
}
...
.onClick(() => {
this.currentIndex = targetIndex;
this.tabsController.changeIndex(this.currentIndex);
})
}
```
使用自定义导航栏时,在tabBar属性中传入对应的\@Builder,并传入相应的参数。
```ts
Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
TabContent(){
...
}.tabBar(this.TabBuilder('首页',0))
TabContent(){
...
}.tabBar(this.TabBuilder('发现',1))
TabContent(){
...
}.tabBar(this.TabBuilder('推荐',2))
TabContent(){
...
}
.tabBar(this.TabBuilder('我的',3))
}
```
## 滑动切换导航栏
在不使用自定义导航栏的情况下,Tabs默认会实现tabBar与TabContent的切换联动。但在使用了自定义导航栏后,使用TabsController可以实现点击页签与页面内容的联动,但不能实现滑动页面时,页面内容对应页签的联动。即用户在使用滑动屏幕切换页面内容时,页签栏需要同步切换至内容对应的页签。
**图11** 滑动切换时页签内容不联动  
![最终效果11](figures/最终效果11.gif)
此时需要使用Tabs提供的onChange事件方法,监听索引index的变化,并将其当前活跃的index值传递给currentIndex,实现页签内容的切换。
```ts
Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
TabContent() {
...
}.tabBar(this.TabBuilder('首页', 0))
TabContent() {
...
}.tabBar(this.TabBuilder('发现', 1))
TabContent() {
...
}.tabBar(this.TabBuilder('推荐', 2))
TabContent() {
...
}
.tabBar(this.TabBuilder('我的', 3))
}.onChange((index) => {
this.currentIndex = index
})
```
**图12** 内容与页签联动 
![最终效果](figures/最终效果.gif)
## 相关实例
如需详细了解Tabs的更多实现,请参考以下示例:
- [健康生活](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/Healthy_life)
- [常用组件与布局](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/ArkTSComponents)
# 页面转场动画
两个页面间发生跳转,一个页面消失,另一个页面出现,这时可以配置各自页面的页面转场参数实现自定义的页面转场效果。[页面转场](../reference/arkui-ts/ts-page-transition-animation.md)效果写在pageTransition函数中,通过PageTransitionEnter和PageTransitionExit指定页面进入和退出的动画效果。
PageTransitionEnter的接口为:
```ts
PageTransitionEnter({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number})
```
PageTransitionExit的接口为:
```ts
PageTransitionExit({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number})
```
上述接口定义了PageTransitionEnter和PageTransitionExit组件,可通过slide、translate、scale、opacity属性定义不同的页面转场效果。对于PageTransitionEnter而言,这些效果表示入场时起点值,对于PageTransitionExit而言,这些效果表示退场的终点值,这一点与组件转场transition配置方法类似。此外,PageTransitionEnter提供了onEnter接口进行自定义页面入场动画的回调,PageTransitionExit提供了onExit接口进行自定义页面退场动画的回调。
上述接口中的参数type,表示路由生效的类型,这一点开发者容易混淆其含义。页面转场的两个页面,必定有一个页面退出,一个页面进入。如果通过router.pushUrl操作从页面A跳转到页面B,则页面A退出,做页面退场动画,页面B进入,做页面入场动画。如果通过router.back操作从页面B返回到页面A,则页面B退出,做页面退场动画,页面A进入,做页面入场动画。即页面的PageTransitionEnter既可能是由于新增页面(push,入栈)引起的新页面的入场动画,也可能是由于页面返回(back,或pop,出栈)引起的页面栈中老页面的入场动画,为了能区分这两种形式的入场动画,提供了type参数,这样开发者能完全定义所有类型的页面转场效果。
## type配置为RouteType.None
type为RouteType.None表示对页面栈的push、pop操作均生效,type的默认值为RouteType.None。
```ts
// page A
pageTransition() {
// 定义页面进入时的效果,从左侧滑入,时长为1200ms,无论页面栈发生push还是pop操作均可生效
PageTransitionEnter({ type: RouteType.None, duration: 1200 })
.slide(SlideEffect.Left)
// 定义页面退出时的效果,向左侧滑出,时长为1000ms,无论页面栈发生push还是pop操作均可生效
PageTransitionExit({ type: RouteType.None, duration: 1000 })
.slide(SlideEffect.Left)
}
```
```ts
// page B
pageTransition() {
// 定义页面进入时的效果,从右侧滑入,时长为1000ms,无论页面栈发生push还是pop操作均可生效
PageTransitionEnter({ type: RouteType.None, duration: 1000 })
.slide(SlideEffect.Right)
// 定义页面退出时的效果,向右侧滑出,时长为1200ms,无论页面栈发生push还是pop操作均可生效
PageTransitionExit({ type: RouteType.None, duration: 1200 })
.slide(SlideEffect.Right)
}
```
假设页面栈为标准实例模式,即页面栈中允许存在重复的页面。可能会有4种场景,对应的页面转场效果如下表。
| 路由操作 | 页面A转场效果 | 页面B转场效果 |
| ---------------------------- | ---------------------------------- | ---------------------------------- |
| router.pushUrl,从页面A跳转到新增的页面B | 页面退出,PageTransitionExit生效,向左侧滑出屏幕 | 页面进入,PageTransitionEnter生效,从右侧滑入屏幕 |
| router.back,从页面B返回到页面A | 页面进入,PageTransitionEnter生效,从左侧滑入屏幕 | 页面退出,PageTransitionExit生效,向右侧滑出屏幕 |
| router.pushUrl,从页面B跳转到新增的页面A | 页面进入,PageTransitionEnter生效,从左侧滑入屏幕 | 页面退出,PageTransitionExit生效,向右侧滑出屏幕 |
| router.back,从页面A返回到页面B | 页面退出,PageTransitionExit生效,向左侧滑出屏幕 | 页面进入,PageTransitionEnter生效,从右侧滑入屏幕 |
如果希望pushUrl进入的页面总是从右侧滑入,back时退出的页面总是从右侧滑出,则上表中的第3、4种情况不满足要求,那么需要完整的定义4个页面转场效果。
## type配置为RouteType.Push或RouteType.Pop
type为RouteType.Push表示仅对页面栈的push操作生效,type为RouteType.Pop表示仅对页面栈的pop操作生效。
```ts
// page A
pageTransition() {
// 定义页面进入时的效果,从右侧滑入,时长为1200ms,页面栈发生push操作时该效果才生效
PageTransitionEnter({ type: RouteType.Push, duration: 1200 })
.slide(SlideEffect.Right)
// 定义页面进入时的效果,从左侧滑入,时长为1200ms,页面栈发生pop操作时该效果才生效
PageTransitionEnter({ type: RouteType.Pop, duration: 1200 })
.slide(SlideEffect.Left)
// 定义页面退出时的效果,向左侧滑出,时长为1000ms,页面栈发生push操作时该效果才生效
PageTransitionExit({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Left)
// 定义页面退出时的效果,向右侧滑出,时长为1000ms,页面栈发生pop操作时该效果才生效
PageTransitionExit({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Right)
}
```
```ts
// page B
pageTransition() {
// 定义页面进入时的效果,从右侧滑入,时长为1000ms,页面栈发生push操作时该效果才生效
PageTransitionEnter({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Right)
// 定义页面进入时的效果,从左侧滑入,时长为1000ms,页面栈发生pop操作时该效果才生效
PageTransitionEnter({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Left)
// 定义页面退出时的效果,向左侧滑出,时长为1200ms,页面栈发生push操作时该效果才生效
PageTransitionExit({ type: RouteType.Push, duration: 1200 })
.slide(SlideEffect.Left)
// 定义页面退出时的效果,向右侧滑出,时长为1200ms,页面栈发生pop操作时该效果才生效
PageTransitionExit({ type: RouteType.Pop, duration: 1200 })
.slide(SlideEffect.Right)
}
```
以上代码则完整的定义了所有可能的页面转场样式。假设页面栈为标准实例模式,即页面栈中允许存在重复的页面。可能会有4种场景,对应的页面转场效果如下表。
| 路由操作 | 页面A转场效果 | 页面B转场效果 |
| ---------------------------- | ---------------------------------------- | ---------------------------------------- |
| router.pushUrl,从页面A跳转到新增的页面B | 页面退出,PageTransitionExit且type为RouteType.Push的转场样式生效,向左侧滑出屏幕 | 页面进入,PageTransitionEnter且type为RouteType.Push的转场样式生效,从右侧滑入屏幕 |
| router.back,从页面B返回到页面A | 页面进入,PageTransitionEnter且type为RouteType.Pop的转场样式生效,从左侧滑入屏幕 | 页面退出,PageTransitionExit且type为RouteType.Pop的转场样式生效,向右侧滑出屏幕 |
| router.pushUrl,从页面B跳转到新增的页面A | 页面进入,PageTransitionEnter且type为RouteType.Push的转场样式生效,从右侧滑入屏幕 | 页面退出,PageTransitionExit且type为RouteType.Push的转场样式生效,向左侧滑出屏幕 |
| router.back,从页面A返回到页面B | 页面退出,PageTransitionExit且type为RouteType.Pop的转场样式生效,向右侧滑出屏幕 | 页面进入,PageTransitionEnter且type为RouteType.Pop的转场样式生效,从左侧滑入屏幕 |
>**说明:**
>
> 1. 由于每个页面的页面转场样式都可由开发者独立配置,而页面转场涉及到两个页面,开发者应考虑两个页面的页面转场效果的衔接,如时长尽量保持一致。
>
> 2. 如果没有定义匹配的页面转场样式,则该页面使用系统默认的页面转场样式。
## 禁用某页面的页面转场
```ts
pageTransition() {
PageTransitionEnter({ type: RouteType.None, duration: 0 })
PageTransitionExit({ type: RouteType.None, duration: 0 })
}
```
通过设置页面转场的时长为0,可使该页面无页面转场动画。
## 场景示例
下面介绍定义了所有的四种页面转场样式的页面转场动画示例。
```ts
// page A
import router from '@ohos.router';
@Entry
@Component
struct PageTransitionSrc1 {
build() {
Column() {
Image($r('app.media.mountain'))
.width('90%')
.height('80%')
.objectFit(ImageFit.Fill)
.syncLoad(true) // 同步加载图片,使页面出现时图片已经加载完成
.margin(30)
Row({ space: 10 }) {
Button("pushUrl")
.onClick(() => {
// 路由到下一个页面,push操作
router.pushUrl({ url: 'pages/myTest/pageTransitionDst1' });
})
Button("back")
.onClick(() => {
// 返回到上一页面,相当于pop操作
router.back();
})
}.justifyContent(FlexAlign.Center)
}
.width("100%").height("100%")
.alignItems(HorizontalAlign.Center)
}
pageTransition() {
// 定义页面进入时的效果,从右侧滑入,时长为1000ms,页面栈发生push操作时该效果才生效
PageTransitionEnter({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Right)
// 定义页面进入时的效果,从左侧滑入,时长为1000ms,页面栈发生pop操作时该效果才生效
PageTransitionEnter({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Left)
// 定义页面退出时的效果,向左侧滑出,时长为1000ms,页面栈发生push操作时该效果才生效
PageTransitionExit({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Left)
// 定义页面退出时的效果,向右侧滑出,时长为1000ms,页面栈发生pop操作时该效果才生效
PageTransitionExit({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Right)
}
}
```
```ts
// page B
import router from '@ohos.router';
@Entry
@Component
struct PageTransitionDst1 {
build() {
Column() {
Image($r('app.media.forest'))
.width('90%')
.height('80%')
.objectFit(ImageFit.Fill)
.syncLoad(true) // 同步加载图片,使页面出现时图片已经加载完成
.margin(30)
Row({ space: 10 }) {
Button("pushUrl")
.onClick(() => {
// 路由到下一页面,push操作
router.pushUrl({ url: 'pages/myTest/pageTransitionSrc1' });
})
Button("back")
.onClick(() => {
// 返回到上一页面,相当于pop操作
router.back();
})
}.justifyContent(FlexAlign.Center)
}
.width("100%").height("100%")
.alignItems(HorizontalAlign.Center)
}
pageTransition() {
// 定义页面进入时的效果,从右侧滑入,时长为1000ms,页面栈发生push操作时该效果才生效
PageTransitionEnter({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Right)
// 定义页面进入时的效果,从左侧滑入,时长为1000ms,页面栈发生pop操作时该效果才生效
PageTransitionEnter({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Left)
// 定义页面退出时的效果,向左侧滑出,时长为1000ms,页面栈发生push操作时该效果才生效
PageTransitionExit({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Left)
// 定义页面退出时的效果,向右侧滑出,时长为1000ms,页面栈发生pop操作时该效果才生效
PageTransitionExit({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Right)
}
}
```
![pageTransition_PushPop](figures/pageTransition_PushPop.gif)
下面介绍使用了type为None的页面转场动画示例。
```ts
// page A
import router from '@ohos.router';
@Entry
@Component
struct PageTransitionSrc2 {
build() {
Column() {
Image($r('app.media.mountain'))
.width('90%')
.height('80%')
.objectFit(ImageFit.Fill)
.syncLoad(true) // 同步加载图片,使页面出现时图片已经加载完成
.margin(30)
Row({ space: 10 }) {
Button("pushUrl")
.onClick(() => {
// 路由到下一页面,push操作
router.pushUrl({ url: 'pages/myTest/pageTransitionDst2' });
})
Button("back")
.onClick(() => {
// 返回到上一页面,相当于pop操作
router.back();
})
}.justifyContent(FlexAlign.Center)
}
.width("100%").height("100%")
.alignItems(HorizontalAlign.Center)
}
pageTransition() {
// 定义页面进入时的效果,从左侧滑入,时长为1000ms,无论页面栈发生push还是pop操作均可生效
PageTransitionEnter({ duration: 1000 })
.slide(SlideEffect.Left)
// 定义页面退出时的效果,相对于正常页面位置x方向平移100vp,y方向平移100vp,透明度变为0,时长为1200ms,无论页面栈发生push还是pop操作均可生效
PageTransitionExit({ duration: 1200 })
.translate({ x: 100.0, y: 100.0 })
.opacity(0)
}
}
```
```ts
// page B
import router from '@ohos.router';
@Entry
@Component
struct PageTransitionDst2 {
build() {
Column() {
Image($r('app.media.forest'))
.width('90%')
.height('80%')
.objectFit(ImageFit.Fill)
.syncLoad(true) // 同步加载图片,使页面出现时图片已经加载完成
.margin(30)
Row({ space: 10 }) {
Button("pushUrl")
.onClick(() => {
// 路由到下一页面,push操作
router.pushUrl({ url: 'pages/myTest/pageTransitionSrc2' });
})
Button("back")
.onClick(() => {
// 返回到上一页面,相当于pop操作
router.back();
})
}.justifyContent(FlexAlign.Center)
}
.width("100%").height("100%")
.alignItems(HorizontalAlign.Center)
}
pageTransition() {
// 定义页面进入时的效果,从左侧滑入,时长为1200ms,无论页面栈发生push还是pop操作均可生效
PageTransitionEnter({ duration: 1200 })
.slide(SlideEffect.Left)
// 定义页面退出时的效果,相对于正常页面位置x方向平移100vp,y方向平移100vp,透明度变为0,时长为1000ms,无论页面栈发生push还是pop操作均可生效
PageTransitionExit({ duration: 1000 })
.translate({ x: 100.0, y: 100.0 })
.opacity(0)
}
}
```
![pageTransition_None](figures/pageTransition_None.gif)
# 菜单
Menu是菜单接口,一般用于鼠标右键弹窗、点击弹窗等。具体用法请参考[Menu控制](../reference/arkui-ts/ts-universal-attributes-menu.md)
## 创建默认样式的菜单
菜单需要调用bindMenu接口来实现。bindMenu响应绑定组件的点击事件,绑定组件后手势点击对应组件后即可弹出。
```ts
Button('click for Menu')
.bindMenu([
{
value: 'Menu1',
action: () => {
console.info('handle Menu1 select')
}
}
])
```
![zh-cn_image_0000001562940565](figures/zh-cn_image_0000001562940565.png)
## 创建自定义样式的菜单
当默认样式不满足开发需求时,可使用\@CustomBuilder自定义菜单内容。可通过bindContextMenu接口进行菜单的自定义。
### \@Builder开发菜单内的内容
```ts
@State select: boolean = true
private iconStr: ResourceStr = $r("app.media.view_list_filled")
private iconStr2: ResourceStr = $r("app.media.view_list_filled")
@Builder
SubMenu() {
Menu() {
MenuItem({ content: "复制", labelInfo: "Ctrl+C" })
MenuItem({ content: "粘贴", labelInfo: "Ctrl+V" })
}
}
@Builder
MyMenu(){
Menu() {
MenuItem({ startIcon: $r("app.media.icon"), content: "菜单选项" })
MenuItem({ startIcon: $r("app.media.icon"), content: "菜单选项" }).enabled(false)
MenuItem({
startIcon: this.iconStr,
content: "菜单选项",
endIcon: $r("app.media.arrow_right_filled"),
// 当builder参数进行配置时,表示与menuItem项绑定了子菜单。鼠标hover在该菜单项时,会显示子菜单。
builder: this.SubMenu.bind(this),
})
MenuItemGroup({ header: '小标题' }) {
MenuItem({ content: "菜单选项" })
.selectIcon(true)
.selected(this.select)
.onChange((selected) => {
console.info("menuItem select" + selected);
this.iconStr2 = $r("app.media.icon");
})
MenuItem({
startIcon: $r("app.media.view_list_filled"),
content: "菜单选项",
endIcon: $r("app.media.arrow_right_filled"),
builder: this.SubMenu.bind(this)\
})
}
MenuItem({
startIcon: this.iconStr2,
content: "菜单选项",
endIcon: $r("app.media.arrow_right_filled")
})
}
}
```
### bindMenu属性绑定组件
```ts
Button('click for Menu')
.bindMenu(this.MyMenu)
```
![zh-cn_image_0000001511580924](figures/zh-cn_image_0000001511580924.png)
## 创建支持右键或长按的菜单
通过bindContextMenu接口进行菜单的自定义及菜单弹出的触发方式:右键或长按。使用bindContextMenu弹出的菜单项是在独立子窗口内的,可显示在应用窗口外部。
- [@Builder开发菜单内的内容](#builder开发菜单内的内容)与上文写法相同。
- 确认菜单的弹出方式,使用bindContextMenu属性绑定组件。示例中为右键弹出菜单。
```ts
Button('click for Menu')
.bindContextMenu(this.MyMenu, ResponseType.RightClick)
```
# 气泡提示
Popup属性可绑定在组件上显示气泡弹窗提示,设置弹窗内容、交互逻辑和显示状态。主要用于屏幕录制、信息弹出提醒等显示状态。
气泡分为两种类型,一种是系统提供的气泡[PopupOptions](../reference/arkui-ts/ts-universal-attributes-popup.md#popupoptions%E7%B1%BB%E5%9E%8B%E8%AF%B4%E6%98%8E),一种是开发者可以自定义的气泡[CustomPopupOptions](../reference/arkui-ts/ts-universal-attributes-popup.md#custompopupoptions8%E7%B1%BB%E5%9E%8B%E8%AF%B4%E6%98%8E)。其中PopupOptions为系统提供的气泡,通过配置primaryButton、secondaryButton来设置带按钮的气泡。CustomPopupOptions通过配置[builder](../quick-start/arkts-builder.md)参数来设置自定义的气泡。
## 文本提示气泡
文本提示气泡常用于只展示带有文本的信息提示,不带有任何交互的场景。Popup属性需绑定组件,当bindPopup属性中参数show为true的时候会弹出气泡提示。
在Button组件上绑定Popup属性,每次点击Button按钮,handlePopup会切换布尔值,当其为true时,触发bindPopup弹出气泡。
```ts
@Entry
@Component
struct PopupExample {
@State handlePopup: boolean = false
build() {
Column() {
Button('PopupOptions')
.onClick(() => {
this.handlePopup = !this.handlePopup
})
.bindPopup(this.handlePopup, {
message: 'This is a popup with PopupOptions',
})
}.width('100%').padding({ top: 5 })
}
}
```
![zh-cn_image_0000001511740524](figures/zh-cn_image_0000001511740524.png)
## 带按钮的提示气泡
通过primaryButton、secondaryButton属性为气泡最多设置两个Button按钮,通过此按钮进行简单的交互;开发者可以通过配置action参数来设置想要触发的操作。
```ts
@Entry
@Component
struct PopupExample22 {
@State handlePopup: boolean = false
build() {
Column() {
Button('PopupOptions').margin({top:200})
.onClick(() => {
this.handlePopup = !this.handlePopup
})
.bindPopup(this.handlePopup, {
message: 'This is a popup with PopupOptions',
primaryButton:{
value:'Confirm',
action: () => {
this.handlePopup = !this.handlePopup
console.info('confirm Button click')
}
},
secondaryButton: {
value: 'Cancel',
action: () => {
this.handlePopup = !this.handlePopup
}
},
})
}.width('100%').padding({ top: 5 })
}
}
```
![zh-cn_other_0000001500740342](figures/zh-cn_other_0000001500740342.jpeg)
## 自定义气泡
开发者可以使用构建器CustomPopupOptions创建自定义气泡,\@Builder中可以放自定义的内容。除此之外,还可以通过popupColor等参数控制气泡样式。
```ts
@Entry
@Component
struct Index {
@State customPopup: boolean = false
// popup构造器定义弹框内容
@Builder popupBuilder() {
Row({ space: 2 }) {
Image($r("app.media.icon")).width(24).height(24).margin({ left: 5 })
Text('This is Custom Popup').fontSize(15)
}.width(200).height(50).padding(5)
}
build() {
Column() {
Button('CustomPopupOptions')
.position({x:100,y:200})
.onClick(() => {
this.customPopup = !this.customPopup
})
.bindPopup(this.customPopup, {
builder: this.popupBuilder, // 气泡的内容
placement:Placement.Bottom, // 气泡的弹出位置
popupColor:Color.Pink // 气泡的背景色
})
}
.height('100%')
}
}
```
使用者通过配置placement参数将弹出的气泡放到需要提示的位置。弹窗构造器会触发弹出提示信息,来引导使用者完成操作,也让使用者有更好的UI体验。
![zh-cn_other_0000001500900234](figures/zh-cn_other_0000001500900234.jpeg)
```ts
@Entry
@Component
struct Index {
@State customPopup: boolean = false
// popup构造器定义弹框内容
@Builder popupBuilder() {
Row({ space: 2 }) {
Image('/images/shengWhite.png').width(30).objectFit(ImageFit.Contain)
Column(){
Text('控制人生').fontSize(14).fontWeight(900).fontColor(Color.White).width('100%')
Text('想要跟唱时,数千万歌曲任你选择,人声随心调整。').fontSize(12).fontColor('#ffeeeeee').width('100%')
}
}.width(230).height(80).padding(5)
}
build() {
Row() {
Text('我要K歌')
Image('/images/sheng.png').width(35).objectFit(ImageFit.Contain)
.onClick(() => {
this.customPopup = !this.customPopup
})
.bindPopup(this.customPopup, {
builder: this.popupBuilder,
})
}
.margin(20)
.height('100%')
}
}
```
# 页面路由
页面路由指在应用程序中实现不同页面之间的跳转和数据传递。OpenHarmony提供了Router模块,通过不同的url地址,可以方便地进行页面路由,轻松地访问不同的页面。本文将从[页面跳转](#页面跳转)[页面返回](#页面返回)[页面返回前增加一个询问框](#页面返回前增加一个询问框)几个方面介绍Router模块提供的功能。
## 页面跳转
页面跳转是开发过程中的一个重要组成部分。在使用应用程序时,通常需要在不同的页面之间跳转,有时还需要将数据从一个页面传递到另一个页面。
**图1** 页面跳转
![router-jump-to-detail](figures/router-jump-to-detail.gif)
Router模块提供了两种跳转模式,分别是[router.pushUrl()](../reference/apis/js-apis-router.md#routerpushurl9)[router.replaceUrl()](../reference/apis/js-apis-router.md#routerreplaceurl9)。这两种模式决定了目标页是否会替换当前页。
- router.pushUrl():目标页不会替换当前页,而是压入[页面栈](../application-models/page-mission-stack.md)。这样可以保留当前页的状态,并且可以通过返回键或者调用[router.back()](../reference/apis/js-apis-router.md#routerback)方法返回到当前页。
- router.replaceUrl():目标页会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页。
>**说明:**
>
>页面栈的最大容量为32个页面。如果超过这个限制,可以调用[router.clear()](../reference/apis/js-apis-router.md#routerclear)方法清空历史页面栈,释放内存空间。
同时,Router模块提供了两种实例模式,分别是Standard和Single。这两种模式决定了目标url是否会对应多个实例。
- Standard:标准实例模式,也是默认情况下的实例模式。每次调用该方法都会新建一个目标页,并压入栈顶。
- Single:单实例模式。即如果目标页的url在页面栈中已经存在同url页面,则离栈顶最近的同url页面会被移动到栈顶,并重新加载;如果目标页的url在页面栈中不存在同url页面,则按照标准模式跳转。
在使用页面路由Router相关功能之前,需要在代码中先导入Router模块。
```ts
import router from '@ohos.router';
```
- 场景一:有一个主页(Home)和一个详情页(Detail),希望从主页点击一个商品,跳转到详情页。同时,需要保留主页在页面栈中,以便返回时恢复状态。这种场景下,可以使用pushUrl()方法,并且使用Standard实例模式(或者省略)。
```ts
// 在Home页面中
function onJumpClick(): void {
router.pushUrl({
url: 'pages/Detail' // 目标url
}, router.RouterMode.Standard, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke pushUrl succeeded.');
});
}
```
>**说明:**
>
>标准实例模式下,router.RouterMode.Standard参数可以省略。
- 场景二:有一个登录页(Login)和一个个人中心页(Profile),希望从登录页成功登录后,跳转到个人中心页。同时,销毁登录页,在返回时直接退出应用。这种场景下,可以使用replaceUrl()方法,并且使用Standard实例模式(或者省略)。
```ts
// 在Login页面中
function onJumpClick(): void {
router.replaceUrl({
url: 'pages/Profile' // 目标url
}, router.RouterMode.Standard, (err) => {
if (err) {
console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke replaceUrl succeeded.');
})
}
```
>**说明:**
>
>标准实例模式下,router.RouterMode.Standard参数可以省略。
- 场景三:有一个设置页(Setting)和一个主题切换页(Theme),希望从设置页点击主题选项,跳转到主题切换页。同时,需要保证每次只有一个主题切换页存在于页面栈中,在返回时直接回到设置页。这种场景下,可以使用pushUrl()方法,并且使用Single实例模式。
```ts
// 在Setting页面中
function onJumpClick(): void {
router.pushUrl({
url: 'pages/Theme' // 目标url
}, router.RouterMode.Single, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke pushUrl succeeded.');
});
}
```
- 场景四:有一个搜索结果列表页(SearchResult)和一个搜索结果详情页(SearchDetail),希望从搜索结果列表页点击某一项结果,跳转到搜索结果详情页。同时,如果该结果已经被查看过,则不需要再新建一个详情页,而是直接跳转到已经存在的详情页。这种场景下,可以使用replaceUrl()方法,并且使用Single实例模式。
```ts
// 在SearchResult页面中
function onJumpClick(): void {
router.replaceUrl({
url: 'pages/SearchDetail' // 目标url
}, router.RouterMode.Single, (err) => {
if (err) {
console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke replaceUrl succeeded.');})
}
```
以上是不带参数传递的场景。
如果需要在跳转时传递一些数据给目标页,则可以在调用Router模块的方法时,添加一个params属性,并指定一个对象作为参数。例如:
```ts
class DataModelInfo {
age: number;
}
class DataModel {
id: number;
info: DataModelInfo;
}
function onJumpClick(): void {
// 在Home页面中
let paramsInfo: DataModel = {
id: 123,
info: {
age: 20
}
};
router.pushUrl({
url: 'pages/Detail', // 目标url
params: paramsInfo // 添加params属性,传递自定义参数
}, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke pushUrl succeeded.');
})
}
```
在目标页中,可以通过调用Router模块的[getParams()](../reference/apis/js-apis-router.md#routergetparams)方法来获取传递过来的参数。例如:
```ts
const params = router.getParams(); // 获取传递过来的参数对象
const id = params['id']; // 获取id属性的值
const age = params['info'].age; // 获取age属性的值
```
## 页面返回
当用户在一个页面完成操作后,通常需要返回到上一个页面或者指定页面,这就需要用到页面返回功能。在返回的过程中,可能需要将数据传递给目标页,这就需要用到数据传递功能。
**图2** 页面返回  
![router-back-to-home](figures/router-back-to-home.gif)
在使用页面路由Router相关功能之前,需要在代码中先导入Router模块。
```ts
import router from '@ohos.router';
```
可以使用以下几种方式进行页面返回:
- 方式一:返回到上一个页面。
```ts
router.back();
```
这种方式会返回到上一个页面,即上一个页面在页面栈中的位置。但是,上一个页面必须存在于页面栈中才能够返回,否则该方法将无效。
- 方式二:返回到指定页面。
```ts
router.back({
url: 'pages/Home'
});
```
这种方式可以返回到指定页面,需要指定目标页的路径。目标页必须存在于页面栈中才能够返回。
- 方式三:返回到指定页面,并传递自定义参数信息。
```ts
router.back({
url: 'pages/Home',
params: {
info: '来自Home页'
}
});
```
这种方式不仅可以返回到指定页面,还可以在返回的同时传递自定义参数信息。这些参数信息可以在目标页中通过调用router.getParams()方法进行获取和解析。
在目标页中,在需要获取参数的位置调用router.getParams()方法即可,例如在onPageShow()生命周期回调中:
```ts
onPageShow() {
const params = router.getParams(); // 获取传递过来的参数对象
const info = params['info']; // 获取info属性的值
}
```
>**说明:**
>
>当使用router.back()方法返回到指定页面时,该页面会被重新压入栈顶,而原栈顶页面(包括)到指定页面(不包括)之间的所有页面栈都将被销毁。
>
> 另外,如果使用router.back()方法返回到原来的页面,原页面不会被重复创建,因此使用\@State声明的变量不会重复声明,也不会触发页面的aboutToAppear()生命周期回调。如果需要在原页面中使用返回页面传递的自定义参数,可以在需要的位置进行参数解析。例如,在onPageShow()生命周期回调中进行参数解析。
## 页面返回前增加一个询问框
在开发应用时,为了避免用户误操作或者丢失数据,有时候需要在用户从一个页面返回到另一个页面之前,弹出一个询问框,让用户确认是否要执行这个操作。
本文将从[系统默认询问框](#系统默认询问框)[自定义询问框](#自定义询问框)两个方面来介绍如何实现页面返回前增加一个询问框的功能。
**图3** 页面返回前增加一个询问框  
![router-add-query-box-before-back](figures/router-add-query-box-before-back.gif)
### 系统默认询问框
为了实现这个功能,可以使用页面路由Router模块提供的两个方法:[router.showAlertBeforeBackPage()](../reference/apis/js-apis-router.md#routershowalertbeforebackpage9)[router.back()](../reference/apis/js-apis-router.md#routerback)来实现这个功能。
在使用页面路由Router相关功能之前,需要在代码中先导入Router模块。
```ts
import router from '@ohos.router';
```
如果想要在目标界面开启页面返回询问框,需要在调用[router.back()](../reference/apis/js-apis-router.md#routerback)方法之前,通过调用[router.showAlertBeforeBackPage()](../reference/apis/js-apis-router.md#routershowalertbeforebackpage9)方法设置返回询问框的信息。例如,在支付页面中定义一个返回按钮的点击事件处理函数:
```ts
// 定义一个返回按钮的点击事件处理函数
function onBackClick(): void {
// 调用router.showAlertBeforeBackPage()方法,设置返回询问框的信息
try {
router.showAlertBeforeBackPage({
message: '您还没有完成支付,确定要返回吗?' // 设置询问框的内容
});
} catch (err) {
console.error(`Invoke showAlertBeforeBackPage failed, code is ${err.code}, message is ${err.message}`);
}
// 调用router.back()方法,返回上一个页面
router.back();
}
```
其中,router.showAlertBeforeBackPage()方法接收一个对象作为参数,该对象包含以下属性:
- message:string类型,表示询问框的内容。
如果调用成功,则会在目标界面开启页面返回询问框;如果调用失败,则会抛出异常,并通过err.code和err.message获取错误码和错误信息。
当用户点击“返回”按钮时,会弹出确认对话框,询问用户是否确认返回。选择“取消”将停留在当前页目标页;选择“确认”将触发router.back()方法,并根据参数决定如何执行跳转。
### 自定义询问框
自定义询问框的方式,可以使用[弹窗](../reference/apis/js-apis-promptAction.md#promptactionshowdialog)或者自定义弹窗实现。这样可以让应用界面与系统默认询问框有所区别,提高应用的用户体验度。本文以弹窗为例,介绍如何实现自定义询问框。
在使用页面路由Router相关功能之前,需要在代码中先导入Router模块。
```ts
import router from '@ohos.router';
```
在事件回调中,调用弹窗的[promptAction.showDialog()](../reference/apis/js-apis-promptAction.md#promptactionshowdialog)方法:
```ts
function onBackClick() {
// 弹出自定义的询问框
promptAction.showDialog({
message: '您还没有完成支付,确定要返回吗?',
buttons: [
{
text: '取消',
color: '#FF0000'
},
{
text: '确认',
color: '#0099FF'
}
]
}).then((result) => {
if (result.index === 0) {
// 用户点击了“取消”按钮
console.info('User canceled the operation.');
} else if (result.index === 1) {
// 用户点击了“确认”按钮
console.info('User confirmed the operation.');
// 调用router.back()方法,返回上一个页面
router.back();
}
}).catch((err) => {
console.error(`Invoke showDialog failed, code is ${err.code}, message is ${err.message}`);
})
}
```
当用户点击“返回”按钮时,会弹出自定义的询问框,询问用户是否确认返回。选择“取消”将停留在当前页目标页;选择“确认”将触发router.back()方法,并根据参数决定如何执行跳转。
# 弹簧曲线动画
ArkUI提供了[预置动画曲线](../reference/arkui-ts/ts-appendix-enums.md#curve),指定了动画属性从起始值到终止值的变化规律,如Linear、Ease、EaseIn等。同时,ArkUI也提供了由弹簧振子物理模型产生的弹簧曲线。通过弹簧曲线,开发者可以设置超过设置的终止值,在终止值附近震荡,直至最终停下来的效果。弹簧曲线的动画效果比其他曲线具有更强的互动性、可玩性。
弹簧曲线的接口包括两类,一类是[springCurve](../reference/apis/js-apis-curve.md#curvesspringcurve9),另一类是[springMotion](../reference/apis/js-apis-curve.md#curvesspringmotion9)[responsiveSpringMotion](../reference/apis/js-apis-curve.md#curvesresponsivespringmotion9),这两种方式都可以产生弹簧曲线。
## 使用springCurve
springCurve的接口为:
```ts
springCurve(velocity: number, mass: number, stiffness: number, damping: number)
```
构造参数包括初速度,弹簧系统的质量、刚度、阻尼。构建springCurve时,可指定质量为1,根据springCurve中的参数说明,调节刚度、阻尼两个参数,达到想要的震荡效果。
```ts
import curves from '@ohos.curves';
@Entry
@Component
struct SpringTest {
@State translateX: number = 0;
private jumpWithSpeed(speed: number) {
this.translateX = -1;
animateTo({ duration: 2000, curve: curves.springCurve(speed, 1, 1, 1.2) }, () => {
// 以指定初速度进行x方向的平移的弹簧动画
this.translateX = 0;
})
}
build() {
Column() {
Button("button")
.fontSize(14)
.width(100)
.height(50)
.margin(30)
.translate({ x: this.translateX })
Row({space:50}) {
Button("jump 50").fontSize(14)
.onClick(() => {
// 以初速度50的弹簧曲线进行平移
this.jumpWithSpeed(50);
})
Button("jump 200").fontSize(14)
.onClick(() => {
// 以初速度200的弹簧曲线进行平移
this.jumpWithSpeed(200);
})
}.margin(30)
}.height('100%').width('100%')
}
}
```
![springCurve](figures/springCurve.gif)
以上示例中,点击不同的按钮,给定springCurve的不同初速度,button会有“弹性”的到达指定位置,且button的振幅随着速度的增大而变大。另外也可以修改springCurve的质量、刚度、阻尼参数,达到想要的弹性的程度。
>**说明:**
>
>速度只是放大了振荡的效果,但系统能否产生振荡的效果,取决于弹簧振子本身的物理参数,即质量、刚度、阻尼三个参数。刚度越小、阻尼越大,springCurve的“弹性”越弱,振荡效果越弱。随着刚度减小或阻尼变大,达到过阻尼状态后,无论速度为多大,都不会有在终点值附近振荡的效果。
## 使用springMotion和responsiveSpringMotion
[springMotion](../reference/apis/js-apis-curve.md#curvesspringmotion9)的接口为:
```ts
springMotion(response?: number, dampingFraction?: number, overlapDuration?: number)
```
[responsiveSpringMotion](../reference/apis/js-apis-curve.md#curvesresponsivespringmotion9)的接口为:
```ts
responsiveSpringMotion(response?: number, dampingFraction?: number, overlapDuration?: number)
```
它们的构造参数包括弹簧自然振动周期、阻尼系数、弹性动画衔接时长这三个可选参数,参数的含义请参考其文档。
使用springMotion和responsiveSpringMotion曲线时,duration不生效,适合于跟手动画。
```ts
import curves from '@ohos.curves';
@Entry
@Component
struct SpringMotionTest {
@State positionX: number = 100;
@State positionY: number = 100;
diameter: number = 50;
build() {
Column() {
Row() {
Circle({ width: this.diameter, height: this.diameter })
.fill(Color.Blue)
.position({ x: this.positionX, y: this.positionY })
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Move) {
// 跟手过程,使用responsiveSpringMotion曲线
animateTo({ curve: curves.responsiveSpringMotion() }, () => {
// 减去半径,以使球的中心运动到手指位置
this.positionX = event.touches[0].screenX - this.diameter / 2;
this.positionY = event.touches[0].screenY - this.diameter / 2;
console.info(`move, animateTo x:${this.positionX}, y:${this.positionY}`);
})
} else if (event.type === TouchType.Up) {
// 离手时,使用springMotion曲线
animateTo({ curve: curves.springMotion() }, () => {
this.positionX = 100;
this.positionY = 100;
console.info(`touchUp, animateTo x:100, y:100`);
})
}
})
}
.width("100%").height("80%")
.clip(true) // 如果球超出父组件范围,使球不可见
.backgroundColor(Color.Orange)
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Start, justifyContent: FlexAlign.Center }) {
Text("拖动小球").fontSize(16)
}
.width("100%")
Row() {
Text('点击位置: [x: ' + Math.round(this.positionX) + ', y:' + Math.round(this.positionY) + ']').fontSize(16)
}
.padding(10)
.width("100%")
}.height('100%').width('100%')
}
}
```
以上代码是跟手动画的一个示例。通过在onTouch事件中,捕捉触摸的位置,改变组件的translate或者position属性,使其在跟手过程中运动到触摸位置,松手后回到原位置。跟手动画的效果如下:
![springMotion](figures/springMotion.gif)
跟手过程推荐使用responsiveSpringMotion曲线,松手过程推荐使用springMotion曲线。跟手过程随着手的位置变化会被多次触发,所以会接连启动多次responsiveSpringMotion动画,松手时启动一次springMotion动画。跟手、松手过程在对同一对象的同一属性上执行动画,且使用了springMotion或responsiveSpringMotion曲线,每次新启动的动画会继承上次动画使用的速度,实现平滑过渡。
>**说明:**
>
> 1. springCurve可以设置初速度,单一属性存在多个动画时不会互相影响,观察到的是多个动画效果的叠加。
>
> 2. springMotion虽然内部有速度机制,但不可由开发者设置。在单一属性存在多个动画时,后一动画会取代前一动画,并继承前一动画的速度。
# 组件内转场动画
组件的插入、删除过程即为组件本身的转场过程,组件的插入、删除动画称为组件内转场动画。通过组件内转场动画,可定义组件出现、消失的效果。
组件内转场动画的接口为:
```ts
transition(value: TransitionOptions)
```
[transition](../reference/arkui-ts/ts-transition-animation-component.md)函数的入参为组件内转场的效果,可以定义平移、透明度、旋转、缩放这几种转场样式的单个或者组合的转场效果,必须和[animateTo](arkts-layout-update-animation.md#使用显式动画产生布局更新动画)一起使用才能产生组件转场效果。
## transition常见用法
type用于指定当前的transition动效生效在组件的变化场景,类型为[TransitionType](../reference/arkui-ts/ts-appendix-enums.md#transitiontype)
- 组件的插入、删除使用同一个动画效果
```ts
Button()
.transition({ type: TransitionType.All, scale: { x: 0, y: 0 } })
```
当type属性为TransitionType.All时,表示指定转场动效生效在组件的所有变化(插入和删除)场景。此时,删除动画和插入动画是相反的过程,删除动画是插入动画的逆播。例如,以上代码定义了一个Button控件。在插入时,组件从scale的x、y均为0的状态,变化到scale的x、y均为1(即完整显示)的默认状态,以逐渐放大的方式出现。在删除时,组件从scale的x、y均为1的默认状态,变化到指定的scale的x、y均为0的状态,逐渐缩小至尺寸为0。
- 组件的插入、删除使用不同的动画效果
```ts
Button()
.transition({ type: TransitionType.Insert, translate: { x: 200, y: -200 }, opacity: 0 })
.transition({ type: TransitionType.Delete, rotate: { x: 0, y: 0, z: 1, angle: 360 } })
```
当组件的插入和删除需要实现不同的转场动画效果时,可以调用两次transition函数,分别设置type属性为TransitionType.Insert和TransitionType.Delete。例如,以上代码定义了一个Button控件。在插入时,组件从相对于组件正常布局位置x方向平移200vp、y方向平移-200vp的位置、透明度为0的初始状态,变化到x、y方向平移量为0、透明度为1的默认状态,插入动画为平移动画和透明度动画的组合。在删除时,组件从旋转角为0的默认状态,变化到绕z轴旋转360度的终止状态,即绕z轴旋转一周。
- 只定义组件的插入或删除其中一种动画效果。
```ts
Button()
.transition({ type: TransitionType.Delete, translate: { x: 200, y: -200 } })
```
当只需要组件的插入或删除的转场动画效果时,仅需设置type属性为TransitionType.Insert或TransitionType.Delete的transition效果。例如,以上代码定义了一个Button控件。删除时,组件从正常位置、没有平移的默认状态,变化到从相对于正常布局位置x方向平移200vp、y方向平移-200vp的位置的状态。插入该组件并不会产生该组件的转场动画。
## if/else产生组件内转场动画
if/else语句可以控制组件的插入和删除。如下代码即可通过Button的点击事件,控制if的条件是否满足,来控制if下的Image组件是否显示。
```ts
@Entry
@Component
struct IfElseTransition {
@State flag: boolean = true;
@State show: string = 'show';
build() {
Column() {
Button(this.show).width(80).height(30).margin(30)
.onClick(() => {
if (this.flag) {
this.show = 'hide';
} else {
this.show = 'show';
}
// 点击Button控制Image的显示和消失
this.flag = !this.flag;
})
if (this.flag) {
Image($r('app.media.mountain')).width(200).height(200)
}
}.height('100%').width('100%')
}
}
```
以上代码没有配置任何动画。接下来,我们将给以上代码加入组件内转场的效果。首先Image组件是由if控制的组件,需要给其加上transition的参数,以指定组件内转场的具体效果。例如,可以如以下代码,给其插入时加上平移效果,删除时加上缩放和透明度效果。
```ts
if (this.flag) {
Image($r('app.media.mountain')).width(200).height(200)
.transition({ type: TransitionType.Insert, translate: { x: 200, y: -200 } })
.transition({ type: TransitionType.Delete, opacity: 0, scale: { x: 0, y: 0 } })
}
```
以上代码虽然指定了动画的样式,但是未指定动画参数,尚不知道需要用多长时间、怎样的曲线完成该动画。transition必须配合animateTo一起使用,并在animateTo的闭包中,控制组件的插入、删除。对于以上示例代码,即为在animateTo闭包中改变flag的值,该部分代码如下所示。指定动画时长为1000ms,曲线使用animateTo函数默认的曲线,改变flag的值。则由flag变化所引起的一切变化,都会按照该动画参数,产生动画。在这里,flag会影响Image的出现和消失。
```ts
animateTo({ duration: 1000 }, () => {
this.flag = !this.flag;
})
```
经过以上过程,当animateTo和transition一起使用时,即产生了组件内转场动画。完整示例代码如下:
```ts
@Entry
@Component
struct IfElseTransition {
@State flag: boolean = true;
@State show: string = 'show';
build() {
Column() {
Button(this.show).width(80).height(30).margin(30)
.onClick(() => {
if (this.flag) {
this.show = 'hide';
} else {
this.show = 'show';
}
animateTo({ duration: 1000 }, () => {
// 动画闭包内控制Image组件的出现和消失
this.flag = !this.flag;
})
})
if (this.flag) {
// Image的出现和消失配置为不同的过渡效果
Image($r('app.media.mountain')).width(200).height(200)
.transition({ type: TransitionType.Insert, translate: { x: 200, y: -200 } })
.transition({ type: TransitionType.Delete, opacity: 0, scale: { x: 0, y: 0 } })
}
}.height('100%').width('100%')
}
}
```
![ifElseTransition](figures/ifElseTransition.gif)
>**说明:**
>
>当配置transition的效果为translate或scale时,本身位置叠加上平移或放大倍数后,动画过程中有可能超过父组件的范围。如果超出父组件的范围时,希望子组件完整的显示,那么可以设置父组件的clip属性为false,使父组件不对子组件产生裁剪。如果超出父组件的范围时,希望超出的子组件部分不显示,那么可以设置父组件的clip属性为true,裁剪掉子组件超出的部分。
## ForEach产生组件内转场动画
和if/else类似,ForEach可以通过控制数组中的元素个数,来控制组件的插入和删除。通过ForEach来产生组件内转场动画,仍然需要两个条件:
- ForEach里的组件配置了transition效果。
- 在animateTo的闭包中控制组件的插入或删除,即控制数组的元素添加和删除。
以下代码是使用ForEach产生组件内转场动画的一个示例。
```ts
@Entry
@Component
struct ForEachTransition {
@State numbers: string[] = ["1", "2", "3", "4", "5"]
startNumber: number = 6;
build() {
Column({ space: 10 }) {
Column() {
ForEach(this.numbers, (item) => {
// ForEach下的直接组件需配置transition效果
Text(item)
.width(240)
.height(60)
.fontSize(18)
.borderWidth(1)
.backgroundColor(Color.Orange)
.textAlign(TextAlign.Center)
.transition({ type: TransitionType.All, translate: { x: 200 }, scale: { x: 0, y: 0 } })
}, item => item)
}
.margin(10)
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Center)
.width("90%")
.height("70%")
Button('向头部添加元素')
.fontSize(16)
.width(160)
.onClick(() => {
animateTo({ duration: 1000 }, () => {
// 往数组头部插入一个元素,导致ForEach在头部增加对应的组件
this.numbers.unshift(this.startNumber.toString());
this.startNumber++;
})
})
Button('向尾部添加元素')
.width(160)
.fontSize(16)
.onClick(() => {
animateTo({ duration: 1000 }, () => {
// 往数组尾部插入一个元素,导致ForEach在尾部增加对应的组件
this.numbers.push(this.startNumber.toString());
this.startNumber++;
})
})
Button('删除头部元素')
.width(160)
.fontSize(16)
.onClick(() => {
animateTo({ duration: 1000 }, () => {
// 删除数组的头部元素,导致ForEach删除头部的组件
this.numbers.shift();
})
})
Button('删除尾部元素')
.width(160)
.fontSize(16)
.onClick(() => {
animateTo({ duration: 1000 }, () => {
// 删除数组的尾部元素,导致ForEach删除头部的组件
this.numbers.pop();
})
})
}
.width('100%')
.height('100%')
}
}
```
效果如下图:
![forEachTransition2](figures/forEachTransition2.gif)
由于Column布局方式设为了FlexAlign.Start,即竖直方向从头部开始布局。所以往数组末尾添加元素时,并不会对数组中现存元素对应的组件位置造成影响,只会触发新增组件的插入动画。而往数组头部添加元素时,原来数组中的所有元素的下标都增加了,虽然不会触发其添加或者删除,但是会影响到对应组件的位置。所以除新增的组件会做transition动画以外,之前存在于ForEach中组件也会做位置动画。
>**说明:**
>
>if/else、ForEach为语法节点,配置组件内转场效果的组件应直接作为语法节点的孩子。由语法节点的增删引起的组件增删,只能触发其直接孩子组件的组件内转场动画,开发者不应期望其对更深层次的组件产生组件转场动画。
# UI开发(ArkTS声明式开发范式)概述
基于ArkTS的声明式开发范式的方舟开发框架是一套开发极简、高性能、支持跨设备的UI开发框架,提供了构建OpenHarmony应用UI所必需的能力,主要包括:
- **ArkTS**
ArkTS是UI开发语言,基于TypeScript(简称TS)语言扩展而来,是TS的超集。扩展能力包含各种装饰器、自定义组件、UI描述机制。状态数据管理作为基于ArkTS的声明式开发范式的特色,通过功能不同的装饰器给开发者提供了清晰的页面更新渲染流程和管道。状态管理包括UI组件状态和应用程序状态,两者协作可以使开发者完整地构建整个应用的数据更新和UI渲染。ArkTS语言的基础知识请参考[学习ArkTS语言](../quick-start/arkts-get-started.md)
- **布局**
布局是UI的必要元素,它定义了组件在界面中的位置。ArkUI框架提供了多种布局方式,除了基础的线性布局、层叠布局、弹性布局、相对布局、栅格布局外,也提供了相对复杂的列表、宫格、轮播。
- **组件**
组件是UI的必要元素,形成了在界面中的样子,由框架直接提供的称为**系统组件**,由开发者定义的称为**自定义组件**。系统内置组件包括按钮、单选框、进度条、文本等。开发者可以通过链式调用的方式设置系统内置组件的渲染效果。开发者可以将系统内置组件组合为自定义组件,通过这种方式将页面组件化为一个个独立的UI单元,实现页面不同单元的独立创建、开发和复用,具有更强的工程性。
- **页面路由和组件导航**
应用可能包含多个页面,可通过页面路由实现页面间的跳转。一个页面内可能存在组件间的导航如典型的分栏,可通过导航组件实现组件间的导航。
- **图形**
方舟开发框架提供了多种类型图片的显示能力和多种自定义绘制的能力,以满足开发者的自定义绘图需求,支持绘制形状、填充颜色、绘制文本、变形与裁剪、嵌入图片等。
- **动画**
动画是UI的重要元素之一。优秀的动画设计能够极大地提升用户体验,框架提供了丰富的动画能力,除了组件内置动画效果外,还包括属性动画、显式动画、自定义转场动画以及动画API等,开发者可以通过封装的物理模型或者调用动画能力API来实现自定义动画轨迹。
- **交互事件**
交互事件是UI和用户交互的必要元素。方舟开发框架提供了多种交互事件,除了触摸事件、鼠标事件、键盘按键事件、焦点事件等通用事件外,还包括基于通用事件进行进一步识别的手势事件。手势事件有单一手势如点击手势、长按手势、拖动手势、捏合手势、旋转手势、滑动手势,以及通过单一手势事件进行组合的组合手势事件。
## 特点
- 开发效率高,开发体验好
- 代码简洁:通过接近自然语义的方式描述UI,不必关心框架如何实现UI绘制和渲染。
- 数据驱动UI变化:让开发者更专注自身业务逻辑的处理。当UI发生变化时,开发者无需编写在不同的UI之间进行切换的UI代码, 开发人员仅需要编写引起界面变化的数据,具体UI如何变化交给框架。
- 开发体验好:界面也是代码,让开发者的编程体验得到提升。
- 性能优越
- 声明式UI前端和UI后端分层:UI后端采用C++语言构建,提供对应前端的基础组件、布局、动效、交互事件、组件状态管理和渲染管线。
- 语言编译器和运行时的优化:统一字节码、高效FFI-Foreign Function Interface、AOT-Ahead Of Time、引擎极小化、类型优化等。
- 生态容易快速推进
能够借力主流语言生态快速推进,语言相对中立友好,有相应的标准组织可以逐步演进。
## 整体架构
**图1** 整体架构图  
![arkui-arkts-framework](figures/arkui-arkts-framework.png)
- **声明式UI前端**
提供了UI开发范式的基础语言规范,并提供内置的UI组件、布局和动画,提供了多种状态管理机制,为应用开发者提供一系列接口支持。
- **语言运行时**
选用方舟语言运行时,提供了针对UI范式语法的解析能力、跨语言调用支持的能力和TS语言高性能运行环境。
- **声明式UI后端引擎**
后端引擎提供了兼容不同开发范式的UI渲染管线,提供多种基础组件、布局计算、动效、交互事件,提供了状态管理和绘制能力。
- **渲染引擎**
提供了高效的绘制能力,将渲染管线收集的渲染指令,绘制到屏幕的能力。
- **平台适配层**
提供了对系统平台的抽象接口,具备接入不同系统的能力,如系统渲染管线、生命周期调度等。
## 开发流程
使用UI开发框架开发应用时,主要涉及如下开发过程。开发者可以先通过[第一个入门](../quick-start/start-with-ets-stage.md)实例了解整个应用的UI开发过程。
| 任务 | 简介 | 相关指导 |
| ----------- | ----------------------------------- | ---------------------------------------- |
| 学习ArkTS | 介绍了ArkTS的基本语法、状态管理和渲染控制的场景。 | - [基本语法](../quick-start/arkts-basic-syntax-overview.md)<br>- [状态管理](../quick-start/arkts-state-management-overview.md)<br>- [渲染控制](../quick-start/arkts-rendering-control-overview.md) |
| 开发布局 | 介绍了几种常用的布局方式以及如何提升布局性能。 | -&nbsp;[常用布局](arkts-layout-development-overview.md)<br/>-&nbsp;[布局性能](arkts-layout-development-performance-boost.md) |
| 添加组件 | 介绍了几种常用的内置组件、自定义组件以及通过API方式支持的界面元素。 | -&nbsp;[常用组件](arkts-common-components-button.md)<br/>- [自定义组件](../quick-start/arkts-create-custom-components.md)<br>-&nbsp;[气泡和菜单](arkts-popup-and-menu-components-popup.md) |
| 设置页面路由和组件导航 | 介绍了如何设置页面路由以及组件间的导航。 | -&nbsp;[页面路由](arkts-routing.md)<br/>-&nbsp;[组件导航](arkts-navigation-navigation.md) |
| 显示图形 | 介绍了如何显示图片、绘制自定义几何图形以及使用画布绘制自定义图形。 | -&nbsp;[图片](arkts-graphics-display.md)<br/>-&nbsp;[几何图形](arkts-geometric-shape-drawing.md)<br/>-&nbsp;[画布](arkts-drawing-customization-on-canvas.md) |
| 使用动画 | 介绍了组件和页面使用动画的典型场景。 | -&nbsp;[页面内的动画](arkts-layout-update-animation.md)<br/>-&nbsp;[页面间的动画](arkts-zoom-animation.md) |
| 绑定事件 | 介绍了事件的基本概念和如何使用通用事件和手势事件。 | -&nbsp;[通用事件](arkts-common-events-touch-screen-event.md)<br/>-&nbsp;[手势事件](arkts-gesture-events-binding.md) |
## 相关实例
基于ArkTS的声明式开发范式,可参考以下实例:
[ArkTS组件集](https://gitee.com/openharmony/applications_app_samples/tree/master/code/UI/ArkTsComponentClollection/ComponentCollection):组件、通用方法、动画、全局方法的集合。
# 放大缩小视图
在不同页面间,有使用相同的元素(例如同一幅图)的场景,可以使用[共享元素转场](../reference/arkui-ts/ts-transition-animation-shared-elements.md)动画衔接。为了突出不同页面间相同元素的关联性,可为它们添加共享元素转场动画。如果相同元素在不同页面间的大小有明显差异,即可达到放大缩小视图的效果。
共享元素转场的接口为:
```ts
sharedTransition(id: string, options?: sharedTransitionOptions)
```
其中根据sharedTransitionOptions中的type参数,共享元素转场分为Exchange类型的共享元素转场和Static类型的共享元素转场。
## Exchange类型的共享元素转场
交换型的共享元素转场,需要两个页面中,存在通过sharedTransition函数配置为相同id的组件,它们称为共享元素。这种类型的共享元素转场适用于两个页面间相同元素的衔接,会从起始页共享元素的位置、大小过渡到目标页的共享元素的位置、大小。如果不指定type,默认为Exchange类型的共享元素转场,这也是最常见的共享元素转场的方式。使用Exchange类型的共享元素转场时,共享元素转场的动画参数由目标页options中的动画参数决定。
## Static类型的共享元素转场
静态型的共享元素转场通常用于页面跳转时,标题逐渐出现或隐藏的场景,只需要在一个页面中有Static的共享元素,不能在两个页面中出现相同id的Static类型的共享元素。在跳转到该页面(即目标页)时,配置Static类型sharedTransition的组件做透明度从0到该组件设定的透明度的动画,位置保持不变。在该页面(即起始页)消失时,做透明度逐渐变为0的动画,位置保持不变。
共享元素转场的动画参数由该组件sharedTransition属性中的动画参数决定。
## 场景示例
下面介绍使用共享元素转场进行放大缩小图片的示例。
```ts
// src page
import router from '@ohos.router';
@Entry
@Component
struct SharedTransitionSrc {
build() {
Column() {
// 配置Exchange类型的共享元素转场,共享元素id为"sharedImage1"
Image($r('app.media.mountain')).width(50).height(50)
.sharedTransition('sharedImage1', { duration: 1000, curve: Curve.Linear })
.onClick(() => {
// 点击小图时路由跳转至下一页面
router.pushUrl({ url: 'pages/myTest/sharedTransitionDst' });
})
}
.padding(10)
.width("100%")
.alignItems(HorizontalAlign.Start)
}
}
```
```ts
// dest page
import router from '@ohos.router';
@Entry
@Component
struct SharedTransitionDest {
build() {
Column() {
// 配置Static类型的共享元素转场
Text("SharedTransition dest page")
.fontSize(16)
.sharedTransition('text', { duration: 500, curve: Curve.Linear, type: SharedTransitionEffectType.Static })
.margin({ top: 10 })
// 配置Exchange类型的共享元素转场,共享元素id为"sharedImage1"
Image($r('app.media.mountain'))
.width(150)
.height(150)
.sharedTransition('sharedImage1', { duration: 500, curve: Curve.Linear })
.onClick(() => {
// 点击图片时路由返回至上一页面
router.back();
})
}
.width("100%")
.alignItems(HorizontalAlign.Center)
}
}
```
上述示例中,第一个页面(src page)和第二个页面(dest page)都配置了id为"sharedImage1"的共享元素转场,使两个页面能匹配到这一组共享元素。从第一个页面跳转到第二个页面时,第一个页面为起始页,第二个页面为目标页。配置id为"sharedImage1"的组件按照目标页中500ms的时长进行共享元素转场,达到放大视图的效果,id为"text"的组件按照配置的Static类型sharedTransition参数中的500ms的时长进行共享元素转场,标题逐渐出现。从第二个页面返回到第一个页面时,第二个页面为起始页,第一个页面为目标页。配置id为"sharedImage1"的组件按照目标页中1000ms的时长进行共享元素转场,缩小为原始视图,id为"text"的组件按照配置的Static类型sharedTransition参数中的500ms的时长进行共享元素转场,标题逐渐隐藏。
![sharedTransition](figures/sharedTransition.gif)
# 方舟开发框架概述 # 方舟开发框架概述
方舟开发框架(简称:ArkUI),是一套构建OpenHarmony应用界面的UI开发框架,它提供了极简的UI语法与包括UI组件、动画机制、事件交互等在内的UI开发基础设施,以满足应用开发者的可视化界面开发需求。
## 基本概念 方舟开发框架(简称ArkUI)为OpenHarmony应用的UI开发提供了完整的基础设施,包括简洁的UI语法、丰富的UI功能(组件、布局、动画以及交互事件),以及实时界面预览工具等,可以支持开发者进行可视化界面开发。
- **组件:** 组件是界面搭建与显示的最小单位。开发者通过多种组件的组合,构建出满足自身应用诉求的完整界面。
- **页面:** page页面是方舟开发框架最小的调度分割单位。开发者可以将应用设计为多个功能页面,每个页面进行单独的文件管理,并通过[页面路由](../reference/apis/js-apis-router.md)API完成页面间的调度管理,以实现应用内功能的解耦。
## 主要特征 ## 基本概念
- **UI组件:** 方舟开发框架内置了丰富的多态组件,包括文本、图片、按钮等基础组件,可包含一个或多个子组件的容器组件,满足开发者自定义绘图需求的绘制组件,以及提供视频播放能力的媒体组件等。其中“多态”是指组件针对不同类型设备进行了设计,提供了在不同平台上的样式适配能力。
- **布局:** UI界面设计离不开布局的参与。方舟开发框架提供了多种布局方式,除了基础的线性布局、弹性布局外,也提供了相对复杂的列表、宫格、栅格布局,以及自适应多分辨率场景开发的原子布局能力 - **UI:** 即用户界面。开发者可以将应用的用户界面设计为多个功能页面,每个页面进行单独的文件管理,并通过[页面路由](arkts-routing.md)API完成页面间的调度管理如跳转、回退等操作,以实现应用内的功能解耦
- **动画:** 动画是UI界面的重要元素之一,优秀的动画设计能够极大地提升用户体验,方舟开发框架提供了丰富的动画能力,除了组件内置动画效果外,还包括属性动画、自定义转场动画以及动画API等 - **组件:** UI构建与显示的最小单位,如列表、网格、按钮、单选框、进度条、文本等。开发者通过多种组件的组合,构建出满足自身应用诉求的完整界面
- **绘制:** 方舟开发框架提供了多种绘制能力,以满足开发者的自定义绘图需求,支持绘制形状、颜色填充、绘制文本、变形与裁剪、嵌入图片等。
- **交互事件:** 方舟开发框架提供了多种交互能力,以满足应用在不同平台通过不同输入设备进行UI交互响应的需求,默认适配了触摸手势、遥控器按键输入、键鼠输入,同时提供了相应的事件回调以便开发者添加交互逻辑。 ## 两种开发范式
- **平台API通道:** 方舟开发框架提供了API扩展机制,可通过该机制对平台能力进行封装,提供风格统一的JS接口 针对不用的应用场景及技术背景,方舟开发框架提供了两种开发范式,分别是[基于ArkTS的声明式开发范式](arkts-ui-development-overview.md)(简称“声明式开发范式”)和[兼容JS的类Web开发范式](../ui/ui-js-overview.md)(简称“类Web开发范式”)
- **两种开发范式:** 方舟开发框架针对不同的应用场景以及不同技术背景的开发者提供了两种开发范式,分别是[基于ArkTS的声明式开发范式](./ui-ts-overview.md)(简称“声明式开发范式”)和[兼容JS的类Web开发范式](./ui-js-overview.md)(简称“类Web开发范式”) - **声明式开发范式**:采用基于TypeScript声明式UI语法扩展而来的[ArkTS语言](../quick-start/arkts-get-started.md),从组件、动画和状态管理三个维度提供UI绘制能力
| 开发范式名称 | 简介 | 适用场景 | 适用人群 | - **类Web开发范式**:采用经典的HML、CSS、JavaScript三段式开发方式,即使用HML标签文件搭建布局、使用CSS文件描述样式、使用JavaScript文件处理逻辑。该范式更符合于Web前端开发者的使用习惯,便于快速将已有的Web应用改造成方舟开发框架应用。
| -------- | ---------------------------------------- | ---------------- | ------------------- |
| 声明式开发范式 | 采用基于TypeScript进行声明式UI语法扩展而来的[ArkTS语言](../quick-start/arkts-get-started.md),从组件、动画和状态管理三个维度提供了UI绘制能力。声明式开发范式更接近自然语义的编程方式,让开发者直观地描述UI界面,不必关心框架如何实现UI绘制和渲染,实现极简高效开发。 | 复杂度较大、团队合作度较高的应用 | 移动系统应用开发人员、系统应用开发人员 |
| 类Web开发范式 | 采用经典的HML、CSS、JavaScript三段式开发方式,使用HML标签文件进行布局搭建,使用CSS文件进行样式描述,使用JavaScript文件进行逻辑处理。UI组件与数据之间通过单向数据绑定的方式建立关联,当数据发生变化时,UI界面自动触发刷新。该开发方式更接近Web前端开发者的使用习惯,便于快速将已有的Web应用改造成方舟开发框架应用。 | 界面较简单的中小型应用和卡片 | Web前端开发人员 |
## 框架结构 在开发一款新应用时,推荐采用声明式开发范式来构建UI,主要基于以下几点考虑:
![zh-cn_image_0000001183709904](figures/zh-cn_image_0000001183709904.png) - **开发效率:** 声明式开发范式更接近自然语义的编程方式,开发者可以直观地描述UI,无需关心如何实现UI绘制和渲染,开发高效简洁。
从上图可以看出,类Web开发范式与声明式开发范式的UI后端引擎和语言运行时是共用的,其中,UI后端引擎实现了方舟开发框架的六种基本能力。声明式开发范式无需JS Framework进行页面DOM管理,渲染更新链路更为精简,占用内存更少,因此更推荐开发者选用声明式开发范式来搭建应用UI界面 - **应用性能:** 如下图所示,两种开发范式的UI后端引擎和语言运行时是共用的,但是相比类Web开发范式,声明式开发范式无需JS框架进行页面DOM管理,渲染更新链路更为精简,占用内存更少,应用性能更佳
## UI与Ability框架的关系 - **发展趋势**:声明式开发范式后续会作为主推的开发范式持续演进,为开发者提供更丰富、更强大的能力。
OpenHarmony提供了FA模型与Stage模型两种[应用模型](../application-models/application-model-description.md)。下表给出了两种模型分别与方舟开发框架的两种开发范式的关系。 **图1** 方舟开发框架示意图  
**FA模型:** ![arkui-framework](figures/arkui-framework.png)
| 类型 | UI开发范式 | 说明 |
| ---- | -------- | ---------------------------------------- |
| 应用 | 类web开发范式 | UI开发语言:使用hml/css/js <br>业务入口:使用固定文件名app.ets(Page类型Ability)/service.ts(Service类型Ability)/data.ts(Data类型Ability)<br>业务逻辑语言:js/ts |
| | 声明式开发范式 | UI开发语言:ArkTS <br>业务入口:使用固定文件名app.ets(Page类型Ability)/service.ts(Service类型Ability)/data.ts(Data类型Ability) <br>业务逻辑语言:js/ts |
| 服务卡片 | 类web开发范式 | UI开发语言:卡片显示使用hml+css+json(action) <br>业务入口:form.ts <br>卡片业务逻辑语言:js/ts |
| | 声明式开发范式 | 当前不支持 |
**Stage模型:** ## 不同应用类型支持的开发范式
| 类型 | UI开发范式 | 说明 | 根据所选用OpenHarmony[应用模型](../application-models/application-model-composition.md)(Stage模型、FA模型)和页面形态(应用或服务的普通页面、卡片)的不同,对应支持的UI开发范式也有所差异,详见下表。
| ---- | -------- | ---------------------------------------- |
| 应用 | 类web开发范式 | 当前不支持 |
| | 声明式开发范式 | UI开发语言:ArkTS <br>业务入口:应用模型基于ohos.application.Ability/ExtensionAbility等派生 <br>业务逻辑语言:ts |
| 服务卡片 | 类web开发范式 | UI开发语言:卡片显示使用hml+css+json(action) <br>业务入口:从FormExtensionAbility派生 <br>业务逻辑语言:ts |
| | 声明式开发范式 | 当前不支持 |
**表1** 支持的UI开发范式
| 应用模型 | 页面形态 | 支持的UI开发范式 |
| ----------- | -------- | ------------------------ |
| Stage模型(推荐) | 应用或服务的页面 | 声明式开发范式(推荐) |
| | 卡片 | 声明式开发范式(推荐)<br/>类Web开发范式 |
| FA模型 | 应用或服务的页面 | 声明式开发范式<br/>类Web开发范式 |
| | 卡片 | 类Web开发范式 |
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
## 定义资源文件 ## 定义资源文件
资源文件用于存放应用在多种语言场景下的资源内容,开发框架使用JSON文件保存资源定义。在[文件组织](../ui/js-framework-file.md)中指定的i18n文件夹内放置语言资源文件,其中语言资源文件的命名是由语言、文字、国家或地区的限定词通过中划线连接组成,其中文字和国家或地区可以省略,如zh-Hant-HK(中国香港地区使用的繁体中文)、zh-CN(中国使用的简体中文)、zh(中文)。命名规则如下: 资源文件用于存放应用在多种语言场景下的资源内容,开发框架使用JSON文件保存资源定义。在[文件组织](js-framework-file.md)中指定的i18n文件夹内放置语言资源文件,其中语言资源文件的命名是由语言、文字、国家或地区的限定词通过中划线连接组成,其中文字和国家或地区可以省略,如zh-Hant-HK(中国香港地区使用的繁体中文)、zh-CN(中国使用的简体中文)、zh(中文)。命名规则如下:
``` ```
language[-script-region].json language[-script-region].json
...@@ -119,7 +119,7 @@ ar-AE.json ...@@ -119,7 +119,7 @@ ar-AE.json
<text>{{ replaceObject }}</text> <text>{{ replaceObject }}</text>
<!-- 先在js中获取资源内容,并将占位符{0}替换为“Hello world”,再在text中显示“Array type parameter substitution-Hello world” --> <!-- 先在js中获取资源内容,并将占位符{0}替换为“Hello world”,再在text中显示“Array type parameter substitution-Hello world” -->
<text>{{ replaceArray }}</text> <text>{{ replaceArray }}</text>
<!-- 获取图片路径 --> <!-- 获取图片路径 -->
<image src="{{ $t('files.image') }}" class="image"></image> <image src="{{ $t('files.image') }}" class="image"></image>
<!-- 先在js中获取图片路径,再在image中显示图片 --> <!-- 先在js中获取图片路径,再在image中显示图片 -->
...@@ -150,9 +150,9 @@ ar-AE.json ...@@ -150,9 +150,9 @@ ar-AE.json
表4 单复数格式化 表4 单复数格式化
| 属性 | 类型 | 参数 | 必填 | 描述 | | 属性 | 类型 | 参数 | 必填 | 描述 |
| ---- | -------- | ----------- | ---- | ---------------------------------------- | | ---- | -------- | ------------------ | ---- | ------------------------------------------------------------ |
| $tc | Function | 请见表 $tc参数说明 | 是 | 根据系统语言完成单复数替换:this.$tc('strings.people')<br/>>&nbsp;![icon-note.gif](public_sys-resources/icon-note.gif)&nbsp;说明:<br/>>&nbsp;定义资源的内容通过json格式的key为“zero”、“one”、“two”、“few”、“many”和“other”区分。 | | $tc | Function | 请见表 $tc参数说明 | 是 | 根据系统语言完成单复数替换:this.$tc('strings.people')<br/>> 说明:<br/>>&nbsp;定义资源的内容通过json格式的key为“zero”、“one”、“two”、“few”、“many”和“other”区分。 |
表5 $tc参数说明 表5 $tc参数说明
...@@ -183,4 +183,4 @@ ar-AE.json ...@@ -183,4 +183,4 @@ ar-AE.json
## 获取语言 ## 获取语言
获取语言功能请参考[应用配置](../reference/apis/js-apis-application-configuration.md) 获取语言功能请参考[应用配置](../reference/apis/js-apis-app-ability-configuration.md)
...@@ -37,18 +37,18 @@ ...@@ -37,18 +37,18 @@
## 引用JS模块内resources资源 ## 引用JS模块内resources资源
在应用开发的hml和js文件中使用$r的语法,可以对JS模块内的resources目录下的json资源进行格式化,获取相应的资源内容。 在应用开发的hml和js文件中使用$r的语法,可以对JS模块内的resources目录下的json资源进行格式化,获取相应的资源内容,该目录与pages目录同级,具体目录结构请参考[文件组织](js-framework-file.md)
| 属性 | 类型 | 描述 | | 属性 | 类型 | 描述 |
| -------- | -------- | -------- | | -------- | -------- | -------- |
| $r | (key:&nbsp;string)&nbsp;=&gt;&nbsp;string | 获取资源限定下具体的资源内容。例如:this.$r('strings.hello')。<br/>参数说明:<br/>-&nbsp;key:定义在资源限定文件中的键值,如strings.hello。 | | $r | (key:&nbsp;string)&nbsp;=&gt;&nbsp;string | 获取资源限定下具体的资源内容。例如:$r('strings.hello')。<br/>参数说明:<br/>-&nbsp;key:定义在资源限定文件中的键值,如strings.hello。 |
**res-defaults.json示例:**<br/> **res-defaults.json示例:**<br/>
```json ```json
{ {
"strings": { "strings": {
"hello": "hello world" "hello": "hello world"
} }
} }
``` ```
......
...@@ -25,9 +25,9 @@ JS文件用来定义HML页面的业务逻辑,支持ECMA规范的JavaScript语 ...@@ -25,9 +25,9 @@ JS文件用来定义HML页面的业务逻辑,支持ECMA规范的JavaScript语
## 对象 ## 对象
- 应用对象 - 应用对象
| 属性 | 类型 | 描述 | | 属性 | 类型 | 描述 |
| ---- | ------ | ---------------------------------------- | | ---- | ------ | ------------------------------------------------------------ |
| $def | Object | 使用this.$app.$def获取在app.js中暴露的对象。<br/>>&nbsp;![icon-note.gif](public_sys-resources/icon-note.gif)&nbsp;**说明:**<br/>>&nbsp;应用对象不支持数据绑定,需主动触发UI更新。 | | $def | Object | 使用this.$app.$def获取在app.js中暴露的对象。<br/>> **说明:**<br/>>&nbsp;应用对象不支持数据绑定,需主动触发UI更新。 |
示例代码 示例代码
...@@ -103,7 +103,7 @@ JS文件用来定义HML页面的业务逻辑,支持ECMA规范的JavaScript语 ...@@ -103,7 +103,7 @@ JS文件用来定义HML页面的业务逻辑,支持ECMA规范的JavaScript语
getAppVersion() { getAppVersion() {
this.$set('keyMap.Version', '3.0'); this.$set('keyMap.Version', '3.0');
console.info("keyMap.Version = " + this.keyMap.Version); // keyMap.Version = 3.0 console.info("keyMap.Version = " + this.keyMap.Version); // keyMap.Version = 3.0
this.$delete('keyMap'); this.$delete('keyMap');
console.info("keyMap.Version = " + this.keyMap); // log print: keyMap.Version = undefined console.info("keyMap.Version = " + this.keyMap); // log print: keyMap.Version = undefined
} }
......
...@@ -33,28 +33,28 @@ ...@@ -33,28 +33,28 @@
```js ```js
/* xxx.js */ /* xxx.js */
export default { export default {
data: { data: {
animation: '', animation: '',
}, options: {},
onInit() { frames: {}
}, },
onShow() { onInit() {
var options = { this.options = {
duration: 1500, duration: 1500,
}; };
var frames = [ this.frames = [
{ {
width:200,height:200, width: 200, height: 200,
}, },
{ {
width:300,height:300, width: 300, height: 300,
} }
]; ];
this.animation = this.$element('content').animate(frames, options); //获取动画对象 },
}, Show() {
Show() { this.animation = this.$element('content').animate(this.frames, this.options); //获取动画对象
this.animation.play(); this.animation.play();
} }
} }
``` ```
...@@ -172,36 +172,36 @@ export default { ...@@ -172,36 +172,36 @@ export default {
```js ```js
/* xxx.js */ /* xxx.js */
export default { export default {
data: { data: {
animation: '', animation: '',
}, options: {},
onInit() { frames: {}
}, },
onShow() { onInit() {
var options = { this.options = {
duration: 1500, duration: 1500,
easing: 'ease-in', easing: 'ease-in',
delay: 5, delay: 5,
iterations: 2, iterations: 2,
direction: 'normal', direction: 'normal',
}; };
var frames = [ this.frames = [
{ {
transform: { transform: {
translate: '-150px -0px' translate: '-150px -0px'
} }
}, },
{ {
transform: { transform: {
translate: '150px 0px' translate: '150px 0px'
} }
} }
]; ];
this.animation = this.$element('content').animate(frames, options); },
}, Show() {
Show() { this.animation = this.$element('content').animate(this.frames, this.options);
this.animation.play(); this.animation.play();
} }
} }
``` ```
......
...@@ -38,8 +38,8 @@ ...@@ -38,8 +38,8 @@
height: 428px; height: 428px;
background-color: #860303; background-color: #860303;
transform: rotate(45deg); transform: rotate(45deg);
margin-top: 284px; margin-top: 290px;
margin-left: 148px; margin-left: 145px;
} }
.content{ .content{
margin-top: 500px; margin-top: 500px;
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
width: 100px; width: 100px;
height: 150px; height: 150px;
background-color: #1033d9; background-color: #1033d9;
transform: translate(150px,-137px); transform: translate(150px,-152px);
} }
.window{ .window{
z-index: 1; z-index: 1;
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
height: 100px; height: 100px;
border-radius: 15px; border-radius: 15px;
background-color: #9a7404; background-color: #9a7404;
transform: translate(200px,-710px) skewX(-5deg); transform: translate(200px,-700px) skewX(-5deg);
} }
``` ```
......
...@@ -48,6 +48,7 @@ index和detail这两个页面均包含一个text组件和button组件:text组 ...@@ -48,6 +48,7 @@ index和detail这两个页面均包含一个text组件和button组件:text组
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.title { .title {
font-size: 50px; font-size: 50px;
margin-bottom: 50px; margin-bottom: 50px;
......
...@@ -271,6 +271,7 @@ background-color:#F1F3F5; ...@@ -271,6 +271,7 @@ background-color:#F1F3F5;
```js ```js
// xxx.js // xxx.js
import promptAction from '@ohos.promptAction';
export default { export default {
data() { data() {
return { return {
......
...@@ -81,7 +81,7 @@ button是按钮组件,其类型包括胶囊按钮、圆形按钮、文本按 ...@@ -81,7 +81,7 @@ button是按钮组件,其类型包括胶囊按钮、圆形按钮、文本按
如果需要添加ohos.permission.INTERNET权限,则在resources文件夹下的config.json文件里进行权限配置。 如果需要添加ohos.permission.INTERNET权限,则在resources文件夹下的config.json文件里进行权限配置。
```json ```
<!-- config.json --> <!-- config.json -->
"module": { "module": {
"reqPermissions": [{ "reqPermissions": [{
......
...@@ -9,7 +9,7 @@ Canvas组件提供画布,用于自定义绘制图形。具体用法请参考[C ...@@ -9,7 +9,7 @@ Canvas组件提供画布,用于自定义绘制图形。具体用法请参考[C
在pages/index目录下的hml文件中创建一个Canvas组件。 在pages/index目录下的hml文件中创建一个Canvas组件。
```html ```
<!-- xxx.hml --> <!-- xxx.hml -->
<div class="container"> <div class="container">
<canvas></canvas> <canvas></canvas>
...@@ -60,6 +60,8 @@ Canvas组件设置宽(width)、高(height)、背景色(background-colo ...@@ -60,6 +60,8 @@ Canvas组件设置宽(width)、高(height)、背景色(background-colo
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: #F1F3F5; background-color: #F1F3F5;
width: 100%;
height: 100%;
} }
canvas{ canvas{
width: 500px; width: 500px;
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
<div class="container"> <div class="container">
<grid-container id="mygrid" columns="5" gutter="20px" style="background-color: pink;"> <grid-container id="mygrid" columns="5" gutter="20px" style="background-color: pink;">
<grid-row style="height:100px;justify-content:space-around;width: 80%;background-color: #f67002;margin-left: <grid-row style="height:100px;justify-content:space-around;width: 80%;background-color: #f67002;margin-left:
10%;"></grid-row> 10%; margin-right: 10%;"></grid-row>
<grid-row style="height:300px;justify-content:space-around;background-color: #ffcf00;width: 100%;"></grid-row> <grid-row style="height:300px;justify-content:space-around;background-color: #ffcf00;width: 100%;"></grid-row>
<grid-row style="height:150px;justify-content:space-around;background-color: #032cf8;width: 100%;"></grid-row> <grid-row style="height:150px;justify-content:space-around;background-color: #032cf8;width: 100%;"></grid-row>
</grid-container> </grid-container>
...@@ -51,11 +51,11 @@ grid-container点击组件调用getColumns、getColumnWidth、getGutterWidth方 ...@@ -51,11 +51,11 @@ grid-container点击组件调用getColumns、getColumnWidth、getGutterWidth方
<grid-container id="mygrid" columns="6" gutter="20px" style="background-color: pink;padding-top: 100px;" <grid-container id="mygrid" columns="6" gutter="20px" style="background-color: pink;padding-top: 100px;"
onclick="getColumns" onlongpress="getSizeType"> onclick="getColumns" onlongpress="getSizeType">
<grid-row style="height:100px;justify-content:space-around;background-color: #4cedf3;width: 20%;margin-left: <grid-row style="height:100px;justify-content:space-around;background-color: #4cedf3;width: 20%;margin-left:
40%;"></grid-row> 40%; margin-right: 40%;"></grid-row>
<grid-row style="height:150px;justify-content:space-around;background-color: #4cbff3;width: 50%;margin-left: <grid-row style="height:150px;justify-content:space-around;background-color: #4cbff3;width: 50%;margin-left:
25%;"></grid-row> 25%; margin-right: 25%;"></grid-row>
<grid-row style="height:200px;justify-content:space-around;background-color: #465ff6;width: 80%;margin-left: <grid-row style="height:200px;justify-content:space-around;background-color: #465ff6;width: 80%;margin-left:
10%;"></grid-row> 10%; margin-right: 10%;"></grid-row>
<grid-row style="height:200px;justify-content:space-around;background-color: #5011ec;width: 100%;"></grid-row> <grid-row style="height:200px;justify-content:space-around;background-color: #5011ec;width: 100%;"></grid-row>
</grid-container> </grid-container>
</div> </div>
...@@ -217,7 +217,7 @@ text{ ...@@ -217,7 +217,7 @@ text{
```js ```js
// index.js // index.js
import promptAction from '@ohos.promptAction'; import prompt from '@system.prompt';
export default { export default {
data:{ data:{
list:[ list:[
...@@ -228,7 +228,7 @@ export default { ...@@ -228,7 +228,7 @@ export default {
fresh:false fresh:false
}, },
refresh(e) { refresh(e) {
promptAction.showToast({ prompt.showToast({
message: 'refreshing' message: 'refreshing'
}) })
var that = this; var that = this;
...@@ -236,7 +236,7 @@ export default { ...@@ -236,7 +236,7 @@ export default {
setTimeout(function () { setTimeout(function () {
that.fresh = false; that.fresh = false;
that.list.unshift({src: 'common/images/4.png',id:'4'}); that.list.unshift({src: 'common/images/4.png',id:'4'});
promptAction.showToast({ prompt.showToast({
message: 'succeed' message: 'succeed'
}) })
}, 2000) }, 2000)
......
...@@ -285,7 +285,7 @@ button{ ...@@ -285,7 +285,7 @@ button{
```js ```js
// index.js // index.js
import promptAction from '@ohos.promptAction'; import prompt from '@system.prompt';
export default { export default {
data: { data: {
rev:false, rev:false,
...@@ -317,7 +317,7 @@ export default { ...@@ -317,7 +317,7 @@ export default {
this.$element('img').resume() this.$element('img').resume()
}, },
getimgstate(e) { getimgstate(e) {
promptAction.showToast({ prompt.showToast({
message: '当前状态:' + this.$element('img').getState() message: '当前状态:' + this.$element('img').getState()
}) })
}, },
......
...@@ -154,10 +154,10 @@ rating { ...@@ -154,10 +154,10 @@ rating {
```js ```js
// xxx.js // xxx.js
import promptAction from '@ohos.promptAction'; import prompt from '@system.prompt';
export default { export default {
showrating(e) { showrating(e) {
promptAction.showToast({ prompt.showToast({
message: '当前评分' + e.rating message: '当前评分' + e.rating
}) })
} }
......
...@@ -343,6 +343,7 @@ swiper{ ...@@ -343,6 +343,7 @@ swiper{
```js ```js
// xxx.js // xxx.js
import promptAction from '@ohos.promptAction';
export default { export default {
data:{ data:{
index: 0, index: 0,
...@@ -369,4 +370,4 @@ export default { ...@@ -369,4 +370,4 @@ export default {
针对swiper开发,有以下相关实例可供参考: 针对swiper开发,有以下相关实例可供参考:
- [`Swiper`:内容滑动容器(JS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/UI/Swiper) - [`Swiper`:内容滑动容器(JS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/master/UI/Swiper)
# 添加动画效果
动画主要包含了组件动画和页面间动画,并提供了[插值计算](../reference/apis/js-apis-curve.md)[矩阵变换](../reference/apis/js-apis-matrix4.md)的动画能力接口,让开发者极大程度的自主设计动画效果。
在本节主要完成两个动画效果:
1. 启动页的闪屏动画,即Logo图标的渐出和放大效果;
2. 食物列表页和食物详情页的共享元素转场动画效果。
## animateTo实现闪屏动画
组件动画包括属性动画和animateTo显式动画:
1. 属性动画:设置组件通用属性变化的动画效果。
2. 显式动画:设置组件从状态A到状态B的变化动画效果,包括样式、位置信息和节点的增加删除等,开发者无需关注变化过程,只需指定起点和终点的状态。animateTo还提供播放状态的回调接口,是对属性动画的增强与封装。
闪屏页面的动画效果是Logo图标的渐出和放大,动画结束后跳转到食物分类列表页面。接下来,我们就使用animateTo来实现启动页动画的闪屏效果。
1. 动画效果自动播放。闪屏动画的预期效果是,进入Logo页面后,animateTo动画效果自动开始播放,可以借助于组件显隐事件的回调接口来实现。调用Shape的onAppear方法,设置其显式动画。
```ts
Shape() {
...
}
.onAppear(() => {
animateTo()
})
```
2. 创建opacity和scale数值的成员变量,用装饰器@State修饰。表示其为有状态的数据,即改变会触发页面的刷新。
```ts
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo()
})
}
}
```
3. 设置animateTo的动画曲线curve。Logo的加速曲线为先慢后快,使用贝塞尔曲线cubicBezier,cubicBezier(0.4, 0, 1, 1)。
需要使用动画能力接口中的插值计算,首先要导入curves模块。
```ts
import Curves from '@ohos.curves'
```
@ohos.curves模块提供了线性Curve. Linear、阶梯step、三阶贝塞尔(cubicBezier)和弹簧(spring)插值曲线的初始化函数,可以根据入参创建一个插值曲线对象。
```ts
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
curve: this.curve1
})
})
}
}
```
4. 设置动画时长为1s,延时0.1s开始播放,设置显示动效event的闭包函数,即起点状态到终点状态为透明度opacityValue和大小scaleValue从0到1,实现Logo的渐出和放大效果。
```ts
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
duration: 1000,
curve: this.curve1,
delay: 100,
}, () => {
this.opacityValue = 1
this.scaleValue = 1
})
})
}
}
```
5. 闪屏动画播放结束后定格1s,进入FoodCategoryList页面。设置animateTo的onFinish回调接口,调用定时器Timer的setTimeout接口延时1s后,调用router.replace,显示FoodCategoryList页面。
```ts
import router from '@ohos.router'
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
duration: 1000,
curve: this.curve1,
delay: 100,
onFinish: () => {
setTimeout(() => {
router.replaceUrl({ url: "pages/FoodCategoryList" })
}, 1000);
}
}, () => {
this.opacityValue = 1
this.scaleValue = 1
})
})
}
}
```
整体代码如下。
```ts
import Curves from '@ohos.curves'
import router from '@ohos.router'
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
private pathCommands1: string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
private pathCommands2: string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
.stroke(Color.Transparent)
Path()
.commands(this.pathCommands1)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
Path()
.commands(this.pathCommands2)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 50,
colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]]
})
.clip(new Path().commands(this.pathCommands2))
}
.height('630px')
.width('630px')
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
duration: 1000,
curve: this.curve1,
delay: 100,
onFinish: () => {
setTimeout(() => {
router.replaceUrl({ url: "pages/FoodCategoryList" })
}, 1000);
}
}, () => {
this.opacityValue = 1
this.scaleValue = 1
})
})
Text('Healthy Diet')
.fontSize(26)
.fontColor(Color.White)
.margin({ top: 300 })
Text('Healthy life comes from a balanced diet')
.fontSize(17)
.fontColor(Color.White)
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![animation-feature](figures/animation-feature.gif)
## 页面转场动画
食物分类列表页和食物详情页之间的共享元素转场,即点击FoodListItem/FoodGridItem后,食物缩略图会放大,随着页面跳转,到食物详情页的大图。
1. 设置FoodListItem和FoodGridItem的Image组件的共享元素转场方法(sharedTransition)。转场id为foodItem.id,转场动画时长为1s,延时0.1s播放,变化曲线为贝塞尔曲线Curves.cubicBezier(0.2, 0.2, 0.1, 1.0) ,需引入curves模块。
共享转场时会携带当前元素的被设置的属性,所以创建Row组件,使其作为Image的父组件,设置背景颜色在Row上。
在FoodListItem的Image组件上设置autoResize为false,因为image组件默认会根据最终展示的区域,去调整图源的大小,以优化图片渲染性能。在转场动画中,图片在放大的过程中会被重新加载,所以为了转场动画的流畅,autoResize设置为false。
```ts
// FoodList.ets
import Curves from '@ohos.curves'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Navigator({ target: 'pages/FoodDetail' }) {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Row() {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.autoResize(false)
.height(40)
.width(40)
.sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
}
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
}
.params({ foodData: this.foodItem })
.margin({ right: 24, left:32 })
}
}
@Component
struct FoodGridItem {
private foodItem: FoodData
build() {
Column() {
Row() {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.autoResize(false)
.height(152)
.width('100%')
.sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
}
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
.padding({ left: 8 })
Text(this.foodItem.calories + 'kcal')
.fontSize(14)
.margin({ right: 6 })
}
.height(32)
.width('100%')
.backgroundColor('#FFe5e5e5')
}
.height(184)
.width('100%')
.onClick(() => {
router.pushUrl({ url: 'pages/FoodDetail', params: { foodData: this.foodItem } })
})
}
}
```
2. 设置FoodDetail页面的FoodImageDisplay的Image组件的共享元素转场方法(sharedTransition)。设置方法同上。
```ts
import Curves from '@ohos.curves'
@Component
struct FoodImageDisplay {
private foodItem: FoodData
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
Text(this.foodItem.name)
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
.height(357)
}
}
```
![animation-feature1](figures/animation-feature1.gif)
通过对绘制组件和动画的学习,我们已完成了启动Logo的绘制、启动页动画和页面间的转场动画,声明式UI框架提供了丰富的动效接口,合理地应用和组合可以让应用更具有设计感。
# 构建食物分类Grid布局
健康饮食应用在主页提供给用户两种食物显示方式:列表显示和网格显示。开发者将实现通过页签切换不同食物分类的网格布局。
1. 将Category枚举类型引入FoodCategoryList页面。
```ts
import { Category, FoodData } from '../model/FoodData'
```
2. 创建FoodCategoryList和FoodCategory组件,其中FoodCategoryList作为新的页面入口组件,在入口组件调用initializeOnStartup方法。
```ts
@Component
struct FoodList {
private foodItems: FoodData[]
build() {
......
}
}
@Component
struct FoodCategory {
private foodItems: FoodData[]
build() {
......
}
}
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
......
}
}
```
3. 在FoodCategoryList组件内创建showList成员变量,用于控制List布局和Grid布局的渲染切换。需要用到条件渲染语句if...else...。
```ts
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
private showList: boolean = false
build() {
Stack() {
if (this.showList) {
FoodList({ foodItems: this.foodItems })
} else {
FoodCategory({ foodItems: this.foodItems })
}
}
}
}
```
4. 在页面右上角创建切换List/Grid布局的图标。设置Stack对齐方式为顶部尾部对齐TopEnd,创建Image组件,设置其点击事件,即showList取反。
```ts
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
private showList: boolean = false
build() {
Stack({ alignContent: Alignment.TopEnd }) {
if (this.showList) {
FoodList({ foodItems: this.foodItems })
} else {
FoodCategory({ foodItems: this.foodItems })
}
Image($r('app.media.Switch'))
.height(24)
.width(24)
.margin({ top: 15, right: 10 })
.onClick(() => {
this.showList = !this.showList
})
}.height('100%')
}
}
```
5. 添加@State装饰器。点击右上角的switch标签后,页面没有任何变化,这是因为showList不是有状态数据,它的改变不会触发页面的刷新。需要为其添加\@State装饰器,使其成为状态数据,它的改变会引起其所在组件的重新渲染。
```ts
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
@State private showList: boolean = false
build() {
Stack({ alignContent: Alignment.TopEnd }) {
if (this.showList) {
FoodList({ foodItems: this.foodItems })
} else {
FoodCategory({ foodItems: this.foodItems })
}
Image($r('app.media.Switch'))
.height(24)
.width(24)
.margin({ top: 15, right: 10 })
.onClick(() => {
this.showList = !this.showList
})
}.height('100%')
}
}
```
点击切换图标,FoodList组件出现,再次点击,FoodList组件消失。
![zh-cn_image_0000001170411978](figures/zh-cn_image_0000001170411978.gif)
6. 创建显示所有食物的页签(All)。在FoodCategory组件内创建Tabs组件和其子组件TabContent,设置tabBar为All。设置TabBars的宽度为280,布局模式为Scrollable,即超过总长度后可以滑动。Tabs是一种可以通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图TabContent。
```ts
@Component
struct FoodCategory {
private foodItems: FoodData[]
build() {
Stack() {
Tabs() {
TabContent() {}.tabBar('All')
}
.barWidth(280)
.barMode(BarMode.Scrollable)
}
}
}
```
![zh-cn_image_0000001204538065](figures/zh-cn_image_0000001204538065.png)
7. 创建FoodGrid组件,作为TabContent的子组件。
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
build() {}
}
@Component
struct FoodCategory {
private foodItems: FoodData[]
build() {
Stack() {
Tabs() {
TabContent() {
FoodGrid({ foodItems: this.foodItems })
}.tabBar('All')
}
.barWidth(280)
.barMode(BarMode.Scrollable)
}
}
}
```
8. 实现2 \* 6的网格布局(一共12个食物数据资源)。创建Grid组件,设置列数columnsTemplate('1fr 1fr'),行数rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr'),行间距和列间距rowsGap和columnsGap为8。创建Scroll组件,使其可以滑动。
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
}
.scrollBar(BarState.Off)
.padding({left: 16, right: 16})
}
}
```
9. 创建FoodGridItem组件,展示食物图片、名称和卡路里,实现其UI布局,为GridItem的子组件。每个FoodGridItem高度为184,行间距为8,设置Grid总高度为(184 + 8) \* 6 - 8 = 1144。
```ts
@Component
struct FoodGridItem {
private foodItem: FoodData
build() {
Column() {
Row() {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(152)
.width('100%')
}
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
.padding({ left: 8 })
Text(this.foodItem.calories + 'kcal')
.fontSize(14)
.margin({ right: 6 })
}
.height(32)
.width('100%')
.backgroundColor('#FFe5e5e5')
}
.height(184)
.width('100%')
}
}
@Component
struct FoodGrid {
private foodItems: FoodData[]
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {
FoodGridItem({foodItem: item})
}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.height(1144)
}
.scrollBar(BarState.Off)
.padding({ left: 16, right: 16 })
}
}
```
![zh-cn_image_0000001170167520](figures/zh-cn_image_0000001170167520.png)
10. 创建展示蔬菜(Category.Vegetable)、水果(Category.Fruit)、坚果(Category.Nut)、海鲜(Category.SeaFood)和甜品(Category.Dessert)分类的页签。
```ts
@Component
struct FoodCategory {
private foodItems: FoodData[]
build() {
Stack() {
Tabs() {
TabContent() {
FoodGrid({ foodItems: this.foodItems })
}.tabBar('All')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Vegetable)) })
}.tabBar('Vegetable')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Fruit)) })
}.tabBar('Fruit')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Nut)) })
}.tabBar('Nut')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Seafood)) })
}.tabBar('Seafood')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Dessert)) })
}.tabBar('Dessert')
}
.barWidth(280)
.barMode(BarMode.Scrollable)
}
}
}
```
11. 设置不同食物分类的Grid的行数和高度。因为不同分类的食物数量不同,所以不能用'1fr 1fr 1fr 1fr 1fr 1fr '常量来统一设置成6行。
创建gridRowTemplate和HeightValue成员变量,通过成员变量设置Grid行数和高度。
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
private gridRowTemplate: string = ''
private heightValue: number
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {
FoodGridItem({ foodItem: item })
}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate(this.gridRowTemplate)
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.height(this.heightValue)
}
.scrollBar(BarState.Off)
.padding({ left: 16, right: 16 })
}
}
```
调用aboutToAppear接口计算行数(gridRowTemplate )和高度(heightValue)。
```ts
aboutToAppear() {
var rows = Math.round(this.foodItems.length / 2);
this.gridRowTemplate = '1fr '.repeat(rows);
this.heightValue = rows * 192 - 8;
}
```
自定义组件提供了两个生命周期的回调接口aboutToAppear和aboutToDisappear。aboutToAppear的执行时机在创建自定义组件后,执行自定义组件build方法之前。aboutToDisappear在自定义组件销毁之前的时机执行。
![zh-cn_image_0000001215113569](figures/zh-cn_image_0000001215113569.png)
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
private gridRowTemplate: string = ''
private heightValue: number
aboutToAppear() {
var rows = Math.round(this.foodItems.length / 2);
this.gridRowTemplate = '1fr '.repeat(rows);
this.heightValue = rows * 192 - 8;
}
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {
FoodGridItem({ foodItem: item })
}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate(this.gridRowTemplate)
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.height(this.heightValue)
}
.scrollBar(BarState.Off)
.padding({ left: 16, right: 16 })
}
}
```
![zh-cn_image_0000001170008198](figures/zh-cn_image_0000001170008198.gif)
\ No newline at end of file
# 构建食物列表List布局
使用List组件和ForEach循环渲染,构建食物列表布局。
1. 在pages目录新建页面FoodCategoryList.ets,将index.ets改名为FoodDetail.ets。
2. 新建FoodList组件作为页面入口组件,FoodListItem为其子组件。List组件是列表组件,适用于重复同类数据的展示,其子组件为ListItem,适用于展示列表中的单元。
```ts
@Component
struct FoodListItem {
build() {}
}
@Entry
@Component
struct FoodList {
build() {
List() {
ListItem() {
FoodListItem()
}
}
}
}
```
3. 引入FoodData类和initializeOnStartup方法。
应用代码中文件访问方法主要有下面两种:
- **相对路径**:使用相对路径引用代码文件,以"../"访问上一级目录,以"./"访问当前目录,也可以省略不写。
- **绝对路径**:使用当前模块根路径引用代码文件,比如:common/utils/utils。
这里使用相对路径访问:
```
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
```
4. FoodList和FoodListItem组件数值传递。在FoodList组件内创建类型为FoodData[]成员变量foodItems,调用initializeOnStartup方法为其赋值。在FoodListItem组件内创建类型为FoodData的成员变量foodItem。将父组件foodItems数组的第一个元素的foodItems[0]作为参数传递给FoodListItem。
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {}
}
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
List() {
ListItem() {
FoodListItem({ foodItem: this.foodItems[0] })
}
}
}
}
```
5. 声明子组件FoodListItem 的UI布局。创建Flex组件,包含食物图片缩略图,食物名称,和食物对应的卡路里。
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
.margin({ right: 24, left:32 })
}
}
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
List() {
ListItem() {
FoodListItem({ foodItem: this.foodItems[0] })
}
}
}
}
```
![zh-cn_image_0000001204776353](figures/zh-cn_image_0000001204776353.png)
6. 创建两个FoodListItem。在List组件创建两个FoodListItem,分别给FoodListItem传递foodItems数组的第一个元素this.foodItems[0]和第二个元素foodItem: this.foodItems[1]。
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
.margin({ right: 24, left: 32 })
}
}
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
List() {
ListItem() {
FoodListItem({ foodItem: this.foodItems[0] })
}
ListItem() {
FoodListItem({ foodItem: this.foodItems[1] })
}
}
}
}
```
![zh-cn_image1_0000001204776353](figures/zh-cn_image1_0000001204776353.png)
7. 单独创建每一个FoodListItem肯定是不合理的,这就需要引入[ForEach循环渲染](../quick-start/arkts-rendering-control.md#循环渲染)
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
.margin({ right: 24, left:32 })
}
}
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
List() {
ForEach(this.foodItems, item => {
ListItem() {
FoodListItem({ foodItem: item })
}
}, item => item.id.toString())
}
}
}
```
8. 添加FoodList标题。
```
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
Column() {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text('Food List')
.fontSize(20)
.margin({ left: 20 })
}
.height('7%')
.backgroundColor('#FFf1f3f5')
List() {
ForEach(this.foodItems, item => {
ListItem() {
FoodListItem({ foodItem: item })
}
}, item => item.id.toString())
}
.height('93%')
}
}
}
```
![zh-cn_image_0000001169678922](figures/zh-cn_image_0000001169678922.png)
\ No newline at end of file
# 构建食物数据模型
在创建视图中,我们逐一去表述食物的各个信息,如食物名称、卡路里、蛋白质、脂肪、碳水和维生素C。这样的编码形式在实际的开发中肯定是不切实际的,所以要创建食物数据模型来统一存储和管理数据。
![zh-cn_image_0000001215433095](figures/zh-cn_image_0000001215433095.png)
1. 新建model文件夹,在model目录下创建FoodData.ets。
![zh-cn_image_0000001195119619](figures/zh-cn_image_0000001195119619.png)
2. 定义食物数据的存储模型FoodData和枚举变量Category,FoodData类包含食物id、名称(name)、分类(category)、图片(image)、热量(calories)、蛋白质(protein)、脂肪(fat)、碳水(carbohydrates)和维生素C(vitaminC)属性。
ArkTS语言是在ts语言的基础上的扩展,同样支持ts语法。
```ts
enum Category {
Fruit,
Vegetable,
Nut,
Seafood,
Dessert
}
let NextId = 0;
class FoodData {
id: string;
name: string;
image: Resource;
category: Category;
calories: number;
protein: number;
fat: number;
carbohydrates: number;
vitaminC: number;
constructor(name: string, image: Resource, category: Category, calories: number, protein: number, fat: number, carbohydrates: number, vitaminC: number) {
this.id = `${ NextId++ }`;
this.name = name;
this.image = image;
this.category = category;
this.calories = calories;
this.protein = protein;
this.fat = fat;
this.carbohydrates = carbohydrates;
this.vitaminC = vitaminC;
}
}
```
3. 存入食物图片资源。在resources &gt;base&gt; media目录下存入食物图片资源,图片名称为食物名称。
4. 创建食物资源数据。在model文件夹下创建FoodDataModels.ets,在该页面中声明食物成分数组FoodComposition。以下示例创建了两个食物数据。
```ts
const FoodComposition: any[] = [
{ 'name': 'Tomato', 'image': $r('app.media.Tomato'), 'category': Category.Vegetable, 'calories': 17, 'protein': 0.9, 'fat': 0.2, 'carbohydrates': 3.9, 'vitaminC': 17.8 },
{ 'name': 'Walnut', 'image': $r('app.media.Walnut'), 'category': Category.Nut, 'calories': 654 , 'protein': 15, 'fat': 65, 'carbohydrates': 14, 'vitaminC': 1.3 }
]
```
实际开发中,开发者可以自定义更多的数据资源,当食物资源很多时,建议使用[数据懒加载LazyForEach](../quick-start/arkts-rendering-control.md#数据懒加载)
5. 创建initializeOnStartUp方法来初始化FoodData的数组。在FoodDataModels.ets中使用了定义在FoodData.ets的FoodData和Category,所以要将FoodData.ets的FoodData类export,在FoodDataModels.ets内import FoodData和Category。
```ts
// FoodData.ets
export enum Category {
......
}
export class FoodData {
......
}
// FoodDataModels.ets
import { Category, FoodData } from './FoodData'
export function initializeOnStartup(): Array<FoodData> {
let FoodDataArray: Array<FoodData> = []
FoodComposition.forEach(item => {
FoodDataArray.push(new FoodData(item.name, item.image, item.category, item.calories, item.protein, item.fat, item.carbohydrates, item.vitaminC ));
})
return FoodDataArray;
}
```
已完成好健康饮食应用的数据资源准备,接下来将通过加载这些数据来创建食物列表页面。
## 相关实例
针对构建食物分类列表页面和食物详情页,有以下相关实例可供参考:
- [DefiningPageLayoutAndConnection:页面布局和连接(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/DefiningPageLayoutAndConnection)
# 常用组件说明
组件是构建页面的核心,每个组件通过对数据和方法的简单封装,实现独立的可视、可交互功能单元。组件之间相互独立,随取随用,也可以在需求相同的地方重复使用。
声明式开发范式目前可供选择的组件如下表所示。
| 组件类型 | 主要组件 |
| ---------------------------- | ---------------------------------------------------------- |
| [基础组件](../reference/arkui-ts/ts-basic-components-blank.md) | Blank、Button、Checkbox、CheckboxGroup、DataPanel、DatePicker、Divider、Gauge、Image、ImageAnimator、LoadingProgress、Marquee、Navigation、PatternLock、PluginComponent、Progress、QRCode、Radio、Rating、RemoteWindow、RichText、ScrollBar、Search、Select、Slider、Span、Stepper、StepperItem、Text、TextArea、TextClock、TextInput、TextPicker、TextTimer、TimePicker、Toggle、Web、XComponent |
| [容器组件](../reference/arkui-ts/ts-container-ability-component.md) | AbilityComponent、AlphabetIndexer、Badge、Column、ColumnSplit、Counter、Flex、FlowItem、GridContainer、GridCol、GridRow、Grid、GridItem、List、ListItem、ListItemGroup、Navigator、Panel、Refresh、RelativeContainer、Row、RowSplit、Scroll、SideBarContainer、Stack、Swiper、Tabs、TabContent |
| [媒体组件](../reference/arkui-ts/ts-media-components-video.md) | Video |
| [绘制组件](../reference/arkui-ts/ts-drawing-components-circle.md) | Circle、Ellipse、Line、Polyline、Polygon、Path、Rect、Shape |
| [画布组件](../reference/arkui-ts/ts-components-canvas-canvas.md) | Canvas |
## 相关实例
基于ArkTS的常用组件开发,有以下相关实例可供参考:
- [`Shopping`:购物示例应用(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/AppSample/Shopping)
- [`Canvas`:画布组件(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/Canvas)
- [`Clock`:简单时钟(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/Preset/Clock)
- [`PatternLock`:图案密码锁组件(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/PatternLock)
- [`Search`:Search组件(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/Search)
- [`QRCode`:二维码(ArkTS)(API9)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/QRCode)
- [`Gallery`:组件集合(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/Gallery)
- [List组件的使用之商品列表(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/List_HDC)
- [List组件的使用之设置项(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/List_HDC)
# Web
Web是提供网页显示能力的组件,具体用法请参考 [Web API](../reference/arkui-ts/ts-basic-components-web.md)
## 创建组件
在main/ets/MainAbility/pages目录下的ets文件中创建一个Web组件。在web组件中通过src指定引用的网页路径,controller为组件的控制器,通过controller绑定Web组件,用于调用Web组件的方法。
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Web({ src: 'https://www.example.com', controller: this.controller })
}
}
}
```
## 设置样式和属性
Web组件的使用需要添加丰富的页面样式和功能属性。设置height、padding样式可为Web组件添加高和内边距,设置fileAccess属性可为Web组件添加文件访问权限,设置javaScriptAccess属性为true使Web组件具有执行JavaScript代码的能力。
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
fileAccess: boolean = true;
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Text('Hello world!')
.fontSize(20)
Web({ src: 'https://www.example.com', controller: this.controller })
// 设置高和内边距
.height(500)
.padding(20)
// 设置文件访问权限和脚本执行权限
.fileAccess(this.fileAccess)
.javaScriptAccess(true)
Text('End')
.fontSize(20)
}
}
}
```
## 添加事件和方法
为Web组件添加onProgressChange事件,该事件回调Web引擎加载页面的进度值。将该进度值赋值给Progress组件的value,控制Progress组件的状态。当进度为100%时隐藏Progress组件,Web页面加载完成。
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
@State progress: number = 0;
@State hideProgress: boolean = true;
fileAccess: boolean = true;
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Text('Hello world!')
.fontSize(20)
Progress({value: this.progress, total: 100})
.color('#0000ff')
.visibility(this.hideProgress ? Visibility.None : Visibility.Visible)
Web({ src: 'https://www.example.com', controller: this.controller })
.fileAccess(this.fileAccess)
.javaScriptAccess(true)
.height(500)
.padding(20)
// 添加onProgressChange事件
.onProgressChange(e => {
this.progress = e.newProgress;
// 当进度100%时,进度条消失
if (this.progress === 100) {
this.hideProgress = true;
} else {
this.hideProgress = false;
}
})
Text('End')
.fontSize(20)
}
}
}
```
在onPageEnd事件中添加runJavaScript方法。onPageEnd事件是网页加载完成时的回调,runJavaScript方法可以执行HTML中的JavaScript脚本。当页面加载完成时,触发onPageEnd事件,调用HTML文件中的test方法,在控制台打印信息。
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
@State progress: number = 0;
@State hideProgress: boolean = true;
@State webResult: string = ''
fileAccess: boolean = true;
// 定义Web组件的控制器controller
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Text('Hello world!')
.fontSize(20)
Progress({value: this.progress, total: 100})
.color('#0000ff')
.visibility(this.hideProgress ? Visibility.None : Visibility.Visible)
// 初始化Web组件,并绑定controller
Web({ src: $rawfile('index.html'), controller: this.controller })
.fileAccess(this.fileAccess)
.javaScriptAccess(true)
.height(500)
.padding(20)
.onProgressChange(e => {
this.progress = e.newProgress;
if (this.progress === 100) {
this.hideProgress = true;
} else {
this.hideProgress = false;
}
})
.onPageEnd(e => {
// test()在index.html中定义
try {
this.controller.runJavaScript(
'test()',
(error, result) => {
if (error) {
console.info(`run JavaScript error: ` + JSON.stringify(error))
return;
}
if (result) {
this.webResult = result
console.info(`The test() return value is: ${result}`)
}
});
console.info('url: ', e.url);
} catch (error) {
console.error(`ErrorCode: ${error.code}, Message: ${error.message}`);
}
})
Text('End')
.fontSize(20)
}
}
}
```
在main/resources/rawfile目录下创建一个HTML文件。
```html
<!-- index.html -->
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<body>
Hello world!
</body>
<script type="text/javascript">
function test() {
console.info('Ark Web Component');
}
</script>
</html>
```
## 开启网页调试
在PC上启用端口转发,以及设置Web组件属性webDebuggingAccess为true后,便可以在PC上调试通过USB连接的开发设备上的Web组件里的网页。
设置步骤如下:
1、首先设置Web组件属性webDebuggingAccess为true。
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Web({ src: 'www.example.com', controller: this.controller })
.webDebuggingAccess(true) // true表示启用调试功能
}
}
}
```
2、PC上启用端口转发功能,添加TCP端口9222映射。
```ts
hdc fport tcp:9222 tcp:9222
```
添加是否成功可以通过如下命令来查看已存在的映射关系表。
```ts
hdc fport ls
```
如上设置完成后,首先打开应用Web组件、访问要调试的网页,然后在PC上使用chrome浏览器访问:http://localhost:9222, 就可以在PC上调试开发设备刚才访问的网页。
## 场景示例
该场景实现了Web组件中视频的动态播放。首先在HTML页面内嵌入视频资源,再使用Web组件的控制器调用onActive和onInactive方法激活和暂停页面渲染。点击onInactive按钮,Web页面停止渲染,视频暂停播放;点击onActive按钮,激活Web组件,视频继续播放。
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Row() {
Button('onActive').onClick(() => {
console.info("Web Component onActive");
try {
this.controller.onActive();
} catch (error) {
console.error(`Errorcode: ${error.code}, Message: ${error.message}`);
}
})
Button('onInactive').onClick(() => {
console.info("Web Component onInactive");
try {
this.controller.onInactive();
} catch (error) {
console.error(`Errorcode: ${error.code}, Message: ${error.message}`);
}
})
}
Web({ src: $rawfile('index.html'), controller: this.controller })
.fileAccess(true)
}
}
}
```
```html
<!-- index.html -->
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<body>
<video width="320" height="240" controls="controls" muted="muted" autoplay="autoplay">
<!-- test.mp4视频文件放在main/resources/rawfile目录下 -->
<source src="test.mp4" type="video/mp4">
</video>
</body>
</html>
```
## 相关实例
针对Web开发,有以下相关实例可供参考:
- [`Web`:Web(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/Web)
- [Web组件的使用(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/WebCookie)
- [Web组件抽奖案例(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/WebComponent)
\ No newline at end of file
# 创建简单视图
在这一小节中,我们将开始食物详情页的开发,学习如何通过容器组件Stack、Flex和基础组件Image、Text,构建用户自定义组件,完成图文并茂的食物介绍。
在创建页面前,请先创建ArkTS工程,Stage模型请参考[创建Stage模型的ArkTS工程](../quick-start/start-with-ets-stage.md#创建arkts工程),FA模型请参考[创建FA模型的ArkTS工程](../quick-start/start-with-ets-fa.md#创建arkts工程)
## 构建Stack布局
1. 创建食物名称。
在index.ets文件中,创建Stack组件,将Text组件放进Stack组件的花括号中,使其成为Stack组件的子组件。Stack组件为堆叠组件,可以包含一个或多个子组件,其特点是后一个子组件覆盖前一个子组件。
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack() {
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
![zh-cn_image_0000001214128687](figures/zh-cn_image_0000001214128687.png)
2. 食物图片展示。
创建Image组件,指定Image组件的url,Image组件是必选构造参数组件。为了让Text组件在Image组件上方显示,所以要先声明Image组件。图片资源放在resources下的rawfile文件夹内,引用rawfile下资源时使用`$rawfile('filename')`的形式,filename为rawfile目录下的文件相对路径。当前`$rawfile`仅支持Image控件引用图片资源。
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack() {
Image($rawfile('Tomato.png'))
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
![zh-cn_image_0000001168410342](figures/zh-cn_image_0000001168410342.png)
3. 通过资源访问图片。
除指定图片路径外,也可以使用引用媒体资源符$r引用资源,需要遵循resources文件夹的资源限定词的规则。右键resources文件夹,点击New&gt;Resource Directory,选择Resource Type为Media(图片资源)。
将Tomato.png放入media文件夹内。就可以通过`$r('app.type.name')`的形式引用应用资源,即`$r('app.media.Tomato')`
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack() {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
4. 设置Image宽高,并且将image的objectFit属性设置为ImageFit.Contain,即保持图片长宽比的情况下,使得图片完整地显示在边界内。
如果Image填满了整个屏幕,原因如下:
1. Image没有设置宽高。
2. Image的objectFit默认属性是ImageFit.Cover,即在保持长宽比的情况下放大或缩小,使其填满整个显示边界。
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack() {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
![zh-cn_image_0000001214210217](figures/zh-cn_image_0000001214210217.png)
5. 设置食物图片和名称布局。设置Stack的对齐方式为底部起始端对齐,Stack默认为居中对齐。设置Stack构造参数alignContent为Alignment.BottomStart。其中Alignment和FontWeight一样,都是框架提供的内置枚举类型。
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
![zh-cn_image_0000001168728872](figures/zh-cn_image_0000001168728872.png)
6. 调整Text组件的外边距margin,使其距离左侧和底部有一定的距离。margin是简写属性,可以统一指定四个边的外边距,也可以分别指定。具体设置方式如下:
1. 参数为Length时,即统一指定四个边的外边距,比如margin(20),即上、右、下、左四个边的外边距都是20。
2. 参数为{top?: Length, right?: Length, bottom?: Length, left?:Length},即分别指定四个边的边距,比如margin({ left: 26, bottom: 17.4 }),即左边距为26,下边距为17.4。
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
.margin({left: 26, bottom: 17.4})
}
}
}
```
![zh-cn_image_0000001213968747](figures/zh-cn_image_0000001213968747.png)
7. 调整组件间的结构,语义化组件名称。创建页面入口组件为FoodDetail,在FoodDetail中创建Column,设置水平方向上居中对齐 alignItems(HorizontalAlign.Center)。MyComponent组件名改为FoodImageDisplay,为FoodDetail的子组件。
Column是子组件竖直排列的容器组件,本质为线性布局,所以只能设置交叉轴方向的对齐。
```ts
@Component
struct FoodImageDisplay {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
.height(357)
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
}
.alignItems(HorizontalAlign.Center)
}
}
```
## 构建Flex布局
开发者可以使用Flex弹性布局来构建食物的食物成分表,弹性布局在本场景的优势在于可以免去多余的宽高计算,通过比例来设置不同单元格的大小,更加灵活。
1. 创建ContentTable组件,使其成为页面入口组件FoodDetail的子组件。
```ts
@Component
struct FoodImageDisplay {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
}
}
@Component
struct ContentTable {
build() {
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
2. 创建Flex组件展示Tomato两类成分。
一类是热量Calories,包含卡路里(Calories);一类是营养成分Nutrition,包含蛋白质(Protein)、脂肪(Fat)、碳水化合物(Carbohydrates)和维生素C(VitaminC)。
先创建热量这一类。创建Flex组件,高度为280,上、右、左内边距为30,包含三个Text子组件分别代表类别名(Calories),含量名称(Calories)和含量数值(17kcal)。Flex组件默认为水平排列方式。
已省略FoodImageDisplay代码,只针对ContentTable进行扩展。
```ts
@Component
struct ContentTable {
build() {
Flex() {
Text('Calories')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
Text('Calories')
.fontSize(17.4)
Text('17kcal')
.fontSize(17.4)
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
![zh-cn_image_0000001169759552](figures/zh-cn_image_0000001169759552.png)
3. 调整布局,设置各部分占比。分类名占比(layoutWeight)为1,成分名和成分含量一共占比(layoutWeight)2。成分名和成分含量位于同一个Flex中,成分名占据所有剩余空间flexGrow(1)。
```ts
@Component
struct FoodImageDisplay {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
}
}
@Component
struct ContentTable {
build() {
Flex() {
Text('Calories')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('Calories')
.fontSize(17.4)
.flexGrow(1)
Text('17kcal')
.fontSize(17.4)
}
.layoutWeight(2)
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
![zh-cn_image_0000001215079443](figures/zh-cn_image_0000001215079443.png)
4. 仿照热量分类创建营养成分分类。营养成分部分(Nutrition)包含:蛋白质(Protein)、脂肪(Fat)、碳水化合物(Carbohydrates)和维生素C(VitaminC)四个成分,后三个成分在表格中省略分类名,用空格代替。
设置外层Flex为竖直排列FlexDirection.Column, 在主轴方向(竖直方向)上等距排列FlexAlign.SpaceBetween,在交叉轴方向(水平轴方向)上首部对齐排列ItemAlign.Start。
```ts
@Component
struct ContentTable {
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
Flex() {
Text('Calories')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('Calories')
.fontSize(17.4)
.flexGrow(1)
Text('17kcal')
.fontSize(17.4)
}
.layoutWeight(2)
}
Flex() {
Text('Nutrition')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('Protein')
.fontSize(17.4)
.flexGrow(1)
Text('0.9g')
.fontSize(17.4)
}
.layoutWeight(2)
}
Flex() {
Text(' ')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('Fat')
.fontSize(17.4)
.flexGrow(1)
Text('0.2g')
.fontSize(17.4)
}
.layoutWeight(2)
}
Flex() {
Text(' ')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('Carbohydrates')
.fontSize(17.4)
.flexGrow(1)
Text('3.9g')
.fontSize(17.4)
}
.layoutWeight(2)
}
Flex() {
Text(' ')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('vitaminC')
.fontSize(17.4)
.flexGrow(1)
Text('17.8mg')
.fontSize(17.4)
}
.layoutWeight(2)
}
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
![zh-cn_image_0000001215199399](figures/zh-cn_image_0000001215199399.png)
5. 使用自定义构造函数\@Builder简化代码。可以发现,每个成分表中的成分单元其实都是一样的UI结构。
![zh-cn_image_0000001169599582](figures/zh-cn_image_0000001169599582.png)
当前对每个成分单元都进行了声明,造成了代码的重复和冗余。可以使用\@Builder来构建自定义方法,抽象出相同的UI结构声明。\@Builder修饰的方法和Component的build方法都是为了声明一些UI渲染结构,遵循一样的ArkTS语法。开发者可以定义一个或者多个\@Builder修饰的方法,但Component的build方法必须只有一个。
在ContentTable内声明\@Builder修饰的IngredientItem方法,用于声明分类名、成分名称和成分含量UI描述。
```ts
@Component
struct ContentTable {
@Builder IngredientItem(title:string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex({ alignItems: ItemAlign.Center }) {
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
}
```
在ContentTable的build方法内调用IngredientItem接口,需要用this去调用该Component作用域内的方法,以此来区分全局的方法调用。
```ts
@Component
struct ContentTable {
......
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
this.IngredientItem('Calories', 'Calories', '17kcal')
this.IngredientItem('Nutrition', 'Protein', '0.9g')
this.IngredientItem('', 'Fat', '0.2g')
this.IngredientItem('', 'Carbohydrates', '3.9g')
this.IngredientItem('', 'VitaminC', '17.8mg')
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
```
ContentTable组件整体代码如下。
```ts
@Component
struct ContentTable {
@Builder IngredientItem(title:string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
this.IngredientItem('Calories', 'Calories', '17kcal')
this.IngredientItem('Nutrition', 'Protein', '0.9g')
this.IngredientItem('', 'Fat', '0.2g')
this.IngredientItem('', 'Carbohydrates', '3.9g')
this.IngredientItem('', 'VitaminC', '17.8mg')
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
![zh-cn_image_0000001215199399](figures/zh-cn_image_0000001215199399.png)
通过学习Stack布局和Flex布局已完成食物的图文展示和营养成分表,构建出第一个普通视图的食物详情页。在下一个章节中,将开发食物分类列表页,并完成食物分类列表页面和食物详情页面的跳转和数据传递,现在我们就进入下一个章节的学习吧。
## 相关实例
针对创建简单视图,有以下示例工程可供参考:
- [`BuildCommonView`:创建简单视图(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/BuildCommonView)
本示例为构建了简单页面展示食物番茄的图片和营养信息,主要为了展示简单页面的Stack布局和Flex布局。
# 自定义组件的生命周期
自定义组件的生命周期回调函数用于通知用户该自定义组件的生命周期,这些回调函数是私有的,在运行时由开发框架在特定的时间进行调用,不能从应用程序中手动调用这些回调函数。
> **说明:**
>
> - 允许在生命周期函数中使用Promise和异步回调函数,比如网络资源获取,定时器设置等;
>
> - 不允许在生命周期函数中使用async await。
## aboutToAppear
aboutToAppear?(): void
aboutToAppear函数在创建自定义组件的新实例后,在执行其build函数之前执行。允许在aboutToAppear函数中改变状态变量,更改将在后续执行build函数中生效。
从API version 9开始,该接口支持在ArkTS卡片中使用。
## aboutToDisappear
aboutToDisappear?(): void
aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。
从API version 9开始,该接口支持在ArkTS卡片中使用。
**示例1:**
```ts
// xxx.ets
@Entry
@Component
struct CountDownTimerComponent {
@State countDownFrom: number = 10
private timerId: number = -1
aboutToAppear(): void {
this.timerId = setInterval(() => {
if (this.countDownFrom <= 1) {
clearTimeout(this.timerId) // countDownFrom小于等于1时清除计时器
}
this.countDownFrom -= 1
}, 1000) // countDownFrom每1s减1
}
aboutToDisappear(): void {
if (this.timerId > 0) {
clearTimeout(this.timerId)
this.timerId = -1
}
}
build() {
Column() {
Text(`${this.countDownFrom} sec left`)
.fontSize(30)
.margin(30)
}.width('100%')
}
}
```
![aboutToAppear](figures/aboutToAppear.gif)
上述示例表明,生命周期函数对于允许CountDownTimerComponent管理其计时器资源至关重要,类似的函数也包括异步从网络请求加载资源。
## onPageShow
onPageShow?(): void
页面每次显示时触发一次,包括路由过程、应用进入前后台等场景,仅@Entry修饰的自定义组件生效。
## onPageHide
onPageHide?(): void
页面每次隐藏时触发一次,包括路由过程、应用进入前后台等场景,仅@Entry修饰的自定义组件生效。
## onBackPress
onBackPress?(): void
当用户点击返回按钮时触发,仅@Entry修饰的自定义组件生效。返回true表示页面自己处理返回逻辑,不进行页面路由,返回false表示使用默认的路由返回逻辑。不设置返回值按照false处理。
**示例2:**
```ts
// xxx.ets
@Entry
@Component
struct IndexComponent {
@State textColor: Color = Color.Black
onPageShow() {
this.textColor = Color.Blue
console.info('IndexComponent onPageShow')
}
onPageHide() {
this.textColor = Color.Transparent
console.info('IndexComponent onPageHide')
}
onBackPress() {
this.textColor = Color.Red
console.info('IndexComponent onBackPress')
}
build() {
Column() {
Text('Hello World')
.fontColor(this.textColor)
.fontSize(30)
.margin(30)
}.width('100%')
}
}
```
![lifecycle](figures/lifecycle.PNG)
## onLayout<sup>9+</sup>
onLayout?(children: Array\<LayoutChild\>, constraint: ConstraintSizeOptions): void
框架会在自定义组件布局时,将该自定义组件的子节点信息和自身的尺寸范围通过onLayout传递给该自定义组件。不允许在onLayout函数中改变状态变量。
该接口支持在ArkTS卡片中使用。
**参数:**
| 参数名 | 类型 | 说明 |
| ---------- | -------------------------------------------------------------------------------- | ---------------------- |
| children | Array\<[LayoutChild](#layoutchild9)\> | 子组件布局信息。 |
| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | 父组件constraint信息。 |
## onMeasure<sup>9+</sup>
onMeasure?(children: Array\<LayoutChild\>, constraint: ConstraintSizeOptions): void
框架会在自定义组件确定尺寸时,将该自定义组件的子节点信息和自身的尺寸范围通过onMeasure传递给该自定义组件。不允许在onMeasure函数中改变状态变量。
该接口支持在ArkTS卡片中使用。
**参数:**
| 参数名 | 类型 | 说明 |
| ---------- | -------------------------------------------------------------------------------- | ---------------------- |
| children | Array\<[LayoutChild](#layoutchild9)\> | 子组件布局信息。 |
| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | 父组件constraint信息。 |
## LayoutChild<sup>9+</sup>
子组件布局信息。
该接口支持在ArkTS卡片中使用。
| 参数 | 参数类型 | 描述 |
| ---------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| name | string | 子组件名称。 |
| id | string | 子组件id。 |
| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | 子组件约束尺寸。 |
| borderInfo | [LayoutBorderInfo](#layoutborderinfo9) | 子组件border信息。 |
| position | [Position](../reference/arkui-ts/ts-types.md#position) | 子组件位置坐标。 |
| measure | (childConstraint: [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions)) => void | 调用此方法对子组件的尺寸范围进行限制。 |
| layout | (LayoutInfo: [LayoutInfo](#layoutinfo9)) => void | 调用此方法对子组件的位置信息进行限制。 |
## LayoutBorderInfo<sup>9+</sup>
子组件border信息。
该接口支持在ArkTS卡片中使用。
| 参数 | 参数类型 | 描述 |
| ----------- | ---------------------------------------------------------- | ---------------------------------------------- |
| borderWidth | [EdgeWidths](../reference/arkui-ts/ts-types.md#edgewidths) | 边框宽度类型,用于描述组件边框不同方向的宽度。 |
| margin | [Margin](../reference/arkui-ts/ts-types.md#margin) | 外边距类型,用于描述组件不同方向的外边距。 |
| padding | [Padding](../reference/arkui-ts/ts-types.md#padding) | 内边距类型,用于描述组件不同方向的内边距。 |
## LayoutInfo<sup>9+</sup>
子组件layout信息。
该接口支持在ArkTS卡片中使用。
| 参数 | 参数类型 | 描述 |
| ---------- | -------------------------------------------------------------------------------- | ---------------- |
| position | [Position](../reference/arkui-ts/ts-types.md#position) | 子组件位置坐标。 |
| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | 子组件约束尺寸。 |
**示例3:**
```ts
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
CustomLayout() {
ForEach([1, 2, 3], (index) => {
Text('Sub' + index)
.fontSize(30)
.borderWidth(2)
})
}
}
}
}
@Component
struct CustomLayout {
@BuilderParam builder: () => {}
onLayout(children: Array<LayoutChild>, constraint: ConstraintSizeOptions) {
let pos = 0
children.forEach((child) => {
child.layout({ position: { x: pos, y: pos }, constraint: constraint })
pos += 100
})
}
onMeasure(children: Array<LayoutChild>, constraint: ConstraintSizeOptions) {
let size = 100
children.forEach((child) => {
child.measure({ minHeight: size, minWidth: size, maxWidth: size, maxHeight: size })
size += 50
})
}
build() {
this.builder()
}
}
```
![customlayout](figures/customLayout.png)
# 声明式UI开发指导
## 开发说明
| 任务 | 简介 | 相关资源 |
| ----------- | ---------------------------------------- | ---------------------------------------- |
| 准备开发环境 | 了解声明式UI的工程结构。<br>了解资源分类与访问。 | [OpenHarmony工程介绍](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ohos-project-overview-0000001218440650)<br>[资源分类与访问](../quick-start/resource-categories-and-access.md) |
| 学习ArkTS语言 | ArkTS是OpenHarmony优选的主力应用开发语言,当前,ArkTS在TS基础上主要扩展了声明式UI能力。 | [学习ArkTS语言](../quick-start/arkts-get-started.md) |
| 开发页面 | 根据页面的使用场景,选择合适的布局。<br>根据页面需要实现的内容,添加系统内置组件,并修改组件样式。<br>更新页面内容,丰富页面展现形式。 | [创建页面](#创建页面)<br> &nbsp;&nbsp;[常见布局开发指导](ui-ts-layout-linear.md)<br> &nbsp;&nbsp;[常见组件说明](ui-ts-components-intro.md)<br>[修改组件样式](#修改组件样式)<br>[更新页面内容](#更新页面内容) |
| (可选)页面多样化 | 绘图和动画。 | [绘图组件](../reference/arkui-ts/ts-drawing-components-circle.md)<br>[画布组件](../reference/arkui-ts/ts-components-canvas-canvas.md)<br>[动画](../reference/arkui-ts/ts-animatorproperty.md) |
| (可选)页面之间的跳转 | 使用页面路由实现多个页面之前的跳转。 | [页面路由](../reference/apis/js-apis-router.md) |
| (可选)性能提升 | 避免低性能代码对应用的性能造成负面影响。 | [性能提升的推荐方法](ui-ts-performance-improvement-recommendation.md) |
## 创建页面
请先根据页面预期效果选择布局结构创建页面,并在页面中添加基础的系统内置组件。下述示例采用了[弹性布局(Flex)](ui-ts-layout-flex.md),对页面中的Text组件进行横纵向居中布局显示。
```ts
// xxx.ets
@Entry
@Component
struct MyComponent {
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text('Hello')
}
.width('100%')
.height('100%')
}
}
```
## 修改组件样式
在页面中添加系统内置组件时,若不设置属性方法,则会显示其默认样式。通过更改组件的属性样式或者组件支持的[通用属性](../reference/arkui-ts/ts-universal-attributes-size.md)样式,改变组件的UI显示。
1. 通过修改Text组件的构造参数,将Text组件的显示内容修改为“Tomato”。
2. 修改Text组件的fontSize属性更改组件的字体大小,将字体大小设置为26,通过fontWeight属性更改字体粗细,将其设置为500。
```ts
// xxx.ets
@Entry
@Component
struct MyComponent {
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
.width('100%')
.height('100%')
}
}
```
![zh-cn_image_0000001168888224](figures/zh-cn_image_0000001168888224.png)
## 更新页面内容
在创建基本的页面之后,可根据组件的状态来更新页面内容。以下示例展示了简单的更新页面方法。
> **说明:**
>
> 更新组件的状态之前,请先初始化组件的成员变量。自定义组件的成员变量可以通过[本地初始化](../quick-start/arkts-restrictions-and-extensions.md#自定义组件成员变量初始化的方式与约束)和[在构造组件时通过构造参数初始化](../quick-start/arkts-restrictions-and-extensions.md#自定义组件成员变量初始化的方式与约束)两种方式实现,具体允许哪种方式取决于该变量所使用的装饰器。
**示例:**
```ts
// xxx.ets
@Entry
@Component
struct ParentComp {
@State isCountDown: boolean = true
build() {
Column() {
Text(this.isCountDown ? 'Count Down' : 'Stopwatch').fontSize(20).margin(20)
if (this.isCountDown) {
// 图片资源放在media目录下
Image($r("app.media.countdown")).width(120).height(120)
TimerComponent({ counter: 10, changePerSec: -1, showInColor: Color.Red })
} else {
// 图片资源放在media目录下
Image($r("app.media.stopwatch")).width(120).height(120)
TimerComponent({ counter: 0, changePerSec: +1, showInColor: Color.Black })
}
Button(this.isCountDown ? 'Switch to Stopwatch' : 'Switch to Count Down')
.onClick(() => {
this.isCountDown = !this.isCountDown
})
}.width('100%')
}
}
// 自定义计时器/倒计时组件
@Component
struct TimerComponent {
@State counter: number = 0
private changePerSec: number = -1
private showInColor: Color = Color.Black
private timerId: number = -1
build() {
Text(`${this.counter}sec`)
.fontColor(this.showInColor)
.fontSize(20)
.margin(20)
}
aboutToAppear() {
this.timerId = setInterval(() => {
this.counter += this.changePerSec
}, 1000)
}
aboutToDisappear() {
if (this.timerId > 0) {
clearTimeout(this.timerId)
this.timerId = -1
}
}
}
```
![component](figures/component.gif)
**初始创建和渲染:**
1. 创建父组件ParentComp;
2. 本地初始化ParentComp的状态变量isCountDown;
3. 执行ParentComp的build函数;
4. 创建Column组件;
1. 创建Text组件,设置其文本展示内容,并将Text组件实例添加到Column中;
2. 判断if条件,创建true条件下的元素;
1. 创建Image组件,并设置其图片源地址;
2. 使用给定的构造函数创建TimerComponent;
3. 创建Button内置组件,设置相应的内容。
**状态更新:**
用户单击按钮时:
1. ParentComp的isCountDown状态变量的值更改为false;
2. 执行ParentComp的build函数;
3. Column组件被重用并执行重新初始化;
4. Column的子组件会重用内存中的对象,但会进行重新初始化;
1. Text组件会被重用,但使用新的文本内容进行重新初始化;
2. 判断if条件,使用false条件下的元素;
1. 原来true条件的组件不再使用,将这些组件销毁;
2. 创建false条件下的组件;
3. 重用Button组件,使用新的图片源地址。
\ No newline at end of file
# 绘制图形
绘制能力主要是通过框架提供的绘制组件来支撑,支持svg标准绘制命令。
本节主要学习如何使用绘制组件,绘制详情页食物成分标签(基本几何图形)和应用Logo(自定义图形)。
## 绘制基本几何图形
绘制组件封装了一些常见的基本几何图形,比如矩形Rect、圆形Circle、椭圆形Ellipse等,为开发者省去了路线计算的过程。
FoodDetail页面的食物成分表里,给每一项成分名称前都加上一个圆形的图标作为成分标签。
1. 创建Circle组件,在每一项含量成分前增加一个圆形图标作为标签。设置Circle的直径为 6vp。修改FoodDetail页面的ContentTable组件里的IngredientItem方法,在成分名称前添加Circle。
```ts
// FoodDetail.ets
@Component
struct ContentTable {
private foodItem: FoodData
@Builder IngredientItem(title:string, colorValue: string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex({ alignItems: ItemAlign.Center }) {
Circle({width: 6, height: 6})
.margin({right: 12})
.fill(colorValue)
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
build() {
......
}
}
```
2. 每个成分的标签颜色不一样,所以我们在build方法中,调用IngredientItem,给每个Circle填充不一样的颜色。
```ts
// FoodDetail.ets
@Component
struct ContentTable {
private foodItem: FoodData
@Builder IngredientItem(title:string, colorValue: string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex({ alignItems: ItemAlign.Center }) {
Circle({width: 6, height: 6})
.margin({right: 12})
.fill(colorValue)
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
this.IngredientItem('Calories', '#FFf54040', 'Calories', this.foodItem.calories + 'kcal')
this.IngredientItem('Nutrition', '#FFcccccc', 'Protein', this.foodItem.protein + 'g')
this.IngredientItem(' ', '#FFf5d640', 'Fat', this.foodItem.fat + 'g')
this.IngredientItem(' ', '#FF9e9eff', 'Carbohydrates', this.foodItem.carbohydrates + 'g')
this.IngredientItem(' ', '#FF53f540', 'VitaminC', this.foodItem.vitaminC + 'mg')
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
```
![drawing-feature](figures/drawing-feature.png)
## 绘制自定义几何图形
除绘制基础几何图形,开发者还可以使用Path组件来绘制自定义的路线,下面进行绘制应用的Logo图案。
1. 在pages文件夹下创建新的页面Logo.ets。
![drawing-feature1](figures/drawing-feature1.png)
2. Logo.ets中删掉模板代码,创建Logo Component。
```ts
@Entry
@Component
struct Logo {
build() {
}
}
```
3. 创建Flex组件为根节点,宽高设置为100%,设置其在主轴方向和交叉轴方向的对齐方式都为Center,创建Shape组件为Flex子组件。
Shape组件是所有绘制组件的父组件。如果需要组合多个绘制组件成为一个整体,需要创建Shape作为其父组件。
我们要绘制的Logo的大小630px * 630px。声明式UI范式支持多种长度单位的设置,在前面的章节中,我们直接使用number作为参数,即采用了默认长度单位vp,虚拟像素单位。vp和设备分辨率以及屏幕密度有关。比如设备分辨率为1176 * 2400,屏幕基准密度(resolution)为3,vp = px / resolution,则该设备屏幕宽度是392vp。
但是绘制组件采用svg标准,默认采取px为单位的,为方便统一,在这绘制Logo这一部分,统一采取px为单位。声明式UI框架同样也支持px单位,入参类型为string,设置宽度为630px,即210vp,设置方式为width('630px')或者width(210)。
```ts
@Entry
@Component
struct Logo {
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
}
}
```
4. 给页面填充渐变色。设置为线性渐变,偏移角度为180deg,三段渐变 #BDE895 -->95DE7F --> #7AB967,其区间分别为[0, 0.1], (0.1, 0.6], (0.6, 1]。
```ts
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
```
![drawing-feature2](figures/drawing-feature2.png)
```ts
@Entry
@Component
struct Logo {
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature3](figures/drawing-feature3.png)
5. 绘制第一条路线Path,设置其绘制命令。
```ts
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
```
Path的绘制命令采用svg标准,上述命令可分解为:
```ts
M162 128.7
```
将笔触移动到(Moveto)坐标点(162, 128.7)。
```ts
a222 222 0 0 1 100.8 374.4
```
画圆弧线(elliptical arc)半径rx,ry为222,x轴旋转角度x-axis-rotation为0,角度大小large-arc-flag为0,即小弧度角,弧线方向(sweep-flag)为1,即逆时针画弧线,小写a为相对位置,即终点坐标为(162 + 100.8 = 262.8, 128.7 + 374.4 = 503.1)。
```ts
H198
```
画水平线(horizontal lineto)到198,即画(262.8, 503.1)到(198, 503.1)的水平线。
```ts
a36 36 0 0 3 -36 -36
```
画圆弧线(elliptical arc),含义同上,结束点为(198 - 36 = 162, 503.1 - 36 = 467.1)。
```ts
V128.7
```
画垂直线(vertical lineto)到128.7,即画(162, 467.1)到(162, 128.7)的垂直线。
```ts
z
```
关闭路径(closepath)。
![drawing-feature4](figures/drawing-feature4.png)
填充颜色为白色,线条颜色为透明。
```ts
.fill(Color.White)
.stroke(Color.Transparent)
```
```ts
@Entry
@Component
struct Logo {
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
.stroke(Color.Transparent)
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature5](figures/drawing-feature5.png)
6. 在Shape组件内绘制第二个Path。第二条Path的背景色为渐变色,但是渐变色的填充是其整体的box,所以需要clip将其裁剪,入参为Shape,即按照Shape的形状进行裁剪。
```ts
Path()
.commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z')
.fill('none')
.stroke(Corlor.Transparent)
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'))
```
Path的绘制命令比较长,可以将其作为组件的成员变量,通过this调用。
```ts
@Entry
@Component
struct Logo {
private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
build() {
......
Path()
.commands(this.pathCommands1)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
......
}
}
```
![drawing-feature6](figures/drawing-feature6.png)
7. 在Shape组件内绘制第二个Path。
```ts
@Entry
@Component
struct Logo {
private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
private pathCommands2:string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
.stroke(Color.Transparent)
Path()
.commands(this.pathCommands1)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
Path()
.commands(this.pathCommands2)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 50,
colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]]
})
.clip(new Path().commands(this.pathCommands2))
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature7](figures/drawing-feature7.png)
完成应用Logo的绘制。Shape组合了三个Path组件,通过svg命令绘制出一个艺术的叶子,寓意绿色健康饮食方式。
8. 添加应用的标题和slogan。
```ts
@Entry
@Component
struct Logo {
private pathCommands1: string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
private pathCommands2: string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
.stroke(Color.Transparent)
Path()
.commands(this.pathCommands1)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
Path()
.commands(this.pathCommands2)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 50,
colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]]
})
.clip(new Path().commands(this.pathCommands2))
}
.height('630px')
.width('630px')
Text('Healthy Diet')
.fontSize(26)
.fontColor(Color.White)
.margin({ top: 300 })
Text('Healthy life comes from a balanced diet')
.fontSize(17)
.fontColor(Color.White)
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature8](figures/drawing-feature8.png)
\ No newline at end of file
# 弹性布局
弹性布局(Flex布局)是自适应布局中使用最为灵活的布局。弹性布局提供一种更加有效的方式来对容器中的子组件进行排列、对齐和分配空白空间。弹性布局
- 容器: [Flex组件](../reference/arkui-ts/ts-container-flex.md)作为Flex布局的容器,用于设置布局相关属性。
- 子组件: Flex组件内的子组件自动成为布局的子组件。
- 主轴: Flex组件布局方向的轴线,子组件默认沿着主轴排列。主轴开始的位置称为主轴起始端,结束位置称为主轴终点端。
- 交叉轴: 垂直于主轴方向的轴线。交叉轴起始的位置称为交叉轴首部,结束位置称为交叉轴尾部。
以FlexDirection.Row的Flex为例:
![](figures/flex.png)
## 容器组件属性
通过Flex组件提供的Flex接口创建弹性布局。如下:
`Flex(options?: { direction?: FlexDirection, wrap?: FlexWrap, justifyContent?: FlexAlign, alignItems?: ItemAlign, alignContent?: FlexAlign })`
### 弹性布局方向
参数direction决定主轴的方向,即子组件的排列方向。可选值有:
![](figures/direction.png)
- FlexDirection.Row(默认值):主轴为水平方向,子组件从起始端沿着水平方向开始排布。
```ts
Flex({ direction: FlexDirection.Row }) {
Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001218579606](figures/zh-cn_image_0000001218579606.png)
- FlexDirection.RowReverse:主轴为水平方向,子组件从终点端沿着FlexDirection. Row相反的方向开始排布。
```ts
Flex({ direction: FlexDirection.RowReverse }) {
Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001218739566](figures/zh-cn_image_0000001218739566.png)
- FlexDirection.Column:主轴为垂直方向,子组件从起始端沿着垂直方向开始排布。
```ts
Flex({ direction: FlexDirection.Column }) {
Text('1').width('100%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('100%').height(50).backgroundColor(0xD2B48C)
Text('3').width('100%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001263019457](figures/zh-cn_image_0000001263019457.png)
- FlexDirection.ColumnReverse:主轴为垂直方向,子组件从终点端沿着FlexDirection. Column相反的方向开始排布。
```ts
Flex({ direction: FlexDirection.ColumnReverse }) {
Text('1').width('100%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('100%').height(50).backgroundColor(0xD2B48C)
Text('3').width('100%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001263339459](figures/zh-cn_image_0000001263339459.png)
### 弹性布局换行
默认情况下,子组件在Flex容器中都排在一条线(又称"轴线")上。通过wrap参数设置子组件换行方式。可选值有:
- FlexWrap. NoWrap(默认值): 不换行。如果子组件的宽度总和大于父元素的宽度,则子组件会被压缩宽度。
```ts
Flex({ wrap: FlexWrap.NoWrap }) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001263139409](figures/zh-cn_image_0000001263139409.png)
- FlexWrap. Wrap:换行,每一行子组件按照主轴方向排列。
```ts
Flex({ wrap: FlexWrap.Wrap }) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xD2B48C)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001218419614](figures/zh-cn_image_0000001218419614.png)
- FlexWrap. WrapReverse:换行,每一行子组件按照主轴反方向排列。
```ts
Flex({ wrap: FlexWrap.WrapReverse}) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001263259399](figures/zh-cn_image_0000001263259399.png)
### 弹性布局对齐方式
#### 主轴对齐
通过justifyContent参数设置在主轴方向的对齐方式,存在下面六种情况:
![](figures/justifyContent.png)
- FlexAlign.Start(默认值): 子组件在主轴方向起始端对齐, 第一个子组件与父元素边沿对齐,其他元素与前一个元素对齐。
```ts
Flex({ justifyContent: FlexAlign.Start }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001218259634](figures/mainStart.png)
- FlexAlign.Center: 子组件在主轴方向居中对齐。
```ts
Flex({ justifyContent: FlexAlign.Center }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001218579608](figures/mainCenter.png)
- FlexAlign.End: 子组件在主轴方向终点端对齐, 最后一个子组件与父元素边沿对齐,其他元素与后一个元素对齐。
```ts
Flex({ justifyContent: FlexAlign.End }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001218739568](figures/mainEnd.png)
- FlexAlign.SpaceBetween: Flex主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件和最后一个子组件与父元素边沿对齐。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001263019461](figures/mainSpacebetween.png)
- FlexAlign.SpaceAround: Flex主轴方向均匀分配弹性元素,相邻子组件之间距离相同。第一个子组件到主轴起始端的距离和最后一个子组件到主轴终点端的距离是相邻元素之间距离的一半。
```ts
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001263339461](figures/mainSpacearound.png)
- FlexAlign.SpaceEvenly: Flex主轴方向元素等间距布局,相邻子组件之间的间距、第一个子组件与主轴起始端的间距、最后一个子组件到主轴终点端的间距均相等。
```ts
Flex({ justifyContent: FlexAlign.SpaceEvenly }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001263139411](figures/mainSpaceevenly.png)
#### 交叉轴对齐
容器和子组件都可以设置交叉轴对齐方式,且子组件设置的对齐方式优先级较高。
##### 容器组件设置交叉轴对齐
可以通过Flex组件的alignItems参数设置子组件在交叉轴的对齐方式,可选值有:
- ItemAlign.Auto: 使用Flex容器中默认配置。
```ts
Flex({ alignItems: ItemAlign.Auto }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001218419616](figures/zh-cn_image_0000001218419616.png)
- ItemAlign.Start: 交叉轴方向首部对齐。
```ts
Flex({ alignItems: ItemAlign.Start }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001263259401](figures/zh-cn_image_0000001263259401.png)
- ItemAlign.Center: 交叉轴方向居中对齐。
```ts
Flex({ alignItems: ItemAlign.Center }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001218259636](figures/zh-cn_image_0000001218259636.png)
- ItemAlign.End:交叉轴方向底部对齐。
```ts
Flex({ alignItems: ItemAlign.End }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001218579610](figures/zh-cn_image_0000001218579610.png)
- ItemAlign.Stretch:交叉轴方向拉伸填充,在未设置尺寸时,拉伸到容器尺寸。
```ts
Flex({ alignItems: ItemAlign.Stretch }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001218739570](figures/itemalignstretch.png)
- ItemAlign. Baseline:交叉轴方向文本基线对齐。
```ts
Flex({ alignItems: ItemAlign.Baseline }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001263019463](figures/zh-cn_image_0000001263019463.png)
##### 子组件设置交叉轴对齐
子组件的alignSelf属性也可以设置子组件在父容器交叉轴的对齐格式,且会覆盖Flex布局容器中alignItems默认配置。如下例所示:
```ts
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) { //容器组件设置子组件居中
Text('alignSelf Start').width('25%').height(80)
.alignSelf(ItemAlign.Start)
.backgroundColor(0xF5DEB3)
Text('alignSelf Baseline')
.alignSelf(ItemAlign.Baseline)
.width('25%')
.height(80)
.backgroundColor(0xD2B48C)
Text('alignSelf Baseline').width('25%').height(100)
.backgroundColor(0xF5DEB3)
.alignSelf(ItemAlign.Baseline)
Text('no alignSelf').width('25%').height(100)
.backgroundColor(0xD2B48C)
Text('no alignSelf').width('25%').height(100)
.backgroundColor(0xF5DEB3)
}.width('90%').height(220).backgroundColor(0xAFEEEE)
```
![alignself](figures/alignself.png)
上例中,Flex容器中alignItems设置交叉轴子组件的对齐方式为居中,子组件自身设置了alignSelf属性的情况,覆盖父组件的alignItem值,表现为alignSelf的定义。
#### 内容对齐
可以通过alignContent参数设置子组件各行在交叉轴剩余空间内的对齐方式,只在多行的flex布局中生效,可选值有:
- FlexAlign.Start: 子组件各行与交叉轴起点对齐。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Start }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossStart.png](figures/crossStart.png)
- FlexAlign.Center: 子组件各行在交叉轴方向居中对齐。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Center }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossCenter.png](figures/crossCenter.png)
- FlexAlign.End: 子组件各行与交叉轴终点对齐。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.End }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossEnd.png](figures/crossEnd.png)
- FlexAlign.SpaceBetween: 子组件各行与交叉轴两端对齐,各行间垂直间距平均分布。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceBetween }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossSpacebetween.png](figures/crossSpacebetween.png)
- FlexAlign.SpaceAround: 子组件各行间距相等,是元素首尾行与交叉轴两端距离的两倍。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceAround }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossSpacearound.png](figures/crossSpacearound.png)
- FlexAlign.SpaceEvenly: 子组件各行间距,子组件首尾行与交叉轴两端距离都相等。
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceEvenly }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossSpaceevenly.png](figures/crossSpaceevenly.png)
### 弹性布局的自适应拉伸
在弹性布局父组件尺寸不够大的时候,通过子组件的下面几个属性设置其再父容器的占比,达到自适应布局能力。
- flexBasis: 设置子组件在父容器主轴方向上的基准尺寸。如果设置了该值,则子项占用的空间为设置的值;如果没设置或者为auto,那子项的空间为width/height的值。
```ts
Flex() {
Text('flexBasis("auto")')
.flexBasis('auto') // 未设置width以及flexBasis值为auto,内容自身宽松
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexBasis("auto")'+' width("40%")')
.width('40%')
.flexBasis('auto') //设置width以及flexBasis值auto,使用width的值
.height(100)
.backgroundColor(0xD2B48C)
Text('flexBasis(100)') // 未设置width以及flexBasis值为100,宽度为100vp
.flexBasis(100)
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexBasis(100)')
.flexBasis(100)
.width(200) // flexBasis值为100,覆盖width的设置值,宽度为100vp
.height(100)
.backgroundColor(0xD2B48C)
}.width('90%').height(120).padding(10).backgroundColor(0xAFEEEE)
```
![flexbasis](figures/flexbasis.png)
- flexGrow: 设置父容器的剩余空间分配给此属性所在组件的比例。用于"瓜分"父组件的剩余空间。
```ts
Flex() {
Text('flexGrow(1)')
.flexGrow(2)
.width(100)
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexGrow(2)')
.flexGrow(2)
.width(100)
.height(100)
.backgroundColor(0xD2B48C)
Text('no flexGrow')
.width(100)
.height(100)
.backgroundColor(0xF5DEB3)
}.width(400).height(120).padding(10).backgroundColor(0xAFEEEE)
```
![flexgrow](figures/flexgrow.png)
上图中,父容器宽度400vp, 三个子组件原始宽度为100vp,综合300vp,剩余空间100vp根据flexGrow值的占比分配给子组件,未设置flexGrow的子组件不参与“瓜分”。
第一个元素以及第二个元素以2:3分配剩下的100vp。第一个元素为100vp+100vp*2/5=140vp,第二个元素为100vp+100vp*3/5=160vp。
- flexShrink: 当父容器空间不足时,子组件的压缩比例。
```ts
Flex({ direction: FlexDirection.Row }) {
Text('flexShrink(3)')
.flexShrink(3)
.width(200)
.height(100)
.backgroundColor(0xF5DEB3)
Text('no flexShrink')
.width(200)
.height(100)
.backgroundColor(0xD2B48C)
Text('flexShrink(2)')
.flexShrink(2)
.width(200)
.height(100)
.backgroundColor(0xF5DEB3)
}.width(400).height(120).padding(10).backgroundColor(0xAFEEEE)
```
![flexshrink](figures/flexshrink.png)
## 场景示例
使用弹性布局,可以实现子组件沿水平方向排列,两端对齐,子组件间距平分,竖直方向上子组件居中的效果。示例如下:
```ts
@Entry
@Component
struct FlexExample {
build() {
Column() {
Column({ space: 5 }) {
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
Text('1').width('30%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('30%').height(50).backgroundColor(0xD2B48C)
Text('3').width('30%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.backgroundColor(0xAFEEEE)
}.width('100%').margin({ top: 5 })
}.width('100%')
}
}
```
![zh-cn_image_0000001261605867](figures/flexExample.png)
## 相关实例
针对弹性布局开发,有以下相关实例可供参考:
- [弹性布局(ArkTS)(API8)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/FlowLayoutEts)
# 网格布局
网格布局(GridLayout)是自适应布局中一种重要的布局,具备较强的页面均分能力,子组件占比控制能力。
通过[Grid](../reference/arkui-ts/ts-container-grid.md)容器组件和子组件[GridItem](../reference/arkui-ts/ts-container-griditem.md)实现,
Grid用于设置网格布局相关参数,GridItem定义子组件相关特征。优势如下:
1. 容器组件尺寸发生变化时,所有子组件以及间距等比例调整,实现布局的自适应能力。
2. 支持自定义网格布局行数和列数,以及每行每列尺寸占比。
3. 支持设置网格布局中子组件的行列间距。
4. 支持设置子组件横跨几行或者几列。
## 容器组件Grid设置
### 行列数量占比
通过Grid的组件的columnsTemplate和rowTemplate属性设置网格布局行列数量与尺寸占比。
下面以columnsTemplate为例,介绍该属性的设置,该属性值是一个由多个空格和'数字+fr'间隔拼接的字符串,fr的个数即网格布局的列数,fr前面的数值大小,用于计算该列在网格布局宽度上的占比,最终决定该列的宽度。
```ts
struct GridExample {
@State Number: Array<string> = ['1', '2', '3', '4']
build() {
Column({ space: 5 }) {
Grid() {
ForEach(this.Number, (num: string) => {
GridItem() {
Text(`列${num}`)
.fontSize(16)
.textAlign(TextAlign.Center)
.backgroundColor(0xd0d0d0)
.width('100%')
.height('100%')
.borderRadius(5)
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr')
.columnsGap(10)
.rowsGap(20)
.width('90%')
.backgroundColor(0xF0F0F0)
.height(100)
}.width('100%')
}
}
```
定义了四个等分的列,每列宽度相等。
```ts
struct GridExample {
@State Number: Array<string> = ['1', '2', '3', '4']
build() {
Column({ space: 5 }) {
Grid() {
ForEach(this.Number, (num: string) => {
GridItem() {
Text(`列${num}`)
.fontSize(16)
.textAlign(TextAlign.Center)
.backgroundColor(0xd0d0d0)
.width('100%')
.height('100%')
.borderRadius(5)
}
})
}
.columnsTemplate('1fr 2fr 3fr 4fr')
.rowsTemplate('1fr')
.columnsGap(10)
.rowsGap(20)
.width('90%')
.backgroundColor(0xF0F0F0)
.height(100)
}.width('100%')
}
}
```
定义了四列,每列宽度比值为1:2:3:4。
```ts
struct GridExample {
@State Number: Array<string> = ['1', '2', '3']
build() {
Column({ space: 5 }) {
Grid() {
ForEach(this.Number, (num: string) => {
GridItem() {
Text(`列${num}`)
.fontSize(16)
.textAlign(TextAlign.Center)
.backgroundColor(0xd0d0d0)
.width('100%')
.height('100%')
.borderRadius(5)
}
})
}
.columnsTemplate('4fr 2fr 3fr')
.rowsTemplate('1fr')
.columnsGap(10)
.rowsGap(20)
.width('90%')
.backgroundColor(0xF0F0F0)
.height(100)
}.width('100%')
}
}
```
定义了三列,每列宽度比值为4:2:3。
效果如下:
![](figures/columnTemplate.png)
### 排列方式
通过layoutDirection可以设置网格布局的主轴方向,决定子组件的排列方式。
可选值包括Row,RowReverse, Column, ColumnReverse四种情况。
效果如下:
![](figures/gridlayout.png)
### 行列间距
columnsGap用于设置网格子组件GridItem垂直方向的间距,rowsGap用于设置GridItem水平方向的间距。
```ts
Grid()
.columnsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(20)
```
![](figures/columnGap.png)
上图中,设置网格布局子组件间的垂直间距为20,水平间距为10。
## 网格子组件GridItem设置
### 设置子组件占的行列数
网格布局的行列标号从1开始,依次编号。
子组件横跨多行时,通过rowStart设置子组件起始行编号,rowEnd设置终点行编号。当rowStart值与rowEnd值相同时,子组件只占一个网格。示例如下:
```ts
Grid() {
GridItem() {
Text('5')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.rowStart(2).rowEnd(3) // 5子组件从第二行到第三行
GridItem() {
Text('4')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(4).columnEnd(5) // 4从第四列到第五列
GridItem() {
Text('6')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(2).columnEnd(4) // 6从第二列到第四列
GridItem() {
Text('9')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(3).columnEnd(4) // 从第三列到第四列
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(20)
.width('90%')
.backgroundColor(0xF0F0F0)
.height('200vp')
.layoutDirection(GridDirection.Column)
```
![](figures/griditem.png)
## 场景示例
使用grid布局实现一个计算器的排布效果,代码如下:
```ts
@Entry
@Component
struct GridExample {
@State Number: Array<string> = ['1', '2', '3', '+', '4', '5', '6', '-', '7', '8', '9', '*', '0', '.', '/']
@Styles textStyle(){
.backgroundColor(0xd0d0d0)
.width('100%')
.height('100%')
.borderRadius(5)
}
build() {
Column({ space: 5 }) {
Grid() {
GridItem() {
Text('0')
.fontSize(30)
.textStyle()
}.columnStart(1).columnEnd(4)
GridItem() {
Text('清空')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(1).columnEnd(2)
GridItem() {
Text('回退')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(3).columnEnd(4)
ForEach(this.Number, (day: string) => {
if (day === '0') {
GridItem() {
Text(day)
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(1).columnEnd(2)
} else {
GridItem() {
Text(day)
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('2fr 1fr 1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(15)
.width('90%')
.backgroundColor(0xF0F0F0)
.height('70%')
}.width('100%').margin({ top: 5 })
}
}
```
在大屏设备上展示效果如下:
![](figures/gridExp1.png)
在小屏设备下展示效果如下:
![](figures/gridExp2.png)
# 线性布局
线性布局(LinearLayout)是开发中最常用的布局。线性布局的子组件在线性方向上(水平方向和垂直方向)依次排列。
通过线性容器[Row](../reference/arkui-ts/ts-container-row.md)[Column](../reference/arkui-ts/ts-container-column.md)实现线性布局。Column容器内子组件按照垂直方向排列,Row组件中,子组件按照水平方向排列。
## 线性布局的排列
线性布局的排列方向由所选容器组件决定。根据不同的排列方向,选择使用Row或Column容器创建线性布局,通过调整space,alignItems,justifyContent属性调整子组件的间距,水平垂直方向的对齐方式。
1. 通过space参数设置主轴(排列方向)上子组件的间距。达到各子组件在排列方向上的等间距效果。
2. 通过alignItems属性设置子组件在交叉轴(排列方向的垂直方向)的对齐方式。且在各类尺寸屏幕中,表现一致。其中,交叉轴为垂直方向时,取值为[VerticalAlign类型](../reference/arkui-ts/ts-appendix-enums.md#verticalalign),水平方向取值为[HorizontalAlign类型](../reference/arkui-ts/ts-appendix-enums.md#horizontalalign)
3. 通过justifyContent属性设置子组件在主轴(排列方向)上的对齐方式。实现布局的自适应均分能力。取值为[FlexAlign类型](../reference/arkui-ts/ts-appendix-enums.md#flexalign)
具体使用以及效果如下表所示:
|属性名|描述|Row效果图|Column效果图|
|------|---------------------------|----------------------------|---------------------------|
|space |- 横向布局中各子组件的在水平方向的间距<br> - 纵向布局中个子组件垂直方向间距| ![](figures/rowspace.png) | ![](figures/columnspace.png) |
|alignItems |容器排列方向的垂直方向上,子组件在父容器中的对齐方式|![](figures/rowalign.png) |![](figures/columnalign.png)|
|justifyContent |容器排列方向上,子组件在父容器中的对齐方式 |![](figures/rowjustify.png) |![](figures/columnjustify.png)|
## 自适应拉伸
在线性布局下,常用空白填充组件[Blank](../reference/arkui-ts/ts-basic-components-blank.md),在容器主轴方向自动填充空白空间,达到自适应拉伸效果。
```ts
@Entry
@Component
struct BlankExample {
build() {
Column() {
Row() {
Text('Bluetooth').fontSize(18)
Blank()
Toggle({ type: ToggleType.Switch, isOn: true })
}.backgroundColor(0xFFFFFF).borderRadius(15).padding({ left: 12 }).width('100%')
}.backgroundColor(0xEFEFEF).padding(20).width('100%')
}
}
```
![](figures/blank.gif)
## 自适应缩放
自适应缩放是指在各种不同大小设备中,子组件按照预设的比例,尺寸随容器尺寸的变化而变化。在线性布局中有下列方法实现:
1. 父容器尺寸确定时,设置了layoutWeight属性的子组件与兄弟元素占主轴尺寸按照权重进行分配,忽略元素本身尺寸设置,在任意尺寸设备下,自适应占满剩余空间。
```ts
@Entry
@Component
struct layoutWeightExample {
build() {
Column() {
Text('1:2:3').width('100%')
Row() {
Column() {
Text('layoutWeight(1)')
.textAlign(TextAlign.Center)
}.layoutWeight(2).backgroundColor(0xffd306).height('100%')
Column() {
Text('layoutWeight(2)')
.textAlign(TextAlign.Center)
}.layoutWeight(4).backgroundColor(0xffed97).height('100%')
Column() {
Text('layoutWeight(6)')
.textAlign(TextAlign.Center)
}.layoutWeight(6).backgroundColor(0xffd306).height('100%')
}.backgroundColor(0xffd306).height('30%')
Text('2:5:3').width('100%')
Row() {
Column() {
Text('layoutWeight(2)')
.textAlign(TextAlign.Center)
}.layoutWeight(2).backgroundColor(0xffd306).height('100%')
Column() {
Text('layoutWeight(5)')
.textAlign(TextAlign.Center)
}.layoutWeight(5).backgroundColor(0xffed97).height('100%')
Column() {
Text('layoutWeight(3)')
.textAlign(TextAlign.Center)
}.layoutWeight(3).backgroundColor(0xffd306).height('100%')
}.backgroundColor(0xffd306).height('30%')
}
}
}
```
![](figures/layoutWeight.gif)
3. 父容器尺寸确定时,使用百分比设置子组件以及兄弟组件的width宽度,可以保证各自元素在任意尺寸下的自适应占比。
```ts
@Entry
@Component
struct WidthExample {
build() {
Column() {
Row() {
Column() {
Text('left width 20%')
.textAlign(TextAlign.Center)
}.width('20%').backgroundColor(0xffd306).height('100%')
Column() {
Text('center width 50%')
.textAlign(TextAlign.Center)
}.width('50%').backgroundColor(0xffed97).height('100%')
Column() {
Text('right width 30%')
.textAlign(TextAlign.Center)
}.width('30%').backgroundColor(0xffd306).height('100%')
}.backgroundColor(0xffd306).height('30%')
}
}
}
```
![](figures/width.gif)
上例中,在任意大小的设备中,子组件的宽度占比固定。
## 定位能力
- 相对定位
使用组件的[offset属性](../reference/arkui-ts/ts-universal-attributes-location.md)可以实现相对定位,设置元素相对于自身的偏移量。设置该属性,不影响父容器布局,仅在绘制时进行位置调整。使用线性布局和offset可以实现大部分布局的开发。
```ts
@Entry
@Component
struct OffsetExample {
@Styles eleStyle() {
.size({ width: 120, height: '50' })
.backgroundColor(0xbbb2cb)
.border({ width: 1 })
}
build() {
Column({ space: 20 }) {
Row() {
Text('1').size({ width: '15%', height: '50' }).backgroundColor(0xdeb887).border({ width: 1 }).fontSize(16)
Text('2 offset(15, 30)')
.eleStyle()
.fontSize(16)
.align(Alignment.Start)
.offset({ x: 15, y: 30 })
Text('3').size({ width: '15%', height: '50' }).backgroundColor(0xdeb887).border({ width: 1 }).fontSize(16)
Text('4 offset(-10%, 20%)')
.eleStyle()
.fontSize(16)
.offset({ x: '-5%', y: '20%' })
}.width('90%').height(150).border({ width: 1, style: BorderStyle.Dashed })
}
.width('100%')
.margin({ top: 25 })
}
}
```
![](figures/offset.gif)
- 绝对定位
线性布局中可以使用组件的[positon属性](../reference/arkui-ts/ts-universal-attributes-location.md)实现绝对布局(AbsoluteLayout),设置元素左上角相对于父容器左上角偏移位置。对于不同尺寸的设备,使用绝对定位的适应性会比较差,在屏幕的适配上有缺陷。
```ts
@Entry
@Component
struct PositionExample {
@Styles eleStyle(){
.backgroundColor(0xbbb2cb)
.border({ width: 1 })
.size({ width: 120, height: 50 })
}
build() {
Column({ space: 20 }) {
// 设置子组件左上角相对于父组件左上角的偏移位置
Row() {
Text('position(30, 10)')
.eleStyle()
.fontSize(16)
.position({ x: 10, y: 10 })
Text('position(50%, 70%)')
.eleStyle()
.fontSize(16)
.position({ x: '50%', y: '70%' })
Text('position(10%, 90%)')
.eleStyle()
.fontSize(16)
.position({ x: '10%', y: '80%' })
}.width('90%').height('100%').border({ width: 1, style: BorderStyle.Dashed })
}
.width('90%').margin(25)
}
}
```
![](figures/position.gif)
## 自适应延伸
自适应延伸是在不同尺寸设备下,当页面显示内容个数不一并延伸到屏幕外时,可通过滚动条拖动展示。适用于线性布局中内容无法一屏展示的场景。常见以下两类实现方法。
- List组件
List子项过多一屏放不下时,未展示的子项通过滚动条拖动显示。通过scrollBar属性设置滚动条的常驻状态,edgeEffect属性设置拖动到极限的回弹效果。
纵向List:
```ts
@Entry
@Component
struct ListExample1 {
@State arr: string[] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"]
@State alignListItem: ListItemAlign = ListItemAlign.Start
build() {
Column() {
List({ space: 20, initialIndex: 0 }) {
ForEach(this.arr, (item) => {
ListItem() {
Text('' + item)
.width('100%')
.height(100)
.fontSize(16)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
.border({ width: 2, color: Color.Green })
}, item => item)
}
.border({ width: 2, color: Color.Red, style: BorderStyle.Dashed })
.scrollBar(BarState.On) // 滚动条常驻
.edgeEffect(EdgeEffect.Spring) // 滚动到边缘再拖动回弹效果
}.width('100%').height('100%').backgroundColor(0xDCDCDC).padding(20)
}
}
```
![](figures/listcolumn.gif)
横向List:
```ts
@Entry
@Component
struct ListExample2 {
@State arr: string[] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"]
@State alignListItem: ListItemAlign = ListItemAlign.Start
build() {
Column() {
List({ space: 20, initialIndex: 0 }) {
ForEach(this.arr, (item) => {
ListItem() {
Text('' + item)
.height('100%')
.width(100)
.fontSize(16)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
.border({ width: 2, color: Color.Green })
}, item => item)
}
.border({ width: 2, color: Color.Red, style: BorderStyle.Dashed })
.scrollBar(BarState.On) // 滚动条常驻
.edgeEffect(EdgeEffect.Spring) // 滚动到边缘再拖动回弹效果
.listDirection(Axis.Horizontal) // 列表水平排列
}.width('100%').height('100%').backgroundColor(0xDCDCDC).padding(20)
}
}
```
![](figures/listrow.gif)
- Scroll组件
线性布局中,当子组件的布局尺寸超过父组件的尺寸时,内容可以滚动。在Column或者Row外层包裹一个可滚动的容器组件Scroll实现。
纵向Scroll:
```ts
@Entry
@Component
struct ScrollExample {
scroller: Scroller = new Scroller();
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
build() {
Scroll(this.scroller) {
Column() {
ForEach(this.arr, (item) => {
Text(item.toString())
.width('90%')
.height(150)
.backgroundColor(0xFFFFFF)
.borderRadius(15)
.fontSize(16)
.textAlign(TextAlign.Center)
.margin({ top: 10 })
}, item => item)
}.width('100%')
}
.backgroundColor(0xDCDCDC)
.scrollable(ScrollDirection.Vertical) // 滚动方向纵向
.scrollBar(BarState.On) // 滚动条常驻显示
.scrollBarColor(Color.Gray) // 滚动条颜色
.scrollBarWidth(30) // 滚动条宽度
.edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹
}
}
```
![](figures/scrollcolumn.gif)
横向Scroll:
```ts
@Entry
@Component
struct ScrollExample {
scroller: Scroller = new Scroller();
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
build() {
Scroll(this.scroller) {
Row() {
ForEach(this.arr, (item) => {
Text(item.toString())
.height('90%')
.width(150)
.backgroundColor(0xFFFFFF)
.borderRadius(15)
.fontSize(16)
.textAlign(TextAlign.Center)
.margin({ left: 10 })
}, item => item)
}.height('100%')
}
.backgroundColor(0xDCDCDC)
.scrollable(ScrollDirection.Horizontal) // 滚动方向横向
.scrollBar(BarState.On) // 滚动条常驻显示
.scrollBarColor(Color.Gray) // 滚动条颜色
.scrollBarWidth(30) // 滚动条宽度
.edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹
}
}
```
![](figures/scrollrow.gif)
# 媒体查询
[媒体查询(Media Query)](../reference/apis/js-apis-mediaquery.md)作为响应式设计的核心,在移动设备上应用十分广泛。它根据不同设备类型或同设备不同状态修改应用的样式。媒体查询的优势有:
1. 提供丰富的媒体特征监听能力,针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。
2. 当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。
## 媒体查询引入与使用流程
媒体查询通过媒体查询接口,设置查询条件并绑定回调函数,在对应的条件的回调函数里更改页面布局或者实现业务逻辑,实现页面的响应式设计。具体步骤如下:
首先导入媒体查询模块。
```ts
import mediaquery from '@ohos.mediaquery'
```
通过matchMediaSync接口设置媒体查询条件,保存返回的条件监听句柄listener。
```ts
listener = mediaquery.matchMediaSync('(orientation: landscape)')
```
给条件监听句柄listener绑定回调函数onPortrait,当listener检测设备状态变化时执行回调函数。在回调函数内,根据不同设备状态更改页面布局或者实现业务逻辑。
```ts
onPortrait(mediaQueryResult) {
if (mediaQueryResult.matches) {
// do something here
} else {
// do something here
}
}
listener.on('change', onPortrait)
```
## 媒体查询条件
媒体查询条件由媒体类型,逻辑操作符,媒体特征组成,其中媒体类型可省略,逻辑操作符用于连接不同媒体类型与媒体特征,其中,媒体特征要使用()包裹且可以有多个。具体规则如下:
### 语法规则
```
[media-type] [and|not|only] [(media-feature)]
```
例如:
`screen and (round-screen: true)` :当设备屏幕是圆形时条件成立。
`(max-height: 800)` :当高度小于等于800时条件成立。
`(height <= 800) ` :当高度小于等于800时条件成立。
`screen and (device-type: tv) or (resolution < 2)` :包含多个媒体特征的多条件复杂语句查询,当设备类型为tv或设备分辨率小于2时条件成立。
### 媒体类型(media-type)
| 类型 | 说明 |
| ------ | -------------- |
| screen | 按屏幕相关参数进行媒体查询。 |
### 媒体逻辑操作(and|or|not|only)
媒体逻辑操作符:and、or、not、only用于构成复杂媒体查询,也可以通过comma(, )将其组合起来,详细解释说明如下表。
**表1** 媒体逻辑操作符
| 类型 | 说明 |
| -------- | ---------------------------------------- |
| and | 将多个媒体特征(Media&nbsp; Feature)以“与”的方式连接成一个媒体查询,只有当所有媒体特征都为true,查询条件成立。另外,它还可以将媒体类型和媒体功能结合起来。<br/>例如:screen&nbsp; and&nbsp; (device-type:&nbsp; wearable)&nbsp; and&nbsp; (max-height:&nbsp; 600)&nbsp; 表示当设备类型是智能穿戴且应用的最大高度小于等于600个像素单位时成立。 |
| or | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。<br/>例如:screen&nbsp; and&nbsp; (max-height:&nbsp; 1000)&nbsp; or&nbsp; (round-screen:true)表示当应用高度小于等于1000个像素单位或者设备屏幕是圆形时,条件成立。 |
| not | 取反媒体查询结果,媒体查询结果不成立时返回true,否则返回false。<br/>例如:not&nbsp; screen&nbsp; and&nbsp; (min-height:&nbsp; 50)&nbsp; and&nbsp; (max-height:&nbsp; 600)&nbsp; 表示当应用高度小于50个像素单位或者大于600个像素单位时成立。<br/>使用not运算符时必须指定媒体类型。 |
| only | 当整个表达式都匹配时,才会应用选择的样式,可以应用在防止某些较早的版本的浏览器上产生歧义的场景。一些较早版本的浏览器对于同时包含了媒体类型和媒体特征的语句会产生歧义,比如:<br/>screen&nbsp; and&nbsp; (min-height:&nbsp; 50)<br/>老版本浏览器会将这句话理解成screen,从而导致仅仅匹配到媒体类型(screen),就应用了指定样式,使用only可以很好地规避这种情况。<br/>使用only时必须指定媒体类型。 |
| , (comma) | 将多个媒体特征以“或”的方式连接成一个媒体查询,如果存在结果为true的媒体特征,则查询条件成立。其效果等同于or运算符。<br/>例如:screen&nbsp; and&nbsp; (min-height:&nbsp; 1000), &nbsp; &nbsp; (round-screen:true)&nbsp; 表示当应用高度大于等于1000个像素单位或者设备屏幕是圆形时,条件成立。 |
在MediaQuery Level 4中引入了范围查询,使其能够使用max-,min-的同时,也支持了&lt; =,&gt; =,&lt;&gt; 操作符。
**表2** 媒体逻辑范围操作符
| 类型 | 说明 |
| ----- | ---------------------------------------- |
| &lt; = | 小于等于,例如:screen&nbsp; and&nbsp; (height&nbsp; &lt; =&nbsp; 50)。 |
| &gt; = | 大于等于,例如:screen&nbsp; and&nbsp; (height&nbsp; &gt; =&nbsp; 600)。 |
| &lt; | 小于,例如:screen&nbsp; and&nbsp; (height&nbsp; &lt; &nbsp; 50)。 |
| &gt; | 大于,例如:screen&nbsp; and&nbsp; (height&nbsp; &gt; &nbsp; 600)。 |
### 媒体特征(media-feature)
| 类型 | 说明 |
| ----------------- | ------------------------------------------------------------ |
| height | 应用页面显示区域的高度。 |
| min-height | 应用页面显示区域的最小高度。 |
| max-height | 应用页面显示区域的最大高度。 |
| width | 应用页面显示区域的宽度。 |
| min-width | 应用页面显示区域的最小宽度。 |
| max-width | 应用页面显示区域的最大宽度。 |
| resolution | 设备的分辨率,支持dpi,dppx和dpcm单位。其中:<br/>-&nbsp; dpi表示每英寸中物理像素个数,1dpi≈0.39dpcm;<br/>-&nbsp; dpcm表示每厘米上的物理像素个数,1dpcm&nbsp;&nbsp; 2.54dpi;<br/>-&nbsp; dppx表示每个px中的物理像素数(此单位按96px=1英寸为基准,与页面中的px单位计算方式不同),1dppx&nbsp; =&nbsp; 96dpi。 |
| min-resolution | 设备的最小分辨率。 |
| max-resolution | 设备的最大分辨率。 |
| orientation | 屏幕的方向。<br/>可选值:<br/>-&nbsp; orientation:&nbsp; portrait(设备竖屏)<br/>-&nbsp; orientation:&nbsp; landscape(设备横屏) |
| device-height | 设备的高度。 |
| min-device-height | 设备的最小高度。 |
| max-device-height | 设备的最大高度。 |
| device-width | 设备的宽度。 |
| device-type | 设备的类型。<br/>可选值:default、tablet |
| min-device-width | 设备的最小宽度。 |
| max-device-width | 设备的最大宽度。 |
| device-type | 设备的类型。<br/>可选值:default |
| round-screen | 屏幕类型,圆形屏幕为true,&nbsp; 非圆形屏幕为&nbsp; false。 |
| dark-mode | 系统为深色模式时为true,否则为false。 |
## 场景示例
下例中使用媒体查询,实现屏幕横竖屏切换时给页面文本应用不同的内容和样式的效果。
```ts
import mediaquery from '@ohos.mediaquery'
let portraitFunc = null
@Entry
@Component
struct MediaQueryExample {
@State color: string = '#DB7093'
@State text: string = 'Portrait'
listener = mediaquery.matchMediaSync('(orientation: landscape)') // 当设备横屏时条件成立
onPortrait(mediaQueryResult) {
if (mediaQueryResult.matches) {
this.color = '#FFD700'
this.text = 'Landscape'
} else {
this.color = '#DB7093'
this.text = 'Portrait'
}
}
aboutToAppear() {
portraitFunc = this.onPortrait.bind(this) // 绑定当前应用实例
this.listener.on('change', portraitFunc)
}
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text(this.text).fontSize(50).fontColor(this.color)
}
.width('100%').height('100%')
}
}
```
横屏下文本内容为Landscape,颜色为#FFD700。
![zh-cn_image_0000001262954829](figures/zh-cn_image_0000001262954829.png)
非横屏下文本内容为Portrait,颜色为#DB7093。
![zh-cn_image_0000001263074739](figures/zh-cn_image_0000001263074739.png)
## 相关实例
使用媒体查询的自适应布局开发,有以下相关实例可供参考:
- [`MediaQuery`:媒体查询(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/MediaQuery)
# 层叠布局
层叠布局(StackLayout)用于在屏幕上预留一块区域来显示组件中的元素,提供元素可以重叠的布局。
通过层叠容器[Stack](../reference/arkui-ts/ts-container-stack.md)实现,容器中的子元素依次入栈,后一个子元素覆盖前一个子元素显示。
## 对齐方式
设置子元素在容器内的对齐方式。支持左上,上中,右上,左,中,右,右下,中下,右下九种对齐方式,如下表所示:
|名称| 描述| 图示 |
|---| ---|---|
|TopStart| 顶部起始端 |![](figures/stacktopstart.png)|
Top |顶部横向居中 |![](figures/stacktop.png)|
TopEnd| 顶部尾端 |![](figures/stacktopend.png)|
Start| 起始端纵向居中 |![](figures/stackstart.png)|
Center| 横向和纵向居中 |![](figures/stackcenter.png)|
End| 尾端纵向居中 |![](figures/stackend.png)|
BottomStart |底部起始端 |![](figures/stackbottomstart.png)|
Bottom| 底部横向居中 |![](figures/stackbottom.png)|
BottomEnd| 底部尾端 |![](figures/stackbottomend.png)|
## Z序控制
Stack容器中兄弟组件显示层级关系可以通过[zIndex](../reference/arkui-ts/ts-universal-attributes-z-order.md)
属性改变。zIndex值越大,显示层级越高,即zIndex值大的组件会覆盖在zIndex值小的组件上方。
- 在层叠布局中,如果后面子元素尺寸大于前面子元素尺寸,则前面子元素完全隐藏。
```ts
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text('Stack子元素1').textAlign(TextAlign.End).fontSize(20)
}.width(100).height(100).backgroundColor(0xffd306)
Column() {
Text('Stack子元素2').fontSize(20)
}.width(150).height(150).backgroundColor(Color.Pink)
Column() {
Text('Stack子元素3').fontSize(20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0)
```
![](figures/stack2.png)
上图中,最后的子元素3的尺寸大于前面的所有子元素,所以,前面两个元素完全隐藏。改变子元素1,子元素2的zIndex属性后,可以将元素展示出来。
```ts
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text('Stack子元素1').fontSize(20)
}.width(100).height(100).backgroundColor(0xffd306).zIndex(2)
Column() {
Text('Stack子元素2').fontSize(20)
}.width(150).height(150).backgroundColor(Color.Pink).zIndex(1)
Column() {
Text('Stack子元素3').fontSize(20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0)
```
![](figures/stack1.png)
# 概述
基于ArkTS的声明式开发范式的方舟开发框架是一套开发极简、高性能、支持跨设备的UI开发框架,支持开发者高效地构建OpenHarmony应用UI界面。
## 基础能力
使用基于ArkTS的声明式开发范式的方舟开发框架,采用更接近自然语义的编程方式,让开发者可以直观地描述UI界面,不必关心框架如何实现UI绘制和渲染,实现极简高效开发。开发框架不仅从组件、动画和状态管理三个维度来提供UI能力,还提供了系统能力接口以实现系统能力的极简调用。
ArkTS语言的基础知识请参考[学习ArkTS语言](../quick-start/arkts-get-started.md)文档;此外,请参考[基于ArkTS的声明式开发范式API](../reference/arkui-ts/ts-universal-events-click.md)文档,全面地了解内置组件,更好地开发应用。
- **开箱即用的组件**
框架提供丰富的系统内置组件,可以通过链式调用的方式设置系统组件的渲染效果。开发者可以组合系统组件为自定义组件,通过这种方式将页面组件化为一个个独立的UI单元,实现页面不同单元的独立创建、开发和复用,使页面具有更强的工程性。
- **丰富的动效接口**
提供svg标准的绘制图形能力,同时开放了丰富的动效接口,开发者可以通过封装的物理模型或者调用动画能力接口来实现自定义动画轨迹。
- **状态与数据管理**
状态数据管理作为基于ArkTS的声明式开发范式的特色,通过功能不同的装饰器给开发者提供了清晰的页面更新渲染流程和管道。状态管理包括UI组件状态和应用程序状态,两者协作可以使开发者完整地构建整个应用的数据更新和UI渲染。
- **系统能力接口**
使用基于ArkTS的声明式开发范式的方舟开发框架,还封装了丰富的系统能力接口,开发者可以通过简单的接口调用,实现从UI设计到系统能力调用的极简开发。
## 整体架构
![zh-cn_image_0000001169532276](figures/zh-cn_image_0000001169532276.png)
- **声明式UI前端**
提供了UI开发范式的基础语言规范,并提供内置的UI组件、布局和动画,提供了多种状态管理机制,为应用开发者提供一系列接口支持。
- **语言运行时**
选用方舟语言运行时,提供了针对UI范式语法的解析能力、跨语言调用支持的能力和TS语言高性能运行环境。
- **声明式UI后端引擎**
后端引擎提供了兼容不同开发范式的UI渲染管线,提供多种基础组件、布局计算、动效、交互事件,提供了状态管理和绘制能力。
- **渲染引擎**
提供了高效的绘制能力,将渲染管线收集的渲染指令,绘制到屏幕的能力。
- **平台适配层**
提供了对系统平台的抽象接口,具备接入不同系统的能力,如系统渲染管线、生命周期调度等。
## 相关实例
基于ArkTS的声明式开发范式的方舟开发框架,有以下相关实例可供参考:
- [`Drag`:拖拽事件(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/Drag)
- [`ArkUIAnimation`:动画(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/ArkUIAnimation)
- [`MouseEvent`:鼠标事件(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/MouseEvent)
- [`BringApp`:拉起系统应用(ArkTS)(API8)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/BringApp)
- [`Chat`:聊天示例应用(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/AppSample/Chat)
- [`Shopping`:购物示例应用(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/AppSample/Shopping)
- [`Lottie`:Lottie(ArkTS)(API8)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/Lottie)
- [`Flybird`:小鸟避障游戏(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ResourcesSchedule/Flybird)
- [`AdaptiveCapabilities`:多设备自适应能力(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/MultiDeviceAppDev/AdaptiveCapabilities)
- [`Game2048`:2048游戏(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/Game2048)
- [`TransitionAnimation`:转场动画(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/TransitionAnimation)
- [基础组件Slider的使用(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/SliderExample)
- [转场动画的使用(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/TransitionAnimation)
- [极简声明式UI范式(ArkTS)(API8)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/SimpleGalleryEts)
- [购物应用(ArkTS)(API8)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/ShoppingEts)
- [弹窗(ArkTS)(API8)](https://gitee.com/openharmony/codelabs/tree/master/ETSUI/CustomDialogEts)
- [`UpgradePopup`:自定义弹窗(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/UpgradePopup)
- [CustomComponent:组件化(ArkTS)(API8)](https://gitee.com/openharmony/applications_app_samples/tree/OpenHarmony-3.2-Release/ETSUI/CustomComponent)
\ No newline at end of file
# 页面跳转与数据传递
本节将学习页面跳转和数据传递,实现:
1. 页面跳转:点击食物分类列表页面的食物条目后,跳转到食物详情页;点击食物详情页的返回按钮,返回到食物列表页。
2. 页面间数据传递:点击不同的食物条目后,FoodDetail接受前一个页面的数据,渲染对应的食物详情页。
## 页面跳转
声明式UI范式提供了两种机制来实现页面间的跳转:
1. 路由容器组件Navigator,包装了页面路由的能力,指定页面target后,使其包裹的子组件都具有路由能力。
2. 路由RouterAPI接口,通过在页面上引入router,可以调用router的各种接口,从而实现页面路由的各种操作。
下面我们就分别学习这两种跳转机制来实现食物分类列表页面和食物详情页的链接。
1. 点击FoodListItem后跳转到FoodDetail页面。在FoodListItem内创建Navigator组件,使其子组件都具有路由功能,目标页面target为'pages/FoodDetail'。
```ts
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Navigator({ target: 'pages/FoodDetail' }) {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.backgroundColor('#FFf1f3f5')
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
}
.margin({ right: 24, left:32 })
}
}
```
![zh-cn_image_0000001215318403](figures/zh-cn_image_0000001215318403.gif)
2. 点击FoodGridItem后跳转到FoodDetail页面。调用页面路由router模块的push接口,将FoodDetail页面推到路由栈中,实现页面跳转。使用router路由API接口,需要先引入router。
```ts
import router from '@ohos.router'
@Component
struct FoodGridItem {
private foodItem: FoodData
build() {
Column() {
......
}
.height(184)
.width('100%')
.onClick(() => {
router.pushUrl({ url: 'pages/FoodDetail' })
})
}
}
```
![zh-cn_image_0000001169918548](figures/zh-cn_image_0000001169918548.gif)
3. 在FoodDetail页面增加回到食物列表页面的图标。在resources &gt; base &gt; media文件夹下存入回退图标Back.png。新建自定义组件PageTitle,包含后退的图标和Food Detail的文本,调用路由的router.back()接口,弹出路由栈最上面的页面,即返回上一级页面。
```ts
// FoodDetail.ets
import router from '@ohos.router'
@Component
struct PageTitle {
build() {
Flex({ alignItems: ItemAlign.Start }) {
Image($r('app.media.Back'))
.width(21.8)
.height(19.6)
Text('Food Detail')
.fontSize(21.8)
.margin({left: 17.4})
}
.height(61)
.backgroundColor('#FFedf2f5')
.padding({ top: 13, bottom: 15, left: 28.3 })
.onClick(() => {
router.back()
})
}
}
```
4. 在FoodDetail组件内创建Stack组件,包含子组件FoodImageDisplay和PageTitle子组件,设置其对齐方式为左上对齐TopStart。
```ts
@Entry
@Component
struct FoodDetail {
build() {
Column() {
Stack( { alignContent: Alignment.TopStart }) {
FoodImageDisplay()
PageTitle()
}
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
![zh-cn_image_0000001214998349](figures/zh-cn_image_0000001214998349.png)
## 页面间数据传递
我们已经完成了FoodCategoryList页面和FoodDetail页面的跳转和回退,但是点击不同的FoodListItem/FoodGridItem,跳转的FoodDetail页面都是西红柿Tomato的详细介绍,这是因为没有构建起两个页面的数据传递,需要用到携带参数(parameter)路由。
1. 在FoodListItem组件的Navigator设置其params属性,params属性接受key-value的Object。
```ts
// FoodList.ets
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Navigator({ target: 'pages/FoodDetail' }) {
......
}
.params({ foodData: this.foodItem })
}
}
```
FoodGridItem调用的routerAPI同样有携带参数跳转的能力,使用方法和Navigator类似。
```ts
router.pushUrl({
url: 'pages/FoodDetail',
params: { foodData: this.foodItem }
})
```
2. FoodDetail页面引入FoodData类,在FoodDetail组件内添加foodItem成员变量。
```ts
// FoodDetail.ets
import { FoodData } from '../model/FoodData'
@Entry
@Component
struct FoodDetail {
private foodItem: FoodData
build() {
......
}
}
```
3. 获取foodData对应的value。调用router.getParams()['foodData']来获取到FoodCategoryList页面跳转来时携带的foodData对应的数据。
```ts
@Entry
@Component
struct FoodDetail {
private foodItem: FoodData = router.getParams()['foodData']
build() {
......
}
}
```
4. 重构FoodDetail页面的组件。在构建视图时,FoodDetail页面的食物信息都是直接声明的常量,现在要用传递来的FoodData数据来对其进行重新赋值。整体的FoodDetail.ets代码如下。
```ts
@Component
struct PageTitle {
build() {
Flex({ alignItems: ItemAlign.Start }) {
Image($r('app.media.Back'))
.width(21.8)
.height(19.6)
Text('Food Detail')
.fontSize(21.8)
.margin({left: 17.4})
}
.height(61)
.backgroundColor('#FFedf2f5')
.padding({ top: 13, bottom: 15, left: 28.3 })
.onClick(() => {
router.back()
})
}
}
@Component
struct FoodImageDisplay {
private foodItem: FoodData
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
Text(this.foodItem.name)
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
.height(357)
.backgroundColor('#FFedf2f5')
}
}
@Component
struct ContentTable {
private foodItem: FoodData
@Builder IngredientItem(title:string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
this.IngredientItem('Calories', 'Calories', this.foodItem.calories + 'kcal')
this.IngredientItem('Nutrition', 'Protein', this.foodItem.protein + 'g')
this.IngredientItem('', 'Fat', this.foodItem.fat + 'g')
this.IngredientItem('', 'Carbohydrates', this.foodItem.carbohydrates + 'g')
this.IngredientItem('', 'VitaminC', this.foodItem.vitaminC + 'mg')
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
private foodItem: FoodData = router.getParams()['foodData']
build() {
Column() {
Stack( { alignContent: Alignment.TopStart }) {
FoodImageDisplay({ foodItem: this.foodItem })
PageTitle()
}
ContentTable({ foodItem: this.foodItem })
}
.alignItems(HorizontalAlign.Center)
}
}
```
## 相关实例
针对页面布局与连接,有以下示例工程可供参考:
- [`DefiningPageLayoutAndConnection`:页面布局和连接(ArkTS)(API8)](https://gitee.com/openharmony/app_samples/tree/master/ETSUI/DefiningPageLayoutAndConnection)
本示例构建了食物分类列表页面和食物详情页,向开发者展示了List布局、Grid布局以及页面路由的基本用法。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册