Canvas点赞动画之非入侵式思路

效果图

未命名.gif

什么?非入侵式?这是啥东西?也很简单,就是即拿即用,不去为了某个效果而去重写或者自定义布局。

看到上面的效果图,或许脑海里最先想到的是重写ViewGroupdispatchDraw方法然后将自定义的ViewGroup在加入到xml中,这确实是一种实现思路。另一种就是我说的这个'非入侵'思路,即将动画View抽出做成一个工具类,动态添加和删除。

思路

  1. 自定义AnimationView,重写onDraw方法。
  2. 添加的顶层ViewGroup中。(顶层View其实就是DecorView也就是FrameLayout正好将其View覆盖到最上面)
  3. 播放具体动画

addView前需要先拿到目标View到屏幕的具体坐标值,这样才能在准确的位置展示动画

获取顶层View:

Window window = activity.getWindow();
ViewGroup container = (ViewGroup) window.getDecorView();
复制代码

获取目标View对于窗口的坐标

int[] viewXY = new int[2];
targetView.getLocationInWindow(viewXY); //下标0表示x,1表示y=>(x,y)
复制代码

绑定View

Window window = activity.getWindow();
if (window == null || activity.isFinishing()) {
    return;
}
ViewGroup container = (ViewGroup) window.getDecorView();
...
if (likeAnimateView == null) {
    likeAnimateView = new LikeAnimateView(window.getContext());
    int[] viewXY = new int[2];
    targetView.getLocationInWindow(viewXY);
    int centerX = viewXY[0] + targetView.getWidth() / 2;
    int centerY = viewXY[1] + targetView.getHeight() / 2;
    likeAnimateView.setTargetXY(centerX, centerY);
    ViewGroup.LayoutParams params = container.getLayoutParams();
    ViewGroup.LayoutParams animParams = new ViewGroup.LayoutParams(params.width, params.height);
    likeAnimateView.setLayoutParams(animParams);
    ...
    container.addView(likeAnimateView);
}
复制代码

获取目标view的绝对坐标,计算目标view的中心坐标,传入即将绑定的AnimateView,设置AnimateView大小为顶层View的大小,添加view


具体动画实现

这个动画仔细看其实是一个发散式的效果(由内圈逐渐扩散至外圈的过程)

Untitled2.png

内圆上任意一点到外圆上任意一点到路径就是这个动画过程

分析完是一个路径的过程那么就需要用到PathPathMeasure类配合使用

  • Path设置圆路径
this.mPath.addCircle(x, y, 20, Path.Direction.CW);
复制代码
  • PathMeasurePath路径点的坐标追踪
this.mPathMeasure.setPath(this.mPath, true);//第二个参数表示是否闭合路径
复制代码
  • PathMeasure.getPosTan(dis,pos,null)获取Path路径上对应坐标
this.mPathMeasure.getPosTan(discount, point, null);
复制代码

LikeInfo类结构

public static class LikeInfo {
    public Bitmap mBitmap;
    public int x;
    public int y;
    public int startX, startY;
    public Path path = new Path();
    public PathMeasure pathMeasure = new PathMeasure();
    public Paint paint = new Paint();
    public ValueAnimator valueAnimator = new ValueAnimator();
    public int zWidth = 0;
    public LikeInfo() {
        x = 0;
        y = 0;
        paint.setColor(Color.RED);
        paint.setStrokeWidth(1);
        paint.setStyle(Paint.Style.STROKE);
    }
    public void setBitmap(Bitmap bitmap) {
        this.mBitmap = BitmapUtil.bitmapScale(bitmap, 60, 60);
    }
}
复制代码

x,y表示一张图的坐标,startX,startY开始的xy坐标主要用于计算当前坐标点与开始坐标点距离,可根据勾股定理计算斜边长度 a^2 + b^2 = c^2 开根号并与总路径做除法可得到一个比例,根据比例可以进行图片的透明度动画控制, 每个LikeInfo类有自己的动画控制类ValueAnimator和画笔Paint,PathPathMeasure。每个Bitmap需要大小一致,做了图片比例缩放。

内圆部分代码:

this.mPath = new Path();
this.mPath.addCircle(x, y, 20, Path.Direction.CW);
this.mPathMeasure = new PathMeasure();
this.mPathMeasure.setPath(this.mPath, true);
float len = this.mPathMeasure.getLength();
float[] point = new float[2];
int index = 0;
//第一个初始圈
for (int i = 0; i < len; i += (len / LikeAnimationHelper.likesList.size())) {
    if (index < LikeAnimationHelper.likesList.size()) {
        this.mPathMeasure.getPosTan(i, point, null);
        LikeAnimationHelper.LikeInfo likeInfo = LikeAnimationHelper.likesList.get(index);
        likeInfo.x = (int) point[0];
        likeInfo.y = (int) point[1];
        likeInfo.startX = (int) point[0];
        likeInfo.startY = (int) point[1];
    }
    index++;
}
复制代码

