Android动画-View动画

时间:2023-12-23 19:25:31

View动画

Android动画分为三类:View动画,帧动画,和属性动画。帧动画也是View动画的一种。

View动画的作用对象是View,之所以强调这一点是因为其作用对象有别于Android的另一种动画—属性动画。

View动画的种类

View动画分为四种,可以使用XML定义,也可以在代码中定义,无论是哪种方式定义的动画,最终的结果都是创建对应动画的类对象,在代码中定义的话四种效果分别对应Animation的四个子类,具体情况如下表:

名称 标签 子类 效果
平移动画 < translate > TranslateAnimation 对View进行平移
缩放动画 < scale > ScaleAnimation 对View进行缩放
旋转动画 < rotate> RotateAnimation 对View进行旋转
透明度动画 < alpha > AlphaAnimation 对View的透明度变化

在XML中定义View动画

在res包下添加anim包,在anim包中放置我们的View动画

示例代码:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="false">
    <translate
        android:duration="4000"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="300"
        android:toYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        />
    <rotate
        android:duration="4000"
        android:fromDegrees="0"
        android:toDegrees="45"
        />
</set>

上面这个动画的实际效果是对View进行一个平移和旋转同时进行的动画

<set>标签表示动画合集,对应的类是AnimationSet

为动画合集指定插值器,默认情况下是加速减速插值器
android:interpolator="@android:anim/linear_interpolator"

为动画合集里面的动画指定是否共享同一个插值器,如果不指定的话或者指定为false,
就需要子动画指定单独的插值器或者使用默认值
android:shareInterpolator="true"

下面是几种动画的属性:

    //起始透明度和终止透明度0-1
    <alpha android:fromAlpha="float"
        android:toAlpha="float"/>

    //水平方向的缩放起始值。水平方向缩放的终止值
    //竖直方向的缩放起始值。竖直方向缩放的终止值
    //缩放的x轴点,缩放的y轴点
    <scale
        android:fromXScale="float"
        android:toXScale="float"
        android:fromYScale="float"
        android:toYScale="float"
        android:pivotX="float"
        android:pivotY="float"/>

    //平移的x起始值和终点至
    //平移的y起始值和终点值
    <translate
        android:fromXDelta="float"
        android:toXDelta="float"
        android:fromYDelta="float"
        android:toYDelta="float"/>

    //旋转的起始角度和终止角度
    //旋转的x轴点和y轴点
    <rotate
        android:fromDegrees="float"
        android:toDegrees="float"
        android:pivotX="float"
        android:pivotY="float"/>

View动画的工作原理

在代码中使用View动画的入口是:

view.startAnimation(animation);

从startAnimation开始追溯到View的startAnimation方法

public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        setAnimation(animation);
        invalidateParentCaches();
        invalidate(true);
    }

首先是调用View.setAnimation(animation)将这个动画对象赋值给View的mCurrentAnimation的成员变量(Animation对象),然后调用invalidate方法来重绘自己。

在set之后即将我们的View动画和View绑定在一起,所以之后使用应该是使用对应的get方法。

public Animation getAnimation() {
        return mCurrentAnimation;
    }

在View的源码中,调用getAnimation的方法是:draw(Canvas,ViewGroup,long)

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ...
        Transformation transformToApply = null;
        boolean concatMatrix = false;
        final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;

        final Animation a = getAnimation();

        if (a != null) {
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            transformToApply = parent.getChildTransformation();
        }

          else {
          ...

    return more;
}          

注意这一行:

more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);

应该是与动画的绘制有关的,继续追溯:

private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
        Transformation invalidationTransform;
        ...

        final Transformation t = parent.getChildTransformation();
        boolean more = a.getTransformation(drawingTime, t, 1f);
        if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
            if (parent.mInvalidationTransformation == null) {
                parent.mInvalidationTransformation = new Transformation();
            }
            invalidationTransform = parent.mInvalidationTransformation;
            a.getTransformation(drawingTime, invalidationTransform, 1f);
        } else {
            invalidationTransform = t;
        }

       ...
        return more;
    }

注意这一行:

boolean more = a.getTransformation(drawingTime, t, 1f);

追溯getTransformation方法:该方法最终是回调同名方法:

