View 动画 属性动画

动画,分为三种,视图动画、帧动画以及属性动画。视图动画和帧动画出现的比较早,属性动画是3.0以后出来的。视图动画只能被用来设置View的动画,帧动画是像动图一样,设置背景图,属性动画是可扩展的,可以自定义任何类型和属性的动画。

视图动画分为四种:缩放、位移、伸缩、透明以及上述四种的任意组合动画。我比较早的文章中提到过这个视图动画,除了透明动画外,其他三种动画都需要一个坐标点来作为标准进行动画操作,这里面有三种:ABSOLUTE 针对自身左上角,像素 、 RELATIVE_TO_SELF 针对自身左上角,百分比 、 RELATIVE_TO_PARENT 针对自身左上角,父View的百分比,以前篇中有所描述。以位移动画为例,如果从A处移动到了B处,如果点击B处,点击事件是不会触发的,点击A处才会触发,这个有点尴尬,但设计就是如此。

帧动画像幻灯片一样,是图像可以动的动画,这个比较简单,一般现在的 gif和webP 格式图片也可以实现这个效果,使用帧动画要注意OOM的问题。

重点说说属性动画,视图动画能实现的功能,属性动画都能实现,并且扩展性更好,属性动画的点击事件随着视图走,内容从A移动到B,则点击B处有点击事件,点击A处则无反应。属性动画比较重要的两个类, ValueAnimator 和 ObjectAnimator, ValueAnimator 是基类,ObjectAnimator 是子类,但看名字,ValueAnimator 代表的是数值,重点在于数字;ObjectAnimator 代表的是物,重点在于对象。同时我们也知道,既然是父子关系,那么,基类包含的功能,子类肯定也有这个功能。举个简单的例子,我们看看 ValueAnimator 是怎么使用的。

    public static interface AnimatorUpdateListener {
        void onAnimationUpdate(ValueAnimator animation);
    }
有个静态内部类,是个回调,看它的注释,通知动画的另一帧的出现,意思是动画执行过程中,会不断的回调它,那么我们可以在回调时通过参数 animation 获取到当前的getAnimatedValue() 对应的值,这个就是我们出入数据正在变化的当前值,如果我们想让一个view进行翻滚,那么可以

        ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 360.0f);
        animator.setTarget(view);
        animator.setDuration(5000).start();
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation){
                float value = (float) animation.getAnimatedValue();
                view.setRotation(value);
            }
        });
我们传进去的值是0和360,时间为5秒,这个监听就是在动画执行后,5秒内,动画里的值匀速从0变化到360,而getAnimatedValue()则会把当前这一帧的状态返回,我们获取到值后,然后调用view本身包含的旋转的方法,把数值传进去,view就旋转了。上面说了,基类支持的功能,子类都支持。我们如果把上述代码中的 ValueAnimator 替换成 ObjectAnimator,也一样可行。 ObjectAnimator.ofFloat(0.0f, 360.0f) 说白了,还是调用的 ValueAnimator 的 ofFloat()方法,那么 ObjectAnimator 有 ofFloat() 方法吗?有,并且更具体。
既然是属性动画,那么我们有没有不用自己调用view的属性的方式?我们看看 ObjectAnimator 类,它提供的ofFloat()方法,多了一个 String propertyName 参数,这个就是属性的名字的意思,我们知道,上述代码中是对view的旋转,如果用ObjectAnimator, 我们可以 ObjectAnimator.ofFloat(view, "rotation", 0.0f, 360.0f).setDuration(5000).start();就能实现上面一堆代码才实现的功能?我们 "rotation" 对应的就是 String propertyName ,这个方法里面,会把 propertyName 进行转换,会把名字的第一个字母变为大写,即把 "rotation"变为 "Rotation",然后在前面拼上 "set" 和 "get" 字段,组成了 "setRotation" 和 "getRotation",有没有发现很眼熟?这个就是View中的旋转方法的名字啊!实际上这里使用的是"setRotation" 方法,这样,"rotation" 变为 "setRotation",然后通过反射调用  setRotation() 方法,并且传入数据,数据就是5秒内匀速的从0到360的变化的值,这样,就相当于不停的调用setRotation()方法,所以view就旋转了。
我们这个是传入的值,那么view有没有提供这种方法呢? ObjectAnimator.ofFloat(this, View.ROTATION, 0.0f, 360.0f).setDuration(5000).start(); View 中提供了一系列静态内部类,都是 Property 的子类,里面封装好了 set 和 get 方法,我们直接使用即可。

