今天我们好好研究一下动画,首先可以点进来看一下 BouncingBalls 这个动画,这是一个背景颜色不停地在变化,而且一触摸屏幕就出现弹球的动画,我们今天就来看看它是实现这个效果的。首先找到 animation -> BouncingBalls.java 这就是今天的主菜了,来看看它的 onCreate() 方法都做了什么操作:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.bouncing_balls);
LinearLayout container = (LinearLayout) findViewById(R.id.container);
container.addView(new MyAnimationView(this));
}
我们发现它其实什么也没做,就是添加了给 LinearLayout 添加了一个 view,很显然所有的操作都在 MyAnimationView 这个 view 里面处理了,那就继续看 MyAnimationView 的实现了
public MyAnimationView(Context context) {
super(context);
ValueAnimator colorAnim = ObjectAnimator.ofInt(this, "backgroundColor", RED, BLUE);
colorAnim.setDuration(3000);
colorAnim.setEvaluator(new ArgbEvaluator());
colorAnim.setRepeatCount(ValueAnimator.INFINITE);
colorAnim.setRepeatMode(ValueAnimator.REVERSE);
colorAnim.start();
}
这是 MyAnimationView 的构造方法,有木有看出来点什么?这里用到了属性动画,”backgroundColor” 很清晰的告诉我们这是一个关于背景颜色的动画,那么背景变换颜色的得就是通过这几行代码实现。我们来好好理一下。
ValueAnimator 直译过来就是值动画,它是属性动画里面最核心的一个类了,属性动画的实现机制就是通过对目标的属性进行赋值并修改其属性来实现的,而且 ValueAnimator 只有两个子类,分别是 ObjectAnimator 和 TimeAnimator,我们今天主要用到了 ObjectAnimator,并且调用了 ofInt() 方法,看看参数都是什么意思:
public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setIntValues(values);
return anim;
}
- Object target 动画作用的对象 我们这里传入的是 this,也就是 MyAnimationView
- String propertyName 属性名字 这就是属性要操作的属性 可以为任何名字 只要提供 get 和 set 方法就可以,我们这里传入的是 backgroundColor,系统已经定义好的
- int…values 这是一个可变参数传入的操作属性的开始值和结束值,我们这里传入的是 RED 和 BULUE
对于 ofIn() 方法总共有四个方法重载,这里只说到了一个,有兴趣的可以多研究研究。
当我们拿到 ValueAnimator 的实例后,执行 setDuration(3000) 方法,这个是设置动画执行的时间,没什么好说的。接下来就是 setEvaluator(new ArgbEvaluator()) 了,这个是其实可以不用设置的,系统会默认的调用,不过作为 demo 嘛,还是想显示的调用了一下,这也真是属性动画的精髓所在。下面我们看看 ArgbEvaluator 这个对象的内部实现:
public Object evaluate(float fraction, Object startValue, Object endValue) {
int startInt = (Integer) startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = (Integer) endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (int) ((startA + (int) (fraction * (endA - startA))) << 24) |
(int) ((startR + (int) (fraction * (endR - startR))) << 16) |
(int) ((startG + (int) (fraction * (endG - startG))) << 8) |
(int) ((startB + (int) (fraction * (endB - startB))));
}
evaluate() 是 ArgbEvaluator 的核心方法,而且 ArgbEvaluator 是 TypeEvaluator 的一个子类,由系统实现,主要功能就是用来计算 ARGB 颜色值的。说到这里大家肯可能好奇 TypeEvaluator 是个什么鬼? 其实 TypeEvaluator 是系统定义的一个用来动画过渡规则的接口,只有一个 evaluate 方法。在 evaluate 方法里主要实现过渡规则,ArgbEvaluator 就是实现 ARGB 的平滑过渡,它的三个参数含义分别是:
- float fraction 用于表示动画的完成度,这个值决定当前的动画值
- Object startValue 动画的开始值
- Object endValue 动画的结束值
ArgbEvaluator 就是对颜色值进行了计算并返回,系统会自动调用 evaluate 方法,如果我们有别的需求,完全自定义哦。ArgbEvaluator 我们今天就暂时先介绍到这里。
setRepeatCount(ValueAnimator.INFINITE) 就是设置动画播放的次数,我们这里是无限次的播放。setRepeatMode(ValueAnimator.REVERSE) 是设置动画重复播放的模式,我们这是设置的是 REVERSE 模式,简单的说就是先从红色变蓝色,再从蓝色变红色,无限次的循环。
接下来我们在看一下 onDraw() 方法
protected void onDraw(Canvas canvas) {
for (int i = 0; i < balls.size(); ++i) {
ShapeHolder shapeHolder = balls.get(i);
canvas.save();
canvas.translate(shapeHolder.getX(), shapeHolder.getY());
shapeHolder.getShape().draw(canvas);
canvas.restore();
}
}
很简洁的几行代码,就是从一个 balls 集合里拿出来一个 ShapeHolder 然后就行坐标移动和绘制,这个在绘制之前会对当前的 canvas 进行了 save 操作,绘制完成之后再恢复状态。我们都知道 onDraw 方法的调用时机,除了在界面初始化的调用,还有就是界面改变的时候了。这个集合刚开始肯定是空的了,那么什么时候会改变界面呢?对,我们的动画在一直循环的播放,所以 onDraw() 方法会一直在调用,所以当检测到 balls 集合 有弹球的时候,就会把它取出,并绘制出来。我们都知道在触摸屏幕的时候弹球就会出现,到这里我们就要看一下 MyAnimationView 的 onTouchEvent 都做了什么处理了:
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() != MotionEvent.ACTION_DOWN &&
event.getAction() != MotionEvent.ACTION_MOVE) {
return false;
}
ShapeHolder newBall = addBall(event.getX(), event.getY());
// Bouncing animation with squash and stretch
float startY = newBall.getY();
float endY = getHeight() - 50f;
float h = (float) getHeight();
float eventY = event.getY();
int duration = (int) (500 * ((h - eventY) / h));
ValueAnimator bounceAnim = ObjectAnimator.ofFloat(newBall, "y", startY, endY);
bounceAnim.setDuration(duration);
bounceAnim.setInterpolator(new AccelerateInterpolator());
ValueAnimator squashAnim1 = ObjectAnimator.ofFloat(newBall, "x", newBall.getX(),
newBall.getX() - 25f);
squashAnim1.setDuration(duration / 4);
squashAnim1.setRepeatCount(1);
squashAnim1.setRepeatMode(ValueAnimator.REVERSE);
squashAnim1.setInterpolator(new DecelerateInterpolator());
ValueAnimator squashAnim2 = ObjectAnimator.ofFloat(newBall, "width", newBall.getWidth(),
newBall.getWidth() + 50);
squashAnim2.setDuration(duration / 4);
squashAnim2.setRepeatCount(1);
squashAnim2.setRepeatMode(ValueAnimator.REVERSE);
squashAnim2.setInterpolator(new DecelerateInterpolator());
ValueAnimator stretchAnim1 = ObjectAnimator.ofFloat(newBall, "y", endY,
endY + 25f);
stretchAnim1.setDuration(duration / 4);
stretchAnim1.setRepeatCount(1);
stretchAnim1.setInterpolator(new DecelerateInterpolator());
stretchAnim1.setRepeatMode(ValueAnimator.REVERSE);
ValueAnimator stretchAnim2 = ObjectAnimator.ofFloat(newBall, "height",
newBall.getHeight(), newBall.getHeight() - 25);
stretchAnim2.setDuration(duration / 4);
stretchAnim2.setRepeatCount(1);
stretchAnim2.setInterpolator(new DecelerateInterpolator());
stretchAnim2.setRepeatMode(ValueAnimator.REVERSE);
ValueAnimator bounceBackAnim = ObjectAnimator.ofFloat(newBall, "y", endY,
startY);
bounceBackAnim.setDuration(duration);
bounceBackAnim.setInterpolator(new DecelerateInterpolator());
// Sequence the down/squash&stretch/up animations
AnimatorSet bouncer = new AnimatorSet();
bouncer.play(bounceAnim).before(squashAnim1);
bouncer.play(squashAnim1).with(squashAnim2);
bouncer.play(squashAnim1).with(stretchAnim1);
bouncer.play(squashAnim1).with(stretchAnim2);
bouncer.play(bounceBackAnim).after(stretchAnim2);
// Fading animation - remove the ball when the animation is done
ValueAnimator fadeAnim = ObjectAnimator.ofFloat(newBall, "alpha", 1f, 0f);
fadeAnim.setDuration(250);
fadeAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
balls.remove(((ObjectAnimator) animation).getTarget());
}
});
// Sequence the two animations to play one after the other
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(bouncer).before(fadeAnim);
// Start the animation
animatorSet.start();
return true;
}
看到这么一坨代码是不是有点晕?其实没那复杂,这是动画的集合,主要是操作弹球的。动画有弹跳、挤压、张开、背景颜色、褪色,并且在褪色动画结束的时候删除到集合里边的弹球。整个动画的过程在于这些动画组合方式,我们来理一下:
首先是一个 view,有一个循环而且会逆放得背景色渐变动画,这时候 onDraw() 方法会一直调用,并检测 balls 集合,然后触摸屏幕的时候会添加进一个 balls 集合,这时候 ondraw() 方法检测到集合里的数据,就会把小球挨个绘制出来,这时候已经给弹球设置了一系列动画集合,启动动画的之前弹球就已经被绘制踹了,当当动画完成时,从集合里把弹球在删掉。至此,这个动画的流程就大致就是这样。
看到这部分是不是呀好奇怎么添加的弹球呢?看一下 addBall(event.getX(), event.getY()) 方法就知道了:
private ShapeHolder addBall(float x, float y) {
OvalShape circle = new OvalShape();
circle.resize(50f, 50f);
ShapeDrawable drawable = new ShapeDrawable(circle);
ShapeHolder shapeHolder = new ShapeHolder(drawable);
shapeHolder.setX(x - 25f);
shapeHolder.setY(y - 25f);
int red = (int) (Math.random() * 255);
int green = (int) (Math.random() * 255);
int blue = (int) (Math.random() * 255);
int color = 0xff000000 | red << 16 | green << 8 | blue;
Paint paint = drawable.getPaint(); //new Paint(Paint.ANTI_ALIAS_FLAG);
int darkColor = 0xff000000 | red / 4 << 16 | green / 4 << 8 | blue / 4;
RadialGradient gradient = new RadialGradient(37.5f, 12.5f,
50f, color, darkColor, Shader.TileMode.CLAMP);
paint.setShader(gradient);
shapeHolder.setPaint(paint);
balls.add(shapeHolder);
return shapeHolder;
}
在 addBall() 方法里我们可以看到通过 ShapeDrawable 来绘制一个椭圆,确定传进来的位置,并设置一系列属性,而 ShapeHolder 可以理解为一个参数的帮助类,或者说是 ShapeDrawable 的包装类,对一会动画操作的属性进行了包装,查看 ShapeHolder 的内部方法,其实都是 get set 方法,并且作用于ShapeDrawable 。这里非常关键,我们之前也说过,属性动画可以操作任何自定义属性,而这些属性就来自于get set方法,这里 ShapeHolder 更像是一个 JavaBean。还有一点我们要注意的是,我们这里可只是进行了参数的设置,并没有进行绘制哦。当添加进集合后,MyAnimationView 的 onDraw() 方法会调用 ShapeDrawable 把图形绘制出来。
看完 addBall() 方法,我们现在就可以来看 onTouchEvent() 方法了。首先是对手势进行了监听,只有在按下并且有移动的才会去往集合添加弹球,剩下的就是设置动画了,都是操作 ShapeHolder 里边的属性,没什么好说的了。主要讲一下 Interpolator 接口和动画集合的操作符还有监听器。
Interpolator
Interpolator 是可以控制动画改变速率的接口,可以让动画加速、减速、重复等等都是一些线性运动,在 API 11 之后增加 TimeInterpolator 接口,开始支持一些非线性的运动,如加速度、减速的等,也是属性动画的一个很强大的地方,可以通过 setInterpolator() 方法直接设置,但是必须实现 TimeInterpolator
才可以,我们这里就用到了一些系统实现的 AccelerateInterpolator(加速)、DecelerateInterpolator(减速),除了这些我们还可以自定一些,只要重写 getInterplation() 方法即可。
AnimatorSet
AnimatorSet 是一个动画的集合或者说是序列,可以进行延时或指定时间等操作,在通过 play() 方法设置动画后,会得到一个 AnimatorSet.Builder 对象,它是 AnimatorSet 的一个内部类,用于指定集合里各个动画的关系:
- after(Animator anim) 在哪个动画之后播放
- after(long delay) 在动画开始指定的时候后播放
- before(Animator anim) 在哪个动画之前播放
- with(Animator anim) 与某个动画同时播放
AnimatorListener
AnimatorListener 是 ValueAnimator 的一个内部接口,通过 addListener() 添加,主要负责接收动画开始播放、结束播放、取消和重复的通知,我们这里用到了结束播放的通知,直接构造的是 AnimatorListenerAdapter 匿名类,监听动画播放完毕后删除集合中的弹球,可能大家会疑惑 balls.remove(((ObjectAnimator) animation).getTarget()); 是怎么删除弹球的,看一下 ObjectAnimator 的内部实现就明白了:
public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setIntValues(values);
return anim;
}
private ObjectAnimator(Object target, String propertyName) {
setTarget(target);
setPropertyName(propertyName);
}
其实在我们设置动画的时候小弹球就被当做 tag 设置了进去,所以在我们播放结束完后,可以通过 getTag() 方法拿到弹球后在进行删除。
最后这篇关于弹球的动画就分析完了,由于涉及的知识点比较多,可能逻辑上会有一些混乱,朋友们有什么好的剑翊或者不是很明白的地方的哦地方欢迎指正。未完待续~