来一份Android动画全家桶

前言

自上次《MTRVA2.0来啦》发布后,立刻就有小伙伴问我有哪些Android动画,过了一段时间又有小伙伴问我啥时候发布Android动画。其实,在写《MTRVA2.0来啦》的时候,此次要讲的Android动画已经完成的差很少了,而在写这篇文章的时候,下个版本的内容也快写的差很少了(捂脸)。想提早学习的同窗能够去个人开源项目CrazyDaily的develop分支,而后再跟个人文章过一遍,挺不错的学习方法。废话很少说,Android动画彷佛已是老生常谈的技术点,老生常谈的技术感受老是潜移默化成为Android程序员的必备技能。今天,你们再跟我一块儿过一遍Android动画及进阶使用。html

效果图

国际惯例,No picture,say a J8!android

先来看看今天讲解的内容吧!git

  • lottie
  • 3D动画
  • 列表侧滑删除
  • 列表展开闭合
  • 转场动画进阶
  • 卫星菜单

一个大类可能包含多种动画,好比转场动画用到了贝塞尔曲线的路径动画。再则说到Android动画老是避免不了扯到View,因此期间会带你们玩一玩自定义View。程序员

高能预警:本篇较长,能够先选择本身喜欢的内容进行阅读或者收藏一下有空再看。github

舒适提示:想看针对第五章节额外附送的属性动画源码分析请点这里web

分析

此次的分析随着效果图的展现顺序一一讲解,首先,向咱们迎面走来的是...额,不是,首先,是咱们的多是动画趋势的lottie动画。面试

lottie

简介

lottie多是一个革命性的动画库。为何这么说?固然也只是我想这么说,先来看看lottie的震撼特色:算法

  • 跨平台,支持Web、Android、iOS和React Native
  • 在线更新动画,意味着不用发版和减小资源的占用体积等
  • 相对于原生动画和GIF更为轻量
  • 代码实现简单,易于上手和维护

咱们再来看看这样的一个效果图(官方效果图):json

这么说吧,这种效果原生确定是能写的,可是很是费脑子和精力,不信的同窗能够尝试一下。其次用帧动画,缺点也很明显,资源占用很大。最简单的就是一张GIF图片,没有什么动画是GIF搞不定的(手动滑稽),但这应该是最差的方案了。数组

而lottie只要一份记录动画信息的json文件,就能够在各大平台跑起来。是否是很炫酷?

Android

So Easy!除了这个词我还真没想到怎么形容。废话很少说,先上代码:

implementation 'com.airbnb.android:lottie:2.5.4' //gradle依赖

<com.airbnb.lottie.LottieAnimationView
        android:id="@+id/splash_lottie"
        android:layout_width="wrap_content"
        android:layout_height="@dimen/splash_height"
        app:lottie_autoPlay="true"
        app:lottie_fileName="lottie/mtrva.json"
        app:lottie_imageAssetsFolder="images/mtrva/" />
复制代码

是的,就是这么简单,只要把你的json文件传给LottieAnimationView,它就能够流畅地播放起来,像如何在代码中使用及其它我就不一一贴出来了,你们能够点这里

扩展

这才是重头戏,面试的时候说本身会实现复杂的动画多是个卖点,但如今彷佛这个工做能够被设计师取代,咱们先不谈什么级别的设计师,咱们先来讲说为啥咱们Android程序员不能够来完成这份工做呢?跟我抢饭碗?不可能!

正如项目中Splash页面底下文字[什么都懂一点,生活更多彩一些][廋金体],感兴趣就玩一玩,真的颇有意思!

简单分析一下,Splash页面的动画,logo图标沿着s型的路径,淡入,放大。很简单的一个动画,原生实现也是很简单的。重点在于如何开发这样的lottie动画,只须要Adobe After Effects+bodymovin就能够轻松导出一份这样的json文件。而详细的安装及环境配置能够点这里,不过英文要好哦,否则只能看看蹩脚的翻译。

简单的开发能够很快入门,我刚玩了一下子,就开发了这样的一个效果:

很惋惜,好像导出在Android端运行不太兼容,没有达到预期的效果,多是我使用姿式不对。就玩了一下子,弊端就体现出来了,这也是各类跨端的弊端,兼容性问题。一个库开源,使用者老是避免不了踩坑,例如上面xml中lottie_imageAssetsFolder属性添加是由于json中有图片资源,须要图片资源的路径且图片资源名要改成image_0,具体缘由能够打开json文件瞧一瞧。

那么有同窗问有没有什么现成的json吗?AE这玩意好像学起来挺麻烦的,这个确定有,lottiefiles挺不错的一个网站,点击preview能够拖拽你的json文件进行预览和简单地编辑。

关于lottie动画介绍差很少就到这里,关键点都说了,剩下的多是你的AE熟练度了。这玩意无法速成,须要大量经验积累!难点并非技术,而是创意!创意!创意!

3D动画

进入首页,最早刺激我眼球的是右下角的妹子,其次是它的动画,这样的效果我最早在百度贴吧上看到的,如今好像去掉了,这也是我第一家公司面试的时候的做品,颇有亲切感!

咱们简单地拆解一下动画,能够分为这些:3D翻转、平移、阴影渐变和vignette。

3D翻转

这个动画的核心,用的是补间动画,你也能够植入view中,我相信这对你来讲并不难。

拆解动画:view分为两面,一面翻上去或者一面翻下去,而后展现另外一面,因此分为4个动画TopInAnimation、TopOutAnimation、BottomInAnimation和BottomOutAnimation,Top和Bottom为反向操做,所以这里只分析Top。

若是没图,我猜有些同窗很差理解,这里给出一张中间单帧图,画得很差,谅解,谅解。

不知道你们了解setRotationX这个方法吗,不清楚的能够点这里。最多见就是咱们的车轮,车轴就是X轴,而后侧着看。这样一想,是否是A和B都在作rotationX动画,但这是不够的,假如A面绕的X轴是高度中间等分线,直到A消失,也是消失在等分线的位置,脑补一下,而事实是A消失于顶部水平线,所以得作平移动画,也就是一边rotationX一边translationY。

了解这个后,咱们再来了解两个核心类CameraMatrix。篇幅缘由,只给出连接,你们能够深刻了解,其实就算整片文章都介绍,那也说不完。这里说一下Camera的主要做用是将3D模型投影在2D的屏幕上,而Matrix主要经过一个3x3的矩阵对图像进行平移、旋转、缩放和错切变化。

在上代码以前,补充一个知识,左右手坐标系。

Camera是基于左手坐标系的,它也应该是基于OpenGL的,OpenGL貌似是右手坐标系,而Android屏幕坐标系的Y轴方向正好与Camera的Y轴方向相反。

Camera比较好理解,你能够想象摄影大哥扛着摄像机对着屏幕左上角(原点),这个挺形象吧!Camera的默认位置是(0,0,-8),单位是英寸。Matrix相对比较复杂一点,你们能够看看这篇文章Android中图像变换Matrix的原理、代码验证和应用,这是我早期学习时收藏的文章,优秀!

简单介绍完这两大核心类,咱们来看看在项目中的运用:

static class TopOutAnimation extends Animation {
	...
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        mCamera.save();
        final float rotate = -DEGREE + DEGREE * interpolatedTime;
        mCamera.rotateX(rotate);
        mCamera.getMatrix(mMatrix);
        mCamera.restore();
        mMatrix.preTranslate(-mWidth / 2, 0);
        mMatrix.postTranslate(mWidth / 2, mHeight - mHeight * interpolatedTime);
        t.getMatrix().postConcat(mMatrix);
    }
}

 static class TopInAnimation extends Animation {
 	...
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        mCamera.save();
        final float rotate = DEGREE * interpolatedTime;
        mCamera.translate(0, -mHeight + interpolatedTime * mHeight, 0);
        mCamera.rotateX(rotate);
        mCamera.getMatrix(mMatrix);
        mCamera.restore();
        mMatrix.preTranslate(-mWidth / 2, -mHeight);
        mMatrix.postTranslate(mWidth / 2, 0);
        t.getMatrix().postConcat(mMatrix);
    }
}
复制代码

