安卓Android 直播点赞爱心特效,计时器

安卓Android 直播点赞爱心特效,计时器_第1张图片

 

点赞特效,上图:

首先忽略这画质和抠脚的交互效果,首先需求就是 实现类似抖音的点赞效果 飘小心心的效果,UI的方案是做成了gif图,但是这种东西做成gif太low了,于是就有了想法,这边记录一下:

首先 图形是随机产生,这个不多说,ui的图片 然后点赞随机飘爱心的效果要怎么实现呢,想到的是 Android 自定义属性动画 ,那就用这个方式来实现吧,用自定义view的方式实现它,实现如下:

(爱心图标文件来源:IconFont  阿里巴巴矢量图标库 )

安卓Android 直播点赞爱心特效,计时器_第2张图片 你好sao啊!

 

 

package com.dpdp.base_moudle.weight;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;

import com.dpdp.base_moudle.R;

import java.util.ArrayList;
import java.util.Random;

/**
 * Created by ldp.
 * 

* Date: 2021-02-23 *

* Summary: 点赞爱心动画 *

* 固定宽高 *

* 包含一个子view 或者没有 要放在最下面 动效爱心往上飘 *

* @see #setLikeDrawables(int...) 设置 对应的 图片 * @see #clickLikeView() 点击调用 开始动画 点一次出一个 * @see #autoPlayClickView(int, boolean) 自动播放动画 * @see #stopAutoPlay() 停止自动播放 * @see #release() 释放资源 退出时调用 */ public class FloatLikeView extends FrameLayout { private ArrayList mLikeDrawables;// 点赞的drawable集合 private Random mRandom; //产生随机数来产生随机爱心 private final Context context; private int mLikePicWidth = 0;// 爱心的 view 宽 private int mLikePicHeight = 0;// 爱心的 view 高 private LayoutParams mLikePicLayoutParams;// 布局参数 private int mLikePicBottomMargin = 0;// 爱心出现位置距离底部 private int mChildHeight = 0;// 如果底部设置了一个 view 爱心会在其上面 private int mWidth;// 此布局view 宽 private int mHeight;// 此布局view 高 private AnimatorSet animator;// 爱心动画 public FloatLikeView(@NonNull Context context) { this(context, null); } public FloatLikeView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public FloatLikeView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.context = context; init(); } private void init() { // 存放 要显示的 效果 mLikeDrawables = new ArrayList<>(); // 设置一个默认的爱心 mLikeDrawables.add(ContextCompat.getDrawable(context, R.drawable.ui_default_heart)); // 产生随机数 随机选择 出现的图形 mRandom = new Random(); } /** * 设置 动画飘动的资源文件 */ public void setLikeDrawables(int... drawableIds) { if (drawableIds != null && drawableIds.length > 0) { mLikeDrawables.clear(); for (int drawableId : drawableIds) { mLikeDrawables.add(ContextCompat.getDrawable(context, drawableId)); } } } private ValueAnimator valueAnimator; /** * 自动 播放 飘爱心 动画 利用属性动画 进行3秒的 *

* 设置了最大 30 个 看情况自己设置好吧 太多了不好看 * * @param likeCounts 飘心的数量 * @param isRepeat 是否重复播放 * 停止播放 {@link #stopAutoPlay()} */ public void autoPlayClickView(int likeCounts, boolean isRepeat) { // 重复调用 则取消上次的重新开始 if (valueAnimator != null) { valueAnimator.cancel(); valueAnimator.removeAllUpdateListeners(); valueAnimator = null; } valueAnimator = ValueAnimator.ofInt(0, Math.min(likeCounts, 30));//30 valueAnimator.setDuration(3000); valueAnimator.setInterpolator(new LinearInterpolator()); // 是否 开启循环播放 -==== valueAnimator.setRepeatCount(isRepeat ? ValueAnimator.INFINITE : 1); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { int lastValue = 0; @Override public void onAnimationUpdate(ValueAnimator animation) { int animatedValue = ((int) animation.getAnimatedValue()); // 利用 动画的更新回调 相当于一个定时效果 但是要注意 会有多次相同的回调 要判断一下 if (animatedValue == lastValue) return; // 手动调用一次 相当于点击一次 出一个爱心效果 clickLikeView(); // 记录上次的值 lastValue = animatedValue; } }); valueAnimator.start(); } /** * 停止自动播放 *

* {@link #autoPlayClickView(int, boolean)} 自动播放 */ public void stopAutoPlay() { if (valueAnimator != null) { valueAnimator.cancel(); valueAnimator.removeAllUpdateListeners(); } } /** * 退出时 释放资源 */ public void release() { stopAutoPlay(); if (animator != null) { animator.cancel(); animator.removeAllListeners(); } } /** * 点击调用 产生动效 调用一次 产生一个爱心飘动效果 */ public void clickLikeView() { if (mLikePicWidth == 0 || mLikePicHeight == 0 || mLikePicLayoutParams == null) { // 获取 点赞的 view 尺寸 ,这边定一个 统一尺寸 mLikePicWidth = mLikeDrawables.get(0).getIntrinsicWidth(); mLikePicHeight = mLikeDrawables.get(0).getIntrinsicHeight(); // 指定爱心出现的位置 mLikePicLayoutParams = new LayoutParams(mLikePicWidth, mLikePicHeight); // 位置在底部的中间 mLikePicLayoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; // 如果底部有一个 childView 则会在其上方 飘爱心 mLikePicLayoutParams.bottomMargin = mLikePicBottomMargin; } ImageView likeIv = new ImageView(context); likeIv.setImageDrawable(mLikeDrawables.get(mRandom.nextInt(mLikeDrawables.size()))); likeIv.setLayoutParams(mLikePicLayoutParams); addView(likeIv); addAnimationStart(likeIv); } /** * 飘爱心动画 主要逻辑 */ private void addAnimationStart(ImageView likeIv) { //----------------------------------- 出现 的动画----------------------------------- ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(likeIv, "alpha", 0.5f, 1f); ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(likeIv, "scaleX", 0.6f, 1f); ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(likeIv, "scaleY", 0.6f, 1f); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(alphaAnimator, scaleXAnimator, scaleYAnimator); animatorSet.setDuration(200); animatorSet.setTarget(likeIv); //------------------------ 路径移动动画 基于 三阶贝塞尔曲线------------------------- PathEvaluator pathEvaluator = new PathEvaluator(getControlPointF(1), getControlPointF(2)); // 设置 起点 PointF startPointF = new PointF((float) (mWidth - mLikePicWidth) / 2, mHeight - mLikePicBottomMargin - mLikePicHeight); // 设置 终点 PointF endPointF = new PointF((float) mWidth / 2 + (mRandom.nextBoolean() ? 1 : -1) * mRandom.nextInt(100), 0); // 自定义 属性动画 利用贝塞尔曲线 计算路径上的各个点的位置 ValueAnimator valueAnimator = ValueAnimator.ofObject(pathEvaluator, startPointF, endPointF); valueAnimator.setDuration(3000); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (likeIv == null) return; // 根据各个点的位置 改变 view 的位置 PointF pointF = (PointF) animation.getAnimatedValue(); likeIv.setX(pointF.x); likeIv.setY(pointF.y); // 根据 动画的进度 设置透明度 likeIv.setAlpha(1f - animation.getAnimatedFraction()); } }); valueAnimator.setTarget(likeIv); animator = new AnimatorSet(); animator.setTarget(likeIv); // 动画顺序播放 animator.playSequentially(animatorSet, valueAnimator); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { // 动画结束 移除添加的 view // ~!!! // ~!!! // ~!!! removeView(likeIv); } }); animator.start(); } /** * 随机产生 三阶贝赛尔曲线 中间两个控制点 * * @param value 控制点 */ private PointF getControlPointF(int value) { PointF pointF = new PointF(); pointF.x = (float) mWidth / 2 - mRandom.nextInt(100); pointF.y = mRandom.nextInt((mHeight - mLikePicBottomMargin - mLikePicHeight) / value); return pointF; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int childCount = getChildCount(); // 包含一个子view 或者没有 要放在最下面 if (mChildHeight == 0 && childCount > 0) { View child = getChildAt(0); measureChild(child, widthMeasureSpec, heightMeasureSpec); mChildHeight = child.getMeasuredHeight(); mLikePicBottomMargin = mChildHeight; } mHeight = getMeasuredHeight(); mWidth = getMeasuredWidth(); } private static class PathEvaluator implements TypeEvaluator { private final PointF point01; private final PointF point02; public PathEvaluator(PointF point01, PointF point02) { this.point01 = point01; this.point02 = point02; } @Override public PointF evaluate(float fraction, PointF startValue, PointF endValue) { float change = 1.0f - fraction; PointF pointF = new PointF(); // 三阶贝塞儿曲线 pointF.x = (float) Math.pow(change, 3) * startValue.x + 3 * (float) Math.pow(change, 2) * fraction * point01.x + 3 * change * (float) Math.pow(fraction, 2) * point02.x + (float) Math.pow(fraction, 3) * endValue.x; pointF.y = (float) Math.pow(change, 3) * startValue.y + 3 * (float) Math.pow(change, 2) * fraction * point01.y + 3 * change * fraction * fraction * point02.y + (float) Math.pow(fraction, 3) * endValue.y; return pointF; } } }

使用方法:

    

        
        // 设置爱心 文件
        layoutBinding.likeBtn01.setLikeDrawables(R.drawable.heart_01, R.drawable.heart_02, R.drawable.heart_03,
                R.drawable.heart_04, R.drawable.heart_05, R.drawable.heart_06);
        layoutBinding.praise01.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 爱心单个出现
                layoutBinding.likeBtn01.clickLikeView();
            }
        });

        layoutBinding.praise02.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 爱心动画自动播放
                layoutBinding.likeBtn01.autoPlayClickView(20, true);
                //爱心播放动画停止 
                //layoutBinding.likeBtn01.stopAutoPlay();
            }
        });
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //退出时销毁 防止动画还没播放完就退出出现问题
        layoutBinding.likeBtn01.release();
    }

