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