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