java 完整代码放在最后

接下来是前面那个计时器 ,记录一段时间点赞的次数然后 发送给服务端 如果点击一下 发送一次,接口压力太大,所以考虑过段时间提交一次,于是乎就有了比较奇怪的交互。 

 

安卓Android 直播点赞爱心特效,计时器_第3张图片

 

安卓Android 直播点赞爱心特效,计时器_第4张图片

 

 实现逻辑:

package com.dpdp.base_moudle;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateInterpolator;

import androidx.annotation.Keep;
import androidx.annotation.Nullable;


/**
 * Created by ldp.
 * 

* Date: 2021-03-05 *

* Summary: 点赞计数器 进度 * * @see #firstClick() 可以用于 第一次 点击出现 不进行缩放 按照需要来 * @see #release() 释放资源 * @see #setLikeClickCallback(LikeClickCallback) 回调 点击次数 */ public class LiveClickLikeView extends View implements View.OnClickListener { private int mWidth;// View的宽度 private int mHeight;// View的高度 private Paint bgPaint;// 背景画笔 private RectF rect;// 圆环内切圆矩形 private Paint bgArcPaint;// 圆环画笔 private Paint progressArcPaint;// 进度画笔 private Paint textPaint; // 中间文字画笔 private int angle; // 绘制角度 private int defaultSize = 100; // 默认一个最大尺寸 private int clickCounts = 0;// 点击数 private AnimatorSet animatorSet; // 动画集合 private int bgPaintColor;// 圆形 背景色 private int progressColor;// 进度条的颜色 private int textColor; // 文字的颜色 private int textSize;// 文字的 大小 private int progressWidth; // 进度条宽度 private ObjectAnimator animator; // 第一次显示出来大小不改变只有进度更新的动画 public LiveClickLikeView(Context context) { this(context, null); } public LiveClickLikeView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public LiveClickLikeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); if (attrs != null) { TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.LiveClickLikeView); bgPaintColor = attributes.getColor(R.styleable.LiveClickLikeView_like_click_bg_circle_color, Color.BLACK); progressColor = attributes.getColor(R.styleable.LiveClickLikeView_like_click_progress_color, Color.BLUE); textColor = attributes.getColor(R.styleable.LiveClickLikeView_like_click_text_color, Color.WHITE); textSize = attributes.getDimensionPixelSize(R.styleable.LiveClickLikeView_like_click_text_size, 20); progressWidth = attributes.getDimensionPixelSize(R.styleable.LiveClickLikeView_like_click_progress_width, 10); attributes.recycle(); } initPaint(); setOnClickListener(this); } private void initPaint() { bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); bgPaint.setColor(bgPaintColor); bgPaint.setStyle(Paint.Style.FILL); rect = new RectF(); bgArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG); bgArcPaint.setColor(progressColor); bgArcPaint.setStyle(Paint.Style.STROKE); bgArcPaint.setStrokeWidth(progressWidth); progressArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG); progressArcPaint.setColor(bgPaintColor); progressArcPaint.setStyle(Paint.Style.STROKE); progressArcPaint.setStrokeWidth(progressWidth); textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); textPaint.setColor(textColor); textPaint.setStyle(Paint.Style.FILL); textPaint.setTextSize(textSize); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 测量宽高 setMeasuredDimension(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec)); // 获取宽高 mHeight = getMeasuredHeight(); mWidth = getMeasuredWidth(); // 根据测量的宽高 计算 圆环进度条的 内切圆的矩形的宽高 rect.left = progressWidth >> 1;// 等价于 progressWidth/2 rect.right = mWidth - (progressWidth >> 1); rect.top = progressWidth >> 1; rect.bottom = mHeight - (progressWidth >> 1); } private int measureSize(int measureSpec) { int result = 0; int mode = MeasureSpec.getMode(measureSpec); int size = MeasureSpec.getSize(measureSpec); if (mode == MeasureSpec.EXACTLY) { // //当specMode = EXACTLY时,精确值模式,即当我们在布局文件中为View指定了具体的大小 result = size; } else { result = defaultSize; if (mode == MeasureSpec.AT_MOST) { result = Math.min(defaultSize, size); } } return result; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 背景 canvas.drawCircle(mWidth >> 1, mHeight >> 1, mWidth >> 1, bgPaint); // 圆环 canvas.drawArc(rect, -90, 360, false, bgArcPaint); // 叠加背景色一样的圆环 进度条消失动画的原理 canvas.drawArc(rect, -90, angle, false, progressArcPaint); // 测量文字宽高 float textWidth = textPaint.measureText("x " + clickCounts); Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); float textHeight = fontMetrics.bottom - fontMetrics.top; //绘制文字 宽度超过 view宽度的 4/5,按0.8比例自动进行缩放 while (textWidth > mWidth * 0.8f) { textSize = ((int) (textSize * 0.8f)); textPaint.setTextSize(textSize); textWidth = textPaint.measureText("x " + clickCounts); textHeight = fontMetrics.bottom - fontMetrics.top; } canvas.drawText("x " + clickCounts, (mWidth - textWidth) / 2, (mHeight >> 1) + textHeight / 4, textPaint); } @Keep public void setAngle(int angle) { // 改变角度 不停地重绘 形成进度条效果 this.angle = angle; invalidate(); } // 区分用户频繁点击触发结束 private boolean isUserCancel = false; @Override public void onClick(View v) { // 取消第一次的动画 if (animator != null && animator.isRunning()) { animator.removeAllListeners(); animator.cancel(); } // 取消未完成的动画 if (animatorSet != null && animatorSet.isRunning()) { isUserCancel = true; animatorSet.removeAllListeners(); animatorSet.cancel(); } //增加一次点击次数 clickCounts++; // x缩放动画 ObjectAnimator scaleX = ObjectAnimator.ofFloat(this, "scaleX", 1f, 1.5f, 1f); // y缩放动画 ObjectAnimator scaleY = ObjectAnimator.ofFloat(this, "scaleY", 1f, 1.5f, 1f); // 角度 动画 改变角度 形成进度条动画 ObjectAnimator angle = ObjectAnimator.ofInt(this, "angle", 0, 360); // 进度条动画持续5秒 angle.setDuration(5000); // 动画监听 angle.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); // 如果是用户再次点击 取消上次动画,播放新的动画,上个动画结束不进行操作, // 只有动画自己执行完成才会回调 统计一次点赞次数 if (isUserCancel) return; if (likeClickCallback != null) { // 回调5秒内的点击次数 likeClickCallback.likeCountsCallback(clickCounts); // 重置 点击次数 clickCounts = 0; } } }); animatorSet = new AnimatorSet(); // 组合动画 animatorSet.playTogether(scaleX, scaleY, angle); animatorSet.setTarget(this); // 差值器 animatorSet.setInterpolator(new AccelerateInterpolator()); // 开始播放动画 animatorSet.start(); isUserCancel = false; } /** * 第一次点击的动画 只是进度条 不进行缩放 */ public void firstClick() { clickCounts++; animator = ObjectAnimator.ofInt(this, "angle", 0, 360); animator.setDuration(5000); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (isUserCancel) return; Log.e("animation", "1 auto end " + clickCounts); if (likeClickCallback != null) { likeClickCallback.likeCountsCallback(clickCounts); clickCounts = 0; } } }); animator.start(); } /** * 结束动画释放资源 */ public void release() { if (animator != null) { animator.removeAllListeners(); animator.cancel(); animator = null; } if (animatorSet != null) { animatorSet.removeAllListeners(); animatorSet.cancel(); animatorSet = null; } } private LikeClickCallback likeClickCallback; public void setLikeClickCallback(LikeClickCallback likeClickCallback) { this.likeClickCallback = likeClickCallback; } public interface LikeClickCallback { void likeCountsCallback(int clickLikeCounts); } }


    
         
        
        
        
        
        
        
        
        
        
    

安卓Android 直播点赞爱心特效,计时器_第5张图片

 

使用方法:


    

        layoutBinding.calculateNumBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 第一次点击出现
                layoutBinding.like.setVisibility(View.VISIBLE);
                layoutBinding.like.firstClick();
            }
        });

        layoutBinding.like.setLikeClickCallback(new LiveClickLikeView.LikeClickCallback() {
            @Override
            public void likeCountsCallback(int clickLikeCounts) {
                // 点击回调
                ToastUtil.showMsg("点击了 " + clickLikeCounts + "  次");
            }
        });

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 及时结束动画
        layoutBinding.like.release();
    }

