【引言】(完整代码在最后面)
本文将介绍如何在鸿蒙NEXT中创建一个自定义的"太极Loading"组件,为你的应用增添独特的视觉效果。
【环境准备】
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:mate60 pro
语言:ArkTS、ArkUI
【项目分析】
- 组件结构
我们将创建一个名为 TaiChiLoadingProgress 的自定义组件,它将模拟太极图的旋转效果,作为加载动画展示给用户。组件的基本结构如下:
TypeScript
@Component
struct TaiChiLoadingProgress {
@Prop taiChiWidth: number = 400
@Prop @Watch('animationCurveChanged') animationCurve: Curve = Curve.Linear
@State angle: number = 0
@State cellWidth: number = 0
...
}
- 绘制太极图案
使用鸿蒙NEXT提供的UI组件,如 Rect 和 Circle,构建太极图的黑白两部分。关键在于利用 rotate 方法实现太极图的旋转效果。
TypeScript
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`)
}
- 动画实现
通过 animateTo 方法设置太极图的旋转动画,可以自定义动画曲线以实现不同的动画效果。
TypeScript
startAnim() {
animateTo({
duration: 2000,
iterations: -1,
curve: this.animationCurve
}, () => {
this.angle = 360 * 2
})
}
endAnim() {
animateTo({
duration: 0
}, () => {
this.angle = 0
})
}
【完整代码】
TypeScript
@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")
}
}