先来分析一下TopOutAnimation,当interpolatedTime=0的时候,rotate=-90即模型是与屏幕是垂直的,当interpolatedTime=1的时候,rotate=0即正常位置。preTranslate是在旋转前平移模型的位置,至于为何是-mWidth/2和0,缘由也很简单,记住Camera核心做用就好了,就是我上面说的,将3D模型投影在2D的屏幕上,因为Camera的初始位置就是屏幕的原点,作旋转的时候,投影到画布的图形确定是不正常的,由于不是正向的,那么只要在camera旋转前向左平移宽度的一半即为正向,但要在旋转后回到原来的位置,所以调用postTranslate且x值为mWidth/2。我相信这个比较好理解,因此不贴图了,至于preTranslate的y值是0是由于咱们初始化定义的camera的rotateX是-90即与屏幕垂直,是这样的一个变换过程:

有同窗问,那我可不可一开始就在底部,这样我postTranslate的时候就不用移动了,从逻辑上一点毛病都没有,但事实上效果诧异,为何呢?由于Matrix操做的是咱们的模型而非屏幕,甚至移动的距离是原来的几倍,还发现变小了,为何呢?这很简单,你离光源越远,确定越小,至于为啥移动距离是原来的几倍,我给你画张图:

那么是否是就这一种方法呢?那确定不是,感兴趣的同窗能够本身去尝试,本身动手实践过才印象最深入。回到咱们讨论点,因为咱们preTranslate的y值是0使得投影效果在最顶部所以须要最终的view从底部不断往上偏移,故调用postTranslate的y值是mHeight-mHeight*interpolatedTime。

有了TopOutAnimation的基础分析,咱们理解TopInAnimation相对容易一点。

上面的代码其实能够改为这样:

static class TopInAnimation extends Animation {
 	...
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        mCamera.save();
        final float rotate = DEGREE * interpolatedTime;
        mCamera.rotateX(rotate);
        mCamera.getMatrix(mMatrix);
        mCamera.restore();
        mMatrix.preTranslate(-mWidth / 2, -mHeight);
        mMatrix.postTranslate(mWidth / 2, mHeight - interpolatedTime * mHeight);
        t.getMatrix().postConcat(mMatrix);
    }
}
复制代码

那是否是Camera的y轴平移等同于Matrix的postTranslate平移呢?只能说近似等同。此次我直接画变换过程:

最后跟TopOutAnimation同样调用postTranslate的平移就好了。

关于3D翻转就先介绍到这,不懂的能够问我,若是哪里不对的或者有歧义的地方欢迎指正。

平移

这个就比较简单了,直接上代码:

public void start(boolean isTop, int index) {
        final float distance = -mTranslationYDistance * index;
    if (isTop) {
        mForegroundView.startAnimation(mTopOutAnimation);
        mBackgroundView.startAnimation(mTopInAnimation);
        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f)
                .setDuration(DURATION);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.addUpdateListener(animation -> {
            final float value = (float) animation.getAnimatedValue();
            final float oppositeValue = 1 - value;
            ...
            setTranslationY(distance * oppositeValue);
        });
        animator.start();
    } else {
        mForegroundView.startAnimation(mBottomInAnimation);
        mBackgroundView.startAnimation(mBottomOutAnimation);
        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f)
                .setDuration(DURATION);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.addUpdateListener(animation -> {
            final float value = (float) animation.getAnimatedValue();
            final float oppositeValue = 1 - value;
			...
            setTranslationY(distance * value);
        });
        animator.start();
    }
}
复制代码

代码也给出3D翻转的调用,向上平移的时候向下翻转,向下平移的时候向上翻转,平移动画用的是ValueAnimator,由于咱们还须要根据value计算阴影的颜色,一块儿来看看吧!

阴影渐变

为啥要搞个渐变呢?由于要真,当立方体翻滚的时候,因为光线缘由,一部分确定愈来愈暗,一部分愈来愈亮。卧槽,物理这么6?还行,也就每次考试90来分。

