来一份Android动画全家桶(上篇)

前言

自上次《MTRVA2.0来啦》发布后,马上就有小伙伴问我有哪些Android动画,过了一段时间又有小伙伴问我啥时候发布Android动画。其实,在写《MTRVA2.0来啦》的时候,这次要讲的Android动画已经完成的差不多了,而在写这篇文章的时候,下个版本的内容也快写的差不多了(捂脸)。想提前学习的同学可以去我的开源项目CrazyDaily的develop分支,然后再跟我的文章过一遍,挺不错的学习方法。废话不多说,Android动画似乎已经是老生常谈的技术点,老生常谈的技术感觉总是潜移默化成为Android程序员的必备技能。今天,大家再跟我一起过一遍Android动画及进阶使用。

 


有兴趣的加入Android工程师交流QQ群:752016839 主要针对Android开发人员提升自己,突破瓶颈,相信你来学习,会有提升和收获。在这个群里会有你所需要的内容,朋友们请抓紧时间加入进来吧

 

 

效果图

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

 

 

 

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

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

一个大类可能包含多种动画,比如转场动画用到了贝塞尔曲线的路径动画。再则说到Android动画总是避免不了扯到View,所以期间会带大家玩一玩自定义View。

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

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

分析

这次的分析随着效果图的展示顺序一一讲解,首先,向我们迎面走来的是...额,不是,首先,是我们的可能是动画趋势的lottie动画。

lottie

简介

lottie可能是一个革命性的动画库。为什么这么说?当然也只是我想这么说,先来看看lottie的震撼特点:

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

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

 

 

 

这么说吧,这种效果原生肯定是能写的,但是非常费脑子和精力,不信的同学可以尝试一下。其次用帧动画,缺点也很明显,资源占用很大。最简单的就是一张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]里面的东西,当然支持包里面也有,说不定有你想要的。

 

好吧 其他的写在下篇
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值