作者:肖瑜博

前言:

这是一款基于harmonyOS + java绘制飘动的爱心心形动画特效源码。画面上多个小的心形图案动态组成一个大的心形图案,图案的边缘随着文字滑过伴随着小图案的动画汇集效果。

效果展示

图片.png

实现思路:这个动画的实现主要是四个步骤:

一:16颗小心心,从初始位置,移动到指定的位置,并拼成一颗大心心;

二:16颗小心心渐渐放大,这个时候包括小心心的位置移动和倍数的放大;

三:16颗小心心放大之后,再像中心点移动,并且不断变小;

四:移动到中心点的后,将其中的一颗小心心放大,得到我们最后想要的样子。

一、16颗小心心,从初始位置,移动到指定的位置,并拼成一颗大心心

为了更加方便计算出16颗小心心所移动的位置,我们把拼成大心心之后的图案给做了原点坐标,并根据这个原点,计算出16颗心心的坐标,具体如下图:

coordinates.png

如上图所示,我们把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,所以我们看到放大后的图片是这样的:

bigger.png<img src="./bigger.png" alt="bigger" style="zoom:50%;" />

实际上16颗小心心的大小和坐标是这样的:

trueBigger.png

所以我们就需要再次计算,放大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开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。

想了解更多关于鸿蒙的内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://ost.51cto.com/#bkwz

::: hljs-center

21_9.jpg

:::