前言

不知不觉过去了好多天,因为前一阵子临近期末,不得不加急复习(不然就要挂科啦),加上后来学校老师给的课程设计,因此拖了好久的坑一直没补上,今天终于打算把这个坑填了,我们的重点依旧放在如何实现前篇提到的那个动画上。废话不多说,开始重点。

本文要点


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~,有毒有毒~~