
1.3 实现3D卡片翻转效果
在本节中,我们将通过前面学习的Camera类来进行实战操作。第一个例子实现了3D卡片翻转效果,可扫码查看右侧上面的效果图。

扫码查看动态效果图
项目地址:请移步GitHub并搜索DialogFlipTest。
在本节中,将通过简单的示例来讲解实现原理,本节所实现的效果可扫码查看右侧下面的效果图。

扫码查看动态效果图
其实这个示例最初是Google给出的API Demos里的示例,具体路径为src/com/example/android/apis/animation/Rotate3dAnimation.java。其中具体讲解了Rotate3dAnimation的实现原理,为了方便起见,我会稍做修改,但最终的实现效果是完全相同的。
1.3.1 框架搭建
要实现ImageView的旋转,可使用如下两种函数。
●第一种函数是继承自ImageView类,在onDraw函数中实现图像的翻转,比如1.1节中的示例。类似地,也可以继承自LinearLayout等容器类,同样在dispatchDraw函数中操作Canvas,以实现其所包含的控件的旋转效果。
●第二种函数是自定义Animation,通过给View设置自定义的Animation来实现旋转效果。在这里,我们使用这种函数。
在框架阶段,我们做了一个非常简单的demo,实现一张图片的来回切换,可扫码查看效果图。

扫码查看动态效果图
如效果图所示,当点击按钮时,图像从0°旋转至180°,当再点击按钮时,图像会旋转回来。
1.XML布局
Activity的布局非常简单,就是一个按钮和一个ImageView,代码如下(activity_rotate_ 3d.xml):


大家可能会觉得,在ImageView的外围又包了一个LinearLayout,这样做多此一举。是的,从这里来看,是没有必要,但后面我们会修改这个布局文件,到时候LinearLayout就有用了。为了讲解方便,此处提前进行布局。
需要注意ImageView外围所包装的id为content的LinearLayout,注意它的位置,我们将会在后续的代码中用到。
2.Activity代码
因为我们是通过自定义Animation来旋转控件的,所以肯定会在onCreate函数中对Animation进行初始化,然后在点击按钮时执行startAnimation。
下面先列出完整的代码:


在代码中,我们自定义的Animation叫Rotate3dAnimation,具体实现会在后面详细讲解。
在onCreate函数中,是初始化环节:

注意这里的mContentRoot,它就是XML中包裹ImageView的LinearLayout,表示需要旋转的控件的根布局。
从效果图可以看出,从0°到180°和从180°到0°,是两个不同的动画过程,分别用openAnimation和closeAnimation来表示。下面只讲解openAnimation动画过程:

从这里大概可以看出,Rotate3dAnimation有两个参数,分别是fromDegrees和endDegrees。因为我们需要在完成动画之后,让View保持完成动画时的状态,所以要用到setFillAfter(true)函数。
3.自定义Animation函数
该自定义Animation函数的主要作用是实现控件在中间位置从fromDegrees旋转到endDegrees。关于如何重写Animation的函数,我们还没有讲解过。其实重写Animation的函数比较简单,主要是重写如下两个函数:

上面就是自定义Animation的框架,其中主要涉及3个函数。
构造函数:
很明显,构造函数主要是为了传入一些参数,比如这里的fromDegrees和endDegrees。
initialize:
initialize函数会在执行动画前调用,参数中的width、height表示将要执行动画的View的宽和高,parentWidth、parentHeight表示执行动画的View的父控件的宽和高。因为该函数会在执行动画前调用,所以一般会在该函数中执行一些初始化操作。
applyTransformation:
applyTransformation函数最重要,它就是用来实现自定义Animation的函数,相关参数如下。
●float interpolatedTime:正在执行的Animation的当前进度,取值范围为0~1。
●Transformation t:当前进度下,需要对控件应用的变换操作都保存在Transformation中。
我们知道一般通过Animation.setDuration(long durationMillis)来设置动画时长,在applyTransformation函数中,会将时长转化为进度来表示,这个进度就是interpolatedTime,它是一个浮点数,取值范围为0~1。
动画的进度一般是从0到1,假设动画的最小更新进度为0.001,即进度每隔0.001更新一次界面,每次更新界面都是通过调用applyTransformation函数来实现的。所以,在每次更新动画时,当前的动画进度就是这里的interpolatedTime,而这个进度对应的需要对View控件所做的操作,全部保存在参数Transformation t中。
自定义Animation就是通过上面的步骤完成的,下面来看看如何实现Rotate3dAnimation。
4.Rotate3dAnimation
Rotate3dAnimation的代码比较简单,下面先全部列出,然后逐个讲解:

首先,在构造函数中,传入两个参数fromDegrees和endDegree,fromDegrees表示开始旋转的角度,endDegree表示结束旋转的角度。
然后,在initialize函数中执行初始化操作。根据1.2节的讲解可知,我们要围绕控件中心点旋转,因此需要获取控件中心点的位置坐标。所以,在初始化时,计算出控件中心点的位置坐标:

最后,执行applyTransformation函数中的操作。其中,第1步,根据当前进度计算出当前的旋转角度:

第2步,利用Camera将图片绕Y轴旋转degrees的角度:

第3步,将旋转中心移到控件中心点位置:

第4步,调用super.applyTransformation(interpolatedTime,t)来执行改变过的动画操作,以将操作最终体现在控件上。
到此,实现了我们想要的效果,可扫码查看效果图。

