Android原生繪圖之讓你瞭解View的運動

張風捷特烈發表於2018-11-16

一、前言

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/>
 * 郵箱:1981462002@qq.com<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/>
 * 郵箱:1981462002@qq.com<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.本文成長記錄及勘誤表
專案原始碼 日期 備註
V0.1--github 2018-11-15 Android原生繪圖之讓你瞭解View的運動
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的CSDN 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援

相關文章