• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 组件动画
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @CCFFWW-->
5<!--Designer: @yangfan229-->
6<!--Tester: @lxl007-->
7<!--Adviser: @HelloCrease-->
8
9
10ArkUI为组件提供了通用的属性动画和转场动画能力的同时,还为一些组件提供了默认的动画效果。例如,[List](../reference/apis-arkui/arkui-ts/ts-container-list.md)的滑动动效、[Button](../reference/apis-arkui/arkui-ts/ts-basic-components-button.md)的点击动效,是组件自带的默认动画效果。在组件默认动画效果的基础上,开发者还可以通过属性动画和转场动画对容器组件内的子组件动效进行定制。
11
12
13## 使用组件默认动画
14
15组件默认动效具备以下功能:
16
17- 提示用户当前状态,例如用户点击Button组件时,Button组件默认变灰,用户即确定完成选中操作。
18
19- 提升界面精致程度和生动性。
20
21- 减少开发者工作量,例如列表滑动组件自带滑动动效,开发者直接调用即可。
22
23更多效果,可以参考[组件说明](../reference/apis-arkui/arkui-ts/ts-container-flex.md)。
24
25示例代码和效果如下。
26
27
28```ts
29@Entry
30@Component
31struct ComponentDemo {
32  build() {
33    Row() {
34      Checkbox({ name: 'checkbox1', group: 'checkboxGroup' })
35        .select(true)
36        .shape(CheckBoxShape.CIRCLE)
37        .size({ width: 50, height: 50 })
38    }
39    .width('100%')
40    .height('100%')
41    .justifyContent(FlexAlign.Center)
42  }
43}
44```
45
46![zh-cn_image_0000001649338585](figures/zh-cn_image_0000001649338585.gif)
47
48
49
50## 打造组件定制化动效
51
52部分组件支持通过[属性动画](arkts-attribute-animation-overview.md)和[转场动画](arkts-transition-overview.md)自定义组件子Item的动效,实现定制化动画效果。例如,[Scroll](../reference/apis-arkui/arkui-ts/ts-container-scroll.md)组件中可对各个子组件在滑动时的动画效果进行定制。
53
54- 在滑动或者点击操作时通过改变各个Scroll子组件的仿射属性来实现各种效果。
55
56- 如果要在滑动过程中定制动效,可在滑动回调onScroll中监控滑动距离,并计算每个组件的仿射属性。也可以自己定义手势,通过手势监控位置,手动调用ScrollTo改变滑动位置。
57
58- 在滑动回调onScrollStop或手势结束回调中对滑动的最终位置进行微调。
59
60定制Scroll组件滑动动效示例代码和效果如下。
61
62
63```ts
64import { curves, window, display, mediaquery, UIContext } from '@kit.ArkUI';
65import { UIAbility } from '@kit.AbilityKit';
66
67export default class GlobalContext extends AppStorage {
68  static mainWin: window.Window | undefined = undefined;
69  static mainWindowSize: window.Size | undefined = undefined;
70}
71/**
72 * 窗口、屏幕相关信息管理类
73 */
74export class WindowManager {
75  private static instance: WindowManager | null = null;
76  private displayInfo: display.Display | null = null;
77  private uiContext: UIContext;
78  private orientationListener: mediaquery.MediaQueryListener;
79
80  constructor(uiContext: UIContext) {
81    this.uiContext = uiContext
82    this.orientationListener = this.uiContext.getMediaQuery().matchMediaSync('(orientation: landscape)');
83    this.orientationListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => {
84      this.onPortrait(mediaQueryResult)
85    })
86    this.loadDisplayInfo()
87  }
88
89  /**
90   * 设置主window窗口
91   * @param win 当前app窗口
92   */
93  setMainWin(win: window.Window) {
94    if (win == null) {
95      return
96    }
97    GlobalContext.mainWin = win;
98    win.on("windowSizeChange", (data: window.Size) => {
99      if (GlobalContext.mainWindowSize == undefined || GlobalContext.mainWindowSize == null) {
100        GlobalContext.mainWindowSize = data;
101      } else {
102        if (GlobalContext.mainWindowSize.width == data.width && GlobalContext.mainWindowSize.height == data.height) {
103          return
104        }
105        GlobalContext.mainWindowSize = data;
106      }
107
108      let winWidth = this.getMainWindowWidth();
109      AppStorage.setOrCreate<number>('mainWinWidth', winWidth)
110      let winHeight = this.getMainWindowHeight();
111      AppStorage.setOrCreate<number>('mainWinHeight', winHeight)
112      let context: UIAbility = new UIAbility()
113      context.context.eventHub.emit("windowSizeChange", winWidth, winHeight)
114    })
115  }
116
117  static getInstance(uiContext: UIContext): WindowManager {
118    if (WindowManager.instance == null) {
119      WindowManager.instance = new WindowManager(uiContext);
120    }
121    return WindowManager.instance
122  }
123
124  private onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) {
125    if (mediaQueryResult.matches == AppStorage.get<boolean>('isLandscape')) {
126      return
127    }
128    AppStorage.setOrCreate<boolean>('isLandscape', mediaQueryResult.matches)
129    this.loadDisplayInfo()
130  }
131
132  /**
133   * 切换屏幕方向
134   * @param ori 常量枚举值:window.Orientation
135   */
136  changeOrientation(ori: window.Orientation) {
137    if (GlobalContext.mainWin != null) {
138      GlobalContext.mainWin.setPreferredOrientation(ori)
139    }
140  }
141
142  private loadDisplayInfo() {
143    this.displayInfo = display.getDefaultDisplaySync()
144    AppStorage.setOrCreate<number>('displayWidth', this.getDisplayWidth())
145    AppStorage.setOrCreate<number>('displayHeight', this.getDisplayHeight())
146  }
147
148  /**
149   * 获取main窗口宽度,单位vp
150   */
151  getMainWindowWidth(): number {
152    return GlobalContext.mainWindowSize != null ? this.uiContext.px2vp(GlobalContext.mainWindowSize.width) : 0
153  }
154
155  /**
156   * 获取main窗口高度,单位vp
157   */
158  getMainWindowHeight(): number {
159    return GlobalContext.mainWindowSize != null ? this.uiContext.px2vp(GlobalContext.mainWindowSize.height) : 0
160  }
161
162  /**
163   * 获取屏幕宽度,单位vp
164   */
165  getDisplayWidth(): number {
166    return this.displayInfo != null ? this.uiContext.px2vp(this.displayInfo.width) : 0
167  }
168
169  /**
170   * 获取屏幕高度,单位vp
171   */
172  getDisplayHeight(): number {
173    return this.displayInfo != null ? this.uiContext.px2vp(this.displayInfo.height) : 0
174  }
175
176  /**
177   * 释放资源
178   */
179  release() {
180    if (this.orientationListener) {
181      this.orientationListener.off('change', (mediaQueryResult: mediaquery.MediaQueryResult) => {
182        this.onPortrait(mediaQueryResult)
183      })
184    }
185    if (GlobalContext.mainWin != null) {
186      GlobalContext.mainWin.off('windowSizeChange')
187    }
188    WindowManager.instance = null;
189  }
190}
191
192/**
193 * 封装任务卡片信息数据类
194 */
195export class TaskData {
196  bgColor: Color | string | Resource = Color.White;
197  index: number = 0;
198  taskInfo: string = 'music';
199
200  constructor(bgColor: Color | string | Resource, index: number, taskInfo: string) {
201    this.bgColor = bgColor;
202    this.index = index;
203    this.taskInfo = taskInfo;
204  }
205}
206
207export const taskDataArr: Array<TaskData> =
208  [
209    new TaskData('#317AF7', 0, 'music'),
210    new TaskData('#D94838', 1, 'mall'),
211    new TaskData('#DB6B42', 2, 'photos'),
212    new TaskData('#5BA854', 3, 'setting'),
213    new TaskData('#317AF7', 4, 'call'),
214    new TaskData('#D94838', 5, 'music'),
215    new TaskData('#DB6B42', 6, 'mall'),
216    new TaskData('#5BA854', 7, 'photos'),
217    new TaskData('#D94838', 8, 'setting'),
218    new TaskData('#DB6B42', 9, 'call'),
219    new TaskData('#5BA854', 10, 'music')
220
221  ];
222
223@Entry
224@Component
225export struct TaskSwitchMainPage {
226  displayWidth: number = WindowManager.getInstance(this.getUIContext()).getDisplayWidth();
227  scroller: Scroller = new Scroller();
228  cardSpace: number = 0; // 卡片间距
229  cardWidth: number = this.displayWidth / 2 - this.cardSpace / 2; // 卡片宽度
230  cardHeight: number = 400; // 卡片高度
231  cardPosition: Array<number> = []; // 卡片初始位置
232  clickIndex: boolean = false;
233  @State taskViewOffsetX: number = 0;
234  @State cardOffset: number = this.displayWidth / 4;
235  lastCardOffset: number = this.cardOffset;
236  startTime: number | undefined = undefined
237
238  // 每个卡片初始位置
239  aboutToAppear() {
240    for (let i = 0; i < taskDataArr.length; i++) {
241      this.cardPosition[i] = i * (this.cardWidth + this.cardSpace);
242    }
243  }
244
245  // 每个卡片位置
246  getProgress(index: number): number {
247    let progress = (this.cardOffset + this.cardPosition[index] - this.taskViewOffsetX + this.cardWidth / 2) / this.displayWidth;
248    return progress
249  }
250
251  build() {
252    Stack({ alignContent: Alignment.Bottom }) {
253      // 背景
254      Column()
255        .width('100%')
256        .height('100%')
257        .backgroundColor(0xF0F0F0)
258
259      // 滑动组件
260      Scroll(this.scroller) {
261        Row({ space: this.cardSpace }) {
262          ForEach(taskDataArr, (item: TaskData, index) => {
263            Column()
264              .width(this.cardWidth)
265              .height(this.cardHeight)
266              .backgroundColor(item.bgColor)
267              .borderStyle(BorderStyle.Solid)
268              .borderWidth(1)
269              .borderColor(0xAFEEEE)
270              .borderRadius(15)
271                // 计算子组件的仿射属性
272              .scale((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ?
273                {
274                  x: 1.1 - Math.abs(0.5 - this.getProgress(index)),
275                  y: 1.1 - Math.abs(0.5 - this.getProgress(index))
276                } :
277                { x: 1, y: 1 })
278              .animation({ curve: Curve.Smooth })
279                // 滑动动画
280              .translate({ x: this.cardOffset })
281              .animation({ curve: curves.springMotion() })
282              .zIndex((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ? 2 : 1)
283          }, (item: TaskData) => item.toString())
284        }
285        .width((this.cardWidth + this.cardSpace) * (taskDataArr.length + 1))
286        .height('100%')
287      }
288      .gesture(
289        GestureGroup(GestureMode.Parallel,
290          PanGesture({ direction: PanDirection.Horizontal, distance: 5 })
291            .onActionStart((event: GestureEvent | undefined) => {
292              if (event) {
293                this.startTime = event.timestamp;
294              }
295            })
296            .onActionUpdate((event: GestureEvent | undefined) => {
297              if (event) {
298                this.cardOffset = this.lastCardOffset + event.offsetX;
299              }
300            })
301            .onActionEnd((event: GestureEvent | undefined) => {
302              if (event) {
303                let time = 0
304                if (this.startTime) {
305                  time = event.timestamp - this.startTime;
306                }
307                let speed = event.offsetX / (time / 1000000000);
308                let moveX = Math.pow(speed, 2) / 7000 * (speed > 0 ? 1 : -1);
309
310                this.cardOffset += moveX;
311                // 左滑大于最右侧位置
312                let cardOffsetMax = -(taskDataArr.length - 1) * (this.displayWidth / 2);
313                if (this.cardOffset < cardOffsetMax) {
314                  this.cardOffset = cardOffsetMax;
315                }
316                // 右滑大于最左侧位置
317                if (this.cardOffset > this.displayWidth / 4) {
318                  this.cardOffset = this.displayWidth / 4;
319                }
320
321                // 左右滑动距离不满足/满足切换关系时,补位/退回
322                let remainMargin = this.cardOffset % (this.displayWidth / 2);
323                if (remainMargin < 0) {
324                  remainMargin = this.cardOffset % (this.displayWidth / 2) + this.displayWidth / 2;
325                }
326                if (remainMargin <= this.displayWidth / 4) {
327                  this.cardOffset += this.displayWidth / 4 - remainMargin;
328                } else {
329                  this.cardOffset -= this.displayWidth / 4 - (this.displayWidth / 2 - remainMargin);
330                }
331
332                // 记录本次滑动偏移量
333                this.lastCardOffset = this.cardOffset;
334              }
335            })
336        ), GestureMask.IgnoreInternal)
337      .scrollable(ScrollDirection.Horizontal)
338      .scrollBar(BarState.Off)
339
340      // 滑动到首尾位置
341      Button('Move to first/last')
342        .backgroundColor(0x888888)
343        .margin({ bottom: 30 })
344        .onClick(() => {
345          this.clickIndex = !this.clickIndex;
346
347          if (this.clickIndex) {
348            this.cardOffset = this.displayWidth / 4;
349          } else {
350            this.cardOffset = this.displayWidth / 4 - (taskDataArr.length - 1) * this.displayWidth / 2;
351          }
352          this.lastCardOffset = this.cardOffset;
353        })
354    }
355    .width('100%')
356    .height('100%')
357  }
358}
359```
360
361![zh-cn_image_0000001599808406](figures/zh-cn_image_0000001599808406.gif)
362<!--RP1--><!--RP1End-->