public boolean getTransformation(long currentTime, Transformation outTransformation) {
        //看到了熟悉的duration,所以推测这里应该是与动画的变化时间或者进度有关
        final long duration = mDuration;
        float normalizedTime;
        if (duration != 0) {
            //这个算法就是计算出当前动画的进度相对于体进度的百分比
            normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
                    (float) duration;
        } else {
            normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
        }

        final boolean expired = normalizedTime >= 1.0f || isCanceled();
        mMore = !expired;

        ......

            //在这里将已经计算好的进度传入插值器
            //前面在计算进度的时候,计算结果是线性的
            //但在这里之后,计算的结果则是曲线的
            //这个get方法归属于TimeInterpolator接口,
            //可以追溯该方法得到其类查看doc文档
            //其返回值是非线性的,就是View动画的非匀速动画
            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
            applyTransformation(interpolatedTime, outTransformation);
        }

        ...
        return mMore;
    }

在将进度传入插值器得到新的进度之后,就是调用apply方法,追溯结果如下:是一个空方法。

/**
     * Helper for getTransformation. Subclasses should implement this to apply
     * their transforms given an interpolation value.  Implementations of this
     * method should always replace the specified Transformation or document
     * they are doing otherwise.
     *
     * @param interpolatedTime The value of the normalized time (0.0 to 1.0)
     *        after it has been run through the interpolation function.
     * @param t The Transformation object to fill in with the current
     *        transforms.
     */
    protected void applyTransformation(float interpolatedTime, Transformation t) {
    }

从doc说明来看,该方法的具体实现是由其子类来完成的,就是四种View动画子类了。

//AlphaAnimation
@Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        final float alpha = mFromAlpha;
        t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));
    }
//RotateAnimation
@Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        float degrees = mFromDegrees + ((mToDegrees - mFromDegrees) * interpolatedTime);
        float scale = getScaleFactor();

        if (mPivotX == 0.0f && mPivotY == 0.0f) {
            t.getMatrix().setRotate(degrees);
        } else {
            t.getMatrix().setRotate(degrees, mPivotX * scale, mPivotY * scale);
        }
    }
//ScaleAnimation
@Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        float sx = 1.0f;
        float sy = 1.0f;
        float scale = getScaleFactor();

        if (mFromX != 1.0f || mToX != 1.0f) {
            sx = mFromX + ((mToX - mFromX) * interpolatedTime);
        }
        if (mFromY != 1.0f || mToY != 1.0f) {
            sy = mFromY + ((mToY - mFromY) * interpolatedTime);
        }

        if (mPivotX == 0 && mPivotY == 0) {
            t.getMatrix().setScale(sx, sy);
        } else {
            t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY);
        }
    }
//TranslateAnimation
@Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        float dx = mFromXDelta;
        float dy = mFromYDelta;
        if (mFromXDelta != mToXDelta) {
            dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
        }
        if (mFromYDelta != mToYDelta) {
            dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
        }
        t.getMatrix().setTranslate(dx, dy);
    }

所以applyTransformation()方法就是动画的真正的具体实现方法,也就是说,为了动画的流畅运行这个方法将被统以较高的频率系反复调用,同时,如果我们要实现自定义的动画,我们只需要实现到applyTransformation()就可以了

但是可以发现,在这些方法里面,只是进行了一些相应的坐标之类的计算工作,但是并没有真正的将动画给绘制出来,注意在出Alpha动画之外的其它三个apply方法的最后,都进行了一项工作:将计算得到的坐标数据传给Matrix对象的对应的set方法,最后这个矩阵将被用于动画的绘制,所以还需要找到View的draw方法,在那里应该才是真正的将一帧帧动画给绘制出来。

回到View中,在draw方法里面寻找getMatrix方法的调用:下面贴出的是部分代码:

if (transformToApply != null) {
                    if (concatMatrix) {
                        if (drawingWithRenderNode) {
                            renderNode.setAnimationMatrix(transformToApply.getMatrix());
                        } else {
                            // Undo the scroll translation, apply the transformation matrix,
                            // then redo the scroll translate to get the correct result.
                            //从源注释来看,这里真正的将矩阵中的数值用于canvas中,从而完成动画帧的绘制
                            canvas.translate(-transX, -transY);
                            canvas.concat(transformToApply.getMatrix());
                            canvas.translate(transX, transY);
                        }
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }

                    float transformAlpha = transformToApply.getAlpha();
                    if (transformAlpha < 1) {
                        alpha *= transformAlpha;
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }
                }

