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