美文网首页
[Android]属性动画简介(四)使用属性动画实现直播点赞效果

[Android]属性动画简介(四)使用属性动画实现直播点赞效果

作者: dafasoft | 来源:发表于2020-11-30 21:11 被阅读0次

参考链接:

属性动画简介(一)属性动画的实现和常用方法
属性动画简介(二)属性动画的源码分析
属性动画简介(三)ObjectAnimator中属性的自动设置解析

老张我最近疯狂迷恋黄龄,因为偶然听了她唱的《牵丝戏》,真的好听,单曲循环一天后,忍不住为黄龄女士疯狂打call,怎么打call呢? 写个动效送给她吧

翻看抖音看了一下抖音直播的动效,感觉效果还行,可以参考抖音的动效加以变换写一下,
抖音动效如下图:


抖音点赞.jpg

首先构思一下动效的一些关键元素

  • 四张图片从10个图片资源中随机获取,按照不同的轨迹向上飘动
  • 三种不同的轨迹,分别是向左边画一个圆弧,向右边画一个圆弧,画一个S形
  • 四张图片的飘动在点赞事件发生后0-500ms内随机开始
  • 最终飘动到同一位置消失
  • 终点和飘动范围可控
  • 动画运行在点赞控件上方,点赞后消失

我们尝试使用属性动画实现这一需求

先做一下分析:
1.四张图片要在500ms内随机启动,因此需要四个动画
2.因为四个动画互不干扰,因此需要一个数据结构保存这四个动画的属性
3.半圆弧(C形弧)和S形弧分别是办个Sin函数周期和一个Sin函数周期
4.动画控件的消失要在四个动画全部执行完成后

接着看如何实现:
首先定义一个PraiseView继承于View
PraiseView的主要元素:

四个动画我们用List<ValueAnimator>来保存,500ms随机启动,可以使用Handler的延时消失
   private List<ValueAnimator> animatorList;\
定义一个类AnimDrawable来保存每个动画的属性
    private class AnimDrawable {
        Bitmap bitmap; // 需要向上飘动的Bitmap
        Rect srcRest; // Bitmap  sourceRect
        Rect dstRect;  // Bitmap 位置rect
        int originLeft; // 起始位置
        int originRight; // 起始位置
        int originTop; // 起始位置
        int originBottom; // 起始位置
        Paint paint; // 起始位置

        public AnimDrawable(Bitmap bitmap, Rect srcRest, Rect dstRect, Paint paint) {
            this.bitmap = bitmap;
            this.srcRest = srcRest;
            this.dstRect = dstRect;
            originLeft = dstRect.left;
            originRight = dstRect.right;
            originTop = dstRect.top;
            originBottom = dstRect.bottom;
            this.paint = paint;
        }
    }
初始化数据:
private void init() {
        bitmapSourceArray = new int[10];
        bitmapSourceArray[0] = R.drawable.ic_live_room_like1;
        bitmapSourceArray[1] = R.drawable.ic_live_room_like2;
        bitmapSourceArray[2] = R.drawable.ic_live_room_like3;
        bitmapSourceArray[3] = R.drawable.ic_live_room_like4;
        bitmapSourceArray[4] = R.drawable.ic_live_room_like5;
        bitmapSourceArray[5] = R.drawable.ic_live_room_like6;
        bitmapSourceArray[6] = R.drawable.ic_live_room_like7;
        bitmapSourceArray[7] = R.drawable.ic_live_room_like8;
        bitmapSourceArray[8] = R.drawable.ic_live_room_like9;
        bitmapSourceArray[9] = R.drawable.ic_live_room_like10;
        drawableList = new ArrayList<>();
        animatorList = new ArrayList<>();
        handler = new MyHandler(this);
    }
从外部设置View的宽高:
    public void setPraiseViewSize(int width, int height) {
        this.width = width;
        this.height = height;
    }
重写onMeasure保持View的宽高:
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSpec = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpec = MeasureSpec.getMode(heightMeasureSpec);
        super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthSpec), MeasureSpec.makeMeasureSpec(height, heightSpec));
    }
定义一个Handler来启动动画:
    /***
     * Handler 用于启动动画
     */
    private static class MyHandler extends Handler {
        private WeakReference<PraiseView> holder;

        public MyHandler(PraiseView view) {
            holder = new WeakReference<>(view);
        }

        @Override
        public void handleMessage(Message msg) {
            if (holder.get() != null) {
                int heartIndex = msg.getData().getInt("heartIndex");
                int way = msg.getData().getInt("way");
                holder.get().startAnim(heartIndex, way);
            }
        }
    }

startAnim方法的实现

    /****
     * 启动动画
     * @param heartIndex 图片元素的位置
     * @param way 圆弧的路径  左弧  右弧  S弧
     */