//top
int foreStartColor = (int) mArgbEvaluator.evaluate(oppositeValue * 0.8f, 0x00000000, 0xff000000);
int foreCenterColor = (int) mArgbEvaluator.evaluate(oppositeValue * 0.4f, 0x00000000, 0xff000000);
int[] foreColors = {foreStartColor, foreCenterColor, 0x00000000};
int backStartColor = (int) mArgbEvaluator.evaluate(value * 0.8f, 0x00000000, 0xff000000);
int backCenterColor = (int) mArgbEvaluator.evaluate(value * 0.4f, 0x00000000, 0xff000000);
int[] backColors = {backStartColor, backCenterColor, 0x00000000};
mForegroundDrawable.setColors(foreColors);
mBackgroundDrawable.setColors(backColors);
//bottom
int foreStartColor = (int) mArgbEvaluator.evaluate(value * 0.8f, 0x00000000, 0xff000000);
int foreCenterColor = (int) mArgbEvaluator.evaluate(value * 0.4f, 0x00000000, 0xff000000);
int[] foreColors = {foreStartColor, foreCenterColor, 0x00000000};
int backStartColor = (int) mArgbEvaluator.evaluate(oppositeValue * 0.8f, 0x00000000, 0xff000000);
int backCenterColor = (int) mArgbEvaluator.evaluate(oppositeValue * 0.4f, 0x00000000, 0xff000000);
int[] backColors = {backStartColor, backCenterColor, 0x00000000};
mForegroundDrawable.setColors(foreColors);
mBackgroundDrawable.setColors(backColors);
复制代码

mArgbEvaluator是Android提供的ArgbEvaluator类用来根据[0,1]某个值获取两种颜色渐变过程该进度时候的颜色值,好像挺拗口。咱们这里操做的是drawable来实现渐变,那么drawable是谁的呢?是用户设置的子view吗?那确定不是,否则要被打死。是咱们在获取用户子view的时候在这基础上添加了一个ImageView与子view平级,上代码:

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    ...
    View foregroundView = getChildAt(1);

    FrameLayout foregroundLayout = new FrameLayout(context);
    LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);

    removeAllViewsInLayout();
    ...
    ImageView foregroundImg = new ImageView(context);
    ...
    foregroundImg.setImageDrawable(mForegroundDrawable);
    ...
    foregroundLayout.addView(foregroundView);
    foregroundLayout.addView(foregroundImg);

    addViewInLayout(foregroundLayout, 0, params);
    ...
    mForegroundView = (FrameLayout) getChildAt(1);
}
复制代码

这里只给出ForegroundView的代码,其它贴出来毫无心义,代码也很简单,在子view外层套一层FrameLayout,而后添加负责阴影渐变的ImageView,渐变效果与咱们平移时进度有关。

vignette

之因此加这个,跟咱们的阴影渐变同样,要真。什么是vignette?vignette大概就是暗角的意思。举个栗子?

其主要特征就是在图像四个角有较为显著的亮度降低。这种效果本身去研究又是一堆公式,好在有大佬提供了方便。这里咱们使用GPUImage来达到这样的效果,该库主要提供各类各样的图像处理滤镜,而且支持照相机和摄像机的实时滤镜。很惋惜的是这玩意创意来自于iOS。

那咱们如何在Android中使用呢?

GPUImageVignetteFilter filter = new GPUImageVignetteFilter();
filter.setVignetteCenter(new PointF(0.5f, 0.5f));
filter.setVignetteColor(new float[]{0.0f, 0.0f, 0.0f});
filter.setVignetteStart(0.0f);
filter.setVignetteEnd(0.75f);
GPUImage gpuImage = new GPUImage(context);
BitmapDrawable bitmapDrawable = (BitmapDrawable) ContextCompat.getDrawable(context, id);
gpuImage.setImage(bitmapDrawable.getBitmap());
gpuImage.setFilter(filter);
Bitmap bitmap = gpuImage.getBitmapWithFilterApplied();
复制代码

id是咱们的资源id,主要就是要拿到一个bitmap对象,其它还支持file和uri。vignetteCenter就是咱们向外扩展的起始点,通常在中心,例如咱们上面的图片例子,vignetteColor是暗角的颜色,vignetteStart和vignetteEnd就是渐变程度。

3D动画是否是So Easy?是否是很炫酷?想不想本身实现一个?

关于3D动画先到这结束了,占用篇幅相对较长,看到这挺有耐心。

列表侧滑删除

long long ago,这个效果貌似是模仿...模仿啥来着?记不起来了,算了,早期的时候我也写过这个玩意,代码找不到了,哈哈,其实挺简单,主要就是触摸事件的运用,难点多是用户体验,滑起来卡卡的,那玩个毛。

