5. Android UI动效新标杆:灵动岛动画,动效深度体验!

最近换了个新手机,看了下灵动岛动画还是挺炫丽的! 网上找了下没有,于是自己写了个

这个动画效果是一个充电脉冲波动效果

做动画的核心: 就是录制视频,用剪映工具,放慢看效果,分成几个步骤进行实现!最后 一帧一帧

1.效果图

2.功能需求

充电时候:开始从灵动到的中间圆形,慢慢出来一个圆角矩形,最终变成圆角矩形最大,同时展示文字 拔了充电:从最大的扩展效果,回到中间的圆点,并消失

  1. 手机充电界面(核心场景):

    • 锁屏充电显示
    • 音乐播放器的小组件动画的切换
  2. 科技感UI元素

    • 加载动画
    • 进度指示器
    • 状态切换过渡

3.实现思路

3.1 动画效果解析:

  1. 矩形收缩成圆

    • 圆角矩形同时收缩宽度和高度
    • 圆角逐渐变成完美圆形
    • 形成"电流聚集"的视觉效果
  2. 圆形收缩消失

    • 圆形继续收缩至消失
    • 伴随透明度渐变为0
    • 模拟"能量被吸收"的过程
  3. 反向脉冲扩展

    • 从消失状态扩展为圆形
    • 透明度从0恢复为1
    • 表现"能量释放"的反向过程
  4. 恢复矩形并显示文本

    • 圆形扩展回原始矩形
    • 圆角恢复为初始值
    • 完全展开后显示充电文本信息
    • 形成完整的"充电动画循环"

3.2 具体的动画过程分析

一共分为4步骤:

3.2.1 从无,把圆角矩形慢慢变大

ini 复制代码
  // 第一阶段:反向扩展动画
        private void startExpandAnimation() {
            Log.d("pengcaihua","1 startExpandAnimation ");
            // 先重置为完全透明和小尺寸
            chargingRect.setScaleX(0f);
            chargingRect.setScaleY(0f);
            chargingRect.setAlpha(0f);
            chargingRect.setVisibility(View.VISIBLE);
            // 扩展开始时保持文本隐藏
            percentText.setVisibility(View.INVISIBLE);
            chargingText.setVisibility(View.INVISIBLE);

            AnimatorSet expandAnimator = new AnimatorSet();

            // 1. 圆形扩展
            ObjectAnimator expandX = ObjectAnimator.ofFloat(
                    chargingRect, "scaleX", 0f, 0.13f);
            expandX.setDuration(time);

            ObjectAnimator expandY = ObjectAnimator.ofFloat(
                    chargingRect, "scaleY", 0f, 0.5f);
            expandY.setDuration(time);

            // 2. 透明度恢复
            ObjectAnimator fadeIn = ObjectAnimator.ofFloat(
                    chargingRect, "alpha", 0f, 1f);
            fadeIn.setDuration(time);

            expandAnimator.playTogether(expandX, expandY, fadeIn);
            expandAnimator.setInterpolator(new AccelerateDecelerateInterpolator());

            // 扩展完成后恢复原始形状
            expandAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
//                    startRestoreAnimation(); // 开始恢复原始形状
                }
            });

            expandAnimator.start();
        }
  • 水平扩展scaleX0f0.13f(宽度扩展到原始的13%)。
  • 垂直扩展scaleY0f0.5f(高度扩展到原始的50%)。
  • 淡入效果alpha0f1f(从透明到不透明)

3.2.2 将视图放到最大,完全展开的状态并显示文本

scss 复制代码
  // 第er阶段:恢复原始形状
        private void startRestoreAnimation() {
            Log.d("pengcaihua","2 startRestoreAnimation ");
            AnimatorSet restoreAnimator = new AnimatorSet();

            // 1. 恢复宽度
            ObjectAnimator restoreX = ObjectAnimator.ofFloat(
                    chargingRect, "scaleX", 0.13f, 1f);
            restoreX.setDuration(time);

            // 2. 恢复高度
            ObjectAnimator restoreY = ObjectAnimator.ofFloat(
                    chargingRect, "scaleY", 0.5f, 1f);
            restoreY.setDuration(time);

            // 3. 恢复圆角
            float finalRadius = Math.min(chargingRect.getWidth(), chargingRect.getHeight()) / 2f;
            ObjectAnimator cornerRestore = ObjectAnimator.ofFloat(
                    chargingRect, "cornerRadius", finalRadius, 40f);
            cornerRestore.setDuration(time);

            restoreAnimator.playTogether(restoreX, restoreY, cornerRestore);
            restoreAnimator.setInterpolator(new AccelerateDecelerateInterpolator());

            restoreAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    // 矩形完全恢复后显示文本
                    percentText.setVisibility(View.VISIBLE);
                    chargingText.setVisibility(View.VISIBLE);

                    // 使用Handler延迟替代Thread.sleep,避免阻塞UI线程
                    new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
                        @Override
                        public void run() {
//                            startShrinkAnimation();
                        }
                    }, 3000);
                }
            });

            restoreAnimator.start();
        }
  • 宽度恢复scaleX0.13f1f(宽度恢复到100%)
  • 高度恢复scaleY0.5f1f(高度恢复到100%)
  • 圆角变化cornerRadius圆形半径40f(从圆形变为固定40px圆角)

3.2.3 和第二步反正来,缩小,文字也消失

scss 复制代码
private void startShrinkAnimation() {
    Log.d("pengcaihua","3 startShrinkAnimation ");
    // 收缩开始时隐藏文本
    percentText.setVisibility(View.INVISIBLE);
    chargingText.setVisibility(View.INVISIBLE);

    AnimatorSet animatorSet = new AnimatorSet();

    // 1. 宽度收缩动画 - 从两边向中间收缩
    ObjectAnimator scaleXAnim = ObjectAnimator.ofFloat(
            chargingRect, "scaleX", 1f, 0.13f);
    scaleXAnim.setDuration(time);

    // 2. 高度收缩动画 - 从上下向中间收缩
    ObjectAnimator scaleYAnim = ObjectAnimator.ofFloat(
            chargingRect, "scaleY", 1f, 0.5f);
    scaleYAnim.setDuration(time);

    // 3. 圆角动画 - 变成完美圆形
    float finalRadius = Math.min(chargingRect.getWidth(), chargingRect.getHeight()) / 2f;
    Log.d(TAG, "finalRadius" + finalRadius);

    ObjectAnimator cornerAnim = ObjectAnimator.ofFloat(
            chargingRect, "cornerRadius", 40f, finalRadius);
    cornerAnim.setDuration(time);

    // 第一阶段动画:同时播放所有属性动画
    animatorSet.playTogether(scaleXAnim, scaleYAnim, cornerAnim);
    animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());

    // 添加动画监听器,在第一阶段结束后执行第二阶段动画
    animatorSet.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            // 第二阶段动画:圆形收缩
            startPerfectCircleShrinkAnimation();
        }
    });

    // 启动第一阶段动画
    animatorSet.start();
}

3.2.4 和第一步反正来,矩形圆角缩小成一个圆,然后消失不见

ini 复制代码
private void startPerfectCircleShrinkAnimation() {
    Log.d("pengcaihua","4 startPerfectCircleShrinkAnimation ");
    AnimatorSet shrinkAnimator = new AnimatorSet();

    // 1. 圆形继续收缩
    ObjectAnimator shrinkX = ObjectAnimator.ofFloat(
            chargingRect, "scaleX", 0.13f, 0f);
    shrinkX.setDuration(time);

    ObjectAnimator shrinkY = ObjectAnimator.ofFloat(
            chargingRect, "scaleY", 0.5f, 0f);
    shrinkY.setDuration(time);

    // 2. 透明度动画
    ObjectAnimator fadeOut = ObjectAnimator.ofFloat(
            chargingRect, "alpha", 1f, 0f);
    fadeOut.setDuration(time);

    shrinkAnimator.playTogether(shrinkX, shrinkY, fadeOut);
    shrinkAnimator.setInterpolator(new AccelerateDecelerateInterpolator());

    // 收缩完成后开始反向动画
    shrinkAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            startExpandAnimation(); // 开始反向扩展动画
        }
    });

    shrinkAnimator.start();
}

