• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Component Animation
2
3
4In addition to universal attribute animation and transition animation APIs, ArkUI provides default animation effects for certain components, for example, the swipe effect for the [\<List>](../reference/arkui-ts/ts-container-list.md) component and the click effect of the [\<Button>](../reference/arkui-ts/ts-basic-components-button.md#button) component. Based on these default animation effects, you can apply custom animations to the child components through the attribute animation and transition animation APIs.
5
6
7## Using Default Component Animation
8
9The default animation of a component exhibits the following features:
10
11- Indicate the current state of the component. For example, after the user clicks a **\<Button>** component, the component turns gray, indicating that it is selected.
12
13- Make UI interactions more intuitive and pleasurable.
14
15- Reduce development workload, as the APIs are readily available.
16
17For more effects, see [Component Overview](../reference/arkui-ts/ts-components-summary.md).
18
19Below is the sample code and effect:
20
21
22```ts
23@Entry
24@Component
25struct ComponentDemo {
26  build() {
27    Row() {
28      Checkbox({ name: 'checkbox1', group: 'checkboxGroup' })
29        .select(true)
30        .selectedColor(0xed6f21)
31        .size({ width: 50, height: 50 })
32
33      Checkbox({ name: 'checkbox2', group: 'checkboxGroup' })
34        .select(false)
35        .selectedColor(0x39a2db)
36        .size({ width: 50, height: 50 })
37    }
38    .width('100%')
39    .height('100%')
40    .justifyContent(FlexAlign.Center)
41  }
42}
43```
44
45![en-us_image_0000001649338585](figures/en-us_image_0000001649338585.gif)
46
47
48
49## Customizing Component Animation
50
51Some components allow for animation customization for their child components through the [attribute animation](arkts-attribute-animation-overview.md) and [transition animation](arkts-transition-overview.md) APIs. For example, in the [\<Scroll>](../reference/arkui-ts/ts-container-scroll.md) component, you can customize the animation effect for when scrolling through its child components.
52
53- For a scroll or click gesture, you can implement various effects by changing affine attributes of the child component.
54
55- To customize the animation for a scroll , you can add a listener to listen for scroll distance in the **onScroll** callback and calculate the affine attribute of each component. You can also define gestures, monitor positions through the gestures, and manually call **ScrollTo** to change the scrolled-to position.
56
57- Fine-tune the final scrolled-to position in the **onScrollStop** callback or gesture end callback.
58
59Below is the sample code and effect for customizing the scroll effect of the child components of the **\<Scroll>** component:
60
61
62```ts
63import curves from '@ohos.curves';
64import window from '@ohos.window';
65import display from '@ohos.display';
66import mediaquery from '@ohos.mediaquery';
67import UIAbility from '@ohos.app.ability.UIAbility';
68
69export default class GlobalContext extends AppStorage{
70  static mainWin: window.Window|undefined = undefined;
71  static mainWindowSize:window.Size|undefined = undefined;
72}
73/**
74 * Encapsulates the WindowManager class.
75 */
76export class WindowManager {
77  private static instance: WindowManager|null = null;
78  private displayInfo: display.Display|null = null;
79  private orientationListener = mediaquery.matchMediaSync('(orientation: landscape)');
80
81  constructor() {
82    this.orientationListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => { this.onPortrait(mediaQueryResult) })
83    this.loadDisplayInfo()
84  }
85
86  /**
87   * Sets the main window.
88   * @param win Indicates the current application window.
89   */
90  setMainWin(win: window.Window) {
91    if (win == null) {
92      return
93    }
94    GlobalContext.mainWin = win;
95    win.on("windowSizeChange", (data: window.Size) => {
96      if (GlobalContext.mainWindowSize == undefined || GlobalContext.mainWindowSize == null) {
97        GlobalContext.mainWindowSize = data;
98      } else {
99        if (GlobalContext.mainWindowSize.width == data.width && GlobalContext.mainWindowSize.height == data.height) {
100          return
101        }
102        GlobalContext.mainWindowSize = data;
103      }
104
105      let winWidth = this.getMainWindowWidth();
106      AppStorage.SetOrCreate<number>('mainWinWidth', winWidth)
107      let winHeight = this.getMainWindowHeight();
108      AppStorage.SetOrCreate<number>('mainWinHeight', winHeight)
109      let context:UIAbility = new UIAbility()
110      context.context.eventHub.emit("windowSizeChange", winWidth, winHeight)
111    })
112  }
113
114  static getInstance(): WindowManager {
115    if (WindowManager.instance == null) {
116      WindowManager.instance = new WindowManager();
117    }
118    return WindowManager.instance
119  }
120
121  private onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) {
122    if (mediaQueryResult.matches == AppStorage.Get<boolean>('isLandscape')) {
123      return
124    }
125    AppStorage.SetOrCreate<boolean>('isLandscape', mediaQueryResult.matches)
126    this.loadDisplayInfo()
127  }
128
129  /**
130   * Changes the screen orientation.
131   * @param ori Indicates the orientation.
132   */
133  changeOrientation(ori: window.Orientation) {
134    if (GlobalContext.mainWin != null) {
135      GlobalContext.mainWin.setPreferredOrientation(ori)
136    }
137  }
138
139  private loadDisplayInfo() {
140    this.displayInfo = display.getDefaultDisplaySync()
141    AppStorage.SetOrCreate<number>('displayWidth', this.getDisplayWidth())
142    AppStorage.SetOrCreate<number>('displayHeight', this.getDisplayHeight())
143  }
144
145  /**
146   * Obtains the width of the main window, in vp.
147   */
148  getMainWindowWidth(): number {
149    return GlobalContext.mainWindowSize != null ? px2vp(GlobalContext.mainWindowSize.width) : 0
150  }
151
152  /**
153   * Obtains the height of the main window, in vp.
154   */
155  getMainWindowHeight(): number {
156    return GlobalContext.mainWindowSize != null ? px2vp(GlobalContext.mainWindowSize.height) : 0
157  }
158
159  /**
160   * Obtains the screen width, in vp.
161   */
162  getDisplayWidth(): number {
163    return this.displayInfo != null ? px2vp(this.displayInfo.width) : 0
164  }
165
166  /**
167   * Obtains the screen height, in vp.
168   */
169  getDisplayHeight(): number {
170    return this.displayInfo != null ? px2vp(this.displayInfo.height) : 0
171  }
172
173  /**
174   * Releases resources.
175   */
176  release() {
177    if (this.orientationListener) {
178      this.orientationListener.off('change', (mediaQueryResult: mediaquery.MediaQueryResult) => { this.onPortrait(mediaQueryResult)})
179    }
180    if (GlobalContext.mainWin != null) {
181      GlobalContext.mainWin.off('windowSizeChange')
182    }
183    WindowManager.instance = null;
184  }
185}
186
187/**
188 * Encapsulates the TaskData class.
189 */
190export class TaskData {
191  bgColor: Color | string | Resource = Color.White;
192  index: number = 0;
193  taskInfo: string = 'music';
194
195  constructor(bgColor: Color | string | Resource, index: number, taskInfo: string) {
196    this.bgColor = bgColor;
197    this.index = index;
198    this.taskInfo = taskInfo;
199  }
200}
201
202export const taskDataArr: Array<TaskData> =
203  [
204    new TaskData(0xFA8072, 0, 'music'),
205    new TaskData(0xF4A460, 1, 'mall'),
206    new TaskData(0xFFFACD, 2, 'photos'),
207    new TaskData(0x98FB98, 3, 'setting'),
208    new TaskData(0x7FFFD4, 4, 'call'),
209    new TaskData(0x87CEFA, 5, 'music'),
210    new TaskData(0x7B68EE, 6, 'mall'),
211    new TaskData(0x909399, 7, 'photos'),
212    new TaskData(0x888888, 8, 'setting'),
213    new TaskData(0xFFC0CB, 9, 'call'),
214    new TaskData(0xFFC0CB, 10, 'music'),
215    new TaskData(0x888888, 11, 'mall'),
216    new TaskData(0x909399, 12, 'photos'),
217    new TaskData(0x7B68EE, 13, 'setting'),
218    new TaskData(0x87CEFA, 14, 'call'),
219    new TaskData(0x7FFFD4, 15, 'music'),
220    new TaskData(0x98FB98, 16, 'mall'),
221    new TaskData(0xFFFACD, 17, 'photos'),
222    new TaskData(0xF4A460, 18, 'setting'),
223    new TaskData(0xFA8072, 19, 'call'),
224  ];
225
226@Entry
227@Component
228export struct TaskSwitchMainPage {
229  displayWidth: number = WindowManager.getInstance().getDisplayWidth();
230  scroller: Scroller = new Scroller();
231  cardSpace: number = 0; // Widget spacing
232  cardWidth: number = this.displayWidth / 2 - this.cardSpace / 2; // Widget width
233  cardHeight: number = 400; // Widget height
234  cardPosition: Array<number> = []; // Initial position of the widget
235  clickIndex: boolean = false;
236  @State taskViewOffsetX: number = 0;
237  @State cardOffset: number = this.displayWidth / 4;
238  lastCardOffset: number = this.cardOffset;
239  startTime: number|undefined=undefined
240
241  // Initial position of each widget
242  aboutToAppear() {
243    for (let i = 0; i < taskDataArr.length; i++) {
244      this.cardPosition[i] = i * (this.cardWidth + this.cardSpace);
245    }
246  }
247
248  // Position of each widget
249  getProgress(index: number): number {
250    let progress = (this.cardOffset + this.cardPosition[index] - this.taskViewOffsetX + this.cardWidth / 2) / this.displayWidth;
251    return progress
252  }
253
254  build() {
255    Stack({ alignContent: Alignment.Bottom }) {
256      // Background
257      Column()
258        .width('100%')
259        .height('100%')
260        .backgroundColor(0xF0F0F0)
261
262      // <Scroll> component
263      Scroll(this.scroller) {
264        Row({ space: this.cardSpace }) {
265          ForEach(taskDataArr, (item:TaskData, index:number|undefined) => {
266            if(index){
267              Column()
268                .width(this.cardWidth)
269                .height(this.cardHeight)
270                .backgroundColor(item.bgColor)
271                .borderStyle(BorderStyle.Solid)
272                .borderWidth(1)
273                .borderColor(0xAFEEEE)
274                .borderRadius(15)
275                  // Calculate the affine attributes of child components.
276                .scale((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ?
277                  {
278                    x: 1.1 - Math.abs(0.5 - this.getProgress(index)),
279                    y: 1.1 - Math.abs(0.5 - this.getProgress(index))
280                  } :
281                  { x: 1, y: 1 })
282                .animation({ curve: Curve.Smooth })
283                  // Apply a pan animation.
284                .translate({ x: this.cardOffset })
285                .animation({ curve: curves.springMotion() })
286                .zIndex((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ? 2 : 1)
287            }
288          }, (item:TaskData) => item.toString())
289        }
290        .width((this.cardWidth + this.cardSpace) * (taskDataArr.length + 1))
291        .height('100%')
292      }
293      .gesture(
294        GestureGroup(GestureMode.Parallel,
295          PanGesture({ direction: PanDirection.Horizontal, distance: 5 })
296            .onActionStart((event: GestureEvent|undefined) => {
297              if(event){
298                this.startTime = event.timestamp;
299              }
300            })
301            .onActionUpdate((event: GestureEvent|undefined) => {
302              if(event){
303                this.cardOffset = this.lastCardOffset + event.offsetX;
304              }
305            })
306            .onActionEnd((event: GestureEvent|undefined) => {
307              if(event){
308                let time = 0
309                if(this.startTime){
310                  time = event.timestamp - this.startTime;
311                }
312                let speed = event.offsetX / (time / 1000000000);
313                let moveX = Math.pow(speed, 2) / 7000 * (speed > 0 ? 1 : -1);
314
315                this.cardOffset += moveX;
316                // When panning left to a position beyond the rightmost position
317                let cardOffsetMax = -(taskDataArr.length - 1) * (this.displayWidth / 2);
318                if (this.cardOffset < cardOffsetMax) {
319                  this.cardOffset = cardOffsetMax;
320                }
321                // When panning right to a position beyond the rightmost position
322                if (this.cardOffset > this.displayWidth / 4) {
323                  this.cardOffset = this.displayWidth / 4;
324                }
325
326                // Processing when the pan distance is less than the minimum distance
327                let remainMargin = this.cardOffset % (this.displayWidth / 2);
328                if (remainMargin < 0) {
329                  remainMargin = this.cardOffset % (this.displayWidth / 2) + this.displayWidth / 2;
330                }
331                if (remainMargin <= this.displayWidth / 4) {
332                  this.cardOffset += this.displayWidth / 4 - remainMargin;
333                } else {
334                  this.cardOffset -= this.displayWidth / 4 - (this.displayWidth / 2 - remainMargin);
335                }
336
337                // Record the pan offset.
338                this.lastCardOffset = this.cardOffset;
339              }
340            })
341        ), GestureMask.IgnoreInternal)
342      .scrollable(ScrollDirection.Horizontal)
343      .scrollBar(BarState.Off)
344
345      // Move to the beginning and end positions.
346      Button('Move to first/last')
347        .backgroundColor(0x888888)
348        .margin({ bottom: 30 })
349        .onClick(() => {
350          this.clickIndex = !this.clickIndex;
351
352          if (this.clickIndex) {
353            this.cardOffset = this.displayWidth / 4;
354          } else {
355            this.cardOffset = this.displayWidth / 4 - (taskDataArr.length - 1) * this.displayWidth / 2;
356          }
357          this.lastCardOffset = this.cardOffset;
358        })
359    }
360    .width('100%')
361    .height('100%')
362  }
363}
364```
365
366![en-us_image_0000001599808406](figures/en-us_image_0000001599808406.gif)
367