1# 组件动画 2 3 4ArkUI为组件提供了通用的属性动画和转场动画能力的同时,还为一些组件提供了默认的动画效果。例如,[List](../reference/apis-arkui/arkui-ts/ts-container-list.md)的滑动动效、[Button](../reference/apis-arkui/arkui-ts/ts-basic-components-button.md#button)的点击动效,是组件自带的默认动画效果。在组件默认动画效果的基础上,开发者还可以通过属性动画和转场动画对容器组件内的子组件动效进行定制。 5 6 7## 使用组件默认动画 8 9组件默认动效具备以下功能: 10 11- 提示用户当前状态,例如用户点击Button组件时,Button组件默认变灰,用户即确定完成选中操作。 12 13- 提升界面精致程度和生动性。 14 15- 减少开发者工作量,例如列表滑动组件自带滑动动效,开发者直接调用即可。 16 17更多效果,可以参考[组件说明](../reference/apis-arkui/arkui-ts/ts-container-flex.md)。 18 19示例代码和效果如下。 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## 打造组件定制化动效 45 46部分组件支持通过[属性动画](arkts-attribute-animation-overview.md)和[转场动画](arkts-transition-overview.md)自定义组件子Item的动效,实现定制化动画效果。例如,[Scroll](../reference/apis-arkui/arkui-ts/ts-container-scroll.md)组件中可对各个子组件在滑动时的动画效果进行定制。 47 48- 在滑动或者点击操作时通过改变各个Scroll子组件的仿射属性来实现各种效果。 49 50- 如果要在滑动过程中定制动效,可在滑动回调onScroll中监控滑动距离,并计算每个组件的仿射属性。也可以自己定义手势,通过手势监控位置,手动调用ScrollTo改变滑动位置。 51 52- 在滑动回调onScrollStop或手势结束回调中对滑动的最终位置进行微调。 53 54定制Scroll组件滑动动效示例代码和效果如下。 55 56 57```ts 58import { curves, window, display, mediaquery, UIContext } 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 * 窗口、屏幕相关信息管理类 67 */ 68export class WindowManager { 69 private static instance: WindowManager | null = null; 70 private displayInfo: display.Display | null = null; 71 private uiContext: UIContext; 72 private orientationListener: mediaquery.MediaQueryListener; 73 74 constructor(uiContext: UIContext) { 75 this.uiContext = uiContext 76 this.orientationListener = this.uiContext.getMediaQuery().matchMediaSync('(orientation: landscape)'); 77 this.orientationListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => { 78 this.onPortrait(mediaQueryResult) 79 }) 80 this.loadDisplayInfo() 81 } 82 83 /** 84 * 设置主window窗口 85 * @param win 当前app窗口 86 */ 87 setMainWin(win: window.Window) { 88 if (win == null) { 89 return 90 } 91 GlobalContext.mainWin = win; 92 win.on("windowSizeChange", (data: window.Size) => { 93 if (GlobalContext.mainWindowSize == undefined || GlobalContext.mainWindowSize == null) { 94 GlobalContext.mainWindowSize = data; 95 } else { 96 if (GlobalContext.mainWindowSize.width == data.width && GlobalContext.mainWindowSize.height == data.height) { 97 return 98 } 99 GlobalContext.mainWindowSize = data; 100 } 101 102 let winWidth = this.getMainWindowWidth(); 103 AppStorage.setOrCreate<number>('mainWinWidth', winWidth) 104 let winHeight = this.getMainWindowHeight(); 105 AppStorage.setOrCreate<number>('mainWinHeight', winHeight) 106 let context: UIAbility = new UIAbility() 107 context.context.eventHub.emit("windowSizeChange", winWidth, winHeight) 108 }) 109 } 110 111 static getInstance(uiContext: UIContext): WindowManager { 112 if (WindowManager.instance == null) { 113 WindowManager.instance = new WindowManager(uiContext); 114 } 115 return WindowManager.instance 116 } 117 118 private onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) { 119 if (mediaQueryResult.matches == AppStorage.get<boolean>('isLandscape')) { 120 return 121 } 122 AppStorage.setOrCreate<boolean>('isLandscape', mediaQueryResult.matches) 123 this.loadDisplayInfo() 124 } 125 126 /** 127 * 切换屏幕方向 128 * @param ori 常量枚举值:window.Orientation 129 */ 130 changeOrientation(ori: window.Orientation) { 131 if (GlobalContext.mainWin != null) { 132 GlobalContext.mainWin.setPreferredOrientation(ori) 133 } 134 } 135 136 private loadDisplayInfo() { 137 this.displayInfo = display.getDefaultDisplaySync() 138 AppStorage.setOrCreate<number>('displayWidth', this.getDisplayWidth()) 139 AppStorage.setOrCreate<number>('displayHeight', this.getDisplayHeight()) 140 } 141 142 /** 143 * 获取main窗口宽度,单位vp 144 */ 145 getMainWindowWidth(): number { 146 return GlobalContext.mainWindowSize != null ? this.uiContext.px2vp(GlobalContext.mainWindowSize.width) : 0 147 } 148 149 /** 150 * 获取main窗口高度,单位vp 151 */ 152 getMainWindowHeight(): number { 153 return GlobalContext.mainWindowSize != null ? this.uiContext.px2vp(GlobalContext.mainWindowSize.height) : 0 154 } 155 156 /** 157 * 获取屏幕宽度,单位vp 158 */ 159 getDisplayWidth(): number { 160 return this.displayInfo != null ? this.uiContext.px2vp(this.displayInfo.width) : 0 161 } 162 163 /** 164 * 获取屏幕高度,单位vp 165 */ 166 getDisplayHeight(): number { 167 return this.displayInfo != null ? this.uiContext.px2vp(this.displayInfo.height) : 0 168 } 169 170 /** 171 * 释放资源 172 */ 173 release() { 174 if (this.orientationListener) { 175 this.orientationListener.off('change', (mediaQueryResult: mediaquery.MediaQueryResult) => { 176 this.onPortrait(mediaQueryResult) 177 }) 178 } 179 if (GlobalContext.mainWin != null) { 180 GlobalContext.mainWin.off('windowSizeChange') 181 } 182 WindowManager.instance = null; 183 } 184} 185 186/** 187 * 封装任务卡片信息数据类 188 */ 189export class TaskData { 190 bgColor: Color | string | Resource = Color.White; 191 index: number = 0; 192 taskInfo: string = 'music'; 193 194 constructor(bgColor: Color | string | Resource, index: number, taskInfo: string) { 195 this.bgColor = bgColor; 196 this.index = index; 197 this.taskInfo = taskInfo; 198 } 199} 200 201export const taskDataArr: Array<TaskData> = 202 [ 203 new TaskData('#317AF7', 0, 'music'), 204 new TaskData('#D94838', 1, 'mall'), 205 new TaskData('#DB6B42 ', 2, 'photos'), 206 new TaskData('#5BA854', 3, 'setting'), 207 new TaskData('#317AF7', 4, 'call'), 208 new TaskData('#D94838', 5, 'music'), 209 new TaskData('#DB6B42', 6, 'mall'), 210 new TaskData('#5BA854', 7, 'photos'), 211 new TaskData('#D94838', 8, 'setting'), 212 new TaskData('#DB6B42', 9, 'call'), 213 new TaskData('#5BA854', 10, 'music') 214 215 ]; 216 217@Entry 218@Component 219export struct TaskSwitchMainPage { 220 displayWidth: number = WindowManager.getInstance(this.getUIContext()).getDisplayWidth(); 221 scroller: Scroller = new Scroller(); 222 cardSpace: number = 0; // 卡片间距 223 cardWidth: number = this.displayWidth / 2 - this.cardSpace / 2; // 卡片宽度 224 cardHeight: number = 400; // 卡片高度 225 cardPosition: Array<number> = []; // 卡片初始位置 226 clickIndex: boolean = false; 227 @State taskViewOffsetX: number = 0; 228 @State cardOffset: number = this.displayWidth / 4; 229 lastCardOffset: number = this.cardOffset; 230 startTime: number | undefined = undefined 231 232 // 每个卡片初始位置 233 aboutToAppear() { 234 for (let i = 0; i < taskDataArr.length; i++) { 235 this.cardPosition[i] = i * (this.cardWidth + this.cardSpace); 236 } 237 } 238 239 // 每个卡片位置 240 getProgress(index: number): number { 241 let progress = (this.cardOffset + this.cardPosition[index] - this.taskViewOffsetX + this.cardWidth / 2) / this.displayWidth; 242 return progress 243 } 244 245 build() { 246 Stack({ alignContent: Alignment.Bottom }) { 247 // 背景 248 Column() 249 .width('100%') 250 .height('100%') 251 .backgroundColor(0xF0F0F0) 252 253 // 滑动组件 254 Scroll(this.scroller) { 255 Row({ space: this.cardSpace }) { 256 ForEach(taskDataArr, (item: TaskData, index) => { 257 Column() 258 .width(this.cardWidth) 259 .height(this.cardHeight) 260 .backgroundColor(item.bgColor) 261 .borderStyle(BorderStyle.Solid) 262 .borderWidth(1) 263 .borderColor(0xAFEEEE) 264 .borderRadius(15) 265 // 计算子组件的仿射属性 266 .scale((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ? 267 { 268 x: 1.1 - Math.abs(0.5 - this.getProgress(index)), 269 y: 1.1 - Math.abs(0.5 - this.getProgress(index)) 270 } : 271 { x: 1, y: 1 }) 272 .animation({ curve: Curve.Smooth }) 273 // 滑动动画 274 .translate({ x: this.cardOffset }) 275 .animation({ curve: curves.springMotion() }) 276 .zIndex((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ? 2 : 1) 277 }, (item: TaskData) => item.toString()) 278 } 279 .width((this.cardWidth + this.cardSpace) * (taskDataArr.length + 1)) 280 .height('100%') 281 } 282 .gesture( 283 GestureGroup(GestureMode.Parallel, 284 PanGesture({ direction: PanDirection.Horizontal, distance: 5 }) 285 .onActionStart((event: GestureEvent | undefined) => { 286 if (event) { 287 this.startTime = event.timestamp; 288 } 289 }) 290 .onActionUpdate((event: GestureEvent | undefined) => { 291 if (event) { 292 this.cardOffset = this.lastCardOffset + event.offsetX; 293 } 294 }) 295 .onActionEnd((event: GestureEvent | undefined) => { 296 if (event) { 297 let time = 0 298 if (this.startTime) { 299 time = event.timestamp - this.startTime; 300 } 301 let speed = event.offsetX / (time / 1000000000); 302 let moveX = Math.pow(speed, 2) / 7000 * (speed > 0 ? 1 : -1); 303 304 this.cardOffset += moveX; 305 // 左滑大于最右侧位置 306 let cardOffsetMax = -(taskDataArr.length - 1) * (this.displayWidth / 2); 307 if (this.cardOffset < cardOffsetMax) { 308 this.cardOffset = cardOffsetMax; 309 } 310 // 右滑大于最左侧位置 311 if (this.cardOffset > this.displayWidth / 4) { 312 this.cardOffset = this.displayWidth / 4; 313 } 314 315 // 左右滑动距离不满足/满足切换关系时,补位/退回 316 let remainMargin = this.cardOffset % (this.displayWidth / 2); 317 if (remainMargin < 0) { 318 remainMargin = this.cardOffset % (this.displayWidth / 2) + this.displayWidth / 2; 319 } 320 if (remainMargin <= this.displayWidth / 4) { 321 this.cardOffset += this.displayWidth / 4 - remainMargin; 322 } else { 323 this.cardOffset -= this.displayWidth / 4 - (this.displayWidth / 2 - remainMargin); 324 } 325 326 // 记录本次滑动偏移量 327 this.lastCardOffset = this.cardOffset; 328 } 329 }) 330 ), GestureMask.IgnoreInternal) 331 .scrollable(ScrollDirection.Horizontal) 332 .scrollBar(BarState.Off) 333 334 // 滑动到首尾位置 335 Button('Move to first/last') 336 .backgroundColor(0x888888) 337 .margin({ bottom: 30 }) 338 .onClick(() => { 339 this.clickIndex = !this.clickIndex; 340 341 if (this.clickIndex) { 342 this.cardOffset = this.displayWidth / 4; 343 } else { 344 this.cardOffset = this.displayWidth / 4 - (taskDataArr.length - 1) * this.displayWidth / 2; 345 } 346 this.lastCardOffset = this.cardOffset; 347 }) 348 } 349 .width('100%') 350 .height('100%') 351 } 352} 353``` 354 355 356<!--RP1--><!--RP1End-->