# 组件动画 ArkUI为组件æ供了通用的属性动画和转场动画能力的åŒæ—¶ï¼Œè¿˜ä¸ºä¸€äº›ç»„件æ供了默认的动画效果。例如,[List](../reference/apis-arkui/arkui-ts/ts-container-list.md)的滑动动效ã€[Button](../reference/apis-arkui/arkui-ts/ts-basic-components-button.md#button)的点击动效,是组件自带的默认动画效果。在组件默认动画效果的基础上,开å‘者还å¯ä»¥é€šè¿‡å±žæ€§åŠ¨ç”»å’Œè½¬åœºåŠ¨ç”»å¯¹å®¹å™¨ç»„件内的å组件动效进行定制。 ## 使用组件默认动画 组件默认动效具备以下功能: - æ示用户当å‰çŠ¶æ€ï¼Œä¾‹å¦‚用户点击Button组件时,Button组件默认å˜ç°ï¼Œç”¨æˆ·å³ç¡®å®šå®Œæˆé€‰ä¸æ“作。 - æå‡ç•Œé¢ç²¾è‡´ç¨‹åº¦å’Œç”ŸåŠ¨æ€§ã€‚ - å‡å°‘å¼€å‘者工作é‡ï¼Œä¾‹å¦‚列表滑动组件自带滑动动效,开å‘者直接调用å³å¯ã€‚ 更多效果,å¯ä»¥å‚考[组件说明](../reference/apis-arkui/arkui-ts/ts-container-flex.md)。 示例代ç 和效果如下。 ```ts @Entry @Component struct ComponentDemo { build() { Row() { Checkbox({ name: 'checkbox1', group: 'checkboxGroup' }) .select(true) .shape(CheckBoxShape.CIRCLE) .size({ width: 50, height: 50 }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } } ```  ## æ‰“é€ ç»„ä»¶å®šåˆ¶åŒ–åŠ¨æ•ˆ 部分组件支æŒé€šè¿‡[属性动画](arkts-attribute-animation-overview.md)å’Œ[转场动画](arkts-transition-overview.md)自定义组件åItem的动效,实现定制化动画效果。例如,[Scroll](../reference/apis-arkui/arkui-ts/ts-container-scroll.md)组件ä¸å¯å¯¹å„个å组件在滑动时的动画效果进行定制。 - 在滑动或者点击æ“作时通过改å˜å„个Scrollå组件的仿射属性æ¥å®žçŽ°å„ç§æ•ˆæžœã€‚ - 如果è¦åœ¨æ»‘动过程ä¸å®šåˆ¶åŠ¨æ•ˆï¼Œå¯åœ¨æ»‘动回调onScrollä¸ç›‘控滑动è·ç¦»ï¼Œå¹¶è®¡ç®—æ¯ä¸ªç»„件的仿射属性。也å¯ä»¥è‡ªå·±å®šä¹‰æ‰‹åŠ¿ï¼Œé€šè¿‡æ‰‹åŠ¿ç›‘控ä½ç½®ï¼Œæ‰‹åŠ¨è°ƒç”¨ScrollTo改å˜æ»‘动ä½ç½®ã€‚ - 在滑动回调onScrollStop或手势结æŸå›žè°ƒä¸å¯¹æ»‘动的最终ä½ç½®è¿›è¡Œå¾®è°ƒã€‚ 定制Scroll组件滑动动效示例代ç 和效果如下。 ```ts import { curves, window, display, mediaquery } from '@kit.ArkUI'; import { UIAbility } from '@kit.AbilityKit'; export default class GlobalContext extends AppStorage{ static mainWin: window.Window|undefined = undefined; static mainWindowSize:window.Size|undefined = undefined; } /** * 窗å£ã€å±å¹•ç›¸å…³ä¿¡æ¯ç®¡ç†ç±» */ export class WindowManager { private static instance: WindowManager|null = null; private displayInfo: display.Display|null = null; private orientationListener = mediaquery.matchMediaSync('(orientation: landscape)'); constructor() { this.orientationListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => { this.onPortrait(mediaQueryResult) }) this.loadDisplayInfo() } /** * 设置主windowçª—å£ * @param win 当å‰appçª—å£ */ setMainWin(win: window.Window) { if (win == null) { return } GlobalContext.mainWin = win; win.on("windowSizeChange", (data: window.Size) => { if (GlobalContext.mainWindowSize == undefined || GlobalContext.mainWindowSize == null) { GlobalContext.mainWindowSize = data; } else { if (GlobalContext.mainWindowSize.width == data.width && GlobalContext.mainWindowSize.height == data.height) { return } GlobalContext.mainWindowSize = data; } let winWidth = this.getMainWindowWidth(); AppStorage.setOrCreate<number>('mainWinWidth', winWidth) let winHeight = this.getMainWindowHeight(); AppStorage.setOrCreate<number>('mainWinHeight', winHeight) let context:UIAbility = new UIAbility() context.context.eventHub.emit("windowSizeChange", winWidth, winHeight) }) } static getInstance(): WindowManager { if (WindowManager.instance == null) { WindowManager.instance = new WindowManager(); } return WindowManager.instance } private onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) { if (mediaQueryResult.matches == AppStorage.get<boolean>('isLandscape')) { return } AppStorage.setOrCreate<boolean>('isLandscape', mediaQueryResult.matches) this.loadDisplayInfo() } /** * 切æ¢å±å¹•æ–¹å‘ * @param ori 常é‡æžšä¸¾å€¼ï¼šwindow.Orientation */ changeOrientation(ori: window.Orientation) { if (GlobalContext.mainWin != null) { GlobalContext.mainWin.setPreferredOrientation(ori) } } private loadDisplayInfo() { this.displayInfo = display.getDefaultDisplaySync() AppStorage.setOrCreate<number>('displayWidth', this.getDisplayWidth()) AppStorage.setOrCreate<number>('displayHeight', this.getDisplayHeight()) } /** * 获å–main窗å£å®½åº¦ï¼Œå•ä½vp */ getMainWindowWidth(): number { return GlobalContext.mainWindowSize != null ? px2vp(GlobalContext.mainWindowSize.width) : 0 } /** * 获å–main窗å£é«˜åº¦ï¼Œå•ä½vp */ getMainWindowHeight(): number { return GlobalContext.mainWindowSize != null ? px2vp(GlobalContext.mainWindowSize.height) : 0 } /** * 获å–å±å¹•å®½åº¦ï¼Œå•ä½vp */ getDisplayWidth(): number { return this.displayInfo != null ? px2vp(this.displayInfo.width) : 0 } /** * 获å–å±å¹•é«˜åº¦ï¼Œå•ä½vp */ getDisplayHeight(): number { return this.displayInfo != null ? px2vp(this.displayInfo.height) : 0 } /** * é‡Šæ”¾èµ„æº */ release() { if (this.orientationListener) { this.orientationListener.off('change', (mediaQueryResult: mediaquery.MediaQueryResult) => { this.onPortrait(mediaQueryResult)}) } if (GlobalContext.mainWin != null) { GlobalContext.mainWin.off('windowSizeChange') } WindowManager.instance = null; } } /** * å°è£…任务å¡ç‰‡ä¿¡æ¯æ•°æ®ç±» */ export class TaskData { bgColor: Color | string | Resource = Color.White; index: number = 0; taskInfo: string = 'music'; constructor(bgColor: Color | string | Resource, index: number, taskInfo: string) { this.bgColor = bgColor; this.index = index; this.taskInfo = taskInfo; } } export const taskDataArr: Array<TaskData> = [ new TaskData('#317AF7', 0, 'music'), new TaskData('#D94838', 1, 'mall'), new TaskData('#DB6B42 ', 2, 'photos'), new TaskData('#5BA854', 3, 'setting'), new TaskData('#317AF7', 4, 'call'), new TaskData('#D94838', 5, 'music'), new TaskData('#DB6B42', 6, 'mall'), new TaskData('#5BA854', 7, 'photos'), new TaskData('#D94838', 8, 'setting'), new TaskData('#DB6B42', 9, 'call'), new TaskData('#5BA854', 10, 'music') ]; @Entry @Component export struct TaskSwitchMainPage { displayWidth: number = WindowManager.getInstance().getDisplayWidth(); scroller: Scroller = new Scroller(); cardSpace: number = 0; // å¡ç‰‡é—´è· cardWidth: number = this.displayWidth / 2 - this.cardSpace / 2; // å¡ç‰‡å®½åº¦ cardHeight: number = 400; // å¡ç‰‡é«˜åº¦ cardPosition: Array<number> = []; // å¡ç‰‡åˆå§‹ä½ç½® clickIndex: boolean = false; @State taskViewOffsetX: number = 0; @State cardOffset: number = this.displayWidth / 4; lastCardOffset: number = this.cardOffset; startTime: number|undefined=undefined // æ¯ä¸ªå¡ç‰‡åˆå§‹ä½ç½® aboutToAppear() { for (let i = 0; i < taskDataArr.length; i++) { this.cardPosition[i] = i * (this.cardWidth + this.cardSpace); } } // æ¯ä¸ªå¡ç‰‡ä½ç½® getProgress(index: number): number { let progress = (this.cardOffset + this.cardPosition[index] - this.taskViewOffsetX + this.cardWidth / 2) / this.displayWidth; return progress } build() { Stack({ alignContent: Alignment.Bottom }) { // 背景 Column() .width('100%') .height('100%') .backgroundColor(0xF0F0F0) // 滑动组件 Scroll(this.scroller) { Row({ space: this.cardSpace }) { ForEach(taskDataArr, (item:TaskData, index) => { Column() .width(this.cardWidth) .height(this.cardHeight) .backgroundColor(item.bgColor) .borderStyle(BorderStyle.Solid) .borderWidth(1) .borderColor(0xAFEEEE) .borderRadius(15) // 计算å组件的仿射属性 .scale((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ? { x: 1.1 - Math.abs(0.5 - this.getProgress(index)), y: 1.1 - Math.abs(0.5 - this.getProgress(index)) } : { x: 1, y: 1 }) .animation({ curve: Curve.Smooth }) // 滑动动画 .translate({ x: this.cardOffset }) .animation({ curve: curves.springMotion() }) .zIndex((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ? 2 : 1) }, (item:TaskData) => item.toString()) } .width((this.cardWidth + this.cardSpace) * (taskDataArr.length + 1)) .height('100%') } .gesture( GestureGroup(GestureMode.Parallel, PanGesture({ direction: PanDirection.Horizontal, distance: 5 }) .onActionStart((event: GestureEvent|undefined) => { if(event){ this.startTime = event.timestamp; } }) .onActionUpdate((event: GestureEvent|undefined) => { if(event){ this.cardOffset = this.lastCardOffset + event.offsetX; } }) .onActionEnd((event: GestureEvent|undefined) => { if(event){ let time = 0 if(this.startTime){ time = event.timestamp - this.startTime; } let speed = event.offsetX / (time / 1000000000); let moveX = Math.pow(speed, 2) / 7000 * (speed > 0 ? 1 : -1); this.cardOffset += moveX; // 左滑大于最å³ä¾§ä½ç½® let cardOffsetMax = -(taskDataArr.length - 1) * (this.displayWidth / 2); if (this.cardOffset < cardOffsetMax) { this.cardOffset = cardOffsetMax; } // å³æ»‘大于最左侧ä½ç½® if (this.cardOffset > this.displayWidth / 4) { this.cardOffset = this.displayWidth / 4; } // å·¦å³æ»‘动è·ç¦»ä¸æ»¡è¶³/满足切æ¢å…³ç³»æ—¶ï¼Œè¡¥ä½/退回 let remainMargin = this.cardOffset % (this.displayWidth / 2); if (remainMargin < 0) { remainMargin = this.cardOffset % (this.displayWidth / 2) + this.displayWidth / 2; } if (remainMargin <= this.displayWidth / 4) { this.cardOffset += this.displayWidth / 4 - remainMargin; } else { this.cardOffset -= this.displayWidth / 4 - (this.displayWidth / 2 - remainMargin); } // 记录本次滑动åç§»é‡ this.lastCardOffset = this.cardOffset; } }) ), GestureMask.IgnoreInternal) .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) // 滑动到首尾ä½ç½® Button('Move to first/last') .backgroundColor(0x888888) .margin({ bottom: 30 }) .onClick(() => { this.clickIndex = !this.clickIndex; if (this.clickIndex) { this.cardOffset = this.displayWidth / 4; } else { this.cardOffset = this.displayWidth / 4 - (taskDataArr.length - 1) * this.displayWidth / 2; } this.lastCardOffset = this.cardOffset; }) } .width('100%') .height('100%') } } ```  <!--RP1--><!--RP1End-->