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();
        }
  • 水平扩展 :scaleX 从 0f → 0.13f(宽度扩展到原始的13%)。
  • 垂直扩展 :scaleY 从 0f → 0.5f(高度扩展到原始的50%)。
  • 淡入效果 :alpha 从 0f → 1f(从透明到不透明)

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();
        }
  • 宽度恢复 :scaleX 从 0.13f → 1f(宽度恢复到100%)
  • 高度恢复 :scaleY 从 0.5f → 1f(高度恢复到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...

相关推荐
deli0071 小时前
1 个汉字算几个 token?实测中英文 prompt 与代码的编码差异
前端
无限聚核1 小时前
抖音 certificate.prepare 之后怎么验券?和美团 msconsume 差在哪,无限聚核
前端·微信小程序
2603_965896622 小时前
Grid 网格布局|CSS 二维页面排版体系
前端·css
有味道的男人2 小时前
搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案
服务器·前端·javascript
lovingsoft3 小时前
Chrome Lighthouse 详解
前端·chrome
Qlly3 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝3 小时前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦3 小时前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡3 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI3 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战