作者:肖瑜博
前言:
这是一款基于harmonyOS + java绘制飘动的爱心心形动画特效源码。画面上多个小的心形图案动态组成一个大的心形图案,图案的边缘随着文字滑过伴随着小图案的动画汇集效果。
效果展示
实现思路:这个动画的实现主要是四个步骤:
一:16颗小心心,从初始位置,移动到指定的位置,并拼成一颗大心心;
二:16颗小心心渐渐放大,这个时候包括小心心的位置移动和倍数的放大;
三:16颗小心心放大之后,再像中心点移动,并且不断变小;
四:移动到中心点的后,将其中的一颗小心心放大,得到我们最后想要的样子。
一、16颗小心心,从初始位置,移动到指定的位置,并拼成一颗大心心
为了更加方便计算出16颗小心心所移动的位置,我们把拼成大心心之后的图案给做了原点坐标,并根据这个原点,计算出16颗心心的坐标,具体如下图:
如上图所示,我们把16颗小心心进行编号,分别是1~16心心,然后我们计算出屏幕的中心点,并把2号心心底边长的中心点与屏幕中心点重合,据此分别计算出16颗心心的坐标,其中每一颗心心之间都是没有间隔的。由此我们可以计算出每一颗心心的左边为:
x | y | |
---|---|---|
1号坐标 | centerX-itemWH*1.5 | centerY-itemWH*2 |
2号坐标(中间) | centerX-itemWH*0.5 | centerY-itemWH |
3号坐标 | centerX+itemWH*0.5 | centerY-itemWH*2 |
4号坐标(右边顶部) | centerX+itemWH*1.5 | centerY-itemWH*3 |
5号坐标 | centerX+itemWH*2.5 | centerY-itemWH*2 |
6号坐标(右边) | centerX+itemWH*3.5 | centerY-itemWH |
7号坐标 | centerX+itemWH*2.5 | centerY |
8号坐标 | centerX+itemWH*1.5 | centerY+itemWH |
9号坐标 | centerX+itemWH*0.5 | centerY+itemWH*2 |
10号坐标(底部) | centerX-itemWH*0.5 | centerY+itemWH*3 |
11号坐标 | centerX-itemWH*1.5 | centerY+itemWH*2 |
12号坐标 | centerX-itemWH*2.5 | centerY+itemWH |
13号坐标 | centerX-itemWH*3.5 | centerY |
14号坐标(左边) | centerX-itemWH*4.5 | centerY-itemWH |
15号坐标 | centerX-itemWH*3.5 | centerY-itemWH*2 |
16号坐标(左边顶部) | centerX-itemWH*2.5 | centerY-itemWH*3 |
其中centerX、centerY分别为屏幕中心点x和y值;itemWH为小心心的宽高度(宽度和高度相等)。
我们在页面初始化的时候,把所有的坐标记录在locations数组里边;
float[] loc1 = {centerX-itemWH*1.5f, centerY-itemWH*2};
float[] loc2 = {centerX-itemWH*0.5f, centerY-itemWH};
float[] loc3 = {centerX+itemWH*0.5f, centerY-itemWH*2};
float[] loc4 = {centerX+itemWH*1.5f, centerY-itemWH*3};
float[] loc5 = {centerX+itemWH*2.5f, centerY-itemWH*2};
float[] loc6 = {centerX+itemWH*3.5f, centerY-itemWH};
float[] loc7 = {centerX+itemWH*2.5f, centerY};
float[] loc8 = {centerX+itemWH*1.5f, centerY+itemWH};
float[] loc9 = {centerX+itemWH*0.5f, centerY+itemWH*2};
float[] loc10 = {centerX-itemWH*0.5f, centerY+itemWH*3};
float[] loc11 = {centerX-itemWH*1.5f, centerY+itemWH*2};
float[] loc12 = {centerX-itemWH*2.5f, centerY+itemWH};
float[] loc13 = {centerX-itemWH*3.5f, centerY};
float[] loc14 = {centerX-itemWH*4.5f, centerY-itemWH};
float[] loc15 = {centerX-itemWH*3.5f, centerY-itemWH*2};
float[] loc16 = {centerX-itemWH*2.5f, centerY-itemWH*3};
locations.add(loc1);
...
...
locations.add(loc16);
屏幕宽高度的获取方法:
// 屏幕宽度
public static int getWindowWidthPx(Context context) {
return context.getResourceManager().getDeviceCapability().width * context.getResourceManager().getDeviceCapability().screenDensity / 160;
}
// 屏幕高度
public static int getWindowHeightPx(Context context) {
return context.getResourceManager().getDeviceCapability().height * context.getResourceManager().getDeviceCapability().screenDensity / 160;
}
由此可以计算出centerX、centerY的值为,itemWH设置为30:
// 顶部的高度
private float topHeight = 200f;
// 原点坐标的位置x
private float centerX;
// 原点坐标的位置y
private float centerY;
// 小心心的宽高度
private float itemWH = 30f;
// 中心点x
centerX = getWindowWidthPx(this) * 0.5f;
// 中心点y
centerY = (getWindowHeightPx(this) - topHeight) * 0.5f;
由动画我们可以看出,所有的心心都是由16--->14--->10--->6--->4--->2--->1号这样的轨迹运动的。号码越排前的心心运动的路径越长,动画越多,所以我们可以归结为:
7个动画的心心(1号);
6个动画的心心(2-3号);
5个动画的心心(4-5号);
4个动画的心心(6-9号);
3个动画的心心(10-13号);
2个动画的心心(14-15号);
1个动画的心心(16号)。
// 给定坐标生成动画
private AnimatorProperty creatAnimationPropertyFromLocation(float[] fromLoc, float[] toLoc, long delay, long duration, Image image){
AnimatorProperty animatorProperty = image.createAnimatorProperty();
animatorProperty.moveFromX(fromLoc[0]);
animatorProperty.moveFromY(fromLoc[1]);
animatorProperty.moveToX(toLoc[0]);
animatorProperty.moveToY(toLoc[1]);
animatorProperty.setDelay(delay);
animatorProperty.setDuration(duration);
return animatorProperty;
}
// 初始位置到1号的动画
private AnimatorProperty originLocationToOneAnimation(Image image, long aniDelay){
float[] toLoc1 = locations.get(15);
AnimatorProperty animatorProperty = creatAnimationPropertyFromLocation(image.getContentPosition(), toLoc1, aniDelay, firstAniDur, image);
return animatorProperty;
}
// 16号-14号的动画
private AnimatorProperty sixteenToFourteenAnimation(Image image){
float[] fromLoc = locations.get(15);
float[] toLoc = locations.get(13);
long duration = timePadding * 2;
AnimatorProperty animatorProperty = creatAnimationPropertyFromLocation(fromLoc, toLoc, 0, duration, image);
return animatorProperty;
}
// 14号-10号的动画
private AnimatorProperty fourteenToTenAnimation(Image image){
float[] fromLoc = locations.get(13);
float[] toLoc = locations.get(9);
long duration = timePadding * 4;
AnimatorProperty animatorProperty = creatAnimationPropertyFromLocation(fromLoc, toLoc, 0, duration, image);
return animatorProperty;
}
// 10号-6号的动画
private AnimatorProperty tenToSixAnimation(Image image){
float[] fromLoc = locations.get(9);
float[] toLoc = locations.get(5);
long duration = timePadding * 4;
AnimatorProperty animatorProperty = creatAnimationPropertyFromLocation(fromLoc, toLoc, 0, duration, image);
return animatorProperty;
}
// 6号-4号的动画
private AnimatorProperty sixToFourAnimation(Image image){
float[] fromLoc = locations.get(5);
float[] toLoc = locations.get(3);
long duration = timePadding * 2;
AnimatorProperty animatorProperty = creatAnimationPropertyFromLocation(fromLoc, toLoc, 0, duration, image);
return animatorProperty;
}
// 4号-2号的动画
private AnimatorProperty fourToTwoAnimation(Image image){
float[] fromLoc = locations.get(3);
float[] toLoc = locations.get(1);
long duration = timePadding * 2;
AnimatorProperty animatorProperty = creatAnimationPropertyFromLocation(fromLoc, toLoc, 0, duration, image);
return animatorProperty;
}
// 1个动画
private void oneAnimate(float[] lastLoc, Image image, long aniDelay){
LogUtil.d("abcd","oneAnimate");
AnimatorProperty ani1 = creatAnimationPropertyFromLocation(image.getContentPosition(), lastLoc, aniDelay, firstAniDur, image);
ani1.start();
}
// 2个动画
private void twoAnimate(float[] lastLoc, long lastDuration, Image image, long aniDelay){
AnimatorProperty ani1 = originLocationToOneAnimation(image, aniDelay);
float[] fromLoc = locations.get(15);
AnimatorProperty ani2 = creatAnimationPropertyFromLocation(fromLoc, lastLoc, 0, lastDuration, image);
AnimatorGroup animatorGroup = new AnimatorGroup();
animatorGroup.runSerially(ani1, ani2);
animatorGroup.start();
}
// 3个动画
private void threeAnimate(float[] lastLoc, long lastDuration, Image image, long aniDelay){
AnimatorProperty ani1 = originLocationToOneAnimation(image, aniDelay);
AnimatorProperty ani2 = sixteenToFourteenAnimation(image);
float[] fromLoc = locations.get(13);
AnimatorProperty ani3 = creatAnimationPropertyFromLocation(fromLoc, lastLoc, 0, lastDuration, image);
AnimatorGroup animatorGroup = new AnimatorGroup();
animatorGroup.runSerially(ani1, ani2, ani3);
animatorGroup.start();
}
// 4个动画
private void fourAnimate(float[] lastLoc, long lastDuration, Image image, long aniDelay){
AnimatorProperty ani1 = originLocationToOneAnimation(image, aniDelay);
AnimatorProperty ani2 = sixteenToFourteenAnimation(image);
AnimatorProperty ani3 = fourteenToTenAnimation(image);
float[] fromLoc = locations.get(9);
AnimatorProperty ani4 = creatAnimationPropertyFromLocation(fromLoc, lastLoc, 0, lastDuration, image);
AnimatorGroup animatorGroup = new AnimatorGroup();
animatorGroup.runSerially(ani1, ani2, ani3, ani4);
animatorGroup.start();
}
// 5个动画
private void fiveAnimate(float[] lastLoc, long lastDuration, Image image, long aniDelay){
AnimatorProperty ani1 = originLocationToOneAnimation(image, aniDelay);
AnimatorProperty ani2 = sixteenToFourteenAnimation(image);
AnimatorProperty ani3 = fourteenToTenAnimation(image);
AnimatorProperty ani4 = tenToSixAnimation(image);
float[] fromLoc = locations.get(5);
AnimatorProperty ani5 = creatAnimationPropertyFromLocation(fromLoc, lastLoc, 0, lastDuration, image);
AnimatorGroup animatorGroup = new AnimatorGroup();
animatorGroup.runSerially(ani1, ani2, ani3, ani4, ani5);
animatorGroup.start();
}
// 6个动画
private void sixAnimate(float[] lastLoc, long lastDuration, Image image, long aniDelay){
AnimatorProperty ani1 = originLocationToOneAnimation(image, aniDelay);
AnimatorProperty ani2 = sixteenToFourteenAnimation(image);
AnimatorProperty ani3 = fourteenToTenAnimation(image);
AnimatorProperty ani4 = tenToSixAnimation(image);
AnimatorProperty ani5 = sixToFourAnimation(image);
float[] fromLoc = locations.get(3);
AnimatorProperty ani6 = creatAnimationPropertyFromLocation(fromLoc, lastLoc, 0, lastDuration, image);
AnimatorGroup animatorGroup = new AnimatorGroup();
animatorGroup.runSerially(ani1, ani2, ani3, ani4, ani5, ani6);
animatorGroup.start();
}
// 7个动画
private void sevenAnimate(Image image, long aniDelay){
AnimatorProperty ani1 = originLocationToOneAnimation(image, aniDelay);
AnimatorProperty ani2 = sixteenToFourteenAnimation(image);
AnimatorProperty ani3 = fourteenToTenAnimation(image);
AnimatorProperty ani4 = tenToSixAnimation(image);
AnimatorProperty ani5 = sixToFourAnimation(image);
AnimatorProperty ani6 = fourToTwoAnimation(image);
float[] fromLoc = locations.get(1);
float[] toLastLoc = locations.get(0);
long durationLast = timePadding;
AnimatorProperty ani7 = creatAnimationPropertyFromLocation(fromLoc, toLastLoc, 0, durationLast, image);
AnimatorGroup animatorGroup = new AnimatorGroup();
animatorGroup.setStateChangedListener(new Animator.StateChangedListener() {
@Override
public void onStart(Animator animator) {
}
@Override
public void onStop(Animator animator) {
}
@Override
public void onCancel(Animator animator) {
}
@Override
public void onEnd(Animator animator) {
// 1号心心运动结束之后,给所有心心添加放大的动画
biggerAnimation();
}
@Override
public void onPause(Animator animator) {
}
@Override
public void onResume(Animator animator) {
}
});
animatorGroup.runSerially(ani1, ani2, ani3, ani4, ani5, ani6, ani7);
animatorGroup.start();
}
其中lastLoc为最后一个动画的坐标,即终点坐标,lastDuration为最后一个动画所需要的时间,aniDelay为动画延迟开始的时间,firstAniDur为心心从初始位置移动到16号位置所需要的时间。
然后在页面初始化的时候,生成并用一个数组images保存16个Image对象,把心形图片赋值给Image对象,并延迟3秒给所有Image对象添加动画,具体添加代码为:
// 两个小心心之间的时间间隔
private long timePadding = 500;
// 拼成心形动画
private void getHeartAnimation(){
for (int i = 0; i < images.size(); i++){
long aniDelay = timePadding * i;
Image image = images.get(i);
float[] lastLoc = locations.get(i);
switch (i){
case 0:
sevenAnimate(image, aniDelay);
break;
case 1:
long lastDuration2 = timePadding * 2;
sixAnimate(lastLoc, lastDuration2, image, aniDelay);
break;
case 2:
long lastDuration3 = timePadding;
sixAnimate(lastLoc, lastDuration3, image, aniDelay);
break;
case 3:
long lastDuration4 = timePadding * 2;
fiveAnimate(lastLoc, lastDuration4, image ,aniDelay);
break;
case 4:
long lastDuration5 = timePadding;
fiveAnimate(lastLoc, lastDuration5, image ,aniDelay);
break;
case 5:
long lastDuration6 = timePadding * 4;
fourAnimate(lastLoc, lastDuration6, image, aniDelay);
break;
case 6:
long lastDuration7 = timePadding * 3;
fourAnimate(lastLoc, lastDuration7, image, aniDelay);
break;
case 7:
long lastDuration8 = timePadding * 2;
fourAnimate(lastLoc, lastDuration8, image, aniDelay);
break;
case 8:
long lastDuration9 = timePadding;
fourAnimate(lastLoc, lastDuration9, image, aniDelay);
break;
case 9:
float[] lastLoc10 = locations.get(9);
long lastDuration10 = timePadding * 4;
threeAnimate(lastLoc10, lastDuration10, image, aniDelay);
break;
case 10:
long lastDuration11 = timePadding * 3;
threeAnimate(lastLoc, lastDuration11, image, aniDelay);
break;
case 11:
long lastDuration12 = timePadding * 2;
threeAnimate(lastLoc, lastDuration12, image, aniDelay);
break;
case 12:
long lastDuration13 = timePadding;
threeAnimate(lastLoc, lastDuration13, image, aniDelay);
break;
case 13:
long lastDuration14 = timePadding * 2;
twoAnimate(lastLoc, lastDuration14, image, aniDelay);
break;
case 14:
long lastDuration15 = timePadding;
twoAnimate(lastLoc, lastDuration15, image, aniDelay);
break;
case 15:
oneAnimate(lastLoc, image, aniDelay);
break;
}
}
}
至此,我们的16颗小心心就可以从初始位置,移动到我们想要的位置,拼接成大心心了!
二、16颗小心心渐渐放大,这个时候包括小心心的位置移动和倍数的放大
这里我们把所有心心放大3倍,得到放大后的组合大心心。我们通过AnimatorProperty的scaleX和scaleY进行放大,通过这种方式放大后的组件,其宽高度是不变的,也就是依然是30,所以我们看到放大后的图片是这样的:
<img src="./bigger.png" alt="bigger" style="zoom:50%;" />
实际上16颗小心心的大小和坐标是这样的:
所以我们就需要再次计算,放大3倍之后,16颗小心心的坐标,具体值为:
x | y | |
---|---|---|
1号坐标 | centerX-itemWH*3.5 | centerY-itemWH*5 |
2号坐标(中间) | centerX-itemWH*0.5 | centerY-itemWH*2 |
3号坐标 | centerX+itemWH*2.5 | centerY-itemWH*5 |
4号坐标(右边顶部) | centerX+itemWH*5.5 | centerY-itemWH*8 |
5号坐标 | centerX+itemWH*8.5 | centerY-itemWH*5 |
6号坐标(右边) | centerX+itemWH*11.5 | centerY-itemWH*2 |
7号坐标 | centerX+itemWH*8.5 | centerY+itemWH |
8号坐标 | centerX+itemWH*5.5 | centerY+itemWH*4 |
9号坐标 | centerX+itemWH*2.5 | centerY+itemWH*7 |
10号坐标(底部) | centerX-itemWH*0.5 | centerY+itemWH*10 |
11号坐标 | centerX-itemWH*3.5 | centerY+itemWH*7 |
12号坐标 | centerX-itemWH*6.5 | centerY+itemWH*4 |
13号坐标 | centerX-itemWH*9.5 | centerY+itemWH |
14号坐标(左边) | centerX-itemWH*12.5 | centerY-itemWH*2 |
15号坐标 | centerX-itemWH*9.5 | centerY-itemWH*5 |
16号坐标(左边顶部) | centerX-itemWH*6.5 | centerY-itemWH*8 |
然后执行具体的代码:
// 添加放大缩小动画
// locations:16颗小心心的终点坐标
// scale:放大或缩小的倍数
// duration:执行该动画的用时
private AnimatorGroup addBiggerOrOriginAnimate(ArrayList<float[]> locations, float scale, long duration){
AnimatorProperty ani0 = new AnimatorProperty();
...
...
AnimatorProperty ani15 = new AnimatorProperty();
for (int i =0; i < images.size(); i++){
Image image = images.get(i);
float[] loc = locations.get(i);
AnimatorProperty ani = image.createAnimatorProperty();
ani.scaleX(scale);
ani.scaleY(scale);
ani.setDuration(duration);
ani.moveFromX(image.getContentPositionX());
ani.moveFromY(image.getContentPositionY());
ani.moveToX(loc[0]);
ani.moveToY(loc[1]);
switch (i){
case 0:
ani0 = ani;
break;
...
...
case 15:
ani15 = ani;
break;
}
}
AnimatorGroup animatorGroup = new AnimatorGroup(); animatorGroup.runParallel(ani0,ani1,ani2,ani3,ani4,ani5,ani6,ani7,ani8,ani9,ani10,ani11,ani12,ani13,ani14,ani15);
return animatorGroup;
}
// 放大动画
private void biggerAnimation(){
AnimatorGroup animatorGroup = addBiggerOrOriginAnimate(biggerLocations, 3, bigDuration);
animatorGroup.setStateChangedListener(new Animator.StateChangedListener() {
@Override
public void onStart(Animator animator) {
}
@Override
public void onStop(Animator animator) {
}
@Override
public void onCancel(Animator animator) {
}
@Override
public void onEnd(Animator animator) {
// 放大之后,所有小心心像中心点缩小
smallerAnimate();
}
@Override
public void onPause(Animator animator) {
}
@Override
public void onResume(Animator animator) {
}
});
animatorGroup.start();
}
三、16颗小心心放大之后,再像中心点移动,并且不断变小
由图可以看出,整颗大心心的中心点为:
// 中心点坐标
private float[] centerLoc = {};
// 中心点坐标数组
private ArrayList<float[]> centerLocs;
centerLoc = new float[]{centerX - itemWH * 0.5f, centerY - itemWH * 0.5f};
centerLocs = new ArrayList<>();
for (int i = 0; i < 16; i++){
centerLocs.add(centerLoc);
}
等所有心心放大完成之后,我们给所有心心添加缩小的动画,具体代码为:
// 缩小动画
private void smallerAnimate(){
AnimatorGroup animatorGroup = addBiggerOrOriginAnimate(centerLocs, 1, smallDuration);
animatorGroup.setStateChangedListener(new Animator.StateChangedListener() {
@Override
public void onStart(Animator animator) {
}
@Override
public void onStop(Animator animator) {
}
@Override
public void onCancel(Animator animator) {
}
@Override
public void onEnd(Animator animator) {
}
@Override
public void onPause(Animator animator) {
}
@Override
public void onResume(Animator animator) {
}
});
animatorGroup.start();
}
四、移动到中心点的后,将其中的一颗小心心放大,得到我们最后想要的样子
在所有小心心都缩小的同一个点之后,监听缩小动画完成,在完成之后的回调里,把16号小心心放大。
// 缩小完成之后,把16号心心放大25倍
Image image = images.get(15);
AnimatorProperty animatorProperty = image.createAnimatorProperty();
animatorProperty.scaleX(25);
animatorProperty.scaleY(25);
animatorProperty.setDuration(bigHeartDur);
animatorProperty.start();
总结
AnimatorProperty:可以实现组件的平移、x轴y轴的放大缩小、旋转等动画效果; AnimatorGroup:可以实现多个Animator动画对象的同时播放(runParallel)和顺序播放(runSerially)。
源码地址
https://gitee.com/YiRanRuMeng/harmony-os-love
更多原创内容请关注:中软国际 HarmonyOS 技术团队
入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。
https://ost.51cto.com/#bkwz
::: hljs-center
:::