组合动画呢,比如说想同时沿着x轴和y轴运动,可以使用 AnimatorSet 来实现,代码如下

        ObjectAnimator animator1 = ObjectAnimator.ofFloat(view, "rotationX", 0.0f, 360.0f).setDuration(5000);
        ObjectAnimator animator2 = ObjectAnimator.ofFloat(view, "rotationY", 0.0f, 360.0f).setDuration(5000);
        AnimatorSet set = new AnimatorSet();
        set.playTogether(animator1, animator2);
        set.start();
AnimatorSet 继承于 Animator,里面有 start cancle 等方法,如果想让两个动画先后执行,可以 
    AnimatorSet s = new AnimatorSet();
    s.play(anim1).before(anim2);
    s.play(anim2).before(anim3);
关于这个类,网上有许多文章。关于 ObjectAnimator.ofFloat(this, "rotation", 0.0f, 360.0f).setDuration(5000).start();,点击进去看源码,发现进去就会调用下面的方法
    @Override
    public void setFloatValues(float... values) {
        if (mValues == null || mValues.length == 0) {
            // No values yet - this animator is being constructed piecemeal. Init the values with
            // whatever the current propertyName is
            if (mProperty != null) {
                setValues(PropertyValuesHolder.ofFloat(mProperty, values));
            } else {
                setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
            }
        } else {
            super.setFloatValues(values);
        }
    }

此时 mProperty 为 null, 会调用 setValues(PropertyValuesHolder.ofFloat(mPropertyName, values)); 方法,具体看 PropertyValuesHolder.ofFloat(mPropertyName, values),我们发现,包括上面提到的set方法,都是这里面执行的,我们发现了这一点,可以一试

        PropertyValuesHolder a1 = PropertyValuesHolder.ofFloat("rotationY", 0.0f, 360.0f);
        PropertyValuesHolder a2 = PropertyValuesHolder.ofFloat("rotationX", 0.0f, 360.0f);
        PropertyValuesHolder a3 = PropertyValuesHolder.ofFloat("rotation", 0.0f, 360.0f);
        ObjectAnimator.ofPropertyValuesHolder(view, a1, a2, a3).setDuration(5000).start();
我们发现,动画执行。我们看上面 setFloatValues() 方法,里面对 mProperty 有个判断,如果不为null,则会执行 setValues(PropertyValuesHolder.ofFloat(mProperty, values));操作,接着,我们再次尝试,把方法名替换成 View.ROTATION_X 来试试
        PropertyValuesHolder a1 = PropertyValuesHolder.ofFloat(View.ROTATION_X, 0.0f, 360.0f);
        PropertyValuesHolder a2 = PropertyValuesHolder.ofFloat(View.ROTATION_Y, 0.0f, 360.0f);
        PropertyValuesHolder a3 = PropertyValuesHolder.ofFloat(View.ROTATION, 0.0f, 360.0f);
        ObjectAnimator.ofPropertyValuesHolder(view, a1, a2, a3).setDuration(5000).start();
动画依旧执行。      
android 在3.1中,增加了 ViewPropertyAnimator,这个也比较好用。比如想沿着x轴和y轴旋转,那么可以这样写
    view.animate().rotationX(360).rotationY(360).setDuration(5000).start();
其实,最后的 start()方法没写上去也没关系,系统会隐式的替我们加上。执行后,这个也可以旋转,但是一直停留在该页面,旋转动画执行一次,再次点击时,不会执行,没找到原因。

