【引言】(完整程式碼在最後面)
本文將介紹如何在鴻蒙NEXT中建立一個自定義的“太極Loading”元件,為你的應用增添獨特的視覺效果。
【環境準備】
電腦系統:windows 10
開發工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真機:mate60 pro
語言:ArkTS、ArkUI
【專案分析】
1. 元件結構
我們將建立一個名為 TaiChiLoadingProgress 的自定義元件,它將模擬太極圖的旋轉效果,作為載入動畫展示給使用者。元件的基本結構如下:
@Component struct TaiChiLoadingProgress { @Prop taiChiWidth: number = 400 @Prop @Watch('animationCurveChanged') animationCurve: Curve = Curve.Linear @State angle: number = 0 @State cellWidth: number = 0 ... }
2. 繪製太極圖案
使用鴻蒙NEXT提供的UI元件,如 Rect 和 Circle,構建太極圖的黑白兩部分。關鍵在於利用 rotate 方法實現太極圖的旋轉效果。
build() { Stack() { Stack() { // 黑色半圓背景 Stack() { Rect().width(`${this.cellWidth}px`).height(`${this.cellWidth / 2}px`).backgroundColor(Color.Black) }.width(`${this.cellWidth}px`).height(`${this.cellWidth}px`).rotate({ angle: -90 }).align(Alignment.Top) // 大黑球 上 Stack() { Circle().width(`${this.cellWidth / 2}px`).height(`${this.cellWidth / 2}px`).fill(Color.Black) Circle().width(`${this.cellWidth / 8}px`).height(`${this.cellWidth / 8}px`).fill(Color.White) }.width(`${this.cellWidth}px`).height(`${this.cellWidth}px`).align(Alignment.Top) // 大白球 下 Stack() { Circle().width(`${this.cellWidth / 2}px`).height(`${this.cellWidth / 2}px`).fill(Color.White) Circle().width(`${this.cellWidth / 8}px`).height(`${this.cellWidth / 8}px`).fill(Color.Black) }.width(`${this.cellWidth}px`).height(`${this.cellWidth}px`).align(Alignment.Bottom) } .width(`${this.cellWidth}px`) .height(`${this.cellWidth}px`) .borderWidth(1) .borderColor(Color.Black) .borderRadius('50%') .backgroundColor(Color.White) .clip(true) .rotate({ angle: this.angle }) .onVisibleAreaChange([0.0, 1.0], (isVisible: boolean, currentRatio: number) => { if (isVisible && currentRatio >= 1.0) { this.startAnim() } if (!isVisible && currentRatio <= 0.0) { this.endAnim() } }) } .width(`${this.taiChiWidth}px`) .height(`${this.taiChiWidth}px`) }
3. 動畫實現
透過 animateTo 方法設定太極圖的旋轉動畫,可以自定義動畫曲線以實現不同的動畫效果。
startAnim() { animateTo({ duration: 2000, iterations: -1, curve: this.animationCurve }, () => { this.angle = 360 * 2 }) } endAnim() { animateTo({ duration: 0 }, () => { this.angle = 0 }) }
【完整程式碼】
@Component struct TaiChiLoadingProgress { @Prop taiChiWidth: number = 400 @Prop @Watch('animationCurveChanged') animationCurve: Curve = Curve.Linear @State angle: number = 0 @State cellWidth: number = 0 animationCurveChanged() { this.endAnim() this.startAnim() } startAnim() { animateTo({ duration: 2000, iterations: -1, curve: this.animationCurve }, () => { this.angle = 360 * 2 }) } endAnim() { animateTo({ duration: 0 }, () => { this.angle = 0 }) } aboutToAppear(): void { this.cellWidth = this.taiChiWidth / 2 } build() { Stack() { Stack() { //黑色 半圓 背景 Stack() { Rect().width(`${this.cellWidth}px`).height(`${this.cellWidth / 2}px`).backgroundColor(Color.Black) }.width(`${this.cellWidth}px`).height(`${this.cellWidth}px`).rotate({ angle: -90 }).align(Alignment.Top) //大黑球 上 Stack() { Stack() { Circle().width(`${this.cellWidth / 2}px`).height(`${this.cellWidth / 2}px`).fill(Color.Black) Circle().width(`${this.cellWidth / 8}px`).height(`${this.cellWidth / 8}px`).fill(Color.White) } }.width(`${this.cellWidth}px`).height(`${this.cellWidth}px`).align(Alignment.Top) //大白球 下 Stack() { Stack() { Circle().width(`${this.cellWidth / 2}px`).height(`${this.cellWidth / 2}px`).fill(Color.White) Circle().width(`${this.cellWidth / 8}px`).height(`${this.cellWidth / 8}px`).fill(Color.Black) } }.width(`${this.cellWidth}px`).height(`${this.cellWidth}px`).align(Alignment.Bottom) } .width(`${this.cellWidth}px`) .height(`${this.cellWidth}px`) .borderWidth(1) .borderColor(Color.Black) .borderRadius('50%') .backgroundColor(Color.White) .clip(true) .rotate({ angle: this.angle }) .onVisibleAreaChange([0.0, 1.0], (isVisible: boolean, currentRatio: number) => { console.info('Test Row isVisible:' + isVisible + ', currentRatio:' + currentRatio) if (isVisible && currentRatio >= 1.0) { console.info('Test Row is fully visible.') this.startAnim() } if (!isVisible && currentRatio <= 0.0) { console.info('Test Row is completely invisible.') this.endAnim() } }) } .width(`${this.taiChiWidth}px`) .height(`${this.taiChiWidth}px`) } } @Entry @Component struct Page08 { @State loadingWidth: number = 150 @State isShowLoading: boolean = true; @State animationCurve: Curve = Curve.Linear build() { Column({ space: 20 }) { Text('官方Loading元件') Column() { LoadingProgress().width(this.loadingWidth) .visibility(this.isShowLoading ? Visibility.Visible : Visibility.None) }.height(this.loadingWidth).width(this.loadingWidth) Text('自定義太極Loading元件') Column() { TaiChiLoadingProgress({ taiChiWidth: vp2px(this.loadingWidth), animationCurve: this.animationCurve }) .visibility(this.isShowLoading ? Visibility.Visible : Visibility.Hidden) }.height(this.loadingWidth).width(this.loadingWidth) Row() { Flex({ wrap: FlexWrap.Wrap }) { Text('顯示/隱藏') .textAlign(TextAlign.Center) .width('200lpx') .height('200lpx') .margin('10lpx') .backgroundColor(Color.Black) .borderRadius(5) .backgroundColor(Color.Orange) .fontColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT }) .onClick(() => { this.isShowLoading = !this.isShowLoading }) Text('Linear動畫') .textAlign(TextAlign.Center) .width('200lpx') .height('200lpx') .margin('10lpx') .backgroundColor(Color.Black) .borderRadius(5) .backgroundColor(Color.Orange) .fontColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT }) .onClick(() => { this.animationCurve = Curve.Linear }) Text('FastOutLinearIn動畫') .textAlign(TextAlign.Center) .width('200lpx') .height('200lpx') .margin('10lpx') .backgroundColor(Color.Black) .borderRadius(5) .backgroundColor(Color.Orange) .fontColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT }) .onClick(() => { this.animationCurve = Curve.FastOutLinearIn }) Text('EaseIn動畫') .textAlign(TextAlign.Center) .width('200lpx') .height('200lpx') .margin('10lpx') .backgroundColor(Color.Black) .borderRadius(5) .backgroundColor(Color.Orange) .fontColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT }) .onClick(() => { this.animationCurve = Curve.EaseIn }) Text('EaseOut動畫') .textAlign(TextAlign.Center) .width('200lpx') .height('200lpx') .margin('10lpx') .backgroundColor(Color.Black) .borderRadius(5) .backgroundColor(Color.Orange) .fontColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT }) .onClick(() => { this.animationCurve = Curve.EaseOut }) Text('EaseInOut動畫') .textAlign(TextAlign.Center) .width('200lpx') .height('200lpx') .margin('10lpx') .backgroundColor(Color.Black) .borderRadius(5) .backgroundColor(Color.Orange) .fontColor(Color.White) .clickEffect({ level: ClickEffectLevel.LIGHT }) .onClick(() => { this.animationCurve = Curve.EaseInOut }) }.width('660lpx') }.width('100%').justifyContent(FlexAlign.Center) } .height('100%') .width('100%') .backgroundColor("#f9feff") } }