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

手把手教你实现Android开发中的3D卡片翻转效果其中具体讲解了Rotate3dAnimation的实现原理,为了方便起见,这里会稍做修改,但最终的实现效果是完全相同的。

大家好,欢迎来到IT知识分享网。

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

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


–正文–

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

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

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

为了便于讲解实现原理,本文将通过通过一个简单的示例来进行展示,该示例的效果如下所示。

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

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

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

01

框架搭建

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

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

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

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

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

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

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

1.XML布局

Activity的布局非常简单,就是一个按钮和一个ImageView,代码如下(activity_rotate_ 3d.xml):

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="top|center_horizontal" tools:context=".Rotate3DActivity"> <Button android:id="@+id/btn_open" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="16dp" android:onClick="onClickView" android:text="翻转" android:textColor="@android:color/black" android:textSize="16sp"/> <LinearLayout android:id="@+id/content" android:layout_width="300dp" android:layout_height="200dp" android:layout_below="@id/btn_open" android:orientation="vertical" android:gravity="center_horizontal" android:layout_marginTop="16dp"> <ImageView android:id="@+id/iv_logo" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@mipmap/photo1" android:scaleType="centerCrop"/> </LinearLayout> </LinearLayout>

大家可能会觉得,在ImageView的外围又包了一个LinearLayout,这样做多此一举。

是的,从这里来看,是没有必要,但后面我们会修改这个布局文件,到时候LinearLayout就有用了。为了讲解方便,此处提前进行布局。

需要注意ImageView外围所包装的id为content的LinearLayout,注意它的位置,我们将会在后续的代码中用到。

2.Activity代码

因为我们是通过自定义Animation来旋转控件的,所以肯定会在onCreate函数中对Animation进行初始化,然后在点击按钮时执行startAnimation。

下面先列出完整的代码:

public class Rotate3DActivity extends AppCompatActivity { private View mContentRoot; private int duration = 600; private Rotate3dAnimation openAnimation; private Rotate3dAnimation closeAnimation; private boolean isOpen = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_rotate_3d); mContentRoot = findViewById(R.id.content); initOpenAnim(); initCloseAnim(); } private void initOpenAnim() { openAnimation = new Rotate3dAnimation(0, 180); openAnimation.setDuration(duration); openAnimation.setFillAfter(true); } private void initCloseAnim() { closeAnimation = new Rotate3dAnimation(180, 0); closeAnimation.setDuration(duration); closeAnimation.setFillAfter(true); } public void onClickView(View v) { if (openAnimation.hasStarted() && !openAnimation.hasEnded()) { return; } if (closeAnimation.hasStarted() && !closeAnimation.hasEnded()) { return; } if (isOpen) { mContentRoot.startAnimation(closeAnimation); }else { mContentRoot.startAnimation(openAnimation); } isOpen = !isOpen; } }

在代码中,我们自定义的Animation叫Rotate3dAnimation,具体实现会在后面详细讲解。

在onCreate函数中,是初始化环节:

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_rotate_3d); mContentRoot = findViewById(R.id.content); initOpenAnim(); initCloseAnim(); }

注意这里的mContentRoot,它就是XML中包裹ImageView的LinearLayout,表示需要旋转的控件的根布局。

从效果图可以看出,从0°到180°和从180°到0°,是两个不同的动画过程,分别用openAnimation和closeAnimation来表示。

下面只讲解openAnimation动画过程:

private void initOpenAnim() { openAnimation = new Rotate3dAnimation(0, 180); openAnimation.setDuration(duration); openAnimation.setFillAfter(true); }

从这里大概可以看出,Rotate3dAnimation有两个参数,分别是fromDegrees和endDegrees。因为我们需要在完成动画之后,让View保持完成动画时的状态,所以要用到setFillAfter(true)函数。

3.自定义Animation函数

该自定义Animation函数的主要作用是实现控件在中间位置从fromDegrees旋转到endDegrees。

重写Animation的函数比较简单,主要是重写如下两个函数:

public class Rotate3dAnimation extends Animation { public Rotate3dAnimation(float fromDegrees, float endDegrees) { } @Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); …// 在这里执行初始化操作 } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { …// 执行自定义动画操作 super.applyTransformation(interpolatedTime, t); } }

上面就是自定义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的代码比较简单,下面先全部列出,然后逐个讲解:

public class Rotate3dAnimation extends Animation { private final float mFromDegrees; private final float mEndDegree; private float mCenterX,mCenterY; private Camera mCamera; public Rotate3dAnimation(float fromDegrees, float endDegree) { mFromDegrees = fromDegrees; mEndDegree = endDegree; } @Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); mCenterX = width/2; mCenterY = height/2; mCamera = new Camera(); } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { float degrees = mFromDegrees + ((mEndDegree - mFromDegrees) * interpolatedTime); mCamera.save(); final Matrix matrix = t.getMatrix(); mCamera.rotateY(degrees); mCamera.getMatrix(matrix); mCamera.restore(); matrix.preTranslate(-mCenterX, -mCenterY); matrix.postTranslate(mCenterX, mCenterY); super.applyTransformation(interpolatedTime, t); } }

