Widget部件介绍
Android widget - 直播右下角点击刷礼物特效,自动以及手动点击锚点视图,添加礼物。新添加的礼物沿着贝塞尔曲线,随机选择加速、减速加速器进行运动。
软件架构
Android 礼物动画特效
使用说明及Demo
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(); } }); } }
FavorLayout.java 源码文件
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()); } } }
文章评论
有demo链接吗?贝塞尔曲线这个关键类没有写出来~
@disco 贝塞尔这个类在这里 https://gitee.com/aityu/AndroidUtils/blob/master/androidutils/src/main/java-utils/org/tcshare/widgets/BezierEvaluator.java
完整的demo及库 在这里 https://gitee.com/aityu/AndroidUtils