您的位置:首页 > 其它

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地址

点这里
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: