HarmonyOS 属性动画.animation()
基础使用
- 1. 属性动画响应式(确定谁加动画)
@State scaleX: number = 1
@State scaleY: number = 1
Text().width(50).height(50).backgroundColor(Color.Red)
.scale({ x: this.scaleX, y: this.scaleY })
- 2. 触发改变 (发现没有动画)
//未使用动画属性,缩放变化生硬
Text().width(50).height(50).backgroundColor(Color.Red)
.scale({ x: this.scaleX, y: this.scaleY })
.onClick(()=>{
this.scaleX = 2
this.scaleY = 2
})
- 3. 增加 animation 动画属性 (让动画生效)
//使用动画过渡,属性值变化丝滑、流畅
Text()
.width(50).height(50).backgroundColor(Color.Red)
.scale({ x: this.scaleX, y: this.scaleY })
.animation({
duration: 2000, //动画持续时间(ms),默认为1000
curve: Curve.EaseOut, //动画速率曲线(参数值详见官方文档),默认为匀速linear
iterations: 3, //动画播放次数,-1是无限次,默认为1次
playMode: PlayMode.Normal //动画播放模式,默认播放完成后重头开始播放
})
.onClick(()=>{
this.scaleX = 2
this.scaleY = 2
})
实战案例1: 基础案例
@Entry
@Component
struct Index {
@State widthSize: number = 250
@State heightSize: number = 100
@State rotateAngle: number = 0
@State flag: boolean = true
build() {
Column() {
Button('change size').margin(30)
.width(this.widthSize) // 1 谁 ✅
.height(this.heightSize) // 1 谁 ✅
.animation({ // 3 fixed💕
duration: 2000,
curve: Curve.EaseOut,
iterations: 3, // 重复次数
playMode: PlayMode.Normal // 播放模式
})
.onClick(() => { // 2 find 🔍
if (this.flag) {
this.widthSize = 150
this.heightSize = 60
} else {
this.widthSize = 250
this.heightSize = 100
}
this.flag = !this.flag
})
Button('change rotate angle').margin(50)
.rotate({ angle: this.rotateAngle }) // 1 谁 ✅
.animation({ // 3 fixed💕
duration: 1200,
curve: Curve.Friction,
delay: 500,
iterations: -1, // 设置-1表示动画无限循环
playMode: PlayMode.Alternate,
expectedFrameRateRange: { // 开发者可以为不同的动画配置不同的期望绘制帧率,从而优化动画的流畅度和性能
min: 20,
max: 120,
expected: 90,
}
// 绘制帧率和手机刷新率的关系
// 绘制帧率(Frame Rate)是指在一段时间内屏幕上显示的图像帧数,通常以每秒钟显示的帧数(FPS, Frames Per Second)来衡量。
// 而手机刷新率是指屏幕每秒钟更新的次数,通常以Hz(赫兹)为单位。
// 例如,60FPS表示每秒钟屏幕上显示60张图片,而120Hz的手机屏幕每秒钟会更新120次。
// 为了获得更好的用户体验,绘制帧率应该尽量接近或高于手机的刷新率,否则可能会出现卡顿、掉帧等不流畅的现象
})
.onClick(() => { // 2 find 🔍
this.rotateAngle = 90
})
}.width('100%').margin({ top: 20 })
}
}
实战案例2: 启动页案例
http://tmp00002.zhaodashen.cn/1128df4079ce0a78b6c279cca86a31d4.jpg
@Entry
@Component
struct Index {
@State top: number = 0
@State state: number = 1
onPageShow() { // 2 find 🔍
this.state = 0
this.top = -200
}
build() {
Column() {
Column() {
Text('不').fontSize(30).fontColor('###fff').margin({ top: 10 })
Text('背').fontSize(30).fontColor('###fff').margin({ top: 10 })
Column() {
Text('单').fontSize(30).fontColor('###fff').margin({ top: 10 })
Text('词').fontSize(30).fontColor('###fff').margin({ top: 10 })
}
.opacity(this.state) // 1 who ✅ 单词透明度慢慢变弱
.animation({ duration: 1000, curve: Curve.EaseIn }) // 先慢后快 3 fixed💕
}.width(80)
.translate({ x: 0, y: this.top }) // 1 who ✅ 不背单词一起 负数 向上
.animation({ duration: 1000, curve: Curve.EaseIn }) // 先慢后快 3 fixed💕
}.width('100%').height('100%').backgroundColor('###000').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
}
}
实战案例3: 标签页案例
@Entry
@Component
struct Index {
@State scrollLeft: number = 0
@State tabsIndex: number = 0
@State tabsLeft: number[] = [5, 80, 160, 240, 330]
build() {
Column() {
Row() {
ForEach(['全部', '待付款', '待发货', '待收货', '待评价'], (item: string, index: number) => {
Text(item).height('100%')
.fontWeight(this.tabsIndex === index ? 600 : 400)
.onClick(() => { // 2 find 🔍
this.tabsIndex = index
this.scrollLeft = this.tabsLeft[this.tabsIndex]
})
})
// 文字下方横杠,点击后发生位移
Text().width(24).height(2).backgroundColor('#191919').borderRadius(100000).position({x: 0,y: '100%'})
.translate({ x: this.scrollLeft, y: '-100%' }) // 1 who ✅
.animation({ duration: 300, curve: Curve.Linear }) // 3 fixed 💕
}
.height(60).width('100%').justifyContent(FlexAlign.SpaceBetween).backgroundColor('#fff')
}.height('100%').width('100%').backgroundColor('#f5f5f5')
}
}
实战案例4: 回到顶部案例
@Entry
@Component
struct Index {
private scroller: Scroller = new Scroller()
@State scaleY: number = 0
@State offsetY:number = -50
build() {
Stack({ alignContent: Alignment.BottomEnd }) {
Scroll(this.scroller) {
Column() {
ForEach(new Array(100).fill(6), (item: number, index) => {
Text(index.toString()).width('100%').height(50).margin({ bottom: 50 }).backgroundColor(Color.Black).fontColor(Color.White).fontSize(30)
})
}
}
//滚动时触发
.onDidScroll(() => {
const y = this.scroller.currentOffset().yOffset
console.log('位置:', JSON.stringify(this.scroller.currentOffset()))
console.log('位置:', y)
if (y >= 1000) { // 2 find 🔍
this.offsetY = -70
this.scaleY = 1
} else {
this.offsetY = -10
this.scaleY = 0
}
})
Text('↑').fontSize(18).fontColor(Color.White).width(40).height(40).borderRadius(20).backgroundColor(Color.Green).textAlign(TextAlign.Center).offset({ x: -10, y: this.offsetY })
.scale({ x: 1, y: this.scaleY }) // 1 who ✅
.opacity(this.scaleY) // 1 who ✅
.animation({ // 3 fixed 💕
duration: 500,
curve: Curve.Friction,
})
.onClick(() => {
//回到顶部动画设为true时,所有属性为默认值
//this.scroller.scrollTo({ xOffset: 0, yOffset: 0, animation: true })
this.scroller.scrollTo({ xOffset: 0, yOffset: 0, animation: { duration: 2000, curve: Curve.Friction } })
})
}
}
}
实战案例5:表单科技感
@Entry
@Component
export struct Index {
@State loadingOpacity: number = 0
@State isLogin: boolean = false
@State inputWidth: number = 300
@State inputRadius: number = 8
@State inputVisibility: Visibility = Visibility.Visible
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
Image($r('app.media.startIcon')).width(60).height(60)
Column() {
Column() {
LoadingProgress()
.width(60)
.height(60)
}
.width(80)
.height(80)
.borderRadius(40)
.backgroundColor(Color.Transparent)
.justifyContent(FlexAlign.Center)
.position({ x: 90, y: 0 })
.opacity(this.loadingOpacity)
.animation({
duration: 300,
playMode: PlayMode.Alternate,
expectedFrameRateRange: {
min: 20,
max: 120,
expected: 90,
}
})
TextInput({ placeholder: '请输入用户名' })
.width(this.inputWidth)
.height(50)
.visibility(this.inputVisibility)
.border({
width: {
left: 0,
right: 0,
top: 0,
bottom: 1
},
color: { bottom: Color.Gray },
radius: { topLeft: this.inputRadius, topRight: this.inputRadius },
style: { bottom: BorderStyle.Solid }
})
.id('username')
.defaultFocus(true)
.animation({
duration: 300,
playMode: PlayMode.Alternate,
onFinish: () => {
if (this.isLogin) {
this.inputVisibility = Visibility.Hidden
this.loadingOpacity = 1
this.isLogin = false
}
},
expectedFrameRateRange: {
min: 20,
max: 120,
expected: 90,
}
})
TextInput({ placeholder: '请输入密码' })
.placeholderColor('#D4D3D1')
.type(InputType.Password)
.showPasswordIcon(false)
.backgroundColor('#ffffff')
.width(this.inputWidth)
.height(50)
.visibility(this.inputVisibility)
.width(this.inputWidth)
.borderRadius({ bottomLeft: this.inputRadius, bottomRight: this.inputRadius })
.id('password')
.animation({
duration: 300,
playMode: PlayMode.Alternate,
expectedFrameRateRange: {
min: 20,
max: 120,
expected: 90,
}
})
}
.width(260)
.height(121)
Row() {
Checkbox().unselectedColor('#ffffff').width(16).borderRadius(8).backgroundColor(Color.White)
Text('aaa').fontColor('#ffffff')
}
.width(300)
Button('立即登录')
.width(200).height(45).fontSize(28).type(ButtonType.Normal).backgroundColor('#30FFFFFF')
.border({ width: 1, color: Color.White, radius: 8 }).margin({ top: 50, bottom: 60 })
.onClick(() => {
this.isLogin = true
this.inputWidth = 80
this.inputRadius = 40
return
})
Row() {
Text('bbb').fontColor(Color.White)
Text('ccc').fontColor(Color.White)
}.justifyContent(FlexAlign.SpaceBetween).width(260)
}
.width('100%')
.height('100%')
.backgroundColor(Color.Pink)
.backgroundImageSize(ImageSize.Cover)
}
}
实战案例6:下拉loading
https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/tutorials_NEXT-AnimateRefresh
@State scaleX:number = 1
Row() {
Image().scale({ x: this.scaleX, y: this.scaleY })
.animation({
延迟1s/2s/3s
duration: 2000, //动画持续时间(ms),默认为1000
curve: Curve.EaseOut, //动画速率曲线(参数值详见官方文档),默认为匀速linear
iterations: , //动画播放次数,-1是无限次,默认为1次
playMode: PlayMode.Normal //动画播放模式,默认播放完成后重头开始播放
})
Image()
Image()
Image()
Image()
}
下拉也可以是页面打开
this.scaleX = 1.3

浙公网安备 33010602011771号