前言
不知不觉过去了好多天,因为前一阵子临近期末,不得不加急复习(不然就要挂科啦),加上后来学校老师给的课程设计,因此拖了好久的坑一直没补上,今天终于打算把这个坑填了,我们的重点依旧放在如何实现前篇提到的那个动画上。废话不多说,开始重点。
本文要点
- 1.视图动画(Animation类)和属性Property动画(Animator类)的区别
- 2.插值器(Interpolator)的作用
- 3.我们要实现的载入动画ColorfulAnimView分解
- 4.动画嵌入Dialog
- 5.小小总结
1.视图动画(Animation类)和属性Property动画(Animator类)的区别
Animation只能够为View添加动画,而对于非View对象,我们只能够自己去实现动画。Animation的能力很有限,它只暴露了view对象的一些特性,比如缩放和旋转,而对于背景颜色则无能为力。
Animation更大的一个缺点是,Animation只能够对对象进行表象的处理,即从绘制角度来改变View。这样导致的一个问题就是很多人在教程里说的,移动一个Button后,它的位置其实还是在原来的地方,点击的区域并没有改变,改变的只是它的绘制区域。当然他也有优点,对于简单的动画,使用视图动画能减少很多代码量。
我们甚至可以用属性动画来实现一些非动画的效果,比如我在CACW中,使用了ValueAnimator来实现下滑时的阻尼效果,如下图:
根据上图可以看到,随着下拉的高度越来越大,呈现出了一个类阻尼的效果。
2.插值器(Interpolator)的作用
插值器是动画里面一个很重要的概念,类似于物理里面的加速度,可以定义动画的变化速率。在不同的插值器的作用下,同一个单位时间内,对象属性的变化值也是不一样的。例如我们如果要做到按钮从一个位置移动到另外一个位置,并且速度随着时间越来越小。那么我们可以使用Decelerate这个插值器来实现。此外还有很多类型的插值器。可以结合下图很好的了解他们的作用!
不使用插值器
使用插值器
如果想要进一步体会插值器带来的效果,我们还可以打开Android AVD或Genymotion虚拟机中,一个内置的APP叫API Demos,然后进入Views/Animation/Interpolators查看。值得一提的是该App还有其他的系统API调用示例,非常有用。
3.我们要实现的载入动画ColorfulAnimView分解
①小圆们的存储结构
首先我自定义了一个类RoundBean用于存储小圆们需要的各个属性,例如颜色,位置,大小等等。由此可以很方便的控制圆的各个属性。
RoundBean构造函数需要传入该圆的颜色和一个自定义接口Refersher,目的是为了能够在园内设置颜色、大小等的同时进行自我刷新,即调用View的invalidate方法。
在RoundBean里面的关键方法是draw(Canvas canvas)
public void draw(Canvas canvas){
canvas.drawCircle(getX(), getY(), getSize() / 2, getPaint());
}
传入一个canvas对象,在其上面绘制一个圆形。其余方法则是对属性的set和get方法。
②View的实现
ColorfulAnimView则继承了View类,同时实现了Refersher这个接口,其三个构造方法均添加一个init初始化方法:
private void init(Context context, AttributeSet attrs) {
TypedArray a = getResources().obtainAttributes(attrs, R.styleable.ColorfulAnim);
speedFactor = a.getFloat(R.styleable.ColorfulAnim_speed_factor, 1f);
colorList = new ArrayList<>();//存储四种颜色
colorList.add(getResources().getColor(android.R.color.holo_red_light));
colorList.add(getResources().getColor(android.R.color.holo_orange_light));
colorList.add(getResources().getColor(android.R.color.holo_green_light));
colorList.add(getResources().getColor(android.R.color.holo_blue_light));//ScaleRound 's Color
scaleRound = new RoundBean(colorList.get(3),this);
round1 = new RoundBean(colorList.get(0),this);
round2 = new RoundBean(colorList.get(1),this);
round3 = new RoundBean(colorList.get(2),this);
post(new Runnable() {
@Override
public void run() {
int f1 = Math.min(mHeight, mWidth) / 2;
mPointMargin = mWidth / 5;
bigRoundSize = Math.min(mPointMargin, f1);
normalRoundSize = bigRoundSize / 2;
scaleRound.setAttrs(colorList.get(3), mPointMargin * 4, mHeight / 2, bigRoundSize, 1f, 1f);
round1.setAttrs(colorList.get(0), mPointMargin * 2, mHeight / 2, normalRoundSize, 0f, 1f);
round2.setAttrs(colorList.get(1), mPointMargin * 3, mHeight / 2, normalRoundSize, 0f, 1f);
round3.setAttrs(colorList.get(2), mPointMargin * 4, mHeight / 2, normalRoundSize, 0f, 1f);
createAnimator();//创建Animator对象
}
});
a.recycle(); //需要回收
}
这里有一点需要说明的是,我post了一个runnable是因为需要获取到View的长宽,而在构造方法处使用getWidth等方法是无法获取到view的真实长宽的,因此,通过View.post,我们可以将获取的时间放到绘制完毕之后,这时候获取到的就是真实的View的长宽了。还有需要说明的是,这里对圆的大小的确定是我根据经验尝试出来的。应该能够在大部分的屏幕上获得接近的效果。
③View的测量
我重写了onMeasure方法,以支持warp_content,具体代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
private int measureHeight(int heightMeasureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(heightMeasureSpec);//指定的模式,为EXACTLY时明确了长宽,为AT_MOST时,即wrapContent,
// 需要和指定的大小进行比较,如果是UNSPECIFIED,则像多大就多大
int specSize = MeasureSpec.getSize(heightMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = px2dip(getContext(), 1000);
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
mHeight = result;
return result;
}
因为measureWidth也差不多,因此在此就省略了。我们可以看到,onMeasure主要是通过setMeasuredDimension来设置View的大小,而这大小则需要通过measureHeight和measureWidth来确定。若用户在xml里面确定的是warp_content,则此处的specMode是AT_MOST,需要根据小圆球的活动范围来确定大小。上面这段代码基本可以用于onMeasuer的模板代码。
④View的绘制
View的绘制部分是由onDraw来做的,我在View的onDraw上是这么写的:
protected void onDraw(Canvas canvas) {
round1.draw(canvas);
round2.draw(canvas);
round3.draw(canvas);
scaleRound.draw(canvas);
}
即调用了每个RoundBean上的draw方法,让它们绘制自身。onDarw会在View创建时被调用一次,在调用invalidate方法时也会调用onDraw。因为我们使用了ValueAnimator来不断改变每个RoundBean的属性,同时调用invalidate方法,从而才能达到小圆不断在动的效果。
⑤动画效果的实现
下面我们来看看创建Animator的代码:
private void createAnimator() {
final int upDownDuration = (int) (2000 * speedFactor);//上下浮动动画时长
final int preAnimDuration = (int) (600 * speedFactor);//预动画时长,即一开始的圆形缩放和位移动画
final int alphaInDuration = (int) (200 * speedFactor);//圆形淡出动画时长
//圆缩放动画
ValueAnimator scaleAnimator = ObjectAnimator.ofObject(scaleRound, "Size", new IntEvaluator(), bigRoundSize, normalRoundSize);
scaleAnimator.setDuration(preAnimDuration);
scaleAnimator.setInterpolator(new OvershootInterpolator());
//路径动画
ValueAnimator moveAnimator = ObjectAnimator.ofObject(scaleRound, "X", new IntEvaluator(), round3.getX(), round1.getX() - mPointMargin);
moveAnimator.setDuration(preAnimDuration);
moveAnimator.setInterpolator(new LinearInterpolator());
//淡出动画
ObjectAnimator animatorAlphaIn1 = ObjectAnimator.ofObject(round1, "Alpha", new FloatEvaluator(), 0f, 1f);
animatorAlphaIn1.setDuration(alphaInDuration);
ObjectAnimator animatorUpDown1 = ObjectAnimator.ofObject(round1, "Y", new IntEvaluator(),
mHeight / 2, mHeight / 2 + normalRoundSize);
animatorUpDown1.setDuration(upDownDuration);
animatorUpDown1.setInterpolator(new CycleInterpolator(1));
animatorUpDown1.setRepeatCount(ValueAnimator.INFINITE);
ObjectAnimator animatorAlphaIn2 = ObjectAnimator.ofObject(round2, "Alpha", new FloatEvaluator(), 0f, 1f);
animatorAlphaIn2.setDuration(alphaInDuration);
ObjectAnimator animatorUpDown2 = animatorUpDown1.clone();
animatorUpDown2.setTarget(round2);
ObjectAnimator animatorAlphaIn3 = ObjectAnimator.ofObject(round3, "Alpha", new FloatEvaluator(), 0f, 1f);
animatorAlphaIn3.setDuration(alphaInDuration);
ObjectAnimator animatorUpDown3 = animatorUpDown1.clone();
animatorUpDown3.setTarget(round3);
ObjectAnimator animatorUpDown4 = animatorUpDown1.clone();
animatorUpDown4.setTarget(scaleRound);
animatorSet = new AnimatorSet();
animatorSet.play(scaleAnimator).before(moveAnimator);
animatorSet.play(moveAnimator).with(animatorUpDown3);//播放完预动画,时间为2*preAnimDuration,而animatorUpDown3播放的具体时间是preAnimDuration
animatorSet.play(animatorUpDown2).after(preAnimDuration + alphaInDuration);//preAnimDuration+alphaInDuration
animatorSet.play(animatorUpDown1).after(preAnimDuration + (2 * alphaInDuration));
animatorSet.play(animatorUpDown4).after(preAnimDuration + (3 * alphaInDuration));
animatorSet.play(animatorAlphaIn3).after(preAnimDuration); //preAnimDuration
animatorSet.play(animatorAlphaIn2).after(preAnimDuration + alphaInDuration);
animatorSet.play(animatorAlphaIn1).after(preAnimDuration + (2 * alphaInDuration));
animatorSet.setStartDelay(200);//以上动画延时200毫秒执行,解决一开始的缩放动画卡顿问题
}
动画一共分为4部分,第一部分是第一个大圆从大缩放到小圆,第二个部分是这个圆线性位移到左侧,第三个部分是位移的过程中有其余三个小圆淡出,最后是四个小圆一起在垂直方向上做往返运动,并且形成一种波浪效果。
为了实现这些效果,我使用了一些插值器。例如,我在第一个圆的缩放动画的插值器是OvershootInterpolator,它能达到一个有弹性的圆形的效果,第一个圆的位移动画插值器是LinearInterpolator,即线性移动,因为其余三个圆均是在他的位移路径上淡出,因此不宜使用其他插值器。
最后,使小圆不断在垂直方向上来回滚动的插值器是CycleInterpolator,它能够使小圆在出发后再次返回,而且我定义了重复次数是INFINITE(无限),因此小圆们将不断地在垂直方向上来回移动,形成一种类似波浪的效果。这也是我在Google新LOGO上看到的一种类似的效果,于是就拿过来用了,当然Google那个版本要复杂的多,而且要好看得多。
在定义ValueAnimator的时候,可以看到我调用的是ofObject,并且传入了round1、round2等对象,使用的PropertyName是Size、X、Y等,其实这是在告诉ValueAnimator,我在round1等对象中写了setSize、setX等方法,让ValueAnimator不断调用这些方法去实现动画效果。值得注意的是,写在RoundBean里面的这些方法一定要是public的,并且方法名必须准确,否则将会影响Animator的调用,导致动画失效(别问我为什么知道=-=)。
最后我把所有动画按一定顺序放入了animatorSet动画集中,并且设置了启动延时为200ms,这主要是为了防止偶尔发生的启动动画时卡顿的现象。
4.动画嵌入Dialog
我们做的这个View是打算可以放在任何一个地方来使用的,因此我们可以尝试将其放入Dialog中实现一个类ProgressDialog。
首先在Layout中新建一个xml文件,用于定义Dialog的细节,我们可以看到这其实是个很简单的布局文件,仅有一个我们前面一直在讨论的ColorfulAnimView和一个TextView,这样我们就完成了布局。
我们再新建一个继承Dialog的类AnimProgressDialog。
在他的onCreate中,我们简单地设置了去标题栏的样式,同时设置了title,并且启动了动画:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);//去标题栏
setContentView(R.layout.dialog_anim_progress);//关键步骤
titleView = (TextView) findViewById(R.id.tv_title);
if (!title.isEmpty()) {
titleView.setText(title);
}
animView = (ColorfulAnimView) findViewById(R.id.view_anim);
titleView.post(new Runnable() {
@Override
public void run() {
animView.startAnim();
}
});
}
在这一切准备工作完成之后,我们就可以愉快地在MainActivity中new出一个这样的dialog并且调用他的show方法了。^_^
其他的地方我们也可以照样使用这种方法来嵌入,因此就不在赘述。
5.小小总结
其实自定义一个View并没有想象中的那么难,我们一般需要用到以下几个回调方法:
- onFinishInflate(): 从XML加载组件后回调。
- onSizeChanged(): 组件大小改变时回调。
- onMeasure(): 回调该方法来进行测量。
- onLayout(): 回调该方法来确定显示的位置。
- onTouchEvent(): 监听到触摸事件时回调。
当然不一定需要重写以上所有方法,考虑需求实现一部分方法就可以了。
自定义View也有以下三种方法:
- 对现有控件进行拓展。
- 通过组合来实现新的控件。
- 重写View来实现全新的控件。
如果在以后的项目中遇到了这些情况,我会来写下对他们的笔记的。
闲聊时间
不知不觉已经到了2016年,时间过得真快,原来可能出现的一次重要的改变最终也没能实现2333,所有一切还是留给时间解决吧。
注:最近的电视剧太子妃看起来挺不错的,居然连我这个万年不追国产剧的人都开始追剧ing~,有毒有毒~~