emmmm....这次取标题好难啊,我也不知道这个动画叫什么名字好~

同样是一个小伙伴的需求,我帮忙做的实现,然后给我发了几个小红包,今天上班可以任性一会点一杯星巴克了,这里再次感谢扔物线大大教我写动画,哈哈哈哈~

这次的十分钟动画同样只要十分钟就可以实现,没有标题狗哦。

顺便说一下,并不是我写的所有动画都会出十分钟系列的文章,前两次是绘制,这次主要是 ObjectAnimation,以后的十分钟系列也会是不同的知识点,喜欢的朋友关注一下呗。

话题扯远了,先来看效果吧~

这是我在 demo 上的实现效果~

需求:参考设计图实现,越逼真越好。

动画拆解

老规矩,拿到动画,实现之前先惯例拆解,这次动画我们还是拆解成三个阶段吧。

  • 一阶段:在屏幕上以固定的速度依次出现十几个“惊悚”的表情icon,每个表情的旋转角度、位置随机、并且越靠近屏幕中央的 icon 越大。
  • 二阶段:根据各个 icon的旋转角度,“左右”抖动。注意这里不是屏幕的左右,而是 icon 的作用抖动。也就是说不同旋转角度的 icon,抖动的方向不一样。
  • 三阶段:放大到8倍大小,并且在放大动画执行到一半的时候,透明度从1到0.

拆解动画的分步骤实现

看到这里的小伙伴可以自行思考一下实现方式。

思考三分钟。。。

好,三分钟过去了。

本次动画的三个阶段都是基于 ObjectAnimation 做的实现。如果对 ObjectAnimation 还不是特别了解的小伙伴赶紧儿去学一下一下 HenCoder 系列教程的1.6和1.7。

一阶段

这里有三个需求。

  • 以固定的速度在屏幕上出现若干个“惊悚”表情
  • 惊悚表情的旋转角度、位置随机
  • 越靠近屏幕中央的表情越大

首先第一个问题,这个很简单,只牵涉到定时功能。在0 ~ n 的时间内依次绘制0 ~ m 个icon 即可。

第二个问题,关于每个表情的旋转角度、位置的随机,可能会出现icon 重叠的问题,但是如果每次新增一个 icon 都要去检测一遍和现有 icon是否存在覆盖问题的话,性能上会比较尴尬,而且随机出现位置并不是强需求,最后和设计师沟通后,同意17个 icon 的大小、旋转角度写死。

第三个问题,第二个问题解决了,第三个问题自然不存在了。

哈哈哈哈哈,是不是很棒,程序员要记得勇敢的去和设计师沟通。
好了,我还是说一下如果设计师不同意写死位置,又要去 icon 不能相互覆盖的情况下,我们该怎么办。

  • 如果检测新增 icon 是否会遮盖其他已存在 icon?首先我们把一个 看成是一个圆,icon 会随机生成一个圆心 point(x,y),同时我们也能根据大小计算出半径。然后在添加之前去遍历已存在的 icon,判断新老 icon 的圆心距离是否大于半径只和。
  • 如何约靠近屏幕中心表情越大。设置根据 icon 的圆心点 point(x,y),再根据屏幕中间的点 center(centX,centY),设置一个 x 轴的居中系数和一个 y 轴的居中系数,然后根据这两个个系数设置 icon 的 scale 大小。

由于这里的参数牵涉到 icon 的圆心点位置 x、y,旋转角度 rotate,缩放大小 scale。所以我们可以创建一个 bean IconInfo 来保存这些信息。

二阶段

需求:根据 icon 的旋转角度,做左右抖动的操作。

这个有点尴尬,如果只是0度的旋转,那么左右抖动 50px 就只需要 x 轴加减50px 就行了。but,90度的旋转就变成了 y 轴加减 50px了。这两种情况还好,那么45度呢?岂不是变成了 x、y 轴同时加减

$$ \dfrac{ \sqrt{2}}{2} *50$$

咦,当时我是想到了这里,突然有了思路,这特么不就是一个计算正弦余弦的公司嘛。所以,当旋转角度为 rotate的时候,x 轴的偏离方向就是 cos(rotate) · offset, y 轴就是 sin(rotate)·offset。

这个正弦余弦是初中的数学知识,大家应该都看得懂吧。

三阶段