如果动画没有全部绘制完成,就继续调用invalidate方法启动下一次绘制来驱动动画,从而完成整个动画的绘制。

从上可以看出,整个View动画的实现有一个关键点:Matrix(矩阵)

Matrix


在讨论这个矩阵之前,我们可以先想一下在一个二维屏幕上,只存在x,y两个维度,那么就是说,我假设屏幕里面存在一个三维的图像,我们能够看到的依旧是一个二维图像(或者说是这个三维图像在二维上的投影)。

但是如果:我们将屏幕看做一个二维平面,同时给这个平面加一个法线即z轴,那么在z轴上移动屏幕(或者称之为镜头更好理解),镜头里的图像也应该有相应的变化(变大或者变小),就好比在不同的高度看同一个物体,实际上你看到的物体图像大小是不一样。

这样,我们可以得出一个结论:在屏幕上显示的图像,相对屏幕是二维的,但是相对整个空间,却是三维的,我们可以通过调整屏幕在z轴上的高度来实现三维效果,也可以通过调整屏幕上显示图像的大小来实现三维效果

所以对于屏幕里面的每一个像素点,我们用可以用一个3行一列的矩阵来描述:

Android动画-View动画

z的默认高度是1就是相对屏幕是0,这个值的改变将就等同于屏幕在视觉上的拉远拉近。

关于矩阵的运算与View动画之间的关系,在学习这一点的时候,有一个问题就是矩阵是如何对应到View动画的这些效果的,最终在知乎上找到了一个答案:

为什么引入齐次坐标就能表示平移? Yu Mao 的答案

View动画通常不单单是一个简单的平移效果,而是多种效果的复合,就是后面说道的仿射变换。而在计算机图形学中,坐标转换通常不是单一的,一个几何体在每一帧可能都设计了多个平移,旋转,缩放等变化,这些变化我们通常使用串接各个子变化矩阵的方式得到一个最终变化矩阵,从而减少计算量。所以我们需要将平移也表示为变化矩阵的形式。因此,只能引入齐次坐标系。

对于旋转,缩放,错切使用2*2的矩阵乘法可以实现,但是到了平移的时候就需要使用到矩阵的加法,为了统一算法,引入了3x3的齐次式,这样,整个View动画效果就都可以使用矩阵的乘法来实现。

那么关于个人关于这个矩阵的理解就是,为了在一个二维屏幕上实现View的各种效果Android引入了齐次矩阵,通过对矩阵的复合运算,我们最后得到一个三维的矩阵,这个三维矩阵里面包含的就是实现某种效果之后的数据。

由于数学能力实在有限,再往下深究下去也不太可能,对于这一点的理解,可能不一定正确,如果以后有机会继续涉及到这方面的学习的话,一定会深究下去,暂时就到这里了。


关于这个类,API文档的说明是:The Matrix class holds a 3x3 matrix for transforming coordinates.其内部维系着一个3*3的矩阵。Matrix矩阵的最根本的作用就是进行矩阵变换。

Android动画-View动画

关于每一个参数的意义:

MTRANS_X、MTRANS_Y 同时控制着 Translate

MSCALE_X、MSCALE_Y 同时控制着 Scale

MSCALE_X、MSKEW_X、MSCALE_Y、MSKEW_Y 同时控制着 Rotate

同时 MSKEW_X、MSKEW_Y 同时控制着 Skew(错切)

MPERSP_0 、MPERSP_1、MPERSP_3控制着透视(perspective)

Android动画-View动画

四种View动画对应的变换在矩阵的体现上就是线性变换,说的直白点就是矩阵的运算在平面变换中,矩阵的第三行的值一般是固定的001,如果涉及到3D变换,则第三行的值极为重要

这里也会涉及到一个仿射变换的概念:仿射变换实际上就是二维坐标到二维坐标的变换,具体的体现就是线性变换的复合,仿射变换不会改变二维图形的基本性质,比如直线变换之后还是直线,曲线还是曲线,平行关系还是平行关系,直线上点的位置依旧保持不变等,只有透视可以改变z轴的值,此外,矩阵的最后一行是001这样的就是仿射矩阵。

