原标题:【MIUI动效】Android:会呼吸的悬浮气泡

写在前面

这个标题看起来玄乎玄乎的,其实一张图就明白了:

Android 开发 气泡指示器 安卓气泡效果_Android 开发 气泡指示器

悬浮气泡演示图

最早看到这个效果是 MIUI6系统升级界面,有很多五颜六色的气泡悬浮着,觉得很好看。可惜现在找不到动态图了。虽然 MIUI8更新界面也有类似的气泡,不过是静态的,不咋好看。

Android 开发 气泡指示器 安卓气泡效果_android属性动画 呼吸_02

MIUI8

再次见到这个效果是在 Pure天气这款软件中,可惜开发者不开源。不过万能的 Github上有类似的实现,于是果断把自定义 View部分抽出来学习学习。

Android 开发 气泡指示器 安卓气泡效果_子线程_03

Pure

怀着敬意放上原项目地址,很好看的一款天气 APP:

还是那句话,学习自定义 View没有什么捷径,就是看源码、模仿、动手。

具体实现先思考

在看源码之前,我自己想了一下该怎样去实现,思路如下:

自定义一个圆形 View,支持大小、颜色、位置等属性

浮动利用最简单的平移动画来实现

平移的范围通过自定义圆心的移动范围来确定

最后给动画一个循环就行了

虽然看起来比较简单,但是实现起来还是遇到不少坑。首先画圆一点问题都没有,问题出在动画上。动画看起来很迟钝,根本就不是呼吸效果,像哮喘一样。

所以不能用动画,就想到了不断重绘。于是仍然给圆心设置一个小圆,让圆心在小圆上移动,在这个过程中不断重绘,结果直接 Crash了,看了看 Log,发现是线程阻塞了,但是这里并没有开启子线程啊,一看,我去,主线程。

那这条路行不通,又想到用贝塞尔去做,结果突然想起来之前绘制阻塞了主线程,那开子线程绘制不就完了,Android View里面能开子线程绘制的不就是 SurfaceView。于是看了看作者源码,果然是自定义 SurfaceView。

Android 开发 气泡指示器 安卓气泡效果_Android 开发 气泡指示器_04

早已看穿一切

关于 SurfaceView我只在以前学习的视频案例、撕MM衣服案例、还有手写板案例中遇到过,学的不是很深,加上本文它不是重点,所以就不详细说了,如果不了解这个或者想深入了解一下的话,可以点击文末的相关链接,这里只简单提一下比较重要的一点,也就是 SurfaceView跟 View的主要区别:

SurfaceView在一个新起的单独线程中重新绘制画面,而 View必须在 UI线程中更新画面。

这就决定了 SurfaceView的一些特定使用场景:

需要界面迅速更新;

对帧率要求较高的情况;

渲染 UI需要较长的时间。

所以综合来看,SurfaceView无疑是实现这类效果的最佳选择。

再分析

废话不多说,来分析一下思路。

1、首先光从界面上能看到就是圆,且是能浮动的圆,所以不管能不能动,先得把圆画出来。要是我的话,我直接就拿着 Paint在 Canvas上开画了。在源码中开发者单独抽取了绘制圆的类,但这个类的作用不仅仅是绘制圆,后面我们再说。

2、其次就是自定义 SurfaceView,我们需要把画出来的圆放到 SurfaceView中。而自定义 SurfaceView需要实现 SurfaceHolder.Callback接口,就是一些回调方法。同时需要开子线程去不断刷新界面,因为这些圆是需要动起来的.

3、另外重要的一点就是,SurfaceView在渲染过程中需要消耗大量资源,比如内存啊、CPU啊之类的,所以最好提供一个生命周期相关的方法,让它和 Activity的生命周期保持一致,尽量保证及时回收资源,减少消耗。

4、最后需要提一点的是,SurfaceView本身并不需要绘制内容,或者说在这里它的主要作用就是刷新界面就行了。就好像在放视频的时候,只需要刷新视频页面就行,它并不参与视频具体内容的绘制。

