Android原生繪圖之讓你瞭解View的運動
一、前言
1.我一直想寫一篇關於運動的文章,現在總算 千呼萬喚始出來
了。
2.本篇是一個長篇,各位看官自備水果、飲料、花生米,相信會給你會吃的很開心。
3.本專案原始碼見文尾 捷文規範
第一條
先看一下幾個效果:(留圖鎮樓)
1.---瘋狂的分裂

效果1
2.---粉身碎骨

粉身碎骨.gif
3.---畫筆疊合XOR

畫筆疊合XOR.gif
1.前置知識論述:
1).何為運動:視覺上看是一個物體在不同的時間軸上表現出不同的物理位置
2). 位移 = 初位移 + 速度 * 時間
小學生的知識不多說
3). 速度 = 初速度 + 加速度 * 時間
初中生的知識不多說
4).時間、位移、速度、加速度構成了現代科學的運動體系
2.使用View對運動學的模擬
1.時間:ValueAnimator的恆定無限執行----模擬時間流,每次重新整理間隔,記為: 1U
2.位移:物體在螢幕畫素位置----模擬世界,每個畫素距離記為: 1px
3.速度(單位px/U)、加速度(px/U^2):自定義
注意:無論什麼語言,只要能夠模擬時間與位移,本篇的思想都可以適用,只是語法不同罷了
3.測試的物體,封裝類:
public class Ball implements Cloneable { public float aX;//加速度 public float aY;//加速度Y public float vX;//速度X public float vY;//速度Y public float x;//點位X public float y;//點位Y public int color;//顏色 public float r;//半徑 public Ball clone() { Ball clone = null; try { clone = (Ball) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return clone; } }
第一節:物體的勻速直線運動:
1.搭建測試View
開始是一個位於0,0點、x方向速度10、y方向速度0的小球
public class RunBall extends View { private ValueAnimator mAnimator;//時間流 private Ball mBall;//小球物件 private Paint mPaint;//主畫筆 private Point mCoo;//座標系 private float defaultR = 20;//預設小球半徑 private int defaultColor = Color.BLUE;//預設小球顏色 private float defaultVX = 10;//預設小球x方向速度 private float defaultVY = 0;//預設小球y方向速度 public RunBall(Context context) { this(context, null); } public RunBall(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { mCoo = new Point(500, 500); //初始化小球 mBall = new Ball(); mBall.color = defaultColor; mBall.r = defaultR; mBall.vX = defaultVX; mBall.vY = defaultVY; mBall.a = defaultA; //初始畫筆 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); //初始化時間流ValueAnimator mAnimator = ValueAnimator.ofFloat(0, 1); mAnimator.setRepeatCount(-1); mAnimator.setDuration(1000); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { updateBall();//更新小球資訊 invalidate(); } }); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.translate(mCoo.x, mCoo.y); drawBall(canvas, mBall); canvas.restore(); } /** * 繪製小球 * @param canvas * @param ball */ private void drawBall(Canvas canvas, Ball ball) { mPaint.setColor(ball.color); canvas.drawCircle(ball.x, ball.y, ball.r, mPaint); } /** * 更新小球 */ private void updateBall() { //TODO --運動資料都由此函式變換 } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mAnimator.start();//開啟時間流 break; case MotionEvent.ACTION_UP: mAnimator.pause();//暫停時間流 break; } return true; } }
2.水平運動:
注:開錄屏+模擬器比較卡,加上變成gif,看上去一些卡,真機執行很流暢

水平移動.gif
RunBall#updateBall:只需加一句(也就是 位移 = 初位移 + 速度 * 時間
,這裡時間是1U)
private void updateBall() { mBall.x += mBall.vX; }
3.反彈效果:(x大於400反彈):
只需反彈時將vX速度取反就行了,和現實一致

反彈.gif
private void updateBall() { mBall.x += mBall.vX; if (mBall.x > 400) { mBall.vX = -mBall.vX; } }
4.反彈變色,無限迴圈:

反彈變色.gif
/** * 更新小球 */ private void updateBall() { mBall.x += mBall.vX; if (mBall.x > 400) { mBall.vX = -mBall.vX; mBall.color = ColUtils.randomRGB();//更改顏色 } if (mBall.x < -400) { mBall.vX = -mBall.vX; mBall.color = ColUtils.randomRGB();//更改顏色 } }
5.小球的箱式彈跳:
X軸的平移和Y軸的平移基本一致,就不說了,看一下x,y都改變,即速度斜向的情況

速度的合成.png

碰撞分析png

箱子彈跳.gif
先把邊界值定義一下:以便複用
private float defaultVY = 5;//預設小球y方向速度 private float mMaxX = 400;//X最大值 private float mMinX = -400;//X最小值 private float mMaxY = 300;//Y最大值 private float mMinY = -100;//Y最小值
現在updateBall方法裡新增對Y方向的修改:
/** * 更新小球 */ private void updateBall() { mBall.x += mBall.vX; mBall.y += mBall.vY; if (mBall.x > mMaxX) { mBall.vX = -mBall.vX; mBall.color = ColUtils.randomRGB();//更改顏色 } if (mBall.x < mMinX) { mBall.vX = -mBall.vX; mBall.color = ColUtils.randomRGB();//更改顏色 } if (mBall.y > mMaxY) { mBall.vY = -mBall.vY; mBall.color = ColUtils.randomRGB();//更改顏色 } if (mBall.y < mMinY) { mBall.vY = -mBall.vY; mBall.color = ColUtils.randomRGB();//更改顏色 } }
沒錯,就是這麼簡單,勻速運動做成這樣就差不多了,下面看變速運動
二、變速運動
1.自由落體
首先模擬我們最熟悉的自由落體,加速度aY = 0.98f,x,y初速度為0,初始y高度設為-400

自由落體.gif
private float defaultR = 20;//預設小球半徑 private int defaultColor = Color.BLUE;//預設小球顏色 private float defaultVX = 0;//預設小球x方向速度 private float defaultVY = 0;//預設小球y方向速度 private float defaultAY = 0.98f;//預設小球加速度 private float mMaxY = 0;//Y最大值
updateBall里根據豎直加速度aY動態改變vY即可,這裡反彈之後依然會遵循物理定律
注意:你可以在反彈是乘個係數當做損耗值,更能模擬現實
private void updateBall() { mBall.x += mBall.vX; mBall.y += mBall.vY; mBall.vY += mBall.aY; if (mBall.y > mMaxY - mBall.r) { mBall.vY = -mBall.vY; mBall.color = ColUtils.randomRGB();//更改顏色 } }
2.平拋運動+模擬碰撞損耗
平拋也就是有一個初始的x方向速度的自由落體

平拋運動+模擬碰撞損耗.gif
修改初始水平速度和碰撞損耗係數
private float defaultVX = 15;//預設小球x方向速度 private float defaultF = 0.9f;//碰撞損耗
/** * 更新小球 */ private void updateBall() { mBall.x += mBall.vX; mBall.y += mBall.vY; mBall.vY += mBall.aY; if (mBall.x > mMaxX) { mBall.x = mMaxX; mBall.vX = -mBall.vX * defaultF; mBall.color = ColUtils.randomRGB();//更改顏色 } if (mBall.x < mMinX) { mBall.x = mMinX; mBall.vX = -mBall.vX * defaultF; mBall.color = ColUtils.randomRGB();//更改顏色 } if (mBall.y > mMaxY) { mBall.y = mMaxY; mBall.vY = -mBall.vY * defaultF; mBall.color = ColUtils.randomRGB();//更改顏色 } if (mBall.y < mMinY) { mBall.y = mMinY; mBall.vY = -mBall.vY * defaultF; mBall.color = ColUtils.randomRGB();//更改顏色 } }
3.斜拋運動:具有初始水平和垂直速度

斜拋運動.gif
修改一下初始垂直速度即可
private float defaultVY = -12;//預設小球y方向速度
5.圓周運動:
可惜我無法用運動學模擬,需要合速度和合加速度保持不垂直,並且合加速度不變。看以後能不能實現
不過退而求其次,用畫布的旋轉可以讓小球做圓周運動
mark:ValueAnimator預設Interpolator竟然不是線性的,怪不得看著怪怪的

圓周運動.gif
//初始化時間流ValueAnimator mAnimator = ValueAnimator.ofFloat(0, 1); mAnimator.setRepeatCount(-1); mAnimator.setDuration(4000); mAnimator.setInterpolator(new LinearInterpolator()); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mDeg = (float) animation.getAnimatedValue() * 360; updateBall();//更新小球位置 invalidate(); } }); @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.translate(mCoo.x, mCoo.y); canvas.rotate(mDeg+90); canvas.drawLine(0, 0, mBall.x, mBall.y, mPaint); drawBall(canvas, mBall); canvas.restore(); }
6.鐘擺運動:
也是非運動學的鐘擺,通過旋轉畫布模擬:

鐘擺.gif
//初始化時間流ValueAnimator mAnimator = ValueAnimator.ofFloat(0, 1); mAnimator.setRepeatCount(-1); mAnimator.setDuration(2000); mAnimator.setRepeatMode(ValueAnimator.REVERSE); mAnimator.setInterpolator(new LinearInterpolator()); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mDeg = (float) animation.getAnimatedValue() * 360*0.5f; updateBall();//更新小球位置 invalidate(); } });
7.估值器實現指定曲線方程運動:(此處sin為例)

定曲線運動(sin).gif
/** * 作者:張風捷特烈<br/> * 時間:2018/11/16 0016:7:42<br/> * 郵箱:[email protected]<br/> * 說明:sin型估值器 */ public class SinEvaluator implements TypeEvaluator { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { //初始點 Ball startPos = (Ball) startValue; //結束點 Ball endPos = (Ball) endValue; //計算每次更新時的x座標 Ball clone = startPos.clone(); clone.x = startPos.x + fraction * (endPos.x - startPos.x); //將y座標進行聯動 clone.y = (float) (Math.sin(clone.x * Math.PI / 180) * 100); //返回更新後的點 return clone; } }
//初始化時間流ValueAnimator Ball startBall = new Ball();//小球的起點 startBall.color = Color.RED; startBall.r = 20; Ball endBall = startBall.clone();//小球的終點 endBall.x = 1800; endBall.y = 300; //使用ofObject,傳入估值器 mAnimator = ValueAnimator.ofObject(new SinEvaluator(), startBall, endBall); mAnimator.setRepeatCount(-1); mAnimator.setDuration(8000); mAnimator.setRepeatMode(ValueAnimator.REVERSE); mAnimator.setInterpolator(new LinearInterpolator()); mAnimator.addUpdateListener(animation -> { mBall = (Ball) animation.getAnimatedValue();//通過估值器計算,更新小球 invalidate(); });
三、效果實現
1.碰撞分裂的效果實現

粉身碎骨.gif
思路:由繪製一個小球到繪製一個小球集合,每當碰撞時在集合裡新增一個反向的小球
並將兩個小球半徑都減半即可,還是好理解的。
/** * 作者:張風捷特烈<br/> * 時間:2018/11/15 0015:8:10<br/> * 郵箱:[email protected]<br/> * 說明:小球運動測試 */ public class RunBall extends View { private ValueAnimator mAnimator;//時間流 private List<Ball> mBalls;//小球物件 private Paint mPaint;//主畫筆 private Paint mHelpPaint;//輔助線畫筆 private Point mCoo;//座標系 private float defaultR = 80;//預設小球半徑 private int defaultColor = Color.BLUE;//預設小球顏色 private float defaultVX = 10;//預設小球x方向速度 private float defaultF = 0.95f;//碰撞損耗 private float defaultVY = 0;//預設小球y方向速度 private float defaultAY = 0.5f;//預設小球加速度 private float mMaxX = 600;//X最大值 private float mMinX = -200;//X最小值 private float mMaxY = 300;//Y最大值 private float mMinY = -100;//Y最小值 public RunBall(Context context) { this(context, null); } public RunBall(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { mCoo = new Point(500, 500); //初始畫筆 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBalls = new ArrayList<>(); Ball ball = initBall(); mBalls.add(ball); mHelpPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mHelpPaint.setColor(Color.BLACK); mHelpPaint.setStyle(Paint.Style.FILL); mHelpPaint.setStrokeWidth(3); //初始化時間流ValueAnimator mAnimator = ValueAnimator.ofFloat(0, 1); mAnimator.setRepeatCount(-1); mAnimator.setDuration(2000); mAnimator.setRepeatMode(ValueAnimator.REVERSE); mAnimator.setInterpolator(new LinearInterpolator()); mAnimator.addUpdateListener(animation -> { updateBall();//更新小球位置 invalidate(); }); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.translate(mCoo.x, mCoo.y); drawBalls(canvas, mBalls); canvas.restore(); } /** * 繪製小球集合 * * @param canvas * @param balls小球集合 */ private void drawBalls(Canvas canvas, List<Ball> balls) { for (Ball ball : balls) { mPaint.setColor(ball.color); canvas.drawCircle(ball.x, ball.y, ball.r, mPaint); } } /** * 更新小球 */ private void updateBall() { for (int i = 0; i < mBalls.size(); i++) { Ball ball = mBalls.get(i); if (ball.r < 1) {//幫半徑小於1就移除 mBalls.remove(i); } ball.x += ball.vX; ball.y += ball.vY; ball.vY += ball.aY; ball.vX += ball.aX; if (ball.x > mMaxX) { Ball newBall = ball.clone();//新建一個ball同等資訊的球 newBall.r = newBall.r / 2; newBall.vX = -newBall.vX; newBall.vY = -newBall.vY; mBalls.add(newBall); ball.x = mMaxX; ball.vX = -ball.vX * defaultF; ball.color = ColUtils.randomRGB();//更改顏色 ball.r = ball.r / 2; } if (ball.x < mMinX) { Ball newBall = ball.clone(); newBall.r = newBall.r / 2; newBall.vX = -newBall.vX; newBall.vY = -newBall.vY; mBalls.add(newBall); ball.x = mMinX; ball.vX = -ball.vX * defaultF; ball.color = ColUtils.randomRGB(); ball.r = ball.r / 2; } if (ball.y > mMaxY) { ball.y = mMaxY; ball.vY = -ball.vY * defaultF; ball.color = ColUtils.randomRGB(); } if (ball.y < mMinY) { ball.y = mMinY; ball.vY = -ball.vY * defaultF; ball.color = ColUtils.randomRGB(); } } } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mAnimator.start(); break; case MotionEvent.ACTION_UP: //mAnimator.pause(); break; } return true; } private Ball initBall() { Ball mBall = new Ball(); mBall.color = defaultColor; mBall.r = defaultR; mBall.vX = defaultVX; mBall.vY = defaultVY; mBall.aY = defaultAY; mBall.x = 0; mBall.y = 0; return mBall; } }
2.畫筆疊合XOR測試:

畫筆疊合XOR.gif
//初始化時準備一個小球陣列---引數值隨機一些 private void initBalls() { for (int i = 0; i < 28; i++) { Ball mBall = new Ball(); mBall.color = ColUtils.randomRGB(); mBall.r = rangeInt(80, 120); mBall.vX = (float) (Math.pow(-1, Math.ceil(Math.random() * 1000)) * 20 * Math.random()); mBall.vY = rangeInt(-15, 35); mBall.aY = 0.98f; mBall.x = 0; mBall.y = 0; mBalls.add(mBall); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //建立一個圖層,在圖層上演示圖形混合後的效果 int sc = 0; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { sc = canvas.saveLayer(new RectF(0, 0, 2500, 2500), null); } mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));//設定對源的疊合模式 canvas.translate(mCoo.x, mCoo.y); drawBalls(canvas, mBalls); canvas.restoreToCount(sc); }
3.兩個小球的碰撞反彈

兩個小球的碰撞反彈.gif
//準備兩個球 private void initBalls() { for (int i = 0; i < 2; i++) { Ball mBall = new Ball(); mBall.color = Color.RED; mBall.r = 80; mBall.vX = (float) (Math.pow(-1, Math.ceil(Math.random() * 1000)) * 20 * Math.random()); mBall.vY = rangeInt(-15, 35); mBall.aY = 0.98f; mBalls.add(mBall); } mBalls.get(1).x = 300; mBalls.get(1).y = 300; mBalls.get(1).color = Color.BLUE; } /** * 兩點間距離函式 */ public static float disPos2d(float x1, float y1, float x2, float y2) { return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } /** * 更新小球 */ private void updateBall() { Ball redBall = mBalls.get(0); Ball blueBall = mBalls.get(1); //校驗兩個小球的距離 if (disPos2d(redBall.x, redBall.y, blueBall.x, blueBall.y) < 80 * 2) { redBall.vX = -redBall.vX; redBall.vY = -redBall.vY; blueBall.vX = -blueBall.vX; blueBall.vY = -blueBall.vY; } for (int i = 0; i < mBalls.size(); i++) { Ball ball = mBalls.get(i); ball.x += ball.vX; ball.y += ball.vY; ball.vY += ball.aY; ball.vX += ball.aX; if (ball.x > mMaxX) { ball.x = mMaxX; ball.vX = -ball.vX * defaultF; } if (ball.x < mMinX) { ball.x = mMinX; ball.vX = -ball.vX * defaultF; } if (ball.y > mMaxY) { ball.y = mMaxY; ball.vY = -ball.vY * defaultF; } if (ball.y < mMinY) { ball.y = mMinY; ball.vY = -ball.vY * defaultF; } } }
好了,就到這裡,關於View的運動還有很多可變化的東西,有興趣的可以去探索一些
後記:捷文規範
1.本文成長記錄及勘誤表
ofollow,noindex">專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-11-15 | Android原生繪圖之讓你瞭解View的運動 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的CSDN | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援