一、前言
1.我一直想写一篇关于运动的文章,现在总算
千呼万唤始出来
了。
2.本篇是一个长篇,各位看官自备水果、饮料、花生米,相信会给你会吃的很开心。
3.本项目源码见文尾捷文规范
第一条
先看一下几个效果:(留图镇楼)
1.---疯狂的分裂

2.---粉身碎骨

3.---画笔叠合XOR

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,看上去一些卡,真机运行很流畅

RunBall#updateBall:只需加一句(也就是
位移 = 初位移 + 速度 * 时间
,这里时间是1U)
private void updateBall() {
mBall.x += mBall.vX;
}
3.反弹效果:(x大于400反弹):
只需反弹时将vX速度取反就行了,和现实一致

private void updateBall() {
mBall.x += mBall.vX;
if (mBall.x > 400) {
mBall.vX = -mBall.vX;
}
}
4.反弹变色,无限循环:

/**
* 更新小球
*/
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都改变,即速度斜向的情况



先把边界值定义一下:以便复用
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

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方向速度的自由落体

修改初始水平速度和碰撞损耗系数
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.斜抛运动:具有初始水平和垂直速度

修改一下初始垂直速度即可
private float defaultVY = -12;//默认小球y方向速度
5.圆周运动:
可惜我无法用运动学模拟,需要合速度和合加速度保持不垂直,并且合加速度不变。看以后能不能实现
不过退而求其次,用画布的旋转可以让小球做圆周运动
mark:ValueAnimator默认Interpolator竟然不是线性的,怪不得看着怪怪的

//初始化时间流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.钟摆运动:
也是非运动学的钟摆,通过旋转画布模拟:

//初始化时间流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为例)

/**
* 作者:张风捷特烈<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.碰撞分裂的效果实现

思路:由绘制一个小球到绘制一个小球集合,每当碰撞时在集合里添加一个反向的小球
并将两个小球半径都减半即可,还是好理解的。
/**
* 作者:张风捷特烈<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测试:

//初始化时准备一个小球数组---参数值随机一些
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.两个小球的碰撞反弹

//准备两个球
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.更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的github | 我的简书 | 我的CSDN | 个人网站 |
3.声明
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持
网友评论