但如今都什么年代了,实现这种效果更简单了,Android直接给咱们提供了ItemTouchHelper快速实现拖拽和侧滑删除。但须要简单地修改一下,由于Android提供给咱们的好像与预期差了那么一点点,因为我比较懒且正好看到一个库itemtouchhelper-extension是针对侧滑删除的,体验还不错,但仍是无法知足个人需求,例如我想用户关闭界面的时候知道菜单是不是打开状态。那我只能使用CV大法而后修改,果真最灵活的仍是CV源码进行修改。

本节主要是想介绍ItemTouchHelper,除了这,Android还提供了不少RecyclerView的帮助类,例如DiffUtil[MTRVA就是基于这个],SnapHelper[帮助咱们在某一个地方停住,例如你想尝试用RecyclerView实现卡片滑动,不妨试试],这两个比较经常使用,固然还有其它的哦,你们不妨去看看这个包[androidx.recyclerview.widget]里面的东西,固然支持包里面也有,说不定有你想要的。

列表展开闭合

关于这个点貌似挺尴尬的,由于用MTRVA能够轻松实现,我都不知道要不要介绍,关键有几个用户同窗会问这个,趁这个机会简单说一下吧。

好比效果图中到联系人列表有展开闭合的效果,怎么实现的呢?首先咱们要排除直接操做view,好比让view隐藏等等。这是不可取的,应该用数据去驱动UI,这个我已经强调不少遍了。先看一看代码:

private void switchStatus(ContactHeader item, View icon) {
    final int index = mData.indexOf(item);
    final String flag = item.getIndex();
    if (item.isFlod()) {
        icon.animate().rotation(90).setDuration(500).start();
        mHelper.addData(index + 1, item.getChilds());
    } else {
        icon.animate().rotation(-90).setDuration(500).start();
        ...
        childs = mHelper.removeData(index + 1, count);
        item.setChilds(childs);
    }
    item.switchStatus();
    mHelper.setData(index, item);
}
复制代码

很简单的逻辑,若是当前是闭合状态,icon须要选择90度且添加数据,反之,旋转-90度并移除数据。

关键点就在于addData和removeData,底层调用的是adapter的notifyItemRangeInserted和notifyItemRangeRemoved方法,所以是附带动画的,且对数组越界作了优化,例如addData的position大于或等于集合数量,那么直接将应添加的数据直接添加在集合的末尾,而不是抛异常。

本节主要但愿你们利用添加和移除数据来达到展开闭合的效果。

转场动画进阶

每次玩Material Design产品的时候,老是有不少炫酷的转场动画刺激我。相信也有不少同窗跟我同样,很喜欢这种风格。今天我就带你们了解一下转场动画并自定义转场动画,走起!

常见转场动画

场景动画核心框架

  • Scene:用来保存场景应用中View 的属性集合
  • Transition:负责元素的过渡,你能够在不一样场景根据属性值操做元素打造不一样的动画
    • 普经过渡
      • Explode:从场景中心进入或移除,一种爆炸的感受
      • Fade:最熟悉的淡入淡出
      • Slide:从场景边缘进入或移除
      • AutoTransition:默认过渡,Fade+ChangeBounds
    • 共享元素过渡
      • ChangeBounds:根据场景先后布局界限变化执行过渡动画
      • ChangeClipBounds:根据场景先后getClipBounds变化执行过渡动画
      • ChangeImageTransform:根据场景先后ImageView的矩阵变化执行过渡动画
      • ChangeScroll:根据场景先后目标滚动属性的变化执行过渡动画
      • ChangeTransform:根据场景先后视图缩放和旋转变化执行过渡动画,固然也能够根据父视图的改变
    • 场景切换调用[共享元素添加SharedElement]
      • setEnterTransition:A->B,B进入的过渡
      • setExitTransition:A->B,A退出的过渡
      • setReturnTransition:B->A,B返回的过渡
      • setReenterTransition:B->A,A重进的过渡
  • TransitionManager:把上面Scene和Transition结合起来,常见的有经过setTransition(Scene, Transition)结合

实战