4.架构图

5.总结

5.1 首次执行顺序:

startExpandAnimation()startRestoreAnimation() → (延迟) → startShrinkAnimation()startPerfectCircleShrinkAnimation() → 回到 startExpandAnimation()

5.2 各阶段作用总结:

方法 阶段 视觉效果 文本状态
startExpandAnimation() 消失→小圆形 隐藏
startRestoreAnimation() 小圆形→完整矩形 显示
startShrinkAnimation() 矩形→圆形 隐藏
startPerfectCircleShrinkAnimation() 圆形→消失 隐藏
整个动画就像一次"心跳":
出现(扩张)→ 饱满状态(显示信息)→ 收缩(隐藏)→ 消失 →

这个架构实现了自包含的动画循环系统,将视图状态管理与动画执行逻辑分离,同时保持各阶段的高内聚性,适合作为灵动岛风格动画的核心引擎。

5.3 扩展:基于这个动画,你能实现心跳的动画么?

ini 复制代码
public class HeartbeatAnimationActivity extends Activity {

    private static final String TAG = "HeartbeatAnimation";

    private View heart;
    private TextView heartbeatText;
    private TextView statusText;
    private Button startButton;

    private boolean isAnimating = false;

    // 动画时长配置(毫秒)
    private final long CONTRACTION_TIME = 200;  // 收缩时间
    private final long EXPANSION_TIME = 300;    // 扩张时间
    private final long CONTRACTION_PAUSE = 100; // 收缩后暂停
    private final long EXPANSION_PAUSE = 800;   // 扩张后暂停

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_heartbeat);

        // 初始化视图
        heart = findViewById(R.id.heart);
        heartbeatText = findViewById(R.id.heartbeat_text);
        statusText = findViewById(R.id.status_text);
        startButton = findViewById(R.id.start_button);

        // 设置心形视图的圆角
        setupHeartShape();

        startButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!isAnimating) {
                    startHeartbeatAnimation();
                    startButton.setText("停止心跳");
                } else {
                    stopHeartbeatAnimation();
                    startButton.setText("开始心跳");
                }
                isAnimating = !isAnimating;
            }
        });
    }

    private void setupHeartShape() {
        GradientDrawable shape = new GradientDrawable();
        shape.setShape(GradientDrawable.OVAL);
        shape.setColor(0xffff6b6b); // 心形颜色
        shape.setCornerRadius(100); // 圆角半径

        heart.setBackground(shape);
    }

    private void startHeartbeatAnimation() {
        heartbeatText.setVisibility(View.VISIBLE);
        statusText.setText("状态: 心跳中...");
        startContractionAnimation();
    }

    private void stopHeartbeatAnimation() {
        // 重置动画状态
        heart.animate().cancel();
        heartbeatText.animate().cancel();

        // 重置视图状态
        heart.setScaleX(1f);
        heart.setScaleY(1f);
        heartbeatText.setScaleX(1f);
        heartbeatText.setScaleY(1f);
        heartbeatText.setAlpha(1f);
        heartbeatText.setVisibility(View.INVISIBLE);

        statusText.setText("状态: 已停止");
    }

    // 第一阶段:收缩动画
    private void startContractionAnimation() {
        Log.d(TAG, "开始收缩动画");
        statusText.setText("状态: 收缩中");

        AnimatorSet animatorSet = new AnimatorSet();

        // 心形收缩
        ObjectAnimator heartScaleX = ObjectAnimator.ofFloat(heart, "scaleX", 1f, 0.8f);
        heartScaleX.setDuration(CONTRACTION_TIME);

        ObjectAnimator heartScaleY = ObjectAnimator.ofFloat(heart, "scaleY", 1f, 0.8f);
        heartScaleY.setDuration(CONTRACTION_TIME);

        // 心跳文本收缩
        ObjectAnimator textScaleX = ObjectAnimator.ofFloat(heartbeatText, "scaleX", 1f, 0.8f);
        textScaleX.setDuration(CONTRACTION_TIME);

        ObjectAnimator textScaleY = ObjectAnimator.ofFloat(heartbeatText, "scaleY", 1f, 0.8f);
        textScaleY.setDuration(CONTRACTION_TIME);

        // 心跳文本变暗
        ObjectAnimator textAlpha = ObjectAnimator.ofFloat(heartbeatText, "alpha", 1f, 0.7f);
        textAlpha.setDuration(CONTRACTION_TIME);

        animatorSet.playTogether(heartScaleX, heartScaleY, textScaleX, textScaleY, textAlpha);
        animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());

        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                // 收缩完成后暂停片刻
                new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        startExpansionAnimation();
                    }
                }, CONTRACTION_PAUSE);
            }
        });

        animatorSet.start();
    }

    // 第二阶段:扩张动画
    private void startExpansionAnimation() {
        Log.d(TAG, "开始扩张动画");
        statusText.setText("状态: 扩张中");

        AnimatorSet animatorSet = new AnimatorSet();

        // 心形扩张
        ObjectAnimator heartScaleX = ObjectAnimator.ofFloat(heart, "scaleX", 0.8f, 1.1f, 1f);
        heartScaleX.setDuration(EXPANSION_TIME);

        ObjectAnimator heartScaleY = ObjectAnimator.ofFloat(heart, "scaleY", 0.8f, 1.1f, 1f);
        heartScaleY.setDuration(EXPANSION_TIME);

        // 心跳文本扩张
        ObjectAnimator textScaleX = ObjectAnimator.ofFloat(heartbeatText, "scaleX", 0.8f, 1.1f, 1f);
        textScaleX.setDuration(EXPANSION_TIME);

        ObjectAnimator textScaleY = ObjectAnimator.ofFloat(heartbeatText, "scaleY", 0.8f, 1.1f, 1f);
        textScaleY.setDuration(EXPANSION_TIME);

        // 心跳文本变亮
        ObjectAnimator textAlpha = ObjectAnimator.ofFloat(heartbeatText, "alpha", 0.7f, 1f);
        textAlpha.setDuration(EXPANSION_TIME);

        animatorSet.playTogether(heartScaleX, heartScaleY, textScaleX, textScaleY, textAlpha);
        animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());

        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                // 扩张完成后暂停片刻
                new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if (isAnimating) {
                            startContractionAnimation();
                        }
                    }
                }, EXPANSION_PAUSE);
            }
        });

        animatorSet.start();
    }
}

6.源码

项目的地址:github.com/pengcaihua1...

相关推荐
电商API大数据接口开发Cris2 分钟前
Node.js + TypeScript 开发健壮的淘宝商品 API SDK
前端·数据挖掘·api
还要啥名字4 分钟前
基于elpis下 DSL有感
前端
一只毛驴10 分钟前
谈谈浏览器的DOM事件-从0级到2级
前端·面试
用户81686947472512 分钟前
封装ajax
前端
pengzhuofan12 分钟前
Web开发系列-第13章 Vue3 + ElementPlus
前端·elementui·vue·web
yvvvy13 分钟前
白嫖 React 性能优化?是的,用 React.memo!
前端·javascript
火车叼位21 分钟前
GSAP 动画开发者的终极利器:像素化风格 API 速查表
前端
袁煦丞41 分钟前
全球热点一键抓取!NewsNow:cpolar内网穿透实验室第630个成功挑战
前端·程序员·远程工作
qq_4591317044 分钟前
前端面试问题
前端
拾光拾趣录1 小时前
从“祖传”构造函数到 `class`
前端·javascript