在Android应用开发中,动画效果是提升用户体验的重要手段。今天分享一个灵活可配置的红包雨动画组件,支持自定义扩散范围、动画时长、红包数量等参数,适用于各种点击反馈、奖励动画等场景。
效果如下
2025-10-29 11-28-10
功能特点
-
✅ 自动位置获取 - 自动计算源视图和目标视图位置
-
✅ 灵活配置 - 支持自定义扩散范围、红包数量、动画时长
-
✅ 流畅动画 - 包含散开、收拢、缩放、旋转等多种动画效果
-
✅ 完整回调 - 提供动画开始、进行中、结束等状态回调
-
✅ 内存安全 - 自动清理动画资源,避免内存泄漏
动画工具类
java
package com.zx.gncs;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.os.Handler;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import java.util.Random;
/**
* 红包动画工具类 - 自动获取按钮当前位置
* 可以在Activity、Fragment、适配器、弹窗中调用
*/
public class RedPacketAnimation {
// 默认配置参数 - 缩小扩散范围
private static final int DEFAULT_MIN_COUNT = 6; // 最小红包数量
private static final int DEFAULT_MAX_COUNT = 12; // 最大红包数量
private static final int DEFAULT_SPREAD_DISTANCE = 80; // 默认扩散距离
private static final int MAX_SPREAD_DISTANCE = 80;// 最大扩散范围
private static final int DEFAULT_SPREAD_DURATION = 600; // 扩散动画持续时间
private static final int DEFAULT_GATHER_DURATION = 500; // 收拢动画持续时间
private static final int DEFAULT_PACKET_SIZE = 35; // 红包大小
private static final String RED_PACKET_TAG = "red_packet_animation";
private Context context;
private ViewGroup container;
private View sourceView;
private View targetView;
private int redPacketResId;
private Handler animationHandler;
private Random random;
// 配置参数
private int minPacketCount = DEFAULT_MIN_COUNT;
private int maxPacketCount = DEFAULT_MAX_COUNT;
private int spreadDistance = DEFAULT_SPREAD_DISTANCE; // 使用缩小后的默认值
private int spreadDuration = DEFAULT_SPREAD_DURATION;
private int gatherDuration = DEFAULT_GATHER_DURATION;
private int packetSize = DEFAULT_PACKET_SIZE;
private boolean enableTargetAnimation = true;
// 回调接口
private AnimationListener animationListener;
public interface AnimationListener {
void onAnimationStart();
void onPacketReceived(int receivedCount, int totalCount);
void onAnimationEnd();
void onAnimationError(String error);
}
/**
* 构造方法
*/
public RedPacketAnimation(Context context, ViewGroup container,
View sourceView, View targetView, int redPacketResId) {
this.context = context;
this.container = container;
this.sourceView = sourceView;
this.targetView = targetView;
this.redPacketResId = redPacketResId;
this.animationHandler = new Handler();
this.random = new Random();
}
/**
* 设置动画监听器
*/
public void setAnimationListener(AnimationListener listener) {
this.animationListener = listener;
}
/**
* 设置红包数量范围
*/
public void setPacketCountRange(int min, int max) {
this.minPacketCount = min;
this.maxPacketCount = max;
}
/**
* 设置散开距离 - 限制最大值为120,确保扩散范围不会太大
*/
public void setSpreadDistance(int distance) {
this.spreadDistance = Math.min(distance, MAX_SPREAD_DISTANCE); // 限制最大扩散距离
}
/**
* 设置动画时长
*/
public void setAnimationDuration(int spreadDuration, int gatherDuration) {
this.spreadDuration = spreadDuration;
this.gatherDuration = gatherDuration;
}
/**
* 设置红包大小
*/
public void setPacketSize(int size) {
this.packetSize = size;
}
/**
* 设置是否启用目标视图动画
*/
public void setEnableTargetAnimation(boolean enable) {
this.enableTargetAnimation = enable;
}
/**
* 开始红包动画
*/
public void startAnimation() {
if (container == null || sourceView == null || targetView == null) {
notifyError("动画参数不完整");
return;
}
// 清理之前的动画
cleanup();
// 通知动画开始
if (animationListener != null) {
animationListener.onAnimationStart();
}
// 确保视图已经完成布局后再执行动画
container.post(new Runnable() {
@Override
public void run() {
performAnimationCycle();
}
});
}
/**
* 停止动画并清理资源
*/
public void stopAnimation() {
cleanup();
}
/**
* 执行完整的动画周期
*/
private void performAnimationCycle() {
try {
// 确保视图已经测量和布局完成
if (sourceView.getWidth() == 0 || sourceView.getHeight() == 0) {
// 如果视图尚未测量完成,延迟执行
container.postDelayed(new Runnable() {
@Override
public void run() {
performAnimationCycle();
}
}, 50);
return;
}
// 获取视图位置 - 从按钮中心
int[] sourceLocation = getViewCenterLocation(sourceView);
int[] targetLocation = getViewCenterLocation(targetView);
if (sourceLocation == null || targetLocation == null) {
notifyError("无法获取视图位置");
return;
}
int sourceX = sourceLocation[0];
int sourceY = sourceLocation[1];
int targetX = targetLocation[0];
int targetY = targetLocation[1];
// 随机生成红包数量
int packetCount = minPacketCount + random.nextInt(maxPacketCount - minPacketCount + 1);
final int[] receivedCount = {0};
final int totalCount = packetCount;
// 生成红包并执行动画
for (int i = 0; i < packetCount; i++) {
final ImageView redPacket = createRedPacketImage();
container.addView(redPacket);
// 设置初始位置在源按钮中心
redPacket.setX(sourceX - redPacket.getWidth() / 2f);
redPacket.setY(sourceY - redPacket.getHeight() / 2f);
// 计算散开位置 - 从中心向四周散开,使用更小的范围
float[] spreadPosition = calculateSpreadPosition(sourceX, sourceY, i, packetCount);
// 延迟开始动画
long delay = i * 50 + random.nextInt(100);
animationHandler.postDelayed(() -> {
startSpreadAnimation(redPacket, spreadPosition[0], spreadPosition[1],
() -> startGatherAnimation(redPacket, targetX, targetY,
() -> {
receivedCount[0]++;
notifyPacketReceived(receivedCount[0], totalCount);
if (receivedCount[0] >= totalCount) {
notifyAnimationEnd();
}
}));
}, delay);
}
} catch (Exception e) {
notifyError("动画执行异常: " + e.getMessage());
}
}
/**
* 开始散开动画 - 从中心向四周散开
*/
private void startSpreadAnimation(ImageView redPacket, float targetX, float targetY, Runnable onEnd) {
ObjectAnimator moveX = ObjectAnimator.ofFloat(redPacket, "x", redPacket.getX(), targetX);
ObjectAnimator moveY = ObjectAnimator.ofFloat(redPacket, "y", redPacket.getY(), targetY);
ObjectAnimator scaleUpX = ObjectAnimator.ofFloat(redPacket, "scaleX", 0.3f, 1f);
ObjectAnimator scaleUpY = ObjectAnimator.ofFloat(redPacket, "scaleY", 0.3f, 1f);
float rotation = random.nextFloat() * 60 - 30;
ObjectAnimator rotationAnimator = ObjectAnimator.ofFloat(redPacket, "rotation", 0, rotation);
AnimatorSet spreadSet = new AnimatorSet();
spreadSet.playTogether(moveX, moveY, scaleUpX, scaleUpY, rotationAnimator);
spreadSet.setDuration(spreadDuration);
spreadSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
animationHandler.postDelayed(onEnd, 300);
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
spreadSet.start();
}
/**
* 开始收拢动画
*/
private void startGatherAnimation(ImageView redPacket, int targetX, int targetY, Runnable onEnd) {
float targetCenterX = targetX - redPacket.getWidth() / 2f;
float targetCenterY = targetY - redPacket.getHeight() / 2f;
ObjectAnimator gatherX = ObjectAnimator.ofFloat(redPacket, "x", redPacket.getX(), targetCenterX);
ObjectAnimator gatherY = ObjectAnimator.ofFloat(redPacket, "y", redPacket.getY(), targetCenterY);
ObjectAnimator scaleDownX = ObjectAnimator.ofFloat(redPacket, "scaleX", 1f, 0.5f);
ObjectAnimator scaleDownY = ObjectAnimator.ofFloat(redPacket, "scaleY", 1f, 0.5f);
ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(redPacket, "alpha", 1f, 0f);
ObjectAnimator rotationAnimator = ObjectAnimator.ofFloat(redPacket, "rotation",
redPacket.getRotation(), redPacket.getRotation() + 180);
AnimatorSet gatherSet = new AnimatorSet();
gatherSet.playTogether(gatherX, gatherY, scaleDownX, scaleDownY, alphaAnimator, rotationAnimator);
gatherSet.setDuration(gatherDuration);
gatherSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
container.removeView(redPacket);
// 触发目标视图动画
if (enableTargetAnimation) {
animateTargetView();
}
onEnd.run();
}
@Override
public void onAnimationCancel(Animator animation) {
container.removeView(redPacket);
onEnd.run();
}
@Override
public void onAnimationRepeat(Animator animation) {}
});
gatherSet.start();
}
/**
* 目标视图动画效果
*/
private void animateTargetView() {
ObjectAnimator scaleUpX = ObjectAnimator.ofFloat(targetView, "scaleX", 1f, 1.2f);
ObjectAnimator scaleUpY = ObjectAnimator.ofFloat(targetView, "scaleY", 1f, 1.2f);
ObjectAnimator scaleDownX = ObjectAnimator.ofFloat(targetView, "scaleX", 1.2f, 1f);
ObjectAnimator scaleDownY = ObjectAnimator.ofFloat(targetView, "scaleY", 1.2f, 1f);
AnimatorSet scaleUpSet = new AnimatorSet();
scaleUpSet.playTogether(scaleUpX, scaleUpY);
scaleUpSet.setDuration(150);
AnimatorSet scaleDownSet = new AnimatorSet();
scaleDownSet.playTogether(scaleDownX, scaleDownY);
scaleDownSet.setDuration(150);
AnimatorSet targetAnimation = new AnimatorSet();
targetAnimation.playSequentially(scaleUpSet, scaleDownSet);
targetAnimation.start();
}
/**
* 计算散开位置 - 从中心向四周360度均匀散开,使用更小的范围
*/
private float[] calculateSpreadPosition(int centerX, int centerY, int index, int totalCount) {
// 计算均匀分布的角度
double angle;
if (totalCount <= 12) {
// 对于少量红包,使用均匀分布
angle = 2 * Math.PI * index / totalCount;
} else {
// 对于更多红包,使用随机角度
angle = random.nextDouble() * 2 * Math.PI;
}
// 随机距离,确保在更小的指定范围内
float distance = 20 + random.nextFloat() * (spreadDistance - 20); // 最小距离从30减小到20
// 计算散开位置 - 从中心向四周
float spreadX = (float) (centerX + distance * Math.cos(angle) - packetSize / 2f);
float spreadY = (float) (centerY + distance * Math.sin(angle) - packetSize / 2f);
return new float[]{spreadX, spreadY};
}
/**
* 获取视图中心位置 - 使用getLocationOnScreen确保获取准确位置
*/
private int[] getViewCenterLocation(View view) {
int[] location = new int[2];
try {
// 使用getLocationOnScreen获取屏幕绝对坐标
view.getLocationOnScreen(location);
// 获取容器在屏幕上的位置
int[] containerLocation = new int[2];
container.getLocationOnScreen(containerLocation);
// 计算相对于容器的坐标
int relativeX = location[0] - containerLocation[0] + view.getWidth() / 2;
int relativeY = location[1] - containerLocation[1] + view.getHeight() / 2;
return new int[]{relativeX, relativeY};
} catch (Exception e) {
// 备用方案:使用getLocationInWindow
try {
view.getLocationInWindow(location);
return new int[]{
location[0] + view.getWidth() / 2,
location[1] + view.getHeight() / 2
};
} catch (Exception ex) {
return null;
}
}
}
/**
* 创建红包图片
*/
private ImageView createRedPacketImage() {
ImageView redPacket = new ImageView(context);
redPacket.setImageResource(redPacketResId);
// 根据容器类型设置布局参数
if (container instanceof RelativeLayout) {
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(packetSize, packetSize);
redPacket.setLayoutParams(params);
} else {
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(packetSize, packetSize);
redPacket.setLayoutParams(params);
}
redPacket.setTag(RED_PACKET_TAG);
redPacket.setScaleType(ImageView.ScaleType.FIT_XY);
redPacket.setScaleX(0.3f);
redPacket.setScaleY(0.3f);
redPacket.setAlpha(1f);
return redPacket;
}
/**
* 清理资源
*/
private void cleanup() {
if (animationHandler != null) {
animationHandler.removeCallbacksAndMessages(null);
}
if (container != null) {
// 只移除动态添加的红包视图,保留原有的按钮
for (int i = container.getChildCount() - 1; i >= 0; i--) {
View child = container.getChildAt(i);
// 只移除动态添加的红包,不移除原有的按钮
if (child.getTag() != null && child.getTag().equals(RED_PACKET_TAG)) {
container.removeView(child);
}
}
}
}
/**
* 通知回调方法
*/
private void notifyError(String error) {
if (animationListener != null) {
animationListener.onAnimationError(error);
}
}
private void notifyPacketReceived(int received, int total) {
if (animationListener != null) {
animationListener.onPacketReceived(received, total);
}
}
private void notifyAnimationEnd() {
if (animationListener != null) {
animationListener.onAnimationEnd();
}
}
}
使用
java
package com.zx.gncs;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private ImageView btn_send, btn_receive;
private RelativeLayout container;
private RedPacketAnimation redPacketAnimation;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化视图
initViews();
// 初始化红包动画
initRedPacketAnimation();
// 设置点击事件
setupClickListeners();
}
private void initViews() {
btn_send = findViewById(R.id.btn_send);
btn_receive = findViewById(R.id.btn_receive);
container = findViewById(R.id.container);
// setupButtonPositionTest();
}
private void initRedPacketAnimation() {
// 创建红包动画实例
redPacketAnimation = new RedPacketAnimation(
this,
container, // 动画容器
btn_send, // 发红包按钮 - 红包从这里中心散开
btn_receive, // 收红包按钮
R.mipmap.hongbao // 红包图片资源
);
// 自定义参数(可选)
redPacketAnimation.setPacketCountRange(8, 12);
redPacketAnimation.setSpreadDistance(200);
redPacketAnimation.setAnimationDuration(800, 600);
redPacketAnimation.setPacketSize(40);
redPacketAnimation.setEnableTargetAnimation(true);
// 设置动画监听
redPacketAnimation.setAnimationListener(new RedPacketAnimation.AnimationListener() {
@Override
public void onAnimationStart() {
Log.d("RedPacket", "红包动画开始");
// 可以在这里禁用按钮,防止重复点击
btn_send.setEnabled(false);
}
@Override
public void onPacketReceived(int receivedCount, int totalCount) {
Log.d("RedPacket", "接收进度: " + receivedCount + "/" + totalCount);
// 可以在这里更新UI,显示接收进度
}
@Override
public void onAnimationEnd() {
Log.d("RedPacket", "红包动画结束");
// 重新启用按钮
btn_send.setEnabled(true);
Toast.makeText(MainActivity.this, "红包发送完成!", Toast.LENGTH_SHORT).show();
}
@Override
public void onAnimationError(String error) {
Log.e("RedPacket", "动画错误: " + error);
// 重新启用按钮
btn_send.setEnabled(true);
Toast.makeText(MainActivity.this, "动画出错: " + error, Toast.LENGTH_SHORT).show();
}
});
}
private void setupClickListeners() {
// 点击发红包按钮
btn_send.setOnClickListener(v -> {
if (redPacketAnimation != null) {
redPacketAnimation.startAnimation();
}
});
// 测试移动按钮位置
// findViewById(R.id.btn_move).setOnClickListener(v -> moveButtonPosition());
}
/**
* 测试按钮位置变化的功能
*/
private void setupButtonPositionTest() {
// 添加一个移动按钮的测试按钮
ImageView btnMove = new ImageView(this);
btnMove.setImageResource(R.mipmap.hongbao);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(50, 50);
params.addRule(RelativeLayout.CENTER_HORIZONTAL);
params.addRule(RelativeLayout.CENTER_VERTICAL);
btnMove.setLayoutParams(params);
btnMove.setTag("btn_move");
btnMove.setOnClickListener(v -> moveButtonPosition());
container.addView(btnMove);
}
/**
* 移动按钮位置测试
*/
private void moveButtonPosition() {
// 随机移动btn_send的位置
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) btn_send.getLayoutParams();
// 随机设置新的位置
int randomMargin = (int) (Math.random() * 300);
params.bottomMargin = randomMargin;
// 随机设置水平位置
params.addRule(RelativeLayout.CENTER_HORIZONTAL, 0); // 清除居中规则
if (Math.random() > 0.5) {
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
params.leftMargin = randomMargin;
} else {
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
params.rightMargin = randomMargin;
}
btn_send.setLayoutParams(params);
Toast.makeText(this, "按钮位置已改变,现在点击红包测试", Toast.LENGTH_SHORT).show();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 停止动画,避免内存泄漏
if (redPacketAnimation != null) {
redPacketAnimation.stopAnimation();
}
}
}
对应的xml布局
XML
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F5F5F5"
android:id="@+id/container"
tools:context=".MainActivity">
<!-- 收红包按钮(顶部) -->
<ImageView
android:id="@+id/btn_receive"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerHorizontal="true"
android:layout_alignParentTop="true"
android:layout_marginTop="100dp"
android:background="@mipmap/hongbao"
android:contentDescription="收红包"
android:scaleType="fitXY"
android:clickable="true"
android:focusable="true" />
<!-- 发红包按钮(底部) -->
<ImageView
android:id="@+id/btn_send"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:layout_marginBottom="100dp"
android:background="@mipmap/hongbao"
android:contentDescription="发红包"
android:scaleType="fitXY"
android:clickable="true"
android:focusable="true" />
</RelativeLayout>
对应的资源图片

实现原理
核心动画流程
-
位置计算 - 使用
getLocationOnScreen()精确获取视图位置 -
红包创建 - 动态创建 ImageView 并设置初始位置
-
散开动画 - 从中心向四周360度均匀扩散
-
收拢动画 - 向目标位置聚集并消失
-
资源清理 - 动画结束后自动移除视图