首先,在构造函数中,传入两个参数fromDegrees和endDegree,fromDegrees表示开始旋转的角度,endDegree表示结束旋转的角度。

然后,在initialize函数中执行初始化操作。根据本书1.2节的讲解可知,我们要围绕控件中心点旋转,因此需要获取控件中心点的位置坐标。所以,在初始化时,计算出控件中心点的位置坐标:

public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); mCenterX = width/2; mCenterY = height/2; mCamera = new Camera(); }

最后,执行applyTransformation函数中的操作。

其中:

第1步,根据当前进度计算出当前的旋转角度:

float degrees = mFromDegrees + ((mEndDegree - mFromDegrees) * interpolatedTime);

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

mCamera.save(); final Matrix matrix = t.getMatrix(); mCamera.rotateY(degrees); mCamera.getMatrix(matrix); mCamera.restore();

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

matrix.preTranslate(-mCenterX, -mCenterY); matrix.postTranslate(mCenterX, mCenterY);
手把手教你实现Android开发中的3D卡片翻转效果

第4步,调用super.applyTransformation(interpolatedTime, t)来执行改变过的动画操作,以将操作最终体现在控件上。

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

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

02

效果改进

1.图片缩放原理概述

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

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

图1

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

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

图2

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

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

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

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

图3

从当前的效果图可以看出,随着旋转角度的增加,倾斜之后的图像会变大,在旋转角度达到90°时图像最大。

同样地,要解决这个问题,就得随着图像变大,将View与Camera的距离增大,这样View就会变小。所以,这个View与Camera的距离变化过程就形成了上面的曲线。

当图像需要从0°旋转至90°时,View与Camera的距离需要越来越大,并在旋转到90°时达到最大。而当图像需要从90°旋转至180°时,整个距离变化过程与从0°旋转至90°时的相反,这点从曲线的变化情况就可以看出。

因此需要将图像从0°至180°的整个旋转过程分为两段,从0°旋转至90°时执行下面的代码,使View与Camera的距离逐渐增大:

z = mDepthZ * interpolatedTime; camera.translate(0.0f, 0.0f, z);

这里的mDepthZ是固定数值,默认值为400。如果动画中图像的旋转角度区间就是从0°旋转至90°,那么View与Camera的距离会随着动画的播放越变越大,在旋转角度达到90°时距离达到最大,这与图3中的情况相同。

而在第2段过程中,即从90°旋转至180°时,整个View与Camera的距离变化情况就要反过来,在90°时距离达到最大,在180°时距离回归到初始值:

z = mDepthZ * (1.0f - interpolatedTime); camera.translate(0.0f, 0.0f, z);

很明显,这段代码是符合要求的。所以,后面我们为了区分是从0°旋转至90°的逐渐增大曲线还是从90°旋转至180°的逐渐减小曲线,引入了一个reverse变量来进行标识。

2.改造Rotate3dAnimation

根据上面的原理,我们对Rotate3dAnimation函数进行改造,改造后的代码如下。下面先列出完整代码,然后详细讲解:

public class Rotate3dAnimation extends Animation { private final float mFromDegrees; private final float mEndDegree; private float mDepthZ = 400; private float mCenterX,mCenterY; private final boolean mReverse; private Camera mCamera; public Rotate3dAnimation(float fromDegrees, float toDegrees, boolean reverse) { mFromDegrees = fromDegrees; mEndDegree = toDegrees; mReverse = reverse; } @Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); mCamera = new Camera(); mCenterX = width/2; mCenterY = height/2; } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { float degrees = mFromDegrees + ((mEndDegree - mFromDegrees) * interpolatedTime); mCamera.save(); float z; if (mReverse) { z = mDepthZ * interpolatedTime; mCamera.translate(0.0f, 0.0f, z); } else { z = mDepthZ * (1.0f - interpolatedTime); mCamera.translate(0.0f, 0.0f, z); } final Matrix matrix = t.getMatrix(); mCamera.rotateY(degrees); mCamera.getMatrix(matrix); mCamera.restore(); matrix.preTranslate(-mCenterX, -mCenterY); matrix.postTranslate(mCenterX, mCenterY); super.applyTransformation(interpolatedTime, t); } }

首先看初始化函数,在初始化函数中有一个boolean reverse参数,这个参数用于标识曲线是逐渐增大的还是逐渐减小的。reverse为true时,表示距离逐渐增大;reverse为false时,表示距离逐渐减小。

然后在applyTransformation中,增加了沿Z轴移动的代码:

float z; if (mReverse) { z = mDepthZ * interpolatedTime; mCamera.translate(0.0f, 0.0f, z); } else { z = mDepthZ * (1.0f - interpolatedTime); mCamera.translate(0.0f, 0.0f, z); }

很明显,当mReverse为true时,View沿Z轴的移动距离随动画的播放而增大,在动画结束(interpolatedTime等于1)时达到最大。当mReverse为false时,View沿Z轴的移动距离随动画的播放而减小,在动画结束时,View沿Z轴的移动距离回归到0。

3.改造Activity

