Android widget - 直播右下角点击刷礼物特效,自动以及手动点击锚点视图,添加礼物。新添加的礼物沿着贝塞尔曲线,随机选择加速、减速加速器进行运动。
软件架构
Android 礼物动画特效
1. 主Activity布局XML里添加FavorLayout以及锚点View
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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="@android:color/white">
<org.tcshare.widgets.FavorLayout
android:id="@+id/flavorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom">
</org.tcshare.widgets.FavorLayout>
<TextView
android:id="@+id/ancher"
android:layout_width="160dp"
android:layout_height="60dp"
android:layout_gravity="bottom|right"
android:layout_marginBottom="42dp"
android:layout_marginRight="42dp"
android:background="#99999999"
android:text="锚点视图,浮动效果从该View中心点开始"/>
</FrameLayout>
2. 主Activity 完整演示代码如下
public class TCLiveLikeActivity extends AppCompatActivity {
private FavorLayout favor;
private final Handler handler = new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if(msg.what == 1){
handler.sendEmptyMessageDelayed(1, 200);
favor.addFavor();
}
}
};
@Override
protected void onDestroy() {
handler.removeMessages(1);
super.onDestroy();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tc_live_like);
favor = findViewById(R.id.flavorLayout);
Resources res = getResources();
List<Drawable> items = new ArrayList<Drawable>(){
{
add(ResourcesCompat.getDrawable(res, R.mipmap.gift_1, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.gift_2, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.gift_3, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.gift_4, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.gift_5, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.gift_6, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.gift_7, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.gift_8, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.gift_9, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.gift_10, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.gift_11, null));
}
};
// 使用自定义的效果图标,默认使用♥形状
favor.setFavors(items);
// 设置效果图标,左右飘动的范围,以及终止点的范围
favor.setFavorWidthHeight(100, 400);
// 设置AnchorView,效果图标会从该AnchorView的中心点飘出
favor.setAnchor(findViewById(R.id.ancher));
// 自动添加效果图标
handler.sendEmptyMessageDelayed(1, 200);
// 手动点击添加
findViewById(R.id.ancher).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
favor.addFavor();
}
});
}
}
package org.tcshare.widgets;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import androidx.core.content.res.ResourcesCompat;
import org.tcshare.androidutils.R;
import org.tcshare.utils.DensityUtil;
import org.tcshare.utils.RandomUtils;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class FavorLayout extends RelativeLayout {
private static final String TAG = FavorLayout.class.getSimpleName();
private int iHeight = 120;
private int iWidth = 120;
private int mHeight;
private int mWidth;
private LayoutParams lp;
private List<Drawable> loves;
private final List<Interpolator> interpolates = new ArrayList<Interpolator>() {
{
add(new LinearInterpolator());
//add(new AccelerateInterpolator());
add(new DecelerateInterpolator());
add(new AccelerateDecelerateInterpolator());
}
};
private PointF startPoint = new PointF();
private PointF anchorPoint;
private View anchorView;
private int favorWidth = -1, favorHeight = -1;
private boolean stop = false;
public FavorLayout(Context context) {
super(context);
init();
}
public FavorLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public FavorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
startPoint.set((mWidth - iWidth) / 2f, mHeight - iHeight);
}
private void init() {
//底部 并且 水平居中
lp = new LayoutParams(iWidth, iHeight);
lp.addRule(CENTER_HORIZONTAL, TRUE); //这里的TRUE 要注意 不是true
lp.addRule(ALIGN_PARENT_BOTTOM, TRUE);
Resources res = getResources();
//初始化显示的图片
loves = new ArrayList<Drawable>() {
{
add(ResourcesCompat.getDrawable(res, R.mipmap.love_a, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.love_b, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.love_c, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.love_d, null));
add(ResourcesCompat.getDrawable(res, R.mipmap.love_e, null));
}
};
}
public void setAnchor(final View view) {
anchorView = view;
resetAnchorPoint();
}
private void resetAnchorPoint() {
if (anchorView != null) {
anchorView.post(new Runnable() {
@Override
public void run() {
int[] outLocation = new int[2];
anchorView.getLocationOnScreen(outLocation);
float x = outLocation[0] + (anchorView.getWidth() - iWidth) / 2f;
float y = outLocation[1] - (anchorView.getHeight() - iHeight) / 2f;
anchorPoint = new PointF(x, y);
}
});
}
}
/**
* 点赞
* 对外暴露的方法
*/
public void addFavor() {
if (stop) {
return;
}
ImageView imageView = new ImageView(getContext());
// 随机选一个
imageView.setImageDrawable(RandomUtils.getRandomElement(loves));
if (anchorPoint == null) {
imageView.setLayoutParams(lp);
} else {
imageView.setX(anchorPoint.x - getX());
imageView.setY(anchorPoint.y - getY());
}
addView(imageView);
Log.d(TAG, "addFavor: " + "add后子view数:" + getChildCount());
Animator set = getAnimator(imageView);
set.addListener(new AnimEndListener(imageView));
set.start();
}
/**
* 设置动画
*/
private Animator getAnimator(View target) {
AnimatorSet set = getEnterAnimator(target);
ValueAnimator bezierValueAnimator = getBezierValueAnimator(target);
AnimatorSet finalSet = new AnimatorSet();
finalSet.playSequentially(set);
finalSet.playSequentially(set, bezierValueAnimator);
finalSet.setInterpolator(RandomUtils.getRandomElement(interpolates));//实现随机变速
finalSet.setTarget(target);
return finalSet;
}
/**
* 设置初始动画
* 渐变 并且横纵向放大
*/
private AnimatorSet getEnterAnimator(final View target) {
ObjectAnimator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, 0.2f, 1f);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, 0.2f, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, 0.2f, 1f);
AnimatorSet enter = new AnimatorSet();
enter.setDuration(500);
enter.setInterpolator(new LinearInterpolator());
enter.playTogether(alpha, scaleX, scaleY);
enter.setTarget(target);
return enter;
}
public void setFavorWidthHeight(int width, int height) {
this.favorWidth = DensityUtil.dp2px(getContext(), width);
this.favorHeight = DensityUtil.dp2px(getContext(), height);
}
private PointF getPointLow() {
PointF pointF = new PointF();
if (anchorView != null && favorWidth != -1 && favorHeight != -1 && anchorPoint != null) {
// 中心点
float x = anchorPoint.x - getX();
float y = anchorPoint.y - getY();
pointF.x = x - favorWidth / 2f + RandomUtils.getRandomInt(favorWidth);
pointF.y = y - favorHeight / 4f - RandomUtils.getRandomInt(favorHeight / 4);
} else {
//减去100 是为了控制 x轴活动范围
pointF.x = RandomUtils.getRandomInt(mWidth - 100);
//再Y轴上 为了确保第二个控制点 在第一个点之上,我把Y分成了上下两半
pointF.y = RandomUtils.getRandomInt(mHeight - 100) / 2f;
}
return pointF;
}
private PointF getPointHeight() {
PointF pointF = new PointF();
if (anchorView != null && favorWidth != -1 && favorHeight != -1 && anchorPoint != null) {
// 中心点
float x = anchorPoint.x - getX();
float y = anchorPoint.y - getY();
pointF.x = x - favorWidth / 2f + RandomUtils.getRandomInt(favorWidth);
pointF.y = y - favorHeight / 2f - RandomUtils.getRandomInt(favorHeight / 4);
} else {
pointF.x = RandomUtils.getRandomInt(mWidth - 100);
pointF.y = RandomUtils.getRandomInt(mHeight - 100);
}
return pointF;
}
private PointF getEndPoint() {
PointF pointF = new PointF();
if (anchorView != null && favorWidth != -1 && favorHeight != -1 && anchorPoint != null) {
// 中心点
float x = anchorPoint.x - getX();
float y = anchorPoint.y - getY();
pointF.x = x - favorWidth / 2f + RandomUtils.getRandomInt(favorWidth);
pointF.y = y - favorHeight;
} else {
pointF.set(RandomUtils.getRandomInt(getWidth()), 0);
}
return pointF;
}
/**
* 获取贝塞尔曲线动画
*/
private ValueAnimator getBezierValueAnimator(View target) {
//初始化一个BezierEvaluator
BezierEvaluator evaluator = new BezierEvaluator(getPointLow(), getPointHeight());
// 起点固定,终点随机
ValueAnimator animator = ValueAnimator.ofObject(evaluator,
anchorPoint == null ? startPoint : new PointF(anchorPoint.x - getX(), anchorPoint.y - getY()),
getEndPoint());
animator.addUpdateListener(new BezierListener(target));
animator.setTarget(target);
animator.setDuration(3000);
return animator;
}
/**
* 设置点赞效果集合
*
* @param items 浮动的图片
*/
public void setFavors(List<Drawable> items) {
loves.clear();
loves.addAll(items);
if (items.size() == 0) {
throw new UnsupportedOperationException("点赞效果图片不能为空");
}
this.iWidth = items.get(0).getIntrinsicWidth();
this.iHeight = items.get(0).getIntrinsicHeight();
startPoint = new PointF((mWidth - iWidth) / 2f, mHeight - iHeight);
resetAnchorPoint();
}
public void setStat(boolean stop) {
this.stop = stop;
}
private class AnimEndListener extends AnimatorListenerAdapter {
private final View target;
public AnimEndListener(View target) {
this.target = target;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
//因为不停的add 导致子view数量只增不减,所以在view动画结束后remove掉
removeView((target));
Log.v(TAG, "removeView后子view数:" + getChildCount());
}
}
}