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 相關的程式碼如果看不懂可以去看扔物線的文章。
- 以後想到再補充~
喜歡記得點個關注哦~