前一小节貌似有点翻译的味道,但不少都是我亲自体验总结的,同时这也是必不可少的,咱们要理论结合实践,再从实践中领悟真理。

场景分析

以联系人列表页为出发点,点击条目跳转到联系人详情页。期间发生了什么呢?

列表页条目的头像(其实过渡的时候已是详情页的头像了)以贝塞尔曲线路径移动且缩放至详情页的头像;地址和名字平滑过渡到详情页;在共享元素过渡的时候,下一个场景也开始了进场动画,右下角的菜单跟小球同样弹跳下来同时背景以屏幕中心以圆形向外扩散。

代码分析

首先咱们先分析一下进场动画,由于这个相对比较好理解。

public class CircularRevealTransition extends Visibility {

	private static final String PROPNAME_ALPHA = "crazysunj:circularReveal:alpha";
	private static final String PROPNAME_RADIUS = "crazysunj:circularReveal:radius";
	private static final String PROPNAME_TRANSLATION_Y = "crazysunj:circularReveal:translationY";
	
	@Override
	public void captureStartValues(TransitionValues transitionValues) {
	    super.captureStartValues(transitionValues);
	    transitionValues.values.put(PROPNAME_ALPHA, 0.2f);
	    final View view = transitionValues.view;
	    transitionValues.values.put(PROPNAME_RADIUS, 0);
	    transitionValues.values.put(PROPNAME_TRANSLATION_Y, -view.getBottom());
	}
	
	@Override
	public void captureEndValues(TransitionValues transitionValues) {
	    super.captureEndValues(transitionValues);
	    transitionValues.values.put(PROPNAME_ALPHA, 1.0f);
	    final View view = transitionValues.view;
	    int radius = (int) Math.hypot(view.getWidth(), view.getHeight());
	    transitionValues.values.put(PROPNAME_RADIUS, radius);
	    transitionValues.values.put(PROPNAME_TRANSLATION_Y, 0);
	}
	
	@Override
	public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
	    if (null == startValues || null == endValues) {
	        return null;
	    }
	    final int id = view.getId();
	    switch (id) {
	        case R.id.satellite_menu:
	            int startTranslationY = (int) startValues.values.get(PROPNAME_TRANSLATION_Y);
	            float startAlpha = (float) startValues.values.get(PROPNAME_ALPHA);
	            int endTranslationY = (int) endValues.values.get(PROPNAME_TRANSLATION_Y);
	            float endAlpha = (float) endValues.values.get(PROPNAME_ALPHA);
	            PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", startTranslationY, endTranslationY);
	            PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", startAlpha, endAlpha);
	            ObjectAnimator menuAnim = ObjectAnimator.ofPropertyValuesHolder(view, translationY, alpha);
	            menuAnim.setInterpolator(new BounceInterpolator());
	            menuAnim.setDuration(1000);
	            return menuAnim;
	        case R.id.cool_bg_view:
	            int startRadius = (int) startValues.values.get(PROPNAME_RADIUS);
	            int endRadius = (int) endValues.values.get(PROPNAME_RADIUS);
	            Animator coolAnim = new NoPauseAnimator(ViewAnimationUtils.createCircularReveal(view, view.getWidth() / 2, view.getHeight() / 2, startRadius, endRadius));
	            coolAnim.setDuration(1000);
	            coolAnim.setInterpolator(new AccelerateDecelerateInterpolator());
	            return coolAnim;
	        default:
	            return null;
	    }
	}
	
	@Override
	public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
	  ...
	}
	...
}   
复制代码

captureStartValues用来保存动画须要的初始值,key-value的形式,至于key能够模仿系统以两个':'分离,captureEndValues用来保存动画须要的结束值。咱们这里,小球透明度从0.2开始,同时从相同x的屏幕顶部掉落,背景初始的半径为0;结束时小球透明度正常,小球回到老地方,背景扩展至满屏,这里半径取了背景宽高的平方和的二次方根,关于值你们能够本身调。

重点是继承了Visibility,像这种进场动画最好继承Visibility,由于它很好地提供了view的出现和消失方法。