扫码查看动态效果图
1.3.2 效果改进
1.图片缩放原理概述
从1.3.1节最后实现的效果图可以看出一个问题,翻转时的图像效果与1.3.1节开始时看到的效果不完全相同,不同点在于后面实现的翻转效果,翻转过程中图像很大,如图1-30所示。
而1.3.1节开始时看到的效果的翻转过程截图如图1-31所示。
可以看到,在图1-31中,翻转过程中的图像没有那么大,基本保持原大小不变。
从1.2节可以知道,图像旋转时的大小跟其与Z轴的距离有关,View与Camera的距离越大,显示的图像越小。
所以,在图像从0°旋转到180°的过程中,图像与Camera的距离关系如图1-32所示。

图1-30

图1-31

图1-32

扫码查看彩色图
从当前的效果图可以看出,随着旋转角度的增加,倾斜之后的图像会变大,在旋转角度达到90°时图像最大。
同样地,要解决这个问题,就得随着图像变大,将View与Camera的距离增大,这样View就会变小。所以,这个View与Camera的距离变化过程就形成了上面的曲线。
当图像需要从0°旋转至90°时,View与Camera的距离需要越来越大,并在旋转到90°时达到最大。而当图像需要从90°旋转至180°时,整个距离变化过程与从0°旋转至90°时的相反,这点从曲线的变化情况就可以看出。
因此需要将图像从0°至180°的整个旋转过程分为两段,从0°旋转至90°时执行下面的代码,使View与Camera的距离逐渐增大:

这里的mDepthZ是固定数值,默认值为400。如果动画中图像的旋转角度区间就是从0°旋转至90°,那么View与Camera的距离会随着动画的播放越变越大,在旋转角度达到90°时距离达到最大,这与图1-32中的情况相同。
而在第2段过程中,即从90°旋转至180°时,整个View与Camera的距离变化情况就要反过来,在90°时距离达到最大,在180°时距离回归到初始值:

很明显,这段代码是符合要求的。所以,后面我们为了区分是从0°旋转至90°的逐渐增大曲线还是从90°旋转至180°的逐渐减小曲线,引入了一个reverse变量来进行标识。
2.改造Rotate3dAnimation
根据上面的原理,我们对Rotate3dAnimation函数进行改造,改造后的代码如下。下面先列出完整代码,然后详细讲解:


首先看初始化函数,在初始化函数中有一个boolean reverse参数,这个参数用于标识曲线是逐渐增大的还是逐渐减小的。reverse为true时,表示距离逐渐增大;reverse为false时,表示距离逐渐减小。
然后在applyTransformation中,增加了沿Z轴移动的代码:

很明显,当mReverse为true时,View沿Z轴的移动距离随动画的播放而增大,在动画结束(interpolatedTime等于1)时达到最大。当mReverse为false时,View沿Z轴的移动距离随动画的播放而减小,在动画结束时,View沿Z轴的移动距离回归到0。
3.改造Activity
因为我们把原本从0°旋转至180°的动画拆成了两段,所以需要先执行从0°旋转至90°的动画,结束后接着执行从90°旋转至180°的动画,即核心代码如下:

同样地,closeAnimation先执行从180°旋转至90°的动画,结束后再执行从90°旋转至0°的动画。这里就不再列出相关代码了。

扫码查看动态效果图
通过扫码查看右侧的效果图可以看出,基本上完成了动画图像大小不变的旋转动作,但在图像旋转到90°的时候,会明显地卡一下,这是因为此处有一个停顿以便过渡到下一个动画过程,我们可以使用加速器来解决这个问题:


由以上代码可见,从0°旋转至90°时使用加速器,从90°旋转至180°时使用减速器,在90°时旋转速度最快。同样地,closeAnimation也使用加速器来解决这个问题,可扫码查看效果图。

扫码查看动态效果图
从效果图可以看到,这样就初步实现了1.3节开始时的效果,但还是有所不同,开始时的效果在旋转至90°后,显示的是另一张图像,这是怎么做到的呢?
1.3.3 正背面显示不同的内容
回顾下1.3节开始时的动画,扫码查看下面的效果图。可以看到,在图像旋转至90°时,ImageView显示的图像变为另一张图像。
方案一:通过替换图像资源实现
因为我们已经将从0°至180°的旋转过程划分为从0°至90°和从90°至180°这两个过程,所以在90°时为ImageView替换图像,即可实现背面显示另一张图像的效果,可扫码查看效果图。

扫码查看动态效果图
首先,在点击“翻转”按钮的时候,给ImageView配置上初始图像:

然后,在90°时,开始下一个动画前,给ImageView配置上另一张图像:


整个代码的难度不大,这里就不再详述了。这样处理后,就实现了我们想要的效果。
方案二:使用多控件显示/隐藏实现
方案一只能解决同一个控件中显示不同内容的问题,但若要正背面显示不同的控件,就没办法了。
这时可以使用方案二,即在布局中引入两个ImageView控件,用从0°旋转至90°时显示一个控件而从90°旋转至180°时显示另一个控件的方式来实现。
将Activity的布局代码改为如下代码(activity_rotate_3d.xml):


可见,相比原来的布局代码,这里在实现动画的容器(id为content的LinearLayout)中增加了一个ImageView,它的资源是photo2。然后在动画中,在openAnimation结束时,将image1隐藏并显示image2,这时的动画效果就是切换到图片二了:

同样地,在翻转动画中,在closeAnimation结束时,将image2隐藏并显示image1,这时的动画效果就是切换到图片一了:

这样,ImageView显示图像的功能就实现了,通过这种方式实现的控件可以实现正背面不同的布局效果,如图1-33所示。

图1-33
根据以上的原理,我们若要实现这个效果,只需要在图像旋转至90°时显示/隐藏不同的控件即可。