我们知道,这个是自定义的动画,可以定制,怎么办呢?上段中提到,是通过拼接 "set" 和 方法名,然后反射调用的,那么,我们可以根据这个方式,来定义自己的动画。比如我们在一个自定义控件中,画一个小蓝球,控制球的颜色变化。先把简单的控件写出来

    public class TranBlueView extends View {

        public static final float RADIUS = 50f;

        private PointF currentPoint;

        private Paint mPaint;


        public TranBlueView(Context context) {
            this(context, null);
        }

        public TranBlueView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }

        public TranBlueView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mPaint.setColor(Color.BLUE);
            setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    startAnima();
                }
            });
        }

        @Override
        protected void onDraw(Canvas canvas) {
            if (currentPoint == null) {
                currentPoint = new PointF(RADIUS, RADIUS);
            }
            drawCircle(canvas);
        }

        private void drawCircle(Canvas canvas) {
            float x = currentPoint.x;
            float y = currentPoint.y;
            canvas.drawCircle(x, y, RADIUS, mPaint);
        }

        private void startAnima() {
            ...
        }


        public void setColor(int color) {
            mPaint.setColor(color);
            invalidate();
        }

    }

                 android:layout_width="match_parent"
            android:layout_height="match_parent"
            />

startAnima() 是动画方法,我们自定义了一个方法 setColor(int color),用来接收传进来的颜色值,然后传给 paint,然后绘制,改变球的颜色。看这段代码,当点击控件后,开始动画,小蓝球开始颜色变化。怎么写这个startAnima()方法呢,我们需要调用 ObjectAnimator.ofObject()方法,把 view 和 "color" 的值传进去,颜色值由 "#0000FF" 变化到 "#FF0000",怎么把这两个传进去并匀速变化呢? 幸好android提供了个接口 TypeEvaluator ,这里面控制值的变化,
    public interface TypeEvaluator {
        public T evaluate(float fraction, T startValue, T endValue);
    }
fraction 是当前进度的百分比,startValue 开始的状态值, endValue 的状态值,在这个例子中, startValue 是 "#0000FF" ,endValue 是 "#FF0000", fraction 是随着时间的进度条,从0.00 到 1.00 ,好了,我们可以写一个类,实现 TypeEvaluator 接口,来自定义颜色变化的方法,很幸运,android 也提供了一个匀速变化的类

    public class ArgbEvaluator implements TypeEvaluator {
        private static final ArgbEvaluator sInstance = new ArgbEvaluator();

        public static ArgbEvaluator getInstance() {
            return sInstance;
        }

        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))));
        }
    }
似是而非的单利,构造方法没有私有,并且getInstance()方法加了个@hide的注解,能反射调用却不能直接调用,是专门给系统使用的吗?嗯,有点搞不懂想干嘛。重点看一下 evaluate() 方法, startValue, endValue 虽然是 Object ,但是都要转换成 int 类型,也就是这两个值都必须是 int 类型,然后经过一系列的转换,算出下一帧的颜色值,颜色是由 argb 组成,那么就计算下一个值就好说了,返回的值也是 int 类型,这也是我们自定义控件中 setColor(int color)中形参是int类型的原因。颜色值String转为为16进制的int值,用Color.parseColor("#0000FF") 方法,接下来我们就可以写出转换颜色的动画代码了

    private void startAnima() {
        ObjectAnimator anim = ObjectAnimator.ofObject(this, "color", new ArgbEvaluator(),
                Color.parseColor("#0000FF"), Color.parseColor("#FF0000"));
        anim.setDuration(5000);
        anim.start();
    }
然后运行一下,就可以看到小球的颜色变化。我在看 ObjectAnimator 源码时,发现还有一个 ofArgb() 方法,看名字就是颜色转化的,我就试了一下,把动画的代码替换为如下

    private void startAnima() {
        ObjectAnimator anim = ObjectAnimator.ofArgb(this, "color",
                Color.parseColor("#0000FF"), Color.parseColor("#FF0000"));
        anim.setDuration(5000);
        anim.start();
    }