这个没什么意思,就是一个 scaleX、scaleY和 alpha 的操作了。

代码实现

由于这不是我自己的项目,也不清楚是否有其他类似的需求,那么代码实现上,应该尽量的解耦,最好是能够一行代码调用就能够显示这个动画。

想象一下,我们的 Toast、SnackBar ,同样是在屏幕上弹出一个节目,但他们的实现多么解耦。

这次我的实现参考了 SnackBar ,不用在布局文件里面入侵代码,实现了一行代码显示动画,即插即用~哈哈哈哈哈

先来感受一下代码的调用~

findViewById(R.id.fab).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            AnimationHelper.start(v);
        }
    });

好了,不扯犊子了,我直接贴代码。

public class AnimationHelper {

public static void start(View view) {
    ViewGroup suitableParent = findSuitableParent(view);
    MyView child = new MyView(view.getContext());
    suitableParent.addView(child);
}

private static ViewGroup findSuitableParent(View view) {
    ViewGroup fallback = null;
    do {
        if (view instanceof FrameLayout) {
            if (view.getId() == android.R.id.content) {
                return (ViewGroup) view;
            } else {
                fallback = (ViewGroup) view;
            }
        }
        if (view != null) {
            final ViewParent parent = view.getParent();
            view = parent instanceof View ? (View) parent : null;
        }
    } while (view != null);
    return fallback;
}

private static class MyView extends View implements View.OnClickListener {

    private Bitmap mIcon;
    private Paint mPaint;
    private int mWidth;
    private int mHeight;
    private int showCount;
    private int shake;
    private ArrayList<AnimationInfo> mInfo = new ArrayList<>();
    Matrix mMatrix = new Matrix();

    public MyView(Context context) {
        super(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mWidth = MeasureSpec.getSize(widthMeasureSpec);
        mHeight = MeasureSpec.getSize(heightMeasureSpec);
        init();
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }


    private void init() {
        if (mWidth == 0)
            return;
        mIcon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_face_shock);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.WHITE);


        mInfo.clear();

        int width = (int) (mIcon.getWidth() * 0.5);
        int height = (int) (mIcon.getHeight() * 0.5);
        int centerX = mWidth / 2 - width / 2;
        mInfo.add(new AnimationInfo(0.5F, 0, centerX + width * 4F, mHeight * 3 / 4 - height));
        mInfo.add(new AnimationInfo(0.55F, 20, centerX + width * 3.4F, mHeight * 3 / 4 - height * 2.2F));
        mInfo.add(new AnimationInfo(0.6F, 340, centerX + width * 2.6F, mHeight * 3 / 4 - height * 3.5F));

        mInfo.add(new AnimationInfo(0.65F, 20, centerX - width, mHeight / 2 - height));
        mInfo.add(new AnimationInfo(0.6F, 340, centerX - width * 2.8F, mHeight / 2 + height));

        mInfo.add(new AnimationInfo(0.5F, 20, centerX + width * 0.5F, mHeight / 4 - height * 2F));

        mInfo.add(new AnimationInfo(0.7F, 320, centerX, mHeight / 2F));

        mInfo.add(new AnimationInfo(0.5F, 40, centerX - width * 0.8F, mHeight / 2 + height * 3F));

        mInfo.add(new AnimationInfo(0.7F, 250, centerX - width * 2F, mHeight / 2 - height * 2F));

        mInfo.add(new AnimationInfo(0.6F, 320, centerX - width * 3F, mHeight / 2 - height * 1.5F));

        mInfo.add(new AnimationInfo(0.7F, 45, centerX + width * 3F, mHeight / 2 - height * 2F));

        mInfo.add(new AnimationInfo(0.75F, 20, centerX, mHeight / 2 - height * 2.5F));

        mInfo.add(new AnimationInfo(0.6F, 320, centerX + width * 1.5F, mHeight / 2 - height * 4F));

        mInfo.add(new AnimationInfo(0.6F, 45, centerX + width * 0.5F, mHeight / 2 - height * 4.5F));

        mInfo.add(new AnimationInfo(0.5F, 320, centerX - width * 1.8F, mHeight / 2 - height * 5F));

        mInfo.add(new AnimationInfo(0.6F, 100, centerX + width * 1.8F, mHeight / 2 + height * 3F));

        mInfo.add(new AnimationInfo(0.5F, 320, centerX, mHeight / 2 + height * 5F));

        mInfo.add(new AnimationInfo(0.6F, 10, centerX - width * 0.5F, mHeight / 2 + height * 1.5F));

        showCount = 0;

        setOnClickListener(this);

        ObjectAnimator animator1, animator2, animator3;


        animator1 = ObjectAnimator.ofInt(this, "showCount", mInfo.size());
        animator1.setDuration(mInfo.size() * 35);

        animator2 = ObjectAnimator.ofInt(this, "shake", 0, 20, 0, -20, 0, 20, 0, -20);
        animator2.setDuration(300);

        PropertyValuesHolder scaleXValuesHolder = PropertyValuesHolder.ofFloat("scaleX", 1, 8);
        PropertyValuesHolder scaleYValuesHolder = PropertyValuesHolder.ofFloat("scaleY", 1, 8);
        Keyframe keyframe1 = Keyframe.ofFloat(0, 1);
        Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 1);
        Keyframe keyframe3 = Keyframe.ofFloat(1, 0);
        PropertyValuesHolder alphaValuesHolder = PropertyValuesHolder.ofKeyframe("alpha", keyframe1, keyframe2, keyframe3);
        animator3 = ObjectAnimator.ofPropertyValuesHolder(this, scaleXValuesHolder, scaleYValuesHolder, alphaValuesHolder);
        animator3.setDuration(200);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playSequentially(animator1, animator2, animator3);
        animatorSet.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (showCount < mInfo.size()) {
            drawStep1(canvas);
        } else {
            drawStep2(canvas);
        }

    }

    private void drawStep1(Canvas canvas) {
        for (int i = 0; i < showCount && i < mInfo.size(); i++) {
            AnimationInfo info = mInfo.get(i);
            canvas.save();
            mMatrix.reset();
            mMatrix.postScale(info.scale, info.scale, info.x, info.y);
            mMatrix.postRotate(info.rotate, info.x, info.y);
            canvas.concat(mMatrix);
            canvas.drawBitmap(mIcon, info.x, info.y, mPaint);
            canvas.restore();
        }
    }

    private void drawStep2(Canvas canvas) {
        for (int i = 0; i < showCount && i < mInfo.size(); i++) {
            AnimationInfo info = mInfo.get(i);
            canvas.save();
            mMatrix.reset();
            float x = info.calculateTranslationX(shake);
            float y = info.calculateTranslationY(shake);
            mMatrix.postScale(info.scale, info.scale, x, y);
            mMatrix.postRotate(info.rotate, x, y);
            canvas.concat(mMatrix);
            canvas.drawBitmap(mIcon, x, y, mPaint);
            canvas.restore();
        }
    }

    @Override
    public void onClick(View v) {
        ViewGroup parent = (ViewGroup) v.getParent();
        parent.removeView(v);
    }

    @Keep
    private void setShowCount(int showCount) {
        this.showCount = showCount;
        invalidate();
    }

    @Keep
    private void setShake(int shake) {
        this.shake = shake;
        invalidate();
    }

}

private static class AnimationInfo {
    float scale;
    float rotate;
    float x;
    float y;

    public AnimationInfo(float scale, float rotate, float x, float y) {
        this.scale = scale;
        this.rotate = rotate;
        this.x = x;
        this.y = y;
    }

    public float calculateTranslationX(float length) {
        return (float) Math.cos(rotate) * length + x;
    }

    public float calculateTranslationY(float length) {
        return (float) Math.sin(rotate) * length + y;
    }


}

}

代码比较简单,我就不写注释了。

有几个点我想提一下~

  • 正常情况下,MyView 命名是不规范的,MyView 也不应该作为 AnimationHelper 的内部类,可以把 MyView单独提出了,AnimationInfo作为 MyView 的内部类。
  • AnimationHelper 里面的findSuitableParent 方法拷贝自 SnackBar,这个方法我在分析 SnackBar 源码的时候讲过,就不再赘述了。
  • onDraw 方法里面的drawStep1 和drawStep2其实可以合并成一个方法,我为了便于大家理解所以两个方法分开写了。
  • 如果你还有什么可以优化的代码,尽管拍砖,我都接着~
  • 绘制里面用到了 canvas 和Matrix 相关的代码如果看不懂可以去看扔物线的文章。
  • 以后想到再补充~

喜欢记得点个关注哦~