因为我们把原本从0°旋转至180°的动画拆成了两段,所以需要先执行从0°旋转至90°的动画,结束后接着执行从90°旋转至180°的动画,即核心代码如下:

private void initOpenAnim() { openAnimation = new Rotate3dAnimation(0, 90, true); openAnimation.setDuration(duration); openAnimation.setFillAfter(true); openAnimation.setAnimationListener(new AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { mLogoIv.setVisibility(View.GONE); mDescTv.setVisibility(View.VISIBLE); Rotate3dAnimation rotateAnimation = new Rotate3dAnimation(90, 180,false); rotateAnimation.setDuration(duration); rotateAnimation.setFillAfter(true); mContentRl.startAnimation(rotateAnimation); } }); }
手把手教你实现Android开发中的3D卡片翻转效果

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

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

private void initOpenAnim() { openAnimation = new Rotate3dAnimation(0, 90,true); … openAnimation.setInterpolator(new AccelerateInterpolator()); openAnimation.setAnimationListener(new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { Rotate3dAnimation rotateAnimation = new Rotate3dAnimation(90, 180,false); … rotateAnimation.setInterpolator(new DecelerateInterpolator()); mContentRoot.startAnimation(rotateAnimation); } … }); }
手把手教你实现Android开发中的3D卡片翻转效果

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

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

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

03

正背面显示不同的内容

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

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

可以看到,在图像旋转至90°时,ImageView显示的图像变为另一张图像。

方案一:通过替换图像资源实现

因为我们已经将从0°至180°的旋转过程划分为从0°至90°和从90°至180°这两个过程,所以在90°时为ImageView替换图像,即可实现背面显示另一张图像的效果,可扫码查看效果图。

首先,在点击“翻转”按钮的时候,给ImageView配置上初始图像:

public void onClickView(View v) { … if (isOpen) { ((ImageView)findViewById(R.id.iv_logo)).setImageResource(R.mipmap.photo2); mContentRoot.startAnimation(closeAnimation); }else { ((ImageView)findViewById(R.id.iv_logo)).setImageResource(R.mipmap.photo1); mContentRoot.startAnimation(openAnimation); } isOpen = !isOpen; }

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

private void initOpenAnim() { openAnimation = new Rotate3dAnimation(0, 90,true); openAnimation.setDuration(duration); openAnimation.setFillAfter(true); openAnimation.setInterpolator(new AccelerateInterpolator()); openAnimation.setAnimationListener(new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { ((ImageView)findViewById(R.id.iv_logo)).setImageResource(R.mipmap.photo2); … mContentRoot.startAnimation(rotateAnimation); } … }); }

整个代码的难度不大,这里就不再详述了。这样处理后,就实现了我们想要的效果。

方案二:使用多控件显示/隐藏实现

方案一只能解决同一个控件中显示不同内容的问题,但若要正背面显示不同的控件,就没办法了。

这时可以使用方案二,即在布局中引入两个ImageView控件,用从0°旋转至90°时显示一个控件而从90°旋转至180°时显示另一个控件的方式来实现。

将Activity的布局代码改为如下代码(activity_rotate_3d.xml):

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="top|center_horizontal" tools:context=".Rotate3DActivity"> <Button android:id="@+id/btn_open" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="16dp" android:onClick="onClickView" android:text="翻转" android:textColor="@android:color/black" android:textSize="16sp"/> <LinearLayout android:id="@+id/content" android:layout_width="300dp" android:layout_height="200dp" android:layout_below="@id/btn_open" android:orientation="vertical" android:gravity="center_horizontal" android:layout_marginTop="16dp"> <ImageView android:id="@+id/iv_logo" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@mipmap/photo1" android:scaleType="centerCrop"/> <ImageView android:id="@+id/iv_logo_2" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@mipmap/photo2" android:scaleType="centerCrop" android:visibility="gone"/> </LinearLayout> </LinearLayout>

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

private void initOpenAnim() { openAnimation = new Rotate3dAnimation(0, 90,true); openAnimation.setDuration(duration); openAnimation.setFillAfter(true); openAnimation.setInterpolator(new AccelerateInterpolator()); openAnimation.setAnimationListener(new AnimationListener() { … @Override public void onAnimationEnd(Animation animation) { ((ImageView)findViewById(R.id.iv_logo)).setVisibility(View.GONE); ((ImageView)findViewById(R.id.iv_logo_2)).setVisibility(View.VISIBLE); … mContentRoot.startAnimation(rotateAnimation); } }); }

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

private void initCloseAnim() { closeAnimation = new Rotate3dAnimation(180, 90,true); closeAnimation.setDuration(duration); closeAnimation.setFillAfter(true); closeAnimation.setInterpolator(new AccelerateInterpolator()); closeAnimation.setAnimationListener(new AnimationListener() { … @Override public void onAnimationEnd(Animation animation) { ((ImageView)findViewById(R.id.iv_logo)).setVisibility(View.VISIBLE); ((ImageView)findViewById(R.id.iv_logo_2)).setVisibility(View.GONE); … mContentRoot.startAnimation(rotateAnimation); } }); }

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

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

图4

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

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

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

启舰 著

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

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

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

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/50625.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信