这个方法是 21 以后才有的,使用需谨慎,需要做版本判断。
上面的例子,是改变一个变量,从A变化到B,如果我们想同时改变两个变量的值怎么办?我们可以添加 anim.addUpdateListener() 回到,在回调中拿到当前的值,根据当前值变化的程度来修改另外一个值;另一种方法就是封装成一个对象,把对象传进动画的修值器中,来改变相应的两个变量的值。例如我们如果想让小球从左上角滑到右下角,怎么办?我们在代码中drawCircle(Canvas canvas) 方法中,是根据 x 和 y 的值来确定圆心的,我们可以通过改变x和y的值,来移动小球的位置,这里就牵涉改变两个变量了。我们可以先封装一个对象,例如高版本系统中添加的对象 PointF,然后自定义一个 PointFEvaluator ,

    public class PointFEvaluator implements TypeEvaluator {
        private PointF mPoint;
        public PointFEvaluator() {
        }

        public PointFEvaluator(PointF reuse) {
            mPoint = reuse;
        }
        
        @Override
        public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
            float x = startValue.x + (fraction * (endValue.x - startValue.x));
            float y = startValue.y + (fraction * (endValue.y - startValue.y));

            if (mPoint != null) {
                mPoint.set(x, y);
                return mPoint;
            } else {
                return new PointF(x, y);
            }
        }
    }
我们可以看一下 evaluate() 方法,根据起始位置和进度,计算出当前点的值,然后封装到一个对象中返回,我们在View中动画和自定动画方法

    private void startAnima() {
        PointF startPoint = new PointF(RADIUS, RADIUS);
        PointF endPoint = new PointF(getWidth() - RADIUS, getHeight() - RADIUS);
        ValueAnimator anim = ObjectAnimator.ofObject(this, "verpoint",new PointFEvaluator(), startPoint, endPoint);
        anim.setDuration(5000);
        anim.start();
    }

    public void setVerpoint(PointF pointF){
        currentPoint.set(pointF);
        invalidate();
    }

RADIUS 是开始圆心的坐标和半径,所以开始位置是 startPoint,结束的位置需要控件的宽和高减去半径,即endPoint,我们自定义方法名为 "verpoint",所以对应的接收数据方法为setVerpoint(PointF pointF),由于 pointF 就是计算出的值,我们在View中定义的成员变量currentPoint来接收pointF的值,然后刷新界面即可。我们能看到小球匀速下降,如果想做小球加速运动,可以设置插值器 anim.setInterpolator(new AccelerateInterpolator(2f)); // 加速下降, 这样更自然点。如果我们想小球自由落体同时网上反弹两下再落地,可以使用  anim.setInterpolator(new BounceInterpolator()); // 球落地弹起效果,这都是系统封装好的,我们也可以自定义,不过一般用不到,我们可以把小球的宽一直定位到屏幕中间,然后做自由落体,如下

    private void startAnima() {
        PointF startPoint = new PointF(getWidth() / 2, RADIUS);
        PointF endPoint = new PointF(getWidth() / 2, getHeight() - RADIUS);
        ValueAnimator anim = ObjectAnimator.ofObject(this, "verpoint",new PointFEvaluator(), startPoint, endPoint);
        anim.setInterpolator(new BounceInterpolator()); // 球落地弹起效果
        anim.setDuration(5000);
        anim.start();
    }

如果我们想一边落体,一遍颜色变化,我们可以使用组合动画,示例如下

    private void startAnima() {
        PointF startPoint = new PointF(RADIUS, RADIUS);
        PointF endPoint = new PointF(getWidth() - RADIUS, getHeight() - RADIUS);
        ValueAnimator anim = ObjectAnimator.ofObject(this, "verpoint",new PointFEvaluator(), startPoint, endPoint);
        anim.setInterpolator(new BounceInterpolator()); // 球落地弹起效果

        ObjectAnimator anim2 = ObjectAnimator.ofObject(this, "color", new ArgbEvaluator(),
                Color.parseColor("#0000FF"), Color.parseColor("#FF0000"));
        AnimatorSet animSet = new AnimatorSet();
        animSet.play(anim).with(anim2);
        animSet.setDuration(5000);
        animSet.start();
    }

点击一下,就能看到效果。可以自豪的说,看,自由落体把小蓝球的脸都吓得变色了。

你可能感兴趣的:(Android,知识)