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 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 367