BouncingBallView 碰撞的小球
2017-09-06 09:04
351 查看
加载动画BouncingBallView碰撞的小球
前言
为了熟悉自定义View的操作流程,以及练习使用最近学到的Builder模式,再加上前几天脑袋里突发奇想的一个动画效果,所以就有了这篇文章。废话不多说了,先上效果图吧。效果图
真机效果图
属性设置
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="BouncingBallView"> <attr name="ballCount" format="integer" /><!--小球的个数--> <attr name="ballColor" format="color" /><!--小球的颜色--> <attr name="ballRadius" format="dimension" /><!--小球的半径--> <attr name="cycleTime" format="integer" /><!--一个周期持续的时常--> </declare-styleable> </resources> ``` java private int mWidth = 300;//view的宽 private int mHeight = 300;//view的高 private int largeRadius;//(mWidth / 2 - ballRadius) private float wabbyBallX;//摇动中的小球x坐标 private float wabbyBallY;//摇动中的小球y坐标 private float wabbyBallStartAngle;//摇动的小球开始摇动的角度 private float wabbyBallAngle;//摇动中小球的角度 private float runningBallX;//转动小球的x坐标 private float runningBallY;//转动小球的y坐标 private float runningBallStartAngle;//转动小球开始转动的角度 private float runningBallAngle;//转动中小球的角度 private float perAngle;//将圆均分的角度 private float restBallStartAngle = 60;//剩下的五个小球的开始分布的角度 private float phaseAngle;//当两个小球碰撞时他们之间相差的角度 private Paint mPaint; private STATUS current 4000 Status = STATUS.FIRSTCYCLE; private BouncingBallConfig config;//保存小球配置的类 private enum STATUS {//运动状态的集合 FIRSTCYCLE, RESTCYCLE } <div class="se-preview-section-delimiter"></div>
在xml中设置
<view.BouncingBallView android:id="@+id/view" android:layout_width="200dp" android:layout_height="200dp" app:ballRadius="10dp" app:ballColor="@android:color/holo_blue_light" app:ballCount="6" app:cycleTime="1000"/> <div class="se-preview-section-delimiter"></div>
因为是小球的运动,那么自然就需要设置小球的个数,颜色,半径了,值的注意的是,这里
ballCount的值设置为6,而效果途中有7个小球,至于为什么呢,是因为我将这几个小球分成了三部分。一个在摇摆中,一个在转动中,剩下的就是呆在他们各自的位置不动的了。正因为有一个在运动中以及一个在摇摆中,所以将整个圆均分的就少一个了,而ballCount的值正是将圆均分的个数。至于上面摇动中的小球角度等变量我将在下面通过几何图来介绍,而对于BouncingViewConfig这个类来说,不知道大家有没有注意到前言中提到的Builder模式,这是为了练习使用这个模式,所以我建了这个类,下面来看看这个类
public class BouncingBallConfig { public int ballCount; public int ballColor; public int ballRadius; public int cycleTime; public static class Builder { private int ballCount = 6;//默认6个 private int ballColor = Color.BLUE;//默认蓝色 private int ballRadius = 30;//默认半径10 private int cycleTime = 1000;//默认一个周期时常3秒 public Builder setBallCount(int ballCount) { this.ballCount = ballCount; return this; } public Builder setballColor(int color) { this.ballColor = color; return this; } public Builder setBallRadius(int ballRadius) { this.ballRadius = ballRadius; return this; } public Builder setCycleTime(int cycleTime) { this.cycleTime = cycleTime; return this; } public void applyConfig(BouncingBallConfig config) { config.ballColor = this.ballColor; config.ballCount = this.ballCount; config.ballRadius = this.ballRadius; config.cycleTime = this.cycleTime; } public BouncingBallConfig create() { BouncingBallConfig config = new BouncingBallConfig(); applyConfig(config); return config; } } } <div class="se-preview-section-delimiter"></div>
代码非常好懂,BouncingViewConfig类保存了小球的一些属性,至于什么是Builder模式,以及Builder模式是干什么的,这里就不展开了。接下来就是几何分析了。
### 几何分析
几何草图
先来分析一下这几个小球的几何分布,perAngle也就是小球将圆均分的角度,largeRadius为mWidth / 2 - ballRadius,看图应该很好懂,至于phaseAngle,它是两个小球在刚好相碰时的角度差,下面来计算一下。
这是两个小球碰撞的时候,两个小球相切,因为小球属性完全相同,所以两小球的交点与大圆圆心的连线是垂直于两小球圆心的连线的,简单的几何证明问题就不细说了,所以∠a = arcsin(ballRadius / largeRadius),
而角phaseAngle = 2 * a.好了,现在应该看看构造函数里的内容了
### 构造函数
public BouncingBallView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); BouncingBallConfig config = new BouncingBallConfig(); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.BouncingBallView, defStyle, 0); int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.BouncingBallView_ballColor: config.ballColor = a.getColor(attr, Color.BLUE); break; case R.styleable.BouncingBallView_ballCount: config.ballCount = a.getInteger(attr, 6); break; case R.styleable.BouncingBallView_ballRadius: config.ballRadius = a.getDimensionPixelSize(attr, 10); break; case R.styleable.BouncingBallView_cycleTime: config.cycleTime = a.getInteger(attr, 1000); break; default: break; } } init(config); } <div class="se-preview-section-delimiter"></div>
这个没什么好说的,就是将xml中设置的属性获取到,然后保存在一个BouncingViewConfig引用中,看一下init(config);
public void init(BouncingBallConfig config) { this.config = config; runningBallAngle = 0; wabbyBallAngle = 0; mPaint = new Paint(); } <div class="se-preview-section-delimiter"></div>
初始化config的内容以及初始化运动中小球的角度和摇摆小球的角度,下面讲讲这个角度的具体含义
对于小球2来说,角a1就是它的角度,对于小球4来说角a2就是它的角度,不过此时a2的值是负的,对于y’轴来说,它的角度就是0度,总的来说就是以y’轴为基准。说完角度,下面就说计算坐标的问题,注意此时的原点是整个大球的中心,所以得在largeRadius * sina1后再加上mWidth / 2才是小球2的x坐标,y坐标就是largeRadius * cosa1 + mWidth / 2。终于讲完了几何。
onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.EXACTLY) { mWidth = widthSpecSize - getPaddingRight() - getPaddingLeft(); } if (heightSpecMode == MeasureSpec.EXACTLY) { mHeight = heightSpecSize - getPaddingBottom() - getPaddingTop(); } setMeasuredDimension(mWidth, mHeight); largeRadius = mWidth / 2 - config.ballRadius; Log.e(TAG, "alrgeRadius = " + largeRadius); perAngle = 360 / config.ballCount; Log.e(TAG, "perAngle = " + perAngle); Log.e(TAG, "config.ballRadius = " + config.ballRadius); double a = ((double) config.ballRadius) / (((double)largeRadius)); phaseAngle = (float) (2 * Math.asin(a) * 180 / PI); Log.e(TAG, "mWidth = " + mWidth); Log.e(TAG, "mHeight = " + mHeight); } <div class="se-preview-section-delimiter"></div>
onMeasure就是设置mWidth和mHeight以及计算各个角度了,计算方法前面已讲过。注意在计算phaseAngle时有个角度的转换,是因为Math.asin计算结果时弧度制,所以得转化成角度
onDraw
protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(config.ballColor); mPaint.setAntiAlias(true); drawRestBall(canvas);//画剩下为运动得小球 if (currentStatus == STATUS.FIRSTCYCLE) { wabbyBallAngle = 0; wabbyBallStartAngle = 0; } wabbyBallX = (float) Math.sin(wabbyBallAngle / 180 * PI) * largeRadius + mWidth / 2;//计算摇摆中小球得横坐标 wabbyBallY = (float) Math.cos(wabbyBallAngle / 180 * PI) * largeRadius + mHeight / 2;//纵坐标 canvas.drawCircle(wabbyBallX, wabbyBallY, config.ballRadius, mPaint); runningBallX = (float) Math.sin(runningBallAngle / 180 * PI) * largeRadius + mWidth / 2;//计算转动中小球得横坐标 runningBallY = (float) Math.cos(runningBallAngle / 180 * PI) * largeRadius + mHeight / 2;//纵坐标 canvas.drawCircle(runningBallX, runningBallY, config.ballRadius, mPaint); } <div class="se-preview-section-delimiter"></div>
在onDraw中就是根据各个小球得角度计算出所在得坐标,然后画出来。在其中有一个运动状态的判断,为什么呢?因为在整个动画中,是分成两个部分的。第一个部分运动小球重初始位置出发到它第一次与另一个小球相撞的过程,此时摇摆小球一直处于它的初始位置,是没有摇摆动画的,也就是STATUS.FIRSTCYCLE,在动画中,摇摆小球和转动小球的初始位置就是上面的小球3的位置。第二个部分就是剩下的动画了,STATUS.RESTCYCLE。下面看一看画剩下的小球方法drawRestBall(canvas)
public void dr c3da awRestBall(Canvas canvas) { for (float i = restBallStartAngle; i < perAngle * (config.ballCount - 1) + restBallStartAngle; i += perAngle) { //Log.e(TAG, "i = " + i); double x = Math.sin(i * 1.0f / 180 * PI) * largeRadius + mWidth / 2; double y = Math.cos(i * 1.0f / 180 * PI) * largeRadius + mHeight / 2; //Log.e(TAG, "Math.sin = " + Math.sin(i * 1.0f) * largeRadius); //Log.e(TAG, "Math.cos = " + Math.cos(i * 1.0f) * largeRadius); //Log.e(TAG, "x = " + x); //Log.e(TAG, "y = " + y); canvas.drawCircle((float) x, (float) y, config.ballRadius, mPaint); } } <div class="se-preview-section-delimiter"></div>
剩下的小球设定一个球为这几个中的第一个球,它的位置是在转动小球的逆时针第一个,在初始时,也就是小球2。然后这几个小球之间相差perAngle角度,一个循环就画出了剩下的小球。好了,接下来就是动画部分了
ValueAnimator
public void letUsAnimate() { //摇摆小球的动画 final ValueAnimator wabbyBallAnimator = ValueAnimator.ofFloat(0, phaseAngle); wabbyBallAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float fraction = wabbyBallAnimator.getAnimatedFraction(); wabbyBallAngle = wabbyBallStartAngle + (float) (Math.sin(fraction * 1.5 * PI) * phaseAngle); //Log.e(TAG, "wabbyBallAngle = " + wabbyBallAngle); //invalidate(); } }); wabbyBallAnimator.setDuration(config.cycleTime); //转动小球的动画 final ValueAnimator runningBallAnimator = ValueAnimator.ofFloat(0, perAngle - phaseAngle); runningBallAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float fraction = runningBallAnimator.getAnimatedFraction(); runningBallAngle = runningBallStartAngle - fraction * (perAngle - phaseAngle); //Log.e(TAG, "fraction = " + fraction); //Log.e(TAG, "runningBallAngle = " + runningBallAngle); invalidate(); } }); runningBallAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { Log.e(TAG, "Thread = " + Thread.currentThread()); } @Override public void onAnimationEnd(Animator animation) { } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { Log.e(TAG, "onAnimationRepeat: "); runningBallStartAngle -= perAngle;//转动小球开始转动角度减去perAngle //这个在效果图中很容易看出来 restBallStartAngle -= perAngle;//剩下的小球也减去perAngle if (currentStatus == STATUS.FIRSTCYCLE) { wabbyBallStartAngle = 0 - (perAngle - phaseAngle);//如果是第一种状态刚结束时, //此时摇摆小球开始摇摆位置应该在转动小球刚结束第一阶段转动的位置 } else { wabbyBallStartAngle -= perAngle;//第二阶段则每次减去perAngle } invalidate(); currentStatus = STATUS.RESTCYCLE;//第一次重复代表第一个阶段结束 wabbyBallAnimator.start(); //Log.e(TAG, "restBallStartAngle = " + restBallStartAngle); //Log.e(TAG, "runningBallStartAngle = " + runningBallStartAngle); } }); runningBallAnimator.setRepeatCount(ValueAnimator.INFINITE); runningBallAnimator.setDuration(config.cycleTime).start(); } <div class="se-preview-section-delimiter"></div>
代码都已注释,很容易看懂。需要讲的时设置摇摆小球的摇摆动画时,有一个后退,然后前进的过程,也就是0~-1~1的过程,所以我选择了sin函数,sin0~sin(3/2 * π),即wabbyBallAngle = wabbyBallStartAngle +
(float) (Math.sin(fraction * 1.5 * PI) * phaseAngle);在整个动画中,转动小球的结束,就是摇摆小球的开始,所以控制摇摆小球动画的开始我选择了在onAnimationRepeat中进行,不过这里面有一个坑,一个大坑,这个坑我目前还没有找到解决方法,在这里我希望各位前辈指点指点,那么这个坑是什么呢?不知各位有没有注意到效果图中下面的注释为真机图没有,那么我们来看看虚拟机中的图。
看,小球在碰撞时前方有个小球闪了一下,看看小球闪的位置,分析一波应该是转动小球下一次结束的位置,那么我们来给小球打个日志
果然,有一个地方是异常的,在onAnimationRepeat执行后,runningBallAngle突然变为了-107.24126,那么我们来看看runningBallAngle的赋值情况。在onAnimationUpdate中runningBallAngle = runningBallStartAngle - fraction * (perAngle - phaseAngle);也就是说它的位置应该是它的值初始值减去运动的偏移值。再看看小球闪动的时间,第一次闪动是在小球第一次碰撞的时候,也就是说应该实在onAnimationRepeat执行的时候,那就看看在onAnimationRepeat中的情况,runningBallStartAngle -= perAngle;每次重复运动小球开始运动的位置减去perAngle,在效果图中就是60度。再看看日志中异常角度,-107.24126和-47.211426之间不就是相差60度吗?这就值得思考了,问题出在哪儿了呢,难道说是在fraction还没有到达1时就调用onAnimationRepeat吗?如果是这样的话runningBallAngle在onAnimationRepeat中减去了60度,然后又在onAnimationUpdate中减去了1*(perAngle - phaseAngle),这样就解释的通了。那么我们再打印一下fraction的变化情况
果然,在fracti还没有到1时,就执行了onAnimationRepeat,那为何在真机中没有出现这种问题呢?我们再用真机试一下
看到这里,我的心是拔凉拔凉的,想来大家也知道怎么回事了,真机中并未有出现onAnimationRepeat在fraction值变化结束之前调用的情况。拿了室友的魅族来测试,也是一样。查了一些资料也没找到什么解决方法,在这里向各位前辈请教请教。不过本文也应该结束了,哎,是不是忘了什么?说好的Builder呢?那么放上MainActivity中的内容吧
public class MainActivity extends AppCompatActivity { private BouncingBallView bouncingBallView1; private BouncingBallView bouncingBallView2; private LinearLayout linearLayout; private Button button1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); linearLayout = (LinearLayout) findViewById(R.id.linear_layout); bouncingBallView1 = (BouncingBallView) findViewById(R.id.view); button1 = (Button) findViewById(R.id.button1); button1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { bouncingBallView1.letUsAnimate(); } }); BouncingBallConfig config = new BouncingBallConfig(); config = new BouncingBallConfig.Builder() .setballColor(Color.BLUE) .setBallCount(6) .setBallRadius(30) .create(); bouncingBallView2 = new BouncingBallView(this); bouncingBallView2.setLayoutParams(new LinearLayout.LayoutParams(600, 600)); bouncingBallView2.init(config); linearLayout.addView(bouncingBallView2); bouncingBallView2.letUsAnimate(); } }
可以看到,正是使用的Builder来保存BouncingBallView的配置信息,再加上效果图
到了这里,才是真的结束了。最后放上github地址
点这里
相关文章推荐
- BouncingBallView 碰撞的小球
- 小球滑动已八卦方式滑进滑出、可以在外圈滑动的BallSpinningView
- 【移动开发】Android游戏开发SurfaceView应用----手指发动小球(小球碰撞检测例子)
- 【备忘】Android模拟小球自由落体(SurfaceView)
- 用SurfaceView制作简单的android游戏 : 重力小球(2)--------制作重力感应小球
- 17、Android之SurfaceView实例自定义SurfaceView的应用——小球跟着手指移动
- 自定义view视图,小球随手指动
- Android从基础做起——自定义View(随手移动的小球)
- 实现小球随机碰撞,最好分析
- .NET GDI 模拟小球弹性碰撞(质点,动能守恒)
- 37-JavaScript-DOM-小游戏-小球碰撞后折返
- 【原创】小球碰撞动画
- C#模拟小球碰撞(图形界面)
- cocos2d-x 2.0.1 的根据例子写个小球碰撞板块反弹例子
- IOS_加速计_碰撞检测_小球位置修复_dispatch_once单例
- 【MoveBoom 】Android View的拖动、碰撞判断、销毁和销毁爆炸动画
- 小球在矩形框内45°碰撞问题
- Android View 拖拽 移动 碰撞
- Java课程设计---碰撞的小球
- 2018-3-18CCF小球碰撞问题