完整代码在最后

完整代码:布局解析用了 ViewBinding,提一句: Kotlin不用写finviewbyid的方式已经过时了,居然已经过时了!!! Google 推荐使用ViewBing !!!

安卓Android 直播点赞爱心特效,计时器_第6张图片

package com.example.administrator.words;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;

import androidx.annotation.Nullable;

import com.dpdp.base_moudle.LiveClickLikeView;
import com.dpdp.base_moudle.base.ui.BaseActivity;
import com.dpdp.base_moudle.utils.ToastUtil;
import com.example.administrator.words.databinding.ActivityGoodTestLayoutBinding;

/**
 * Created by ldp.
 * 

* Date: 2021-02-23 *

* Summary: *

*/ public class TestGoodActivity extends BaseActivity { private ActivityGoodTestLayoutBinding layoutBinding; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); layoutBinding = ActivityGoodTestLayoutBinding.inflate(LayoutInflater.from(this)); setContentView(layoutBinding.getRoot()); // 设置爱心 文件 layoutBinding.likeBtn01.setLikeDrawables(R.drawable.heart_01, R.drawable.heart_02, R.drawable.heart_03, R.drawable.heart_04, R.drawable.heart_05, R.drawable.heart_06); layoutBinding.praise01.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 爱心单个出现 layoutBinding.likeBtn01.clickLikeView(); } }); layoutBinding.praise02.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 爱心动画自动播放 layoutBinding.likeBtn01.autoPlayClickView(20, true); //爱心播放动画停止 //layoutBinding.likeBtn01.stopAutoPlay(); } }); layoutBinding.calculateNumBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 第一次点击出现 layoutBinding.like.setVisibility(View.VISIBLE); layoutBinding.like.firstClick(); } }); layoutBinding.like.setLikeClickCallback(new LiveClickLikeView.LikeClickCallback() { @Override public void likeCountsCallback(int clickLikeCounts) { // 点击回调 ToastUtil.showMsg("点击了 " + clickLikeCounts + " 次"); } }); } @Override protected void onDestroy() { super.onDestroy(); layoutBinding.likeBtn01.release(); // 及时结束动画 layoutBinding.like.release(); } }

 




    

        

安卓Android 直播点赞爱心特效,计时器_第7张图片

 

感谢您的拜读! 

安卓Android 直播点赞爱心特效,计时器_第8张图片

 

你可能感兴趣的:(Java,教学,android,android,studio,java)