Matrix的多种复合都是采用矩阵的乘法来实现的,但是矩阵的乘法的缺点在于后面操作会影响前面的操作,具体情况参看这篇文章:

android matrix 最全方法详解与进阶(完整篇)

参考链接:

能力实在太菜,只能先mark几篇比较好的专门讲Matrix的文章在这里:

android matrix 最全方法详解与进阶(完整篇)

Android Matrix 最全方法详解与进阶

(关于Matrix的数学原理重点参照这篇) Android Matrix

CS的数学果然还是很重要,只是可惜在了照本宣科的授课方式。

## 帧动画

帧动画就是顺序播放事先定义好的一组图片,使用AnimationDrawable来使用,帧动画的用法简单但是容易引起OOM,应该尽量避免使用过多的大内存的图片。

使用

在资源文件夹下放置对应的资源文件,在drawable文件夹中通过xml定义一个动画文件

<?xml version="1.0" encoding="utf-8"?>
<animation-list android:oneshot="false"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@drawable/fr1" android:duration="500"/>
    <item android:drawable="@drawable/fr_2" android:duration="500"/>
    <item android:drawable="@drawable/fr_3" android:duration="500"/>

</animation-list>

然后将这个drawable文件作为View的背景文件并通过设置drawable来播放动画

AnimationDrawable animationDrawable
    =(AnimationDrawable) ivShowAnim.getDrawable();
animationDrawable.start();

View动画的特殊使用场景

为ViewGroup指定动画

LayoutAnimation的作用对象就是ViewGroup,加载动画之后使得子View在出场的时候就会带上这种动画效果,最常见的就是为ListView或者RecyclerView的item定义动画效果。

为ListView指定动画

XML的方式:
//将这个动画设置添加给ListView
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:delay="0.5"
    android:animationOrder="normal"
    android:animation="@anim/list_anim"
    />

//list_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/linear_interpolator">
    <scale
        android:fromYScale="0"
        android:toYScale="1"
        android:fromXScale="0"
        android:toXScale="1"
        android:pivotX="0"
        android:pivotY="0"/>
</set>

//在ListView中添加标签
<ListView
        ......
        android:layoutAnimation="@anim/anim_layout_list"
        ......>
</ListView>

几个标签的含义

//子View动画时间的延迟比例,例如总时间是1000,那么下面的意思就是
//子View延迟到500的时候再开始动画效果
android:delay="0.5"

//子View的动画顺序
android:animationOrder="normal"

//加载子View的动画文件
android:animation="@anim/list_anim"
代码的方式:
Animation animation = AnimationUtils.loadAnimation(this,R.anim.anim_layout_list);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(controller);

为Activity或Fragment的切换指定动画

调用该方法:

overridePendingTransition(R.anim.enter_anim,R.anim.exit_anim);

为Activity的切换指定动画

定义动画:

//enter_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:shareInterpolator="true" >

   <!-- <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />-->
    <translate
        android:fromXDelta="500"
        android:toXDelta="0" />
</set>

//exit_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:shareInterpolator="true" >

    <!--<alpha
        android:fromAlpha="1.0"
        android:toAlpha="0" />-->

    <translate
        android:fromXDelta="0"
        android:toXDelta="500" />

</set>

在startActivity方法之后调用:

startActivity(new Intent(MainActivity.this,Main2Activity.class));
overridePendingTransition(R.anim.enter_anim,R.anim.exit_anim);

如果要设置结束时候的效果则在重写的onfinish中:

 @Override
    public void finish() {
        super.finish();
        overridePendingTransition(R.anim.enter_anim,R.anim.exit_anim);
    }

为Fragment的切换指定动画

通过FragmentTransaction的setCustomAnimation方法来设定,只能是View动画。(Fragment是API11中添加的新类同属性动画同一个API等级)

transaction.setCustomAnimations(R.anim.enter_anim,R.anim.exit_anim);

transaction.setCustomAnimations(@AnimatorRes int enter,
            @AnimatorRes int exit, @AnimatorRes int popEnter, @AnimatorRes int popExit);

效果图

Android动画-View动画