实现目标: 抖音小视频播放时,双击屏幕会在手指点击位置出现一个小红心,由大变小,然后放大渐变消失,并且点击速度快,可以出现多个小红心动画,每次出现的小红心会有一个小角度的旋转。
实现思路:
1.双击屏幕,在手指点击位置会出现小心心,因此需要监听双击手势,并且获取到点击坐标
2.快速点击,会产生多个小心心的动画,因此需要动态添加view
3.动画过程,大概是出现后先缩小后放大逐渐消失
实现结果:
演示.gif
实现过程:
1.在需要展示动画的父布局中,添加占位组件ContentSlot()用于动态添加点赞动画
2.之前一篇介绍了动态添加布局,需要定一个布局,和一个参数对象
Column(){
ContentSlot(this.content)
}
.height('100%')
.width('100%')
.gesture(
TapGesture({ count: 2 })
.onAction((event: GestureEvent) => {
let buildNode = new BuilderNode<[ParamsInterface]>(this.getUIContext());
let redowAngle=(Math.random()*2-1)*50;
buildNode.build(wrapBuilder<[ParamsInterface]>(buildHeartView), {
x: event.fingerList[0].localX,
y: event.fingerList[0].localY,
opacity:1,
scale:1.3,
duration:300,
angle:redowAngle,
finish:()=>{
buildNode.update({
x: event.fingerList[0].localX,
y: event.fingerList[0].localY,
scale:1,
opacity:1,
duration:300,
angle:redowAngle,
})
setTimeout(()=>{
buildNode.update({
x: event.fingerList[0].localX,
y: event.fingerList[0].localY,
scale:3,
opacity:0,
duration:500,
angle:redowAngle,
})
},300)
setTimeout(()=>{
this.content.removeFrameNode(buildNode.getFrameNode())
},1000)
}
}, { nestingBuilderSupported: true });
this.content.addFrameNode(buildNode.getFrameNode());
})
)
interface ParamsInterface {
x:number
y:number
finish:() => void
opacity:number
scale:number
duration:number
angle:number
}
@Builder
function buildHeartView(params: ParamsInterface) {
Image($r('app.media.heart')).width(40).height(40)
.position({
x: params.x,
y: params.y,
})
.opacity(params.opacity)
.scale({x:params.scale,y:params.scale})
.animation({
duration: params.duration, //动画持续时间,单位为毫秒
curve: Curve.Linear, //动画曲线
iterations: 1, //动画播放次数
playMode: PlayMode.Normal, //动画播放模式 正向
})
.onAppear( params.finish)
}
4.使用动画,需要对Image的属性进行修改,但是buildHeartView不支持内部定义参数,所以需要修改params的值,这时需要用到BuilderNode的update方法更新参数。这里不能直接在buildHeartView内里修改参数,否侧会报错。例如在onAppear中修改透明度的值,会闪退:
报错.png
实现优化:
这里我们需要展示2个动画,需要在主布局中维护想要调整的属性值,后期如果修改或者复用,一点也不方便,如何将动画的控制让Image自己控制呢? 主布局中我只要触发动画就行。实现也很简单,只要再定义一个全局的Component,动画相关的事情让这个子组件去实现,buildHeartView中引用这个全局Component。动态节点只需要负责展示定位和角度就可以了。最后加一个动画结束的回调用于移除节点。
最终源码:
import { BuilderNode, NodeContent } from '@kit.ArkUI';
@Entry
@ComponentV2
struct HeartTest{
content: NodeContent = new NodeContent();
build() {
Column(){
ContentSlot(this.content)
}
.height('100%')
.width('100%')
.gesture(
TapGesture({ count: 2 })
.onAction((event: GestureEvent) => {
let buildNode = new BuilderNode<[ParamsInterface]>(this.getUIContext());
buildNode.build(wrapBuilder<[ParamsInterface]>(buildHeartView), {
x: event.fingerList[0].localX,
y: event.fingerList[0].localY,
finish:()=>{
this.content.removeFrameNode(buildNode.getFrameNode())
}
}, { nestingBuilderSupported: true });
this.content.addFrameNode(buildNode.getFrameNode());
})
)
}
}
interface ParamsInterface {
x:number
y:number
finish:() => void
}
@Builder
function buildHeartView(params: ParamsInterface) {
HeartImageAnimation({ finish:params.finish}).position({
x: params.x,
y: params.y,
}) .rotate({angle:(Math.random()*2-1)*50})
}
@ComponentV2
struct HeartImageAnimation{
@Local opacityNor:number=1
@Local scanOption:ScaleOptions={
x:1.3,
y:1.3
}
@BuilderParam finish:() => void
build() {
Image($r('app.media.heart')).width(40).height(40)
.opacity(this.opacityNor)
.scale(this.scanOption).onAppear(()=>{
this.getUIContext().animateTo({
duration: 300, //动画持续时间,单位为毫秒
curve: Curve.EaseOut, //动画曲线
iterations: 1, //动画播放次数
playMode: PlayMode.Normal, //动画播放模式 正向
onFinish: () => { //动画播放完成回调
this.getUIContext().animateTo({
duration: 500, //动画持续时间,单位为毫秒
curve: Curve.EaseOut, //动画曲线
iterations: 1, //动画播放次数
playMode: PlayMode.Normal, //动画播放模式 正向
onFinish: () => { //动画播放完成回调
this.finish
}
},()=>{
this.scanOption = {
x: 3,
y: 3,
}
this.opacityNor = 0
})
}
},()=>{
this.scanOption = {
x: 1,
y: 1,
}
})
})
}
}