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