diff --git a/zh-cn/third-party-cases/Readme-CN.md b/zh-cn/third-party-cases/Readme-CN.md index 5d34224644c93c699c34e59f74e81757f559c2ec..3fc0814f17b5927316cee7a4cd5f24fd670965c9 100644 --- a/zh-cn/third-party-cases/Readme-CN.md +++ b/zh-cn/third-party-cases/Readme-CN.md @@ -21,7 +21,9 @@ - [时钟开发示例](time-styles-shift.md) - [弹簧动画开发](how-to-develop-spring-animation.md) - [水波纹动画开发](water-wave-animation.md) +- [属性动画示例](property-animation.md) - [如何使用OpenGL实现3D图形](how-to-use-opengl-to-draw-3d-graphics.md) +- [控制页面刷新范围](overall-and-part-refresh.md) ### 网络管理 - [如何请求并加载网络图片](how-to-load-images-from-internet.md) diff --git a/zh-cn/third-party-cases/figures/Elastic_animation.gif b/zh-cn/third-party-cases/figures/Elastic_animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..42106657e28e924aab39f3882761fba51de47803 Binary files /dev/null and b/zh-cn/third-party-cases/figures/Elastic_animation.gif differ diff --git a/zh-cn/third-party-cases/figures/attribute_animation.gif b/zh-cn/third-party-cases/figures/attribute_animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..c3118add02640ccd6ac430da5a4790f7417abcd4 Binary files /dev/null and b/zh-cn/third-party-cases/figures/attribute_animation.gif differ diff --git a/zh-cn/third-party-cases/figures/overall-and-part-refresh.gif b/zh-cn/third-party-cases/figures/overall-and-part-refresh.gif new file mode 100644 index 0000000000000000000000000000000000000000..27390a668a5093138aab568d245daabffbf39741 Binary files /dev/null and b/zh-cn/third-party-cases/figures/overall-and-part-refresh.gif differ diff --git a/zh-cn/third-party-cases/figures/solving-plan.PNG b/zh-cn/third-party-cases/figures/solving-plan.PNG new file mode 100644 index 0000000000000000000000000000000000000000..b41f782365e172c172f25b5a53aed8612121025c Binary files /dev/null and b/zh-cn/third-party-cases/figures/solving-plan.PNG differ diff --git a/zh-cn/third-party-cases/figures/time_curve.gif b/zh-cn/third-party-cases/figures/time_curve.gif new file mode 100644 index 0000000000000000000000000000000000000000..0541817a42da6e1ad0d44b3c92a8f19b8ec79f27 Binary files /dev/null and b/zh-cn/third-party-cases/figures/time_curve.gif differ diff --git a/zh-cn/third-party-cases/overall-and-part-refresh.md b/zh-cn/third-party-cases/overall-and-part-refresh.md new file mode 100644 index 0000000000000000000000000000000000000000..b974142f013c7273f0e32260c5dea4d628f8b08d --- /dev/null +++ b/zh-cn/third-party-cases/overall-and-part-refresh.md @@ -0,0 +1,501 @@ +# 控制页面刷新范围 + +## 场景说明 +在实现页面UI时,业务方需要根据业务逻辑动态更新组件的状态,常见的如在手机桌面长按某个App的图标时,图标背景色、大小等会发生变化。根据业务需要,有时我们需要触发单个组件的状态更新,有时需要触发部分或全部组件的状态更新。那么如何控制组件状态刷新的范围呢?本例将为大家提供一种参考方案。 + +## 效果呈现 +本例最终效果如下: + +![part-and-full-refresh](figures/overall-and-part-refresh.gif) + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发: + +- IDE: DevEco Studio 3.1 Release +- SDK: Ohos_sdk_public 3.2.12.5(API Version 9 Release) + + +## 实现思路 +ArkUI可以通过页面的状态数据驱动UI的更新,其UI更新机制可以通过如下表达式来体现: + +***UI=f(state)*** + +用户构建了UI模型,其中参数state代表页面组件的运行时状态。当state改变时,UI作为返回结果,也将进行对应的改变刷新。f作为状态管理机制,维护组件运行时的状态变化所带来的UI重新渲染。组件的状态改变可通过状态变量进行控制。 + +基于上述理论,如果要控制页面的更新范围,我们必须要:**定义准确状态变量,并控制状态变量影响的组件范围。** + +本例中包含8个APP图标,其中涉及5种状态变化,按照局部刷新和全局刷新可分为: + +- 局部刷新(单个卡片变化) + + - 点击卡片,卡片背景色变为红色。 + - 点击卡片,卡片进行缩放。 + - 拖拽卡片,卡片位置变化。 + +- 全局刷新(全部卡片变化) + + - 长按某个卡片,为所有卡片添加删除图标。 + - 点击删除图标外的任意地方,删除图标消失。 + +所以处理思路为,控制局部刷新的状态变量在子组件中定义,绑定子组件,控制全局刷新的状态变量在父组件中进行定义,并由父组件传递给所有子组件。如下图: + +![solving-plan](figures/solving-plan.PNG) + +## 开发步骤 +由于本例重点讲解刷新区域的控制,所以开发步骤会着重讲解相关实现,不相关的内容不做介绍,全量代码可参考完整代码章节。 +1. 创建APP卡片组件作为子组件,每个卡片包含文本和删除图标。 +具体代码如下: + ```ts + @Component + export struct AppItem { + ... + build() { + Stack({ alignContent: Alignment.TopEnd }) { + Image($r('app.media.ic_public_close')) + .height(30) + .width(30) + .zIndex(2) + .offset({ + x: -12, + y: 12 + }) + Text(this.data.title) + .width(100) + .height(100) + .fontSize(16) + .margin(10) + .textAlign(TextAlign.Center) + .borderRadius(10) + } + } + } + ``` +2. 创建父组件,并在父组件中引用子组件。 +具体代码如下: + ```ts + @Entry + @Component + struct Sample { + ... + build() { + Stack({ alignContent: Alignment.Bottom }) { + Flex({ wrap: FlexWrap.Wrap }) { + // 通过循环渲染加载所有子组件 + ForEach(this.items, (item: ItemProps, index: number) => { + // 引用App卡片子组件 + AppItem({data: this.items[index]}) + }, (item: ItemProps) => item.id.toString()) + } + .width('100%') + .height('100%') + } + .width('100%') + .height('100%') + .backgroundColor('#ffffff') + .margin({ top:50 }) + } + } + ``` +3. 由于卡片背景色变化、卡片缩放、卡片拖拽在触发时都是针对单个卡片的状态变化,所以在卡片子组件中定义相应的状态变量,用来控制单个卡片的状态变化。 +本例中定义状态变量“data”用来控制卡片拖拽时位置的刷新;定义状态变量”downFlag“用来监听卡片是否被按下,从而控制卡片背景色及缩放状态的更新。 +具体代码如下: + ```ts + @Component + export struct AppItem { + // 定义状态变量data,用来控制卡片被拖拽时位置的刷新 + @State data: ItemProps = {}; + // 定义状态变量downFlag用来监听卡片是否被按下,从而控制卡片背景色及缩放状态的更新 + @State downFlag: boolean = false; + ... + build() { + Stack({ alignContent: Alignment.TopEnd }) { + Image($r('app.media.ic_public_close')) + .height(30) + .width(30) + .zIndex(2) + .offset({ + x: -12, + y: 12 + }) + Text(this.data.title) + .width(100) + .height(100) + .fontSize(16) + .margin(10) + .textAlign(TextAlign.Center) + .borderRadius(10) + // 根据状态变量downFlag的变化,更新背景色 + .backgroundColor(this.downFlag ? '#EEA8AB' : '#86C7CC') + // 背景色更新时添加属性动画 + .animation({ + duration: 500, + curve: Curve.Friction + }) + // 绑定onTouch事件,监听卡片是否被按下,根据不同状态改变downFlag的值 + .onTouch((event: TouchEvent) => { + if (event.type == TouchType.Down) { + this.downFlag = true + } else if (event.type == TouchType.Up) { + this.downFlag = false + } + }) + } + // 根据状态变量downFlag的变化,控制卡片的缩放 + .scale(this.downFlag ? { x: 0.8, y: 0.8 } : { x: 1, y: 1 }) + // 通过状态变量data的变化,控制卡片位置的更新 + .offset({ + x: this.data.offsetX, + y: this.data.offsetY + }) + // 拖动触发该手势事件 + .gesture( + GestureGroup(GestureMode.Parallel, + ... + PanGesture(this.panOption) + .onActionStart((event: GestureEvent) => { + console.info('Pan start') + }) + // 拖动卡片时,改变状态变量data的值 + .onActionUpdate((event: GestureEvent) => { + this.data.offsetX = this.data.positionX + event.offsetX + this.data.offsetY = this.data.positionY + event.offsetY + }) + .onActionEnd(() => { + this.data.positionX = this.data.offsetX + this.data.positionY = this.data.offsetY + console.info('Pan end') + }) + ) + ) + } + } + ``` +4. 长按卡片,卡片右上角会出现删除图标。由于所有卡片右上角都会出现删除图标,所以这里需要做全局的刷新。本例在父组件中定义状态变量“deleteVisibility”,在调用子组件时,将其作为参数传递给所有卡片子组件,并且通过@Link装饰器与子组件进行双向绑定,从而可以控制所有卡片子组件中删除图标的更新。 +**父组件代码:** + ```ts + @Entry + @Component + struct Sample { + ... + // 定义状态变量deleteVisibility,控制App卡片上删除图标的更新 + @State deleteVisibility: boolean = false + ... + build() { + Stack({ alignContent: Alignment.Bottom }) { + Flex({ wrap: FlexWrap.Wrap }) { + // 通过循环渲染加载所有子组件 + ForEach(this.items, (item: ItemProps, index: number) => { + // 将状态变量deleteVisibility传递给每一个子组件,从而deleteVisibility变化时可以触发所有子组件的更新 + AppItem({ deleteVisibility: $deleteVisibility, data: this.items[index], onDeleteClick: this.delete }) + }, (item: ItemProps) => item.id.toString()) + } + .width('100%') + .height('100%') + } + .width('100%') + .height('100%') + .backgroundColor('#ffffff') + .margin({ top:50 }) + .onClick(() => { + this.deleteVisibility = false + }) + } + ``` + **子组件代码:** + ```ts + @Component + export struct AppItem { + ... + // 定义deleteVisibility状态变量,并通过@Link装饰器与父组件中的同名变量双向绑定,该变量值发生变化时父子组件可双向同步 + @Link deleteVisibility: boolean; + ... + build() { + Stack({ alignContent: Alignment.TopEnd }) { + // 通过deleteVisibility控制删除图标的隐藏和显示,当deleteVisibility值为true时显示,为false时隐藏 + if(this.deleteVisibility){ + Image($r('app.media.ic_public_close')) + .height(30) + .width(30) + .zIndex(2) + // 控制删除图标的显隐 + .visibility(Visibility.Visible) + .offset({ + x: -12, + y: 12 + }) + .onClick(() => this.onDeleteClick(this.data.id)) + }else{ + Image($r('app.media.ic_public_close')) + .height(30) + .width(30) + .zIndex(2) + .visibility(Visibility.Hidden) + .offset({ + x: -12, + y: 12 + }) + .onClick(() => this.onDeleteClick(this.data.id)) + } + ... + .gesture( + GestureGroup(GestureMode.Parallel, + // 识别长按手势 + LongPressGesture({ repeat: true }) + .onAction((event: GestureEvent) => { + if (event.repeat) { + // 长按时改变deleteVisibility的值为true,从而更新删除图标为显示状态 + this.deleteVisibility = true + } + console.info('LongPress onAction') + }), + ... + ) + ) + } + } + ``` + + +## 完整代码 +本例完整代码如下: +data.ets文件(数据模型文件) +```ts +// data.ets +// AppItem组件接口信息 +export interface ItemProps { + id?: number, + title?: string, + offsetX?: number, // X偏移量 + offsetY?: number, // Y偏移量 + positionX?: number, // 在X的位置 + positionY?: number, // 在Y的位置 +} + +// AppItem初始数据 +export const initItemsData: ItemProps[] = [ + { + id: 1, + title: 'APP1', + offsetX: 0, + offsetY: 0, + positionX: 0, + positionY: 0 + }, + { + id: 2, + title: 'APP2', + offsetX: 0, + offsetY: 0, + positionX: 0, + positionY: 0 + }, + { + id: 3, + title: 'APP3', + offsetX: 0, + offsetY: 0, + positionX: 0, + positionY: 0 + }, + { + id: 4, + title: 'APP4', + offsetX: 0, + offsetY: 0, + positionX: 0, + positionY: 0 + }, + { + id: 5, + title: 'APP5', + offsetX: 0, + offsetY: 0, + positionX: 0, + positionY: 0 + }, + { + id: 6, + title: 'APP6', + offsetX: 0, + offsetY: 0, + positionX: 0, + positionY: 0 + }, + { + id: 7, + title: 'APP7', + offsetX: 0, + offsetY: 0, + positionX: 0, + positionY: 0 + }, + { + id: 8, + title: 'APP8', + offsetX: 0, + offsetY: 0, + positionX: 0, + positionY: 0 + }, +] +``` +AppItem.ets文件(卡片子组件) +```ts +// AppItem.ets +import { ItemProps } from '../model/data'; + +@Component +export struct AppItem { + // 定义状态变量data,用来控制卡片被拖拽时位置的刷新 + @State data: ItemProps = {}; + // 定义状态变量downFlag用来监听卡片是否被按下,从而控制卡片背景色及缩放状态的更新 + @State downFlag: boolean = false; + // 定义deleteVisibility状态变量,并通过@Link装饰器与父组件中的同名变量双向绑定,该变量值发生变化时父子组件可双向同步 + @Link deleteVisibility: boolean; + + private onDeleteClick: (id: number) => void; + private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All }); + build() { + Stack({ alignContent: Alignment.TopEnd }) { + // 通过deleteVisibility控制删除图标的隐藏和显示,当deleteVisibility值为true时显示,为false时隐藏 + if(this.deleteVisibility){ + Image($r('app.media.ic_public_close')) + .height(30) + .width(30) + .zIndex(2) + // 控制删除图标的显隐 + .visibility(Visibility.Visible) + .offset({ + x: -12, + y: 12 + }) + .onClick(() => this.onDeleteClick(this.data.id)) + }else{ + Image($r('app.media.ic_public_close')) + .height(30) + .width(30) + .zIndex(2) + .visibility(Visibility.Hidden) + .offset({ + x: -12, + y: 12 + }) + .onClick(() => this.onDeleteClick(this.data.id)) + } + + Text(this.data.title) + .width(100) + .height(100) + .fontSize(16) + .margin(10) + .textAlign(TextAlign.Center) + .borderRadius(10) + // 根据状态变量downFlag的变化,更新背景色 + .backgroundColor(this.downFlag ? '#EEA8AB' : '#86C7CC') + // 背景色更新时添加属性动画 + .animation({ + duration: 500, + curve: Curve.Friction + }) + // 绑定onTouch事件,监听卡片是否被按下,根据不同状态改变downFlag的值 + .onTouch((event: TouchEvent) => { + if (event.type == TouchType.Down) { + this.downFlag = true + } else if (event.type == TouchType.Up) { // 手指抬起 + this.downFlag = false + } + }) + } + // 根据状态变量downFlag的变化,控制卡片的缩放 + .scale(this.downFlag ? { x: 0.8, y: 0.8 } : { x: 1, y: 1 }) + // 通过状态变量data的变化,控制卡片位置的更新 + .offset({ + x: this.data.offsetX, + y: this.data.offsetY + }) + // 拖动触发该手势事件 + .gesture( + GestureGroup(GestureMode.Parallel, + // 识别长按手势 + LongPressGesture({ repeat: true }) + .onAction((event: GestureEvent) => { + if (event.repeat) { + // 长按时改变deleteVisibility的值为true,从而更新删除图标为显示状态 + this.deleteVisibility = true + } + console.info('LongPress onAction') + }), + PanGesture(this.panOption) + .onActionStart((event: GestureEvent) => { + console.info('Pan start') + }) + // 拖动卡片时,改变状态变量data的值 + .onActionUpdate((event: GestureEvent) => { + this.data.offsetX = this.data.positionX + event.offsetX + this.data.offsetY = this.data.positionY + event.offsetY + }) + .onActionEnd(() => { + this.data.positionX = this.data.offsetX + this.data.positionY = this.data.offsetY + console.info('Pan end') + }) + ) + ) + } +} +``` +Index.ets文件(父组件) +```ts +// Index.ets +import { AppItem } from '../components/MyItem'; +import { initItemsData } from '../model/data'; +import { ItemProps } from '../model/data'; + +@Entry +@Component +struct Sample { + @State items: ItemProps[] = []; + // 定义状态变量deleteVisibility,控制App卡片上删除图标的更新 + @State deleteVisibility: boolean = false + + // 删除指定id组件 + private delete = (id: number) => { + const index = this.items.findIndex(item => item.id === id); + this.items.splice(index, 1); + } + + // 生命周期函数:组件即将出现时调用 + aboutToAppear() { + this.items = [...initItemsData]; + } + + build() { + Stack({ alignContent: Alignment.Bottom }) { + Flex({ wrap: FlexWrap.Wrap }) { + // 通过循环渲染加载所有子组件 + ForEach(this.items, (item: ItemProps, index: number) => { + // 将状态变量deleteVisibility传递给每一个子组件,从而deleteVisibility变化时可以触发所有子组件的更新 + AppItem({ deleteVisibility: $deleteVisibility, data: this.items[index], onDeleteClick: this.delete }) + }, (item: ItemProps) => item.id.toString()) + } + .width('100%') + .height('100%') + } + .width('100%') + .height('100%') + .backgroundColor('#ffffff') + .margin({ top:50 }) + .onClick(() => { + // 点击组件,deleteVisibility值变为false,从而隐藏所有卡片的删除图标 + this.deleteVisibility = false + }) + } +} +``` + + +## 参考 +- [@Link](../application-dev/quick-start/arkts-link.md) + +- [显隐控制](../application-dev/reference/arkui-ts/ts-universal-attributes-visibility.md) +- [组合手势](../application-dev/reference/arkui-ts/ts-combined-gestures.md) \ No newline at end of file diff --git a/zh-cn/third-party-cases/property-animation.md b/zh-cn/third-party-cases/property-animation.md new file mode 100644 index 0000000000000000000000000000000000000000..bc487097c9d5fb8a13273b9d39369ecd0458c2c1 --- /dev/null +++ b/zh-cn/third-party-cases/property-animation.md @@ -0,0 +1,516 @@ +# 属性动画 + +## 场景介绍 +在日常开发过程中,通常会出现因为状态变化导致组件属性值发生变化,如: +- 数字键盘按键时,对应数字按钮背景色加深,呈现被点击效果;放开按键时,呈现取消选中效果, +- UI中图标按下时,图标出现弹性缩放效果, +- 在做数据统计时,随着数据值的变化,统计曲线随之变化等动画效果, +本例将为大家介绍下如何通过属性动画实现上述场景。 + +## 效果呈现 +效果图如下: + +| 场景 | 效果图 | +|------------|-----------------------| +| 场景1:属性动画 | ![属性动画](figures/attribute_animation.gif) | +| 场景2:弹性动态 | ![弹性动态](figures/Elastic_animation.gif) | +| 场景3:曲线动画 | ![时间曲线](figures/time_curve.gif) | + +## 运行环境 +本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发 +- IDE: DevEco Studio 3.1 Release +- SDK: Ohos_sdk_public 3.2.12.5(API Version 9 Release) + +## 场景1:属性动画 +### 实现思路 +本实例涉及到的主要特性及其实现方案如下: +* 通过Column、Grid、button、Text等关键组件以及visibility属性搭建UI局框架,以及渲染数字按钮。 +* 设置状态变量flag,用来控制属性状态的变化,同时向Button组件添加onTouch事件,更新按钮的当前状态,从而可以根据监听到的按钮状态加载对应的动画效果。 + * 默认状态为按钮放开状态(flag为false)。 + * 当按钮按下时,更新按钮的状态(flag:false -> true)。 + * 当按钮放开时,更新按钮的状态(flag:true -> false)。 +* 根据按钮组件的按下/放开状态,通过三元运算判断,使用属性动画更新按钮的选中/取消效果。 + * 当按钮按下时,加载属性动画:按钮被选中效果。 + * 当按钮放开时,加载属性动画:按钮取消选中效果。 +### 开发步骤 +针对实现思路中所提到的内容,具体关键开发步骤如下: +1. 通过Column、Grid、button、Text等关键组件以及visibility属性搭建UI框架,以及渲染数字按钮。 + 具体代码如下: + + ```ts + + private numGrid: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, -1, 0, -1] + + build() { + Row() { + Column() { + Grid() { + //通过ForEach循环遍历,显示数字数字按钮 + ForEach(this.numGrid, (item, index) => { + GridItem() { + Button() { + Text(`${item}`) + .fontSize(30) + } + .backgroundColor('#fff') + .width(100) + .height(100) + // item为-1时,数字按钮不显示 + .visibility(item == -1 ? Visibility.Hidden : Visibility.Visible) + } + }, item => item) + } + .columnsTemplate('1fr 1fr 1fr') + .rowsTemplate('1fr 1fr 1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width(330) + .height(440) + } + .width('100%') + .height('100%') + } + } + + ``` + +2. 设置状态变量flag,用来控制属性状态的变化,同时向Button组件添加onTouch事件,更新按钮的当前状态,从而可以根据监听到的按钮状态加载对应的动画效果。 + + 具体代码如下: + + ```ts + //状态变量flag,用于监听按钮按下和放开的状态,默认至为false(放开状态) + @State flag: boolean = false; + ... + .onTouch((event:TouchEvent) => { + //当按钮按下时,更新按钮的状态(flag:false -> true) + if (event.type == TouchType.Down) { + this.flag = true + this.currIndex = index + } + //当按钮放开时,更新按钮的状态(flag:true -> false) + if (event.type == TouchType.Up) { + this.flag = false + } + }) + ``` + + +3. 根据按钮组件的按下/放开状态,通过三元运算判断并更新按钮背景色。 + + 具体代码如下: + + ```ts + Button(){ + ... + // 当按钮被按下(flag变更为true)时,当前按钮backgroundColor属性变更("#fff" -> "#D2C3D5" ) + // 当按钮被放开(flag变更为false)时,当前按钮backgroundColor属性变更("#D2C3D5" -> "#FFF" ) + .backgroundColor(this.flag && this.currIndex == index ? '#D2C3D5' : '#fff') + .animation({ + duration: 200 + }) + } + ``` + + +### 完整代码 +```ts +@Entry +@Component +struct Index { + private currIndex: number = -1 + private numGrid: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, -1, 0, -1] + //状态变量flag,用于监听按钮按下和放开的状态,默认至为false(放开状态) + @State flag: boolean = false; + + build() { + Row() { + Column() { + Grid() { + ForEach(this.numGrid, (item, index) => { + GridItem() { + Button() { + Text(`${item}`) + .fontSize(30) + } + // 当按钮被按下(flag变更为true)时,当前按钮backgroundColor属性变更("#fff" -> "#D2C3D5" ) + // 当按钮被放开(flag变更为false)时,当前按钮backgroundColor属性变更("#D2C3D5" -> "#FFF" ) + .backgroundColor(this.flag && this.currIndex == index ? '#D2C3D5' : '#fff') + .animation({ + duration: 200 + }) + .width(100) + .height(100) + .visibility(item == -1 ? Visibility.Hidden : Visibility.Visible) + .onTouch((event:TouchEvent) => { + //当按钮按下时,更新按钮的状态(flag:false -> true) + if (event.type == TouchType.Down) { + this.flag = true + this.currIndex = index + } + //当按钮放开时,更新按钮的状态(flag:true -> false) + if (event.type == TouchType.Up) { + this.flag = false + } + }) + } + }, item => item) + } + .columnsTemplate('1fr 1fr 1fr') + .rowsTemplate('1fr 1fr 1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width(330) + .height(440) + } + .width('100%') + .height('100%') + } + } +} +``` + +## 场景2:弹性动效 +### 实现思路 +针对弹性动效,涉及到的主要特征分为两部分: +* 组件描绘:先通过List、ListItem、Image等组件将控件表描绘出来。 +* 组件状态变化:设置状态变量downFlag,控制按钮的当前状态;同时向Column组件添加onTouch事件,监听组件的当前状态。 + * 组件默认状态为放开状态(downFlag:false)。 + * 当按下Image组件时,更新组件的状态为true。 + * 当放开Image组件时,更新组件的状态为false。 +* 动画播放:使用属性动画绘制Image组件不同状态下的曲线动画。 + * 组件按下,触发第一阶段缩小动效,370ms内将组件横纵尺寸均缩小到原尺寸的80%。 + * 组件放开,触发第二阶段放大动效,在370ms内将控件从当前尺寸恢复到原尺寸。 +### 开发步骤 +针对实现思路中所提到的内容,具体关键开发步骤如下: +1. 先通过List、ListItem、Image等组件将控件表描绘出来。 + + 具体代码如下: + + ```ts + private arr: number[]=[1,2,3,4,5] + + ... + List({space:10}){ + ForEach(this.arr,(item,index)=>{ + ListItem(){ + Image($r("app.media.app_icon")) + .width(80) + .height(80) + } + }) + } + .margin({top:20}) + .padding({left:20}) + .listDirection(Axis.Horizontal) + ``` + +2. 设置状态变量downFlag,控制Image组件的当前状态,同时向Image组件添加onTouch事件,获取并更新组件的当前状态,从而可以根据监听到的组件状态加载对应的动画效果。 + 具体代码如下: + + ```ts + ... + // 状态变量downFlag,用于监听Image组件按下和放开的状态 + @State downFlag: boolean = false; + ... + // 添加onTouch事件,监听状态 + .onTouch((event: TouchEvent) => { + // 当Column组件按下时,组件的状态更新为true + if (event.type == TouchType.Down) { + this.downFlag = true + // 当Column组件按下时,组件的状态更新为false + } else if (event.type == TouchType.Up) { + this.downFlag = false + } + }) + ``` + +3. 根据Image组件的按下/放开状态,呈现不同的弹性效果(按下时组件缩小,动画以阻尼曲线的形式缩小至0.8倍,放开时组件动画以阻尼曲线的形式恢复至初始大小)。 + 具体代码如下: + + ```ts + ··· + Image($r("app.media.app_icon")) + // 当downFlag状态为按下(true)时,组件在370ms内缩小至0.5倍;当downFlag状态为放开(false)时,组件在370ms内恢复至初始大小; + .scale(this.downFlag ? { x: 0.8, y: 0.8 } : { x: 1, y: 1 }) + .animation({ + duration: 370, + curve: Curve.Friction + }) + ``` + +### 完整代码 +```ts +@Entry +@Component +struct Index { + // 状态变量downFlag,用于监听Image组件按下和放开的状态 + @State downFlag: boolean = false; + private arr: number[]=[1,2,3,4,5] + private curIndex : number = -1 + + build() { + List({space:10}){ + ForEach(this.arr,(item,index)=>{ + ListItem(){ + Image($r("app.media.app_icon")) + // 当downFlag状态为按下(true)时,组件在370ms内缩小至0.8倍;当downFlag状态为放开(false)时,组件在370ms内恢复至初始大小; + .scale(this.downFlag && this.curIndex == index ? { x: 0.8, y: 0.8 } : { x: 1, y: 1 }) + .animation({ + duration: 370, + curve: Curve.Friction + }) + .width(80) + .height(80) + // 添加onTouch事件,监听状态 + .onTouch((event: TouchEvent) => { + // 当Image组件按下时,组件的状态更新为true + if (event.type == TouchType.Down) { + this.downFlag = true + this.curIndex = index + // 当Image组件按下时,组件的状态更新为false + } else if (event.type == TouchType.Up) { + this.downFlag = false + } + }) + } + }) + } + .margin({top:20}) + .padding({left:20}) + // 列表排列方向水平 + .listDirection(Axis.Horizontal) + } +} +``` + +## 场景3:曲线动画 +曲线动画可以使用不同的曲线属性,呈现出不同的动画。 +ArkUI中,曲线动画有两种使用方式,一种是直接使用Curve类型的枚举,一种是导入ohos.curve模块并使用模块内定义的接口/属性。 + +### 实现思路 +* 设置自定义变量item,用于存放不同类型的曲线,本案例中主要有以下类型曲线: + 1. Curve类型的枚举: + * Linear:表示动画从头到尾的速度都是相同的。 + * EaseInOut:表示动画以低速开始和结束,CubicBezier(0.42, 0.0, 0.58, 1.0)。 + * Smooth:平滑曲线,cubic-bezier(0.4, 0.0, 0.4, 1.0)。 + 2. ohos.curve导入: + * springCurve类的弹簧曲线。 +* 通过Flex、Column、button组件将UI布局以及框架搭建。 +* Column组件内部通过ForEach,遍历展示每条曲线。 +* 设置状态变量flag,监听当前button的状态,同时向button组件添加onClick事件,更新按钮的状态,从而可以根据监听到的按钮状态加载对应的动画效果。 +* 向Text组件添加width属性,根据按钮的状态,在同一周期内,通过width的变化呈现曲线的不同动画。 +### 开发步骤 +1. 设置自定义变量item,用于存放不同类型的曲线。 + + 具体代码如下: + + ```ts + private items: object[] = [ + { + color: '#a320bf', + title: '线性曲线', + // curve类型曲线:表示动画从头到尾的速度都是相同的。 + curve: Curve.Linear, + }, + { + color: '#17d4be', + title: '低速结束曲线', + // curve类型曲线:表示动画以低速结束 + curve: Curve.EaseInOut, + }, + { + color: '#0e2d8c', + title: '平滑曲线', + // curve类型曲线:平滑曲线 + curve: Curve.Smooth, + }, + { + color: '#d4bb19', + title: 'Curves.spring', + // ohos.curve导入的弹簧曲线,以初速度为20的弹簧进行平移 + curve: Curves.springCurve(20, 1, 1, 1.2), + }, + ``` + +2. 通过Flex、Column、button组件将UI布局以及框架搭建。 + + 具体代码如下: + + ```ts + Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { + Column({ space: 50 }) { + ... + } + .alignItems(HorizontalAlign.Start) + .width('100%') + .padding({ left: 12, right: 12, top: 22, bottom: 22 }) + + Button(){ + Text("Go!") + .fontSize(20) + .fontColor('#fff') + } + .borderRadius(20) + .fontColor('#FFFFFF') + .fontSize('12fp') + .fontWeight(FontWeight.Bolder) + .backgroundColor('#15587c') + .padding(25) + .margin({top:50}) + } + .height('100%') + .backgroundColor('#F1F3F5') + .padding({ left: '3%', right: '3%' }) + ``` + +3. Column组件内部通过ForEach,遍历展示每条曲线。 + + 具体代码如下: + + ```ts + ForEach(this.items, (item, index) => { + Text(item['title']) + .fontSize(20) + .fontColor('#FFFFFF') + .height(80) + .textAlign(TextAlign.Start) + .backgroundColor(item['color']) + .borderRadius(20) + .padding({left:10}) + }, item => JSON.stringify(item)) + + ``` + +4. 设置状态变量flag,监听当前button的状态,同时向button组件添加onClick事件,更新按钮的状态,从而可以根据监听到的按钮状态加载对应的动画效果。 + + 具体代码如下: + + ```ts + //状态变量flag,用于监听按钮按下状态 + @State flag: boolean = true + ... + Button(){ + } + // 通过点击事件,反转flag的值 + .onClick(() => { + this.flag = !this.flag + }) + ``` + +5. 向Text组件添加width属性,根据按钮的状态,在同一周期内,呈现不同效果的曲线动画。 + + 具体代码如下: + + ```ts + ForEach(this.items, (item, index) => { + Text(item['title']) + ... + // 当flag为true时,width为15%;当flag为false时,width为95% + .width(this.flag ? '15%': '95%') + .animation({ duration: 2000, curve: item['curve'] }) + }, item => JSON.stringify(item)) + ``` + +### 完整代码 +```ts +import Curves from '@ohos.curves'; + +@Entry +@Component +struct Index { + // 状态变量flag,用于监听按钮按下状态 + @State flag: boolean = true + private items: object[] = [ + { + color: '#a320bf', + title: '线性曲线', + // curve类型曲线:表示动画从头到尾的速度都是相同的。 + curve: Curve.Linear, + }, + { + color: '#17d4be', + title: '低速结束曲线', + // curve类型曲线:表示动画以低速结束 + curve: Curve.EaseInOut, + }, + { + color: '#0e2d8c', + title: '平滑曲线', + // curve类型曲线:平滑曲线 + curve: Curve.Smooth, + }, + { + color: '#d4bb19', + title: '弹簧曲线', + // ohos.curve导入的弹簧曲线动画,以初速度为20的弹簧进行平移 + curve: Curves.springCurve(20, 1, 1, 1.2), + }, + ] + + build() { + Flex({ + direction: FlexDirection.Column, + justifyContent: FlexAlign.Start, + alignItems: ItemAlign.Center + }) { + Column({ space: 50 }) { + ForEach(this.items, (item, index) => { + Text(item['title']) + .fontSize(20) + .fontColor('#FFFFFF') + // 当flag为true时,width为15%;当flag为false时,width为95%; + .width(this.flag ? '15%': '95%') + .height(80) + .textAlign(TextAlign.Start) + .backgroundColor(item['color']) + // 2s之内完成动画展示,每条动画曲线按照item['curve']去展示 + .animation({ duration: 2000, curve: item['curve'] }) + .borderRadius(20) + .padding({left:10}) + }, item => JSON.stringify(item)) + } + .alignItems(HorizontalAlign.Start) + .width('100%') + .padding({ left: 12, right: 12, top: 22, bottom: 22 }) + + Button(){ + Text("Go!") + .fontSize(20) + .fontColor('#fff') + } + .borderRadius(20) + .fontColor('#FFFFFF') + .fontSize('12fp') + .fontWeight(FontWeight.Bolder) + .backgroundColor('#15587c') + .padding(25) + .margin({top:50}) + // 通过点击事件,反转flag的值 + .onClick(() => { + this.flag = !this.flag + }) + } + .height('100%') + .backgroundColor('#F1F3F5') + .padding({ left: '3%', right: '3%' }) + } +} +``` + + +​ + +## 参考 + +[属性动画](../application-dev/reference/arkui-ts/ts-animatorproperty.md#属性动画) + +[Curve](../application-dev/reference/arkui-ts/ts-appendix-enums.md#curve) + +[弹簧曲线动画](../application-dev/ui/arkts-spring-animation.md) + +[Flex布局](../application-dev/reference/arkui-ts/ts-universal-attributes-flex-layout.md) + +[Listitem](../application-dev/reference/arkui-ts/ts-container-listitem.md) + +[List](../application-dev/reference/arkui-ts/ts-container-list.md) \ No newline at end of file