内圆部分this.mPathMeasure.getLength()获取的是路径内所有点坐标的数组长度,point数组用于接收getPosTan获取点具体坐标。i += (len / LikeAnimationHelper.likesList.size())这里主要取根据动画图片list的平均分布坐标,这样可以在内圆中平均的在Canvas中的Bitmap图片绘制到内圆上

内圆绘制效果

Screenshot_20220324-143441.jpg

外圆部分代码:

        this.mPath.reset();
        this.mPath.addCircle(x, y, 250, Path.Direction.CW);
        this.mPathMeasure.setPath(this.mPath, false);
        len = this.mPathMeasure.getLength();
        for (int i = 0; i < len; i += (len / LikeAnimationHelper.likesList.size())) {
            this.mPathMeasure.getPosTan(i, point, null);
            if (index < LikeAnimationHelper.likesList.size()) {
                LikeAnimationHelper.LikeInfo likeInfo = LikeAnimationHelper.likesList.get(index);
                likeInfo.path.reset();//重置
                likeInfo.path.moveTo(likeInfo.x, likeInfo.y);
                likeInfo.path.lineTo(point[0], point[1]);
                likeInfo.zWidth = LikeAnimationHelper.computeDistance(likeInfo.x, likeInfo.y, point[0], point[1]);
                likeInfo.pathMeasure.setPath(likeInfo.path, false);
                likeInfo.valueAnimator.setFloatValues(0, likeInfo.pathMeasure.getLength());
                likeInfo.valueAnimator.setDuration(LikeAnimationHelper.getRandomInt(200,1000));
                likeInfo.valueAnimator.setInterpolator(new DecelerateInterpolator());
                likeInfo.valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        float value = (float) animation.getAnimatedValue();
                        float[] pos = new float[2];
                        likeInfo.pathMeasure.getPosTan(value, pos, null);
                        likeInfo.x = (int) pos[0];
                        likeInfo.y = (int) pos[1];
                        int bW = LikeAnimationHelper.computeDistance(likeInfo.startX, likeInfo.startY, likeInfo.x, likeInfo.y);
                        //当前路程 / 总路程 = 比例
                        float fit = (bW * 1.0f) / (likeInfo.zWidth * 1.0f);
                        //比例 * 255 = 透明度
                        int alpha = (int) Math.min((fit) * 255, 255);
                        //设置画笔透明度
                        likeInfo.paint.setAlpha(255 - alpha);
                        invalidate();
                    }
                });
               
            }
            index++;
        }
复制代码

前几行不用多说,和内圆部分代码相似,获取平均分布坐标点,for循环里面给每个动画对象计算距离和设置动画其中moveTolineTo绘制内圆上一点外圆上一点所形成的路径zWidth代表其总路径长度(勾股定理可得出斜边长度),将路径添加到PathMeasure中后续在动画回调addUpdateListener中通过getPosTan获取当前路径每个坐标,然后依次将坐标设置到LikeInfo中的x,y上,就完成了整体移动的动画操作。

likeInfo.valueAnimator.setDuration(LikeAnimationHelper.getRandomInt(200,1000))设置setDuration这里做了一个随机执行时间到处理,也就在动画中可以看作为打散效果,若用固定的数值则是一个内圆扩散的效果

外圆及其路径效果图
Screenshot_20220324-144735.jpg

问题:如何确定动画全部执行完了?

valueAnimator中有一个addListener方法里面有一个回调onAnimationEnd

likeInfo.valueAnimator.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {

    }

    @Override
    public void onAnimationEnd(Animator animation) {
        endIndex += 1;
    }

    @Override
    public void onAnimationCancel(Animator animation) {

    }

    @Override
    public void onAnimationRepeat(Animator animation) {

    }
});
复制代码

endIndex >= LikeAnimationHelper.likesList.size()时表示动画已经执行完毕

private Thread mThread = new Thread() {
    @Override
    public void run() {
        super.run();

        while (endIndex < LikeAnimationHelper.likesList.size()) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        post(new Runnable() {
            @Override
            public void run() {
                //结束接口回调
                iLikeAniationListener.onEnd();
            }
        });

    }
};
复制代码

启动一个线程,每100毫秒检查一次即可

源码地址

  • LikeAnimationHelper工具类方法 startAnimation(Activity activity, View targetView)参数1.activity 2.目标View
  • LikeAnimateView动画View
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享