手把手教你实现Android开发中的3D卡片翻转效果!

博文小编

2021-06-09

以下内容节选自《Android自定义控件高级进阶与精彩实例》一书!


《Android自定义控件高级进阶与精彩实例》一书中有一个使用Camera类(书中有对该类的详细讲解)实现3D卡片翻转效果的例子(效果如下所示)。

项目地址:请移步GitHub并搜索DialogFlipTest。


其实这个示例最初是Google给出的API Demos里的示例,具体路径为:src/com/example/android/apis/animation/Rotate3dAnimation.java

其中具体讲解了Rotate3dAnimation的实现原理,为了方便起见,这里会稍做修改,但最终的实现效果是完全相同的。

01 框架搭建

要实现ImageView的旋转,可使用如下两种函数。

第一种函数是继承自ImageView类,在onDraw函数中实现图像的翻转。类似地,也可以继承自LinearLayout等容器类,同样在dispatchDraw函数中操作Canvas,以实现其所包含的控件的旋转效果。

第二种函数是自定义Animation,通过给View设置自定义的Animation来实现旋转效果。在这里,我们使用这种函数。

图片在框架阶段,我们做了一个非常简单的demo,实现一张图片的来回切换,效果如下。

如效果图所示,当点击按钮时,图像从0°旋转至180°,当再点击按钮时,图像会旋转回来。

1.XML布局

Activity的布局非常简单,就是一个按钮和一个ImageView,代码如下(activityrotate 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的框架,其中主要涉及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)来执行改变过的动画操作,以将操作最终体现在控件上。

到此,就实现了我们想要的效果,如下所示。

02 效果改进

1.图片缩放原理概述

从最后实现的效果图可以看出一个问题,翻转时的图像效果与开始时看到的效果不完全相同,不同点在于后面实现的翻转效果,翻转过程中图像很大,如图1所示。

而本文开始时看到的效果的翻转过程截图如图2所示。

可以看到,在图2中,翻转过程中的图像没有那么大,基本保持原大小不变。

从本书1.2节可以知道,图像旋转时的大小跟其与Z轴的距离有关,View与Camera的距离越大,显示的图像越小。

所以,在图像从0°旋转到180°的过程中,图像与Camera的距离关系如图3所示。

从当前的效果图可以看出,随着旋转角度的增加,倾斜之后的图像会变大,在旋转角度达到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°时距离达到最大,这与图3中的情况相同。

而在第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也使用加速器来解决这个问题,效果如下。

从效果图可以看到,这样就初步实现了开始时的效果,但还是有所不同,开始时的效果在旋转至90°后,显示的是另一张图像,这是怎么做到的呢?

03 正背面显示不同的内容

回顾一下开始时的动画,效果如下。

可以看到,在图像旋转至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显示图像的功能就实现了,通过这种方式实现的控件可以实现正背面不同的布局效果,如图4所示。

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


想要了解更多自定义控件的使用?那就赶紧去看一下《Android自定义控件高级进阶与精彩实例》这本书吧!

《Android自定义控件高级进阶与精彩实例》
启舰 著

专注于介绍Android自定义控件进阶知识
通过精彩的案例对各种绘制、动画技术进行了糅合讲解

读者可以通过本书从宏观层面、源码层面对Android自定义控件建立完整的认识。

本书主要内容有3D特效的实现、高级矩阵知识、消息处理机制、派生类型的选择方法、多点触控及辅助类、RecyclerView的使用方法及3D卡片的实现、动画框架Lottie的讲解与实战等。本书适合中高级从业者对Android自定义控件相关知识进行查漏补缺和深入学习。

(京东满100减50,快快扫码抢购吧!)

读者评论

相关博文

  • Android开发时的多点触控是如何实现的?

    Android开发时的多点触控是如何实现的?

    博文小编 2021-01-13

    对于Android自定义控件开发,多点触控是一个必须要懂的知识点。因为在正常的情况下操作正常的控件,使用多指操作时,基本上都会出现问题。当需要对多指操作进行兼容时,就需要这方面的知识了。 本文选自《Android自定义控件高级进阶与...

    博文小编 2021-01-13
    244 0 0 0