所以这样来说的话,我们最好定义一个绘制过程的中间者,主要作用就是把绘制出来的圆放在 SurfaceView上,同时也能做一些其他的工作,比如绘制背景、设置尺寸等。这样做的好处就是能让 SurfaceView专心的做一件事:不断刷新,这就够了。

OK,总结一下我们到底需要哪些东西:

专门绘制圆的类

刷新过程中的子线程

实现 SurfaceHolder.Callback接口方法

提供生命周期相关方法

一个绘制过程的中间对象

多提一句,最后的绘制中间者也可以不定义,全部封装到自定义 SurfaceView中,但是从我实践来看,我最后不得不单独抽取出来,因为 SurfaceView类看起来太乱了,这也是源码中的实现方式。

Android 开发 气泡指示器 安卓气泡效果_android属性动画 呼吸_05

23333

后动手

Talk is cheap , Show me the code .

1、画圆

既然要画圆,我们肯定要设置一些圆的基本属性:

圆心坐标

圆的半径

圆的颜色

由于需要圆动起来,也就是说它会偏移,所以要确定一个范围。范围确定了,就需要指定它该怎么变化,因为我们要求它缓慢而顺畅的呼吸,不能瞬间大喘气,也就是它不能瞬间移动偏移量那么多,所以最好指定它每一步变化多少,那就需要下面这两样东西:

圆心偏移范围

每一帧的变化量

额外的,因为移动是每次都需要变的,下一次变化时不能重新开始,所以我们要记录当前已经偏移的距离,然后根据一个标志位不断呼气...吐气...呼气...吐气,所以需要:

当前帧变化量

标志位

好了,看构造函数吧:

Android 开发 气泡指示器 安卓气泡效果_自定义_06

好了,构造好了圆就要开始绘制圆了。之前说到,这个类的作用不仅仅是绘制圆,还要不断更新圆的位置,也就是不断重绘圆。更直接地说,我们需要绘制出不断偏移的每一帧的圆。

步骤如下:

确定当前帧偏移位置

根据当前帧偏移位置计算圆心坐标

设置圆的颜色透明度等属性

真正的开始绘制圆

代码如下,结合上面的步骤和代码中的注释应该很容易看懂:

Android 开发 气泡指示器 安卓气泡效果_生命周期_07

其中的 convertAlphaColor()方法是个工具方法,作用就是转化一下颜色,不必深究:

Android 开发 气泡指示器 安卓气泡效果_android属性动画 呼吸_08

到此,画每一帧圆的工作我们就完成了。

2、绘制中间者对象

现在来说这个特殊的中间者对象,前文说了,单独抽取这个类不是必须的。但最好抽取一下,让 SurfaceView专心做自己的事情。在这个中间者对象中我们做两件事情:

绘制背景

绘制悬浮气泡

先来看绘制背景。为什么需要绘制背景呢,因为 SurfaceView 本身其实是个黑色,从我们日常看视频的软件中也能发现,视频播放时周围都是黑色的。有人问为什么不能直接在布局中设置呢?当然可以直接设置啊,不过要记得添加一句 setZOrderOnTop(true),不然会把之后绘制的悬浮气泡遮挡住。

在这里就来绘制一下吧,因为源码中给出了一个渐变色的绘制,我觉得挺好玩,学一学。直接看代码吧,都是模板代码,没啥好解释的,简单的 get/set再画一下就好了:

Android 开发 气泡指示器 安卓气泡效果_生命周期_09

上面代码就一点需要注意,渐变最少需要两种颜色,不然没法渐变,这个很好理解吧,不再多解释了。现在我们来画气泡,步骤如下:

设置一下圆的范围,一般都为全屏

根据圆的构造方法添加多个圆

绘制添加的这些圆

直接来看代码,其实也很简单:

Android 开发 气泡指示器 安卓气泡效果_Android 开发 气泡指示器_10

从代码中看出,已经将所有添加的圆放到集合里,然后遍历集合去画,这就不用添加一个画一个了,只需统一添加再统一绘制即可。

既然背景绘制好了,气泡也绘制好了,那就到了最后一步,需要提供方法让 SurfaceView 去添加背景和气泡:

Android 开发 气泡指示器 安卓气泡效果_自定义_11

到此,这个绘制中间者对象就完成了。

3、自定义 SurfaceView

终于到了重要的 SurfaceView部分了,这部分不太好描述,因为最好的解释方式就是看代码。

首先自定义 FloatBubbleView继承于 SurfaceView,看一下简单的变量定义、构造方法:

Android 开发 气泡指示器 安卓气泡效果_Android 开发 气泡指示器_12

这里其他的内容都比较好理解,重点提两个变量:

Android 开发 气泡指示器 安卓气泡效果_生命周期_13

这是什么意思呢,开始我也不太理解,那换个思路,大家还记得 ListView中的ViewHolder么,这个 ViewHolder其实就是用来复用的。那 SurfaceView中也有个SurfaceHolder,作用可以看做是相同的,就是用来不断复用不断刷新界面的。

那这里的这两个变量是干什么的呢?就是相当于 当前刷新的中间者对象和 上一次刷新的中间者对象。

那获得这两个对象有什么用呢?注意看,还有个 curDrawerAlpha变量,顾名思义,当前的透明度。

三者结合在一起,再加上一个这样的小循环:

Android 开发 气泡指示器 安卓气泡效果_子线程_14

那这又有什么作用呢,别急,先看下面两张对比图,分别设置 curDrawerAlpha += 0.2f和 curDrawerAlpha += 0.8f:

模拟器太卡,将就着看

Android 开发 气泡指示器 安卓气泡效果_自定义_15

0.2f

再看 0.8f ,从暗到明显然快了点:

Android 开发 气泡指示器 安卓气泡效果_android属性动画 呼吸_16

0.8f

现在知道作用了么,就是实现界面从暗到明的效果。那为什么需要这样的效果呢,我尝试过去掉这个,发现绘制的时候会偶尔出现闪黑屏的现象,黑色刚好是 SurfaceView的本身颜色,加上这个效果就不会出现了。

好,接下来看重中之重的绘制线程方法,为了方便我单独抽取了线程类,并将 run方法按照不同的功能分成好几个方法,注释写的很清晰:

Android 开发 气泡指示器 安卓气泡效果_自定义_17

Android 开发 气泡指示器 安卓气泡效果_Android 开发 气泡指示器_18

知道看这种代码很枯燥,但不能急。首先这里有三种状态:正在绘制、活动、退出。其中活动是一种中间状态,指既没有活动又没有被销毁。在回调类中需要根据这种状态进行绘制线程的控制。

那就来看回调方法:

Android 开发 气泡指示器 安卓气泡效果_Android 开发 气泡指示器_19

可以看到,在销毁的时候绘制线程是在等待状态。

然后就是一些生命周期相关方法了,也很简单,就是设置相关状态:

Android 开发 气泡指示器 安卓气泡效果_生命周期_20

最后就是提供方法,给这个自定义的 SurfaceView 设置中间绘制者对象了:

Android 开发 气泡指示器 安卓气泡效果_android属性动画 呼吸_21

到此,自定义 FloatBubbleView就完成了,代码很长,建议直接看文末的源码。

看结果

好了, 现在只要在 Activity 中这样:

Android 开发 气泡指示器 安卓气泡效果_子线程_22

这样就大功告成了!效果图再贴一下吧,颜色大小位置都可以定义:

Android 开发 气泡指示器 安卓气泡效果_android属性动画 呼吸_23

悬浮气泡演示图

后话

虽然效果实现了,但是我并没有将设置气泡的方法暴露出来,只写死在 BubbleDrawer 中:

Android 开发 气泡指示器 安卓气泡效果_生命周期_24

开始我确实抽取了方法,提供给 Activity,结果发现 Activity中的代码太难看。另一方面因为 SurfaceView消耗资源太多,我们应该不会在主要界面大量使用它,所以我觉得写死就够了,必要的时候动一动写死的数据就行了。

还有一点就是,虽然效果很好看,但是确实消耗资源很大,有时候会很卡,不知道还有没有可以优化的地方,建议只在简单的页面,比如关于软件的页面用这样的效果,其他的主页面还是算了吧。