先上效果圖java
灑豆子的效果,突發奇想,以爲這個動畫挺有意思的,就抽空寫了一個玩玩android
繪製流程:git
定義6個‘’豆子‘’,每一個豆子有各自的屬性,大小,拋出的速度等,而後控制每一個的方向和狀態,回彈效果使用差值器 BounceInterpolatorgithub
package com.fragmentapp.view.beans; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.BounceInterpolator; import com.fragmentapp.R; import com.fragmentapp.helper.RandomUtil; /** * Created by liuzhen on 2017/1/17. */ public class BeansView extends View { private Paint paint; private int mWidth; private int mHeight; private int top; private ValueAnimator va; private Beans beans1,beans2,beans3,beans4,beans5,beans6; public BeansView(Context context) { this(context, null); } public BeansView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BeansView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { Log.e("tag","init"); setWillNotDraw(false); paint = new Paint(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.FILL); paint.setColor(getResources().getColor(R.color.color_ff9c19)); //隨機生成球體的大小、 beans1 = new Beans(RandomUtil.random(5,15)); beans2 = new Beans(RandomUtil.random(5,15)); beans3 = new Beans(RandomUtil.random(5,15)); beans4 = new Beans(RandomUtil.random(5,15)); beans5 = new Beans(RandomUtil.random(5,15)); beans6 = new Beans(RandomUtil.random(5,15)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { mWidth = getWidth(); mHeight = getHeight(); this.top = top; startAnim(); } } @Override protected void onDraw(Canvas canvas) { //正常向右掉落,這裏也能夠利用隨機生成方向,這裏就固定左邊三個右邊三個 canvas.drawCircle(beans1.getCx(), beans1.getCy(), beans1.getRadius(), paint); canvas.drawCircle(beans2.getCx(), beans2.getCy(), beans2.getRadius(), paint); canvas.drawCircle(beans3.getCx(), beans3.getCy(), beans3.getRadius(), paint); //讓球往左邊掉落 canvas.drawCircle(-beans4.getCx()+mWidth, beans4.getCy(), beans4.getRadius(), paint); canvas.drawCircle(-beans5.getCx()+mWidth, beans5.getCy(), beans5.getRadius(), paint); canvas.drawCircle(-beans6.getCx()+mWidth, beans6.getCy(), beans6.getRadius(), paint); } public void startAnim() { if (mWidth == 0) return; beans1.setState(0); beans1.setOff(0); beans1.setRand(RandomUtil.random(20));//隨機生成拋出的速度值 beans2.setState(0); beans2.setOff(0); beans2.setRand(RandomUtil.random(20)); beans3.setState(0); beans3.setOff(0); beans3.setRand(RandomUtil.random(20)); beans4.setState(0); beans4.setOff(0); beans4.setRand(RandomUtil.random(20)); beans5.setState(0); beans5.setOff(0); beans5.setRand(RandomUtil.random(20)); beans6.setState(0); beans6.setOff(0); beans6.setRand(RandomUtil.random(20)); va = ValueAnimator.ofFloat(top, mHeight - top); va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float val = (float)animation.getAnimatedValue(); beans1.setCy(val); beans1.move(mWidth);//先移動座標,其實是改變了off偏移量的值 beans1.setCx(mWidth / 2 + beans1.getOff());//刷新X軸座標 beans2.setCy(val); beans2.move(mWidth); beans2.setCx(mWidth / 2 + beans2.getOff()); beans3.setCy(val); beans3.move(mWidth); beans3.setCx(mWidth / 2 + beans3.getOff()); beans4.setCy(val); beans4.move(mWidth); beans4.setCx(mWidth / 2 + beans4.getOff()); beans5.setCy(val); beans5.move(mWidth); beans5.setCx(mWidth / 2 + beans5.getOff()); beans6.setCy(val); beans6.move(mWidth); beans6.setCx(mWidth / 2 + beans6.getOff()); invalidate(); } }); va.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { //防止中止後球體由於半徑的不同而降落到地面的水平不同,統一水平線 beans1.setCy(mHeight - beans1.getRadius()); beans2.setCy(mHeight - beans2.getRadius()); beans3.setCy(mHeight - beans3.getRadius()); beans4.setCy(mHeight - beans4.getRadius()); beans5.setCy(mHeight - beans5.getRadius()); beans6.setCy(mHeight - beans6.getRadius()); invalidate(); } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); va.setInterpolator(new BounceInterpolator());//重力差值器 va.setDuration(3000); va.setRepeatMode(ValueAnimator.RESTART); va.start(); } public void stopAnim() { va.cancel(); va = null; } }
這裏主要把邏輯封裝到單獨的對象裏面去了,因此view類看起來很清爽算法
下面是豆子類canvas
package com.fragmentapp.view.beans; import android.util.Log; /** * Created by liuzhen on 2018/1/18. */ public class Beans { public Beans(){ } public Beans(int radius){ this.radius = radius; } /**X座標*/ private float cx; /**Y座標*/ private float cy; /**偏移量*/ private float off; /**隨機生成的速度值*/ private float rand; /**是否碰到邊緣*/ private int state; /**圓球的大小*/ private float radius; /**移動 X 座標,而且碰到邊界後回彈*/ public void move(int width){ if (cx < 0 || state == 1) {//碰到左邊的邊緣 state = 1; off += rand; } else if (cx >= width || state == 2) {//碰到右邊的邊緣 state = 2; off -= rand; }else if(state == 0) { state = 0; off += rand; } // Log.e("tag","-- cx "+(int)cx + " width "+width + " state "+state); } public float getCx() { return cx; } public void setCx(float cx) { this.cx = cx; } public float getOff() { return off; } public void setOff(float off) { this.off = off; } public float getRand() { return rand; } public void setRand(float rand) { this.rand = rand; } public int getState() { return state; } public void setState(int state) { this.state = state; } public float getCy() { return cy; } public void setCy(float cy) { this.cy = cy; } public float getRadius() { return radius; } public void setRadius(float radius) { this.radius = radius; } }
主要邏輯集中在move方法中app
默認是正常拋出,而後碰到邊緣後改變狀態往回彈dom
使用上只關注兩個方法就好了ide
這裏是把控件放在了一個dialog裏面,這個看我的喜歡,顯然dialog不是很適合,或者能夠加到下拉庫的頭部上去,效果應該不錯post
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/shape_dialog_bg" android:padding="@dimen/d20.0" android:orientation="vertical" android:id="@+id/root"> <com.fragmentapp.view.beans.BeansView android:id="@+id/beans" android:layout_width="@dimen/d350.0" android:layout_height="@dimen/d300.0" android:layout_gravity="center_horizontal" /> <!--<View--> <!--android:layout_width="match_parent"--> <!--android:layout_height="@dimen/d1.0"--> <!--android:background="@color/white"/>--> <TextView android:id="@+id/tv_val" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="@dimen/d20.0" android:text="加載中..." android:textColor="@color/color_cccccc" android:textSize="@dimen/d43.0" /> </LinearLayout>
到這裏基本完成了,不過這樣的效果繪製顯然不是很好看,並且很low,因此要優化一下繪製的圖形,讓它看起來更高大上一些,最優先須要改的確定是圓球了,各類3D形狀,很好看,不過無法繪製,只能網上找個圖片直接drawbitmap了,大多的華麗都是跟圖片搭配的
然而就是代碼了,代碼看起來也有點low,也須要優化一下
1:代碼優化
之前的是固定對象,而後繪製,確定須要稍微動態一點了
2:圓球繪製
之前的是直接繪製圓,很差看,從網上下載一個圓形 icon,代替圓,看起來更立體一點
這裏的作法是把圓形對象也放進實體類裏面去,方便統一獲取,而後建立一個統一管理實體類的集合
先獲取到咱們的icon,隨機產生圓球的大小,添加進集合
接下來全部的固定的地方都換成for循環來代替
是否是方便多了,看起來簡潔多了,能夠對比兩邊的代碼,你會發現,哎呦,不錯哦
package com.fragmentapp.view.beans; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.drawable.BitmapDrawable; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.BounceInterpolator; import com.fragmentapp.R; import com.fragmentapp.helper.RandomUtil; import java.util.ArrayList; import java.util.List; /** * Created by liuzhen on 2017/1/17. */ public class BeansView extends View { private Paint paint; private int mWidth; private int mHeight; private int top; private ValueAnimator va; private List<Beans> beans = null; public BeansView(Context context) { this(context, null); } public BeansView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BeansView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { Log.e("tag","init"); setWillNotDraw(false); paint = new Paint(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.FILL); paint.setColor(getResources().getColor(R.color.color_ff9c19)); beans = new ArrayList<>(); Bitmap bitmap = ((BitmapDrawable)(getResources().getDrawable(R.mipmap.ball))).getBitmap(); //隨機生成球體的大小、 for(int i = 0;i < 6; i ++){ int radius = RandomUtil.random(15,30); final Beans b = new Beans(radius); b.setDirection(i % 2 == 0 ? Beans.Left : Beans.Right); b.bitmap = Bitmap.createScaledBitmap(bitmap,radius,radius,true); beans.add(b); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { mWidth = getWidth(); mHeight = getHeight(); this.top = top; startAnim(); } } @Override protected void onDraw(Canvas canvas) { for (Beans b : beans) { if (b.bitmap != null) { if (b.getDirection() == Beans.Left) { canvas.drawBitmap(b.bitmap, b.getCx(), b.getCy(), null); } else { canvas.drawBitmap(b.bitmap, -b.getCx() + mWidth, b.getCy(), null); } } } } public void startAnim() { if (mWidth == 0 || beans.size() == 0) return; for (Beans b : beans) { b.setState(0); b.setOff(0); b.setRand(RandomUtil.random(20));//隨機生成拋出的速度值 } va = ValueAnimator.ofFloat(top, mHeight); va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float val = (float)animation.getAnimatedValue(); for (Beans b : beans) { b.setCy(val - b.getRadius()); b.move(mWidth);//先移動座標,其實是改變了off 偏移量的值 b.setCx(mWidth / 2 + b.getOff());//刷新X軸座標 } postInvalidate(); } }); va.setInterpolator(new BounceInterpolator());//重力差值器 va.setDuration(3500); va.setRepeatMode(ValueAnimator.RESTART); va.start(); } public void stopAnim() { va.cancel(); va = null; } }
package com.fragmentapp.view.beans; import android.graphics.Bitmap; import android.util.Log; /** * Created by liuzhen on 2018/1/18. */ public class Beans { public Beans(){ } public Beans(int radius){ this.radius = radius; } public static final int Left = 0; public static final int Right = 1; private int direction; /**X座標*/ private float cx; /**Y座標*/ private float cy; /**偏移量*/ private float off; /**隨機生成的速度值*/ private float rand; /**是否碰到邊緣*/ private int state; /**圓球的大小*/ private float radius; public Bitmap bitmap; /**移動 X 座標,而且碰到邊界後回彈*/ public void move(int width){ if (cx < 0 || state == 1) {//碰到左邊的邊緣 state = 1; off += rand; } else if (cx >= width || state == 2) {//碰到右邊的邊緣 state = 2; off -= rand; }else if(state == 0) { state = 0; off += rand; } // Log.e("tag","-- cx "+(int)cx + " width "+width + " state "+state); } public float getCx() { return cx; } public void setCx(float cx) { this.cx = cx; } public float getOff() { return off; } public void setOff(float off) { this.off = off; } public float getRand() { return rand; } public void setRand(float rand) { this.rand = rand; } public int getState() { return state; } public void setState(int state) { this.state = state; } public float getCy() { return cy; } public void setCy(float cy) { this.cy = cy; } public float getRadius() { return radius; } public void setRadius(float radius) { this.radius = radius; } public int getDirection() { return direction; } public void setDirection(int direction) { this.direction = direction; } }
接下來繼續美化,能夠在下面添加一個回彈的跳板樣式,看起來效果更好,也一樣是用二級貝塞爾來實現,先繪製出接觸面
一條直線,而後物體挑落到底下的時候在觸發接觸面的控制點,達到回彈效果,可是怎麼知道物體落到了地面嗎,差值器我沒找到什麼好的辦法去解決,因此本身先想了一個方法去判斷到底了,可是感受不太好,不過目前本人尚未更好的方法去實現
就是經過本身自定義差值器,而後在差值器裏面添加回調判斷,代碼以下
package com.fragmentapp.view; import android.util.Log; import android.view.animation.BounceInterpolator; import android.view.animation.Interpolator; /** * Created by liuzhen on 2018/2/2. */ public class MyBounceInterpolator implements Interpolator { private CallBack callBack; private boolean t_3 = false,t_7 = false,t_9 = false,t_1 = false; public MyBounceInterpolator(CallBack callBack){ this.callBack = callBack; } private float bounce(float t) { return t * t * 8.0f; } @Override public float getInterpolation(float t) { // Log.e("tag",""+t); t *= 1.1226f; if (t < 0.3535f) { // Log.e("tag","----1");0.1 0.2 0.3 if (t_3 == false){ t_3 = true; // callBack.toLast(); } return bounce(t); } else if (t < 0.7408f) { // Log.e("tag","----2");4=0.6 5=0.8 6=0.8 if (t_7 == false){ t_7 = true; callBack.toLast(); } return bounce(t - 0.54719f) + 0.7f; } else if (t < 0.9644f) { // Log.e("tag","----3");7=0.8 8=0.9 9=1 if (t_9 == false){ t_9 = true; callBack.toLast(); } return bounce(t - 0.8526f) + 0.9f; } else { // Log.e("tag","----4"); if (t_1 == false){ t_1 = true; callBack.toLast(); } return bounce(t - 1.0435f) + 0.95f; } } public interface CallBack{ void toLast(); } }
這樣就是說在物體開始回彈的時候回調,而且只有一次,不過看效果後發現有點偏差,就是前面的幾回回彈物體並無在最底部就回彈了,這個想一想後發現沒有什麼合適的方法解決,看看是否是能夠改變它本來的算法去控制,後來在判斷的地方把數值提升了一點,
發現果真有效果,好了,由於每一個回彈的數值都是通過那裏的
到這裏告一段落了,不過不知道有沒有發現有點問題,由於個人背景設置的是白色的,並且吃食的那段動畫其實也是從新繪製了一層白色,因此看不出來,可是若是背景沒有設置或者不是白色,那麼就會出現問題了,這顯然也不是咱們想要的
因而仍是得在次去優化,這裏想來想去也只能在建立一個畫板去繪製了,分兩塊,就相似於橡皮擦同樣的效果,因此得小小的修改一下
而後把吃食物的那段的繪製移到新的畫板中,這樣就能夠達到吃的效果了
protected void onDraw(Canvas canvas) { if (!isDraw) return; //繪製大球 path.reset(); path.moveTo(startPoint.x + faceRadius/2,startPoint.y); path.cubicTo(movePoint1.x,movePoint1.y + faceRadius/2,movePoint2.x,movePoint2.y + faceRadius/2,endPoint.x - faceRadius/2,endPoint.y); canvas.drawPath(path, facePaint); //繪製小球,須要在最後面繪製 canvas.drawArc(rectF, angle, 360 - angle * 2, true, facePaint); if (mBitmap != null) { canvas.drawBitmap(mBitmap, 0, 0, defPaint); } } private void draw(){ //繪製「食物」 foodPath.reset(); foodPath.moveTo(startPoint.x,startPoint.y); foodPath.cubicTo(movePoint1.x,movePoint1.y,movePoint2.x,movePoint2.y,endPoint.x,endPoint.y); mCanvas.drawPath(foodPath, effectPaint); //吃掉「食物」 for (PointF f : clears) { RectF rectF = new RectF(f.x-foodRadius*2,f.y-foodRadius*2,f.x+foodRadius*2,f.y+foodRadius*2); mCanvas.drawOval(rectF,clearPaint); } postInvalidate(); }
在次運行,能夠看到,背景已經都移除,在無背景的狀態下正常顯示動畫
下面是下載地址,謝謝收藏 ^_^