private void startAnim(int heartIndex, int way) {
        Bitmap bitmap = BitmapFactory.decodeResource(this.getContext().getResources(), bitmapSourceArray[heartIndex]);
        int bitmapHeight = bitmap.getHeight();
        int bitmapWidth = bitmap.getWidth();
        AnimDrawable animDrawable;
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        if (way == WAY_CENTER) {
            // 生成S弧的初始位置保存在AnimDrawable中
            animDrawable = new AnimDrawable(bitmap, new Rect(0, 0, bitmapWidth, bitmapHeight), new Rect((width - ScreenUtils.dp2px(getContext(),22.5f)) / 2
                    , height - ScreenUtils.dp2px(getContext(),22.5f), (width + ScreenUtils.dp2px(getContext(),22.5f)) / 2, height), paint);
        } else {
            // // 生成C弧的初始位置保存在AnimDrawable中
            animDrawable = new AnimDrawable(bitmap, new Rect(0, 0, bitmapWidth, bitmapHeight), new Rect((width) / 2, height - ScreenUtils.dp2px(getContext(),22.5f)
                    , (width) / 2 + ScreenUtils.dp2px(getContext(),22.5f), height), paint);
        }
        drawableList.add(animDrawable);
        // 这个值代表不同动画宽度变化的最大宽度
        int temMaxWidth = width / 2;
        // 这个值代表不同动画高度变化的最大值
        int maxHeight = height - ScreenUtils.dp2px(getContext(),22.5f);
        float value = 360f;
        if (way == WAY_CENTER) {
            // S弧Sin函数走一个完整的圆
            value = 360f;
            temMaxWidth = (width - ScreenUtils.dp2px(getContext(),22.5f)) / 2;
        } else if (way == WAY_LEFT) {
            // 左C弧Sin函数走一个半圆
            value = 180f;
            temMaxWidth = width / 2;
        } else if (way == WAY_RIGHT) {
            // 右C弧Sin函数走一个半圆
            value = 180f;
            temMaxWidth = width / 2 - ScreenUtils.dp2px(getContext(),22.5f);
        }
        int maxWidth = temMaxWidth;
        float animValue = value;
        ValueAnimator animator = ValueAnimator.ofFloat(animValue);
        animator.setDuration(1000);
        animator.addUpdateListener(animation -> {
            float tempValue = (float) animation.getAnimatedValue();
            // 动画图片的展示纵坐标由初始位置减移动的height决定,当 tempValue = animValue时,正好为animDrawable.originTop - maxHeight
            animDrawable.dstRect.top = animDrawable.originTop - (int) (tempValue * (maxHeight / animValue));
            animDrawable.dstRect.bottom = animDrawable.originBottom - (int) (tempValue * (maxHeight / animValue));
            if (way == WAY_RIGHT) {
                //右弧动画图片的展示横坐标由 初始位置加sin(Math.toRadians(tempValue) 决定
                animDrawable.dstRect.left = animDrawable.originLeft + (int) (maxWidth * Math.sin(Math.toRadians(tempValue)));
                animDrawable.dstRect.right = animDrawable.originRight + (int) (maxWidth * Math.sin(Math.toRadians(tempValue)));
            } else {
                // 左弧动画图片的展示横坐标由 初始位置减sin(Math.toRadians(tempValue) 决定
                animDrawable.dstRect.left = animDrawable.originLeft - (int) (maxWidth * Math.sin(Math.toRadians(tempValue)));
                animDrawable.dstRect.right = animDrawable.originRight - (int) (maxWidth * Math.sin(Math.toRadians(tempValue)));
            }
            // 透明度从 255 到 0
            animDrawable.paint.setAlpha(255 - (int)(255 * tempValue / animValue));
            invalidate();
        });
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                animCount++;
                if (animCount >= COUNT) {
                    if (animEndListener != null) {
                        // 当 COUNT 数的动画全部执行完成,执行动画完成的回调
                        animEndListener.onAnimEnd();
                    }
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                animCount++;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        animator.start();
    }

定义一个PraiseLayout, 宽高均为MathParent 作为承载动画的容器

public class PraiseLayout extends RelativeLayout {
    private PraiseView praiseView;
    private int width;
    private int height;

    public PraiseLayout(Context context) {
        super(context);
        init();
    }

    public PraiseLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PraiseLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        LayoutInflater.from(getContext()).inflate(R.layout.layout_praise, this, true);
        praiseView = findViewById(R.id.praiseView);
        // 动画完成事件的监听
        praiseView.setOnAnimEndListener(() -> hide());
    }

    /***
     * 从外部获取点赞View的宽高
     * @param width
     * @param height
     */
    public void setPraiseViewSize(int width, int height) {
        this.width = width;
        this.height = height;
        praiseView.setPraiseViewSize(width, height);
    }

    public void show(View anchor) {
        // 定义一个int数组用来记录点赞View的位置
        int[] location = new int[2];
        // 获取点赞View的位置
        anchor.getLocationInWindow(location);
        // 获取点赞View的LayoutParams
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) praiseView.getLayoutParams();
        // 设置praiseView的位置
        params.leftMargin = location[0] - (width - anchor.getWidth()) / 2;
        params.topMargin = location[1] - height;
        praiseView.setLayoutParams(params);
        // 将本View添加到DecoreView中
        ((ViewGroup)((Activity)getContext()).getWindow().getDecorView()).addView(PraiseLayout.this);
        praiseView.show();
    }

    private void hide() {
        // 将本View从DecoreView中移除
        ((ViewGroup)((Activity)getContext()).getWindow().getDecorView()).removeView(this);
    }
}

使用方式:
在Activity中设置点赞按钮的监听:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button praiseBtn = findViewById(R.id.praiseBtn);
        praiseBtn.setOnClickListener(v -> {
            PraiseLayout layout = new PraiseLayout(this);
            layout.setPraiseViewSize(ScreenUtils.dp2px(this, 60), ScreenUtils.dp2px(this, 200));
            layout.show(praiseBtn);
        });
    }

效果:


yellow_zero_call[00_00_04--00_00_24].gif

为龄龄子女士疯狂打call任务完成

源码:

属性动画时间之点赞动效

相关文章

网友评论

      本文标题:[Android]属性动画简介(四)使用属性动画实现直播点赞效果

      本文链接:https://www.haomeiwen.com/subject/lrcrwktx.html