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