咱们再来看看onAppear方法,咱们利用id来标识一个view,但这样就不灵活了。咱们利用PropertyValuesHolder把translationY和alpha在一个view上同时执行,像这种针对同一个view且须要执行多个属性动画,就能够采用PropertyValuesHolder。背景的圆形扩散能够用ViewAnimationUtils来建立,这是Android提供的,必属精品。传入操做的view,扩散点坐标以及起始和结束半径。

onDisappear就是一个相反的过程,就不介绍了。咱们再来看看共享元素动画代码:

public class BezierTransition extends Transition {
	...
    public BezierTransition() {
        setPathMotion(new PathMotion() {
            @Override
            public Path getPath(float startX, float startY, float endX, float endY) {
                Path path = new Path();
                path.moveTo(startX, startY);
                float controlPointX = (startX + endX) / 4;
                float controlPointY = (startY + endY) * 1.0f / 2;
                path.quadTo(controlPointX, controlPointY, endX, endY);
                return path;
            }
        });
    }

    private void captureValues(TransitionValues transitionValues) {
        Rect rect = new Rect();
        transitionValues.view.getGlobalVisibleRect(rect);
        transitionValues.values.put(PROPNAME_SCREEN_LOCATION, rect);
    }

    @Override
    public void captureStartValues(@NonNull TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public void captureEndValues(@NonNull TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot,
                                   TransitionValues startValues, TransitionValues endValues) {
        if (null == startValues || null == endValues) {
            return null;
        }
        final int id = startValues.view.getId();
        if (id <= 0) {
            return null;
        }
        Rect startRect = (Rect) startValues.values.get(PROPNAME_SCREEN_LOCATION);
        Rect endRect = (Rect) endValues.values.get(PROPNAME_SCREEN_LOCATION);
        final View view = endValues.view;
        Path path = getPathMotion().getPath(startRect.centerX(), startRect.centerY(), endRect.centerX(), endRect.centerY());
        return ObjectAnimator.ofObject(view, new PropPosition(PointF.class, "position", new PointF(endRect.centerX(), endRect.centerY())), null, path);
    }
    ...
}
复制代码

首先发现咱们继承的是Transition,其次在构造函数里面咱们执行了setPathMotion方法。PathMotion是Transition的一种扩展,提供以某种路径运动,有两个实现类ArcMotionPatternPathMotion,感兴趣的能够研究下它的算法。前者是三阶贝塞尔曲线,后者是矢量曲线。

咱们先来看看咱们本身实现的有点low的算法,哈哈,就是一个简单的贝塞尔路径,难点多是控制点的计算,如何让路径更优雅,这个艰巨的任务就交给大家了。

若是继承Transition,咱们实现的是createAnimator,拿到咱们建立的path,经过ObjectAnimator传入PropPosition实现的。

static class PropPosition extends Property<View, PointF> {
    PointF endPoint;
    PropPosition(Class<PointF> type, String name, PointF endPoint) {
        super(type, name);
        this.endPoint = endPoint;
    }
    @Override
    public void set(View view, PointF value) {
        int x = Math.round(value.x);
        int y = Math.round(value.y);
        int startX = Math.round(endPoint.x);
        int startY = Math.round(endPoint.y);
        int transY = y - startY;
        int transX = x - startX;
        view.setTranslationX(transX);
        view.setTranslationY(transY);
    }
    @Override
    public PointF get(View object) {
        return null;
    }
}

ObjectAnimator ofObject (T target, 
                Property<T, V> property, 
                TypeConverter<PointF, V> converter, 
                Path path)
复制代码

可能ObjectAnimator的这个方法咱们不经常使用,其实也很简单,就是根据传入的path,每一个进度会回调一个对象,咱们这里是PointF,因为默认的就是PointF,因此咱们第三个参数传null就好了。假设咱们须要回调的是Point对象,那么咱们要实现一个TypeConverter<PointF, Point>,很好理解,就是用来类型转换的。

PropPosition的set方法回调中会返回每一个进度根据path计算出来的PointF,这样咱们就能够经过结束点计算出view须要平移的距离。

我知道你们很好奇是怎么传值的,这里我单独写了一篇文章玩一玩Android属性动画源码来辅助你们理解。

回到咱们的主题,既然类已经写完了,如何调用的呢?

//详情页
private void initTransition() {
    Window window = getWindow();
    TransitionSet set = new TransitionSet();
    AutoTransition autoTransition = new AutoTransition();
    autoTransition.excludeTarget(R.id.ic_head, true);
    autoTransition.addTarget(R.id.tx_name);
    autoTransition.addTarget(R.id.ic_location);
    autoTransition.addTarget(R.id.tx_location);
    autoTransition.setDuration(600);
    autoTransition.setInterpolator(new DecelerateInterpolator());
    set.addTransition(autoTransition);
    
    BezierTransition bezierTransition = new BezierTransition();
    bezierTransition.addTarget(R.id.ic_head);
    bezierTransition.excludeTarget(R.id.tx_name, true);
    bezierTransition.excludeTarget(R.id.ic_location, true);
    bezierTransition.excludeTarget(R.id.tx_location, true);
    bezierTransition.setDuration(600);
    bezierTransition.setInterpolator(new DecelerateInterpolator());
    set.addTransition(bezierTransition);
    
    CircularRevealTransition transition = new CircularRevealTransition();
    transition.excludeTarget(android.R.id.statusBarBackground, true);
    
    window.setEnterTransition(transition);
    window.setReenterTransition(transition);
    window.setReturnTransition(transition);
    window.setExitTransition(transition);
    
    window.setSharedElementEnterTransition(set);
    window.setSharedElementReturnTransition(set);
}
复制代码

根据前面知识普及,我相信不须要解释太多,可能须要解释的地方就是addTarget和excludeTarget。addTarget就是指定参与过渡的view,excludeTarget就是排除过渡的view。

有些小伙伴可能对那个背景很好奇,它并非一张背景图,中间以贝塞尔曲线分割,下面是白色,上面是前一个场景高斯模糊。贝塞尔曲线虽然不是什么很新鲜的东西,可是运用普遍,好比lottie章节开发用的AE中钢笔的运用就是贝塞尔曲线。

小总结

整个过程下来,是否是发现转场动画也并不难,有些同窗看到这可能已经本身写了几个过渡动画了。这我就很开心了,能帮到你们真正运用这些知识。固然了不要忘了添加windowContentTransitions属性哦,还有windowAllowEnterTransitionOverlap和windowAllowReturnTransitionOverlap来控制两个场景的动画要不要同步。NM的,咋不早说?

卫星菜单

我记得在我毕业的时候,这玩意挺火的,看起来也很牛皮,实则实现起来很简单。能够说是动画的入门,那为啥要放这里呢?我就放这里,没说我要分析啊,告诉一下你们项目有这个动画。

骚聊

又到了紧张刺激的骚聊环节。到这里,确定有小伙伴质疑我了,你肯定这是Android动画全家桶吗?我能够很负责任的告诉你,是。只不过是普通规格的全家桶。不管是动画的种类仍是动画的用法确定是不全的,可是经常使用的已经八九不离十,重点是给你们总结个大概,感兴趣的能够深刻了解。

趁此次机会,我回答一下,不少同窗问个人问题,Android到什么地步才算厉害?首先我以为这种问题很无聊,其次我本身的水平也并不高,不知道有没有资格回答。

鄙人在这发表一得之见,不少同窗可能认为懂底层源码的人才是牛皮。懂底层确实很牛皮,但不是最牛皮的,只要你有耐心去阅读分析,不断深刻,我相信你也能够和大佬同样写出深入的源码分析,可能用的时间比大佬长那么一点,那么结果就出来了,学习能力。在这技术不断迭代更新的时代,最重要的是学习能力,由于可能你今天用的技术明天就被弃用了,固然这可能夸张一点,正现在天的Android动画分享,学习能力强的人看一遍本身过一遍已经能够触类旁通了,再则,源码也是人写的,也是有迭代的,那你是否是须要从新分析一遍?看源码更多的是看大牛如何写代码,而后学以至用,今天主题动画的难点不是源码也不是如何使用,而是动画的创意!

哦,对了,我答应别人天天只能吹#个牛,祝你们生活愉快,溜了,溜了。有问题能够加我好友,我博客有联系方式,文章的代码都是开源项目CrazyDaily里面的。

最后,感谢一直支持个人人!

传送门

Github:github.com/crazysunj/

博客:crazysunj.com/

相关文章
相关标签/搜索