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 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 349<!--RP1--><!--RP1End--> 350