十分鐘搞定酷炫動畫,萬聖節驚悚的聊天介面

diamond_lin發表於2017-10-26

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 相關的程式碼如果看不懂可以去看扔物線的文章。
  • 以後想到再補充~

喜歡記得點個關注哦~

相關文章