Android:会呼吸的悬浮气泡
Fidelia0391
8年前
<h2><strong>写在前面</strong></h2> <p>这个标题看起来玄乎玄乎的,其实一张图就明白了:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/068ca1f663462c4ffc1a4f5814ee430a.gif"></p> <p style="text-align:center">悬浮气泡演示图</p> <p>最早看到这个效果是 <strong>MIUI6</strong> 系统升级界面,有很多五颜六色的气泡悬浮着,觉得很好看。可惜现在找不到动态图了。虽然 <strong>MIUI8</strong> 更新界面也有类似的气泡,不过是静态的,不咋好看。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/61b2695e7138992a8c9c8181f7b6d95d.jpg"></p> <p style="text-align:center">MIUI8</p> <p>再次见到这个效果是在 <strong>Pure</strong> 天气这款软件中,可惜开发者不开源。不过万能的 <strong>Github</strong> 上有类似的实现,于是果断把自定义 <strong>View</strong> 部分抽出来学习学习。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/e0dcf91ecb1ed87a650ce004cacd9299.jpg"></p> <p style="text-align:center">Pure</p> <p>怀着敬意放上原项目地址,很好看的一款天气 APP:</p> <p>还是那句话,学习自定义 <strong>View</strong> 没有什么捷径,就是看源码、模仿、动手。</p> <h2><strong>具体实现</strong></h2> <h3><strong>先思考</strong></h3> <p>在看源码之前,我自己想了一下该怎样去实现,思路如下:</p> <ul> <li>自定义一个圆形 <strong>View</strong> ,支持大小、颜色、位置等属性</li> <li>浮动利用最简单的平移动画来实现</li> <li>平移的范围通过自定义圆心的移动范围来确定</li> <li>最后给动画一个循环就行了</li> </ul> <p>虽然看起来比较简单,但是实现起来还是遇到不少坑。首先画圆一点问题都没有,问题出在动画上。动画看起来很迟钝,根本就不是呼吸效果,像哮喘一样。</p> <p>所以不能用动画,就想到了不断重绘。于是仍然给圆心设置一个小圆,让圆心在小圆上移动,在这个过程中不断重绘,结果直接 <strong>Crash</strong> 了,看了看 <strong>Log</strong> ,发现是线程阻塞了,但是这里并没有开启子线程啊,一看,我去,主线程。</p> <p>那这条路行不通,又想到用贝塞尔去做,结果突然想起来之前绘制阻塞了主线程,那开子线程绘制不就完了, <strong>Android View</strong> 里面能开子线程绘制的不就是 <strong>SurfaceView</strong> 。于是看了看作者源码,果然是自定义 <strong>SurfaceView</strong> 。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/dda9165f1966b93f55738c60c7fcb757.jpg"></p> <p style="text-align:center">早已看穿一切</p> <p>关于 <strong>SurfaceView</strong> 我只在以前学习的视频案例、撕MM衣服案例、还有手写板案例中遇到过,学的不是很深,加上本文它不是重点,所以就不详细说了,如果不了解这个或者想深入了解一下的话,可以点击文末的相关链接,这里只简单提一下比较重要的一点,也就是 <strong>SurfaceView</strong> 跟 <strong>View</strong> 的主要区别:</p> <p>SurfaceView在一个新起的单独线程中重新绘制画面,而 <strong>View</strong> 必须在 <strong>UI</strong> 线程中更新画面。</p> <p>这就决定了 <strong>SurfaceView</strong> 的一些特定使用场景:</p> <ul> <li> <p>需要界面迅速更新;</p> </li> <li> <p>对帧率要求较高的情况;</p> </li> <li> <p>渲染 <strong>UI</strong> 需要较长的时间。</p> </li> </ul> <p>所以综合来看, <strong>SurfaceView</strong> 无疑是实现这类效果的最佳选择。</p> <h3><strong>再分析</strong></h3> <p>废话不多说,来分析一下思路。</p> <p>1、首先光从界面上能看到就是圆,且是能浮动的圆,所以不管能不能动,先得把圆画出来。要是我的话,我直接就拿着 <strong>Paint</strong> 在 <strong>Canvas</strong> 上开画了。在源码中开发者单独抽取了绘制圆的类,但这个类的作用不仅仅是绘制圆,后面我们再说。</p> <p>2、其次就是自定义 <strong>SurfaceView</strong> ,我们需要把画出来的圆放到 <strong>SurfaceView</strong> 中。而自定义 <strong>SurfaceView</strong> 需要实现 <strong>SurfaceHolder.Callback</strong> 接口,就是一些回调方法。同时需要开子线程去不断刷新界面,因为这些圆是需要动起来的.</p> <p>3、另外重要的一点就是, <strong>SurfaceView</strong> 在渲染过程中需要消耗大量资源,比如内存啊、 <strong>CPU</strong> 啊之类的,所以最好提供一个生命周期相关的方法,让它和 <strong>Activity</strong> 的生命周期保持一致,尽量保证及时回收资源,减少消耗。</p> <p>4、最后需要提一点的是, <strong>SurfaceView</strong> 本身并不需要绘制内容,或者说在这里它的主要作用就是刷新界面就行了。就好像在放视频的时候,只需要刷新视频页面就行,它并不参与视频具体内容的绘制。</p> <p>所以这样来说的话,我们最好定义一个绘制过程的中间者,主要作用就是把绘制出来的圆放在 <strong>SurfaceView</strong> 上,同时也能做一些其他的工作,比如绘制背景、设置尺寸等。这样做的好处就是能让 <strong>SurfaceView</strong> 专心的做一件事:不断刷新,这就够了。</p> <p>OK,总结一下我们到底需要哪些东西:</p> <ul> <li> <p>专门绘制圆的类</p> </li> <li> <p>刷新过程中的子线程</p> </li> <li> <p>实现 <strong>SurfaceHolder.Callback</strong> 接口方法</p> </li> <li> <p>提供生命周期相关方法</p> </li> <li> <p>一个绘制过程的中间对象</p> </li> </ul> <p>多提一句,最后的绘制中间者也可以不定义,全部封装到自定义 <strong>SurfaceView</strong> 中,但是从我实践来看,我最后不得不单独抽取出来,因为 <strong>SurfaceView</strong> 类看起来太乱了,这也是源码中的实现方式。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/40dbf17b2345c8036c6064b957e72eb0.jpg"></p> <p> </p> <h3><strong>后动手</strong></h3> <p>Talk is cheap,Show me the code .</p> <p><strong>1、画圆</strong></p> <p>既然要画圆,我们肯定要设置一些圆的基本属性:</p> <ul> <li> <p>圆心坐标</p> </li> <li> <p>圆的半径</p> </li> <li> <p>圆的颜色</p> </li> </ul> <p>由于需要圆动起来,也就是说它会偏移,所以要确定一个范围。范围确定了,就需要指定它该怎么变化,因为我们要求它缓慢而顺畅的呼吸,不能瞬间大喘气,也就是它不能瞬间移动偏移量那么多,所以最好指定它每一步变化多少,那就需要下面这两样东西:</p> <ul> <li> <p>圆心偏移范围</p> </li> <li> <p>每一帧的变化量</p> </li> </ul> <p>额外的,因为移动是每次都需要变的,下一次变化时不能重新开始,所以我们要记录当前已经偏移的距离,然后根据一个标志位不断呼气...吐气...呼气...吐气,所以需要:</p> <ul> <li> <p>当前帧变化量</p> </li> <li> <p>标志位</p> </li> </ul> <p>好了,看构造函数吧:</p> <pre> <code class="language-java">/** * @author Mixiaoxiao * @revision xiarui 16/09/27 * @description 圆形浮动气泡 */ class CircleBubble { private final float cx, cy; //圆心坐标 private final float dx, dy; //圆心偏移距离 private final float radius; //半径 private final int color; //画笔颜色 private final float variationOfFrame; //设置每帧变化量 private boolean isGrowing = true; //根据此标志位判断左右移动 private float curVariationOfFrame = 0f; //当前帧变化量 CircleBubble(float cx, float cy, float dx, float dy, float radius, float variationOfFrame, int color) { this.cx = cx; this.cy = cy; this.dx = dx; this.dy = dy; this.radius = radius; this.variationOfFrame = variationOfFrame; this.color = color; } //...画圆方法先省略 }</code></pre> <p>好了,构造好了圆就要开始绘制圆了。之前说到,这个类的作用不仅仅是绘制圆,还要不断更新圆的位置,也就是不断重绘圆。更直接地说,我们需要绘制出不断偏移的每一帧的圆。</p> <p>步骤如下:</p> <ul> <li> <p>确定当前帧偏移位置</p> </li> <li> <p>根据当前帧偏移位置计算圆心坐标</p> </li> <li> <p>设置圆的颜色透明度等属性</p> </li> <li> <p>真正的开始绘制圆</p> </li> </ul> <p>代码如下,结合上面的步骤和代码中的注释应该很容易看懂:</p> <pre> <code class="language-java">/** * 更新位置并重新绘制 * * @param canvas 画布 * @param paint 画笔 * @param alpha 透明值 */ void updateAndDraw(Canvas canvas, Paint paint, float alpha) { /** * 每次绘制时都根据标志位(isGrowing)和每帧变化量(variationOfFrame)进行更新 * 说白了其实就是每帧都会变化一段距离 连在一起就产生动画效果 */ if (isGrowing) { curVariationOfFrame += variationOfFrame; if (curVariationOfFrame > 1f) { curVariationOfFrame = 1f; isGrowing = false; } } else { curVariationOfFrame -= variationOfFrame; if (curVariationOfFrame < 0f) { curVariationOfFrame = 0f; isGrowing = true; } } //根据当前帧变化量计算圆心偏移后的位置 float curCX = cx + dx * curVariationOfFrame; float curCY = cy + dy * curVariationOfFrame; //设置画笔颜色 int curColor = convertAlphaColor(alpha * (Color.alpha(color) / 255f), color); paint.setColor(curColor); //这里才真正的开始画圆形气泡 canvas.drawCircle(curCX, curCY, radius, paint); }</code></pre> <p>其中的 <strong>convertAlphaColor()</strong> 方法是个工具方法,作用就是转化一下颜色,不必深究:</p> <pre> <code class="language-java">/** * 转成透明颜色 * * @param percent 百分比 * @param originalColor 初始颜色 * @return 带有透明效果的颜色 */ private static int convertAlphaColor(float percent, final int originalColor) { int newAlpha = (int) (percent * 255) & 0xFF; return (newAlpha << 24) | (originalColor & 0xFFFFFF); }</code></pre> <p>到此,画每一帧圆的工作我们就完成了。</p> <p><strong>2、绘制中间者对象</strong></p> <p>现在来说这个特殊的中间者对象,前文说了,单独抽取这个类不是必须的。但最好抽取一下,让 <strong>SurfaceView</strong> 专心做自己的事情。在这个中间者对象中我们做两件事情:</p> <ul> <li> <p>绘制背景</p> </li> <li> <p>绘制悬浮气泡</p> </li> </ul> <p>先来看绘制背景。为什么需要绘制背景呢,因为 SurfaceView 本身其实是个黑色,从我们日常看视频的软件中也能发现,视频播放时周围都是黑色的。有人问为什么不能直接在布局中设置呢?当然可以直接设置啊,不过要记得添加一句 <strong>setZOrderOnTop(true)</strong> ,不然会把之后绘制的悬浮气泡遮挡住。</p> <p>在这里就来绘制一下吧,因为源码中给出了一个渐变色的绘制,我觉得挺好玩,学一学。直接看代码吧,都是模板代码,没啥好解释的,简单的 <strong>get/set</strong> 再画一下就好了:</p> <pre> <code class="language-java">/** * @author Mixiaoxiao * @revision xiarui 16/09/27 * @description 绘制圆形浮动气泡及设定渐变背景的绘制对象 */ public class BubbleDrawer { /*===== 图形相关 =====*/ private GradientDrawable mGradientBg; //渐变背景 private int[] mGradientColors; //渐变颜色数组 /** * 设置渐变背景色 * * @param gradientColors 渐变色数组 必须 >= 2 不然没法渐变 */ public void setBackgroundGradient(int[] gradientColors) { this.mGradientColors = gradientColors; } /** * 获取渐变色数组 * * @return 渐变色数组 */ private int[] getBackgroundGradient() { return mGradientColors; } /** * 绘制渐变背景色 * * @param canvas 画布 * @param alpha 透明值 */ private void drawGradientBackground(Canvas canvas, float alpha) { if (mGradientBg == null) { //设置渐变模式和颜色 mGradientBg = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, getBackgroundGradient()); //规定背景宽高 一般都为整屏 mGradientBg.setBounds(0, 0, mWidth, mHeight); } //然后开始画 mGradientBg.setAlpha(Math.round(alpha * 255f)); mGradientBg.draw(canvas); } //...暂时省略圆的绘制方法 }</code></pre> <p>上面代码就一点需要注意,渐变最少需要两种颜色,不然没法渐变,这个很好理解吧,不再多解释了。现在我们来画气泡,步骤如下:</p> <ul> <li>设置一下圆的范围,一般都为全屏</li> <li>根据圆的构造方法添加多个圆</li> <li>绘制添加的这些圆</li> </ul> <p>直接来看代码,其实也很简单:</p> <pre> <code class="language-java">/*===== 图形相关 =====*/ private Paint mPaint; //抗锯齿画笔 private int mWidth, mHeight; //上下文对象 private ArrayList<CircleBubble> mBubbles; //存放气泡的集合 /** * 构造函数 * * @param context 上下文对象 可能会用到 */ public BubbleDrawer(Context context) { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBubbles = new ArrayList<>(); } /** * 设置显示悬浮气泡的范围 * @param width 宽度 * @param height 高度 */ void setViewSize(int width, int height) { if (this.mWidth != width && this.mHeight != height) { this.mWidth = width; this.mHeight = height; if (this.mGradientBg != null) { mGradientBg.setBounds(0, 0, width, height); } } //设置一些默认的气泡 initDefaultBubble(width); } /** * 初始化默认的气泡 * * @param width 宽度 */ private void initDefaultBubble(int width) { if (mBubbles.size() == 0) { mBubbles.add(new CircleBubble(0.20f * width, -0.30f * width, 0.06f * width, 0.022f * width, 0.56f * width, 0.0150f, 0x56ffc7c7)); mBubbles.add(new CircleBubble(0.58f * width, -0.15f * width, -0.15f * width, 0.032f * width, 0.6f * width, 0.00600f, 0x45fffc9e)); //... } } /** * 用画笔在画布上画气泡 * * @param canvas 画布 * @param alpha 透明值 */ private void drawCircleBubble(Canvas canvas, float alpha) { //循环遍历所有设置的圆形气泡 for (CircleBubble bubble : this.mBubbles) { bubble.updateAndDraw(canvas, mPaint, alpha); } }</code></pre> <p>从代码中看出,已经将所有添加的圆放到集合里,然后遍历集合去画,这就不用添加一个画一个了,只需统一添加再统一绘制即可。</p> <p>既然背景绘制好了,气泡也绘制好了,那就到了最后一步,需要提供方法让 SurfaceView 去添加背景和气泡:</p> <pre> <code class="language-java">/** * 画背景 画所有的气泡 * * @param canvas 画布 * @param alpha 透明值 */ void drawBgAndBubble(Canvas canvas, float alpha) { drawGradientBackground(canvas, alpha); drawCircleBubble(canvas, alpha); }</code></pre> <p>到此,这个绘制中间者对象就完成了。</p> <p><strong>3、自定义 SurfaceView</strong></p> <p>终于到了重要的 <strong>SurfaceView</strong> 部分了,这部分不太好描述,因为最好的解释方式就是看代码。</p> <p>首先自定义 <strong>FloatBubbleView</strong> 继承于 <strong>SurfaceView</strong> ,看一下简单的变量定义、构造方法:</p> <pre> <code class="language-java">/** * @author Mixiaoxiao * @revision xiarui 16/09/27 * @description 用圆形浮动气泡填充的View * @remark 因为气泡需要不断绘制 所以防止阻塞UI线程 需要继承 SurfaceView 开启线程更新 并实现回调类 */ public class FloatBubbleView extends SurfaceView implements SurfaceHolder.Callback { private DrawThread mDrawThread; //绘制线程 private BubbleDrawer mPreDrawer; //上一次绘制对象 private BubbleDrawer mCurDrawer; //当前绘制对象 private float curDrawerAlpha = 0f; //当前透明度 (范围为0f~1f,因为 CircleBubble 中 convertAlphaColor 方法已经处理过了) private int mWidth, mHeight; //当前屏幕宽高 public FloatBubbleView(Context context) { super(context); initThreadAndHolder(context); } //...省略其他构造方法 /** * 初始化绘制线程和 SurfaceHolder * * @param context 上下文对象 可能会用到 */ private void initThreadAndHolder(Context context) { mDrawThread = new DrawThread(); SurfaceHolder surfaceHolder = getHolder(); surfaceHolder.addCallback(this); //添加回调 surfaceHolder.setFormat(PixelFormat.RGBA_8888); //渐变效果 就是显示SurfaceView的时候从暗到明 mDrawThread.start(); //开启绘制线程 } /** * 当view的大小发生变化时触发 * * @param w 当前宽度 * @param h 当前高度 * @param oldw 变化前宽度 * @param oldh 变化前高度 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; } //...省略其他方法 }</code></pre> <p>这里其他的内容都比较好理解,重点提两个变量:</p> <pre> <code class="language-java">private BubbleDrawer mPreDrawer; //上一次绘制对象 private BubbleDrawer mCurDrawer; //当前绘制对象</code></pre> <p>这是什么意思呢,开始我也不太理解,那换个思路,大家还记得 <strong>ListView</strong> 中的 <strong>ViewHolder</strong> 么,这个 <strong>ViewHolder</strong> 其实就是用来复用的。那 <strong>SurfaceView</strong> 中也有个 <strong>SurfaceHolder</strong> ,作用可以看做是相同的,就是用来不断复用不断刷新界面的。</p> <p>那这里的这两个变量是干什么的呢?就是相当于 <strong>当前刷新的中间者对象</strong> 和 <strong>上一次刷新的中间者对象</strong> 。</p> <p>那获得这两个对象有什么用呢?注意看,还有个 <strong>curDrawerAlpha</strong> 变量,顾名思义,当前的透明度。</p> <p>三者结合在一起,再加上一个这样的小循环:</p> <pre> <code class="language-java">if (curDrawerAlpha < 1f) { curDrawerAlpha += 0.5f; if (curDrawerAlpha > 1f) { curDrawerAlpha = 1f; mPreDrawer = null; } }</code></pre> <p>那这又有什么作用呢,别急,先看下面两张对比图,分别设置 <strong>curDrawerAlpha += 0.2f</strong> 和 <strong>curDrawerAlpha += 0.8f</strong> :</p> <p>模拟器太卡,将就着看</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/1674032aed3d53bfcc95bb00f201f7c1.gif"></p> <p style="text-align:center">0.2f</p> <p>再看 0.8f ,从暗到明显然快了点:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/788cb61f0bd4e5765fe45d24fd68dece.gif"></p> <p style="text-align:center">0.8f</p> <p>现在知道作用了么,就是实现界面从暗到明的效果。那为什么需要这样的效果呢,我尝试过去掉这个,发现绘制的时候会偶尔出现闪黑屏的现象,黑色刚好是 <strong>SurfaceView</strong> 的本身颜色,加上这个效果就不会出现了。</p> <p>好,接下来看重中之重的绘制线程方法,为了方便我单独抽取了线程类,并将 <strong>run</strong> 方法按照不同的功能分成好几个方法,注释写的很清晰:</p> <pre> <code class="language-java">/** * 绘制线程 必须开启子线程绘制 防止出现阻塞主线程的情况 */ private class DrawThread extends Thread { SurfaceHolder mSurface; boolean mRunning, mActive, mQuit; //三种状态 Canvas mCanvas; @Override public void run() { //一直循环 不断绘制 while (true) { synchronized (this) { //根据返回值 判断是否直接返回 不进行绘制 if (!processDrawThreadState()) { return; } //动画开始时间 final long startTime = AnimationUtils.currentAnimationTimeMillis(); //处理画布并进行绘制 processDrawCanvas(mCanvas); //绘制时间 final long drawTime = AnimationUtils.currentAnimationTimeMillis() - startTime; //处理一下线程需要的睡眠时间 processDrawThreadSleep(drawTime); } } } /** * 处理绘制线程的状态问题 * * @return true:不结束继续绘制 false:结束且不绘制 */ private boolean processDrawThreadState() { //处理没有运行 或者 Holder 为 null 的情况 while (mSurface == null || !mRunning) { if (mActive) { mActive = false; notify(); //唤醒 } if (mQuit) return false; try { wait(); //等待 } catch (InterruptedException e) { e.printStackTrace(); } } //其他情况肯定是活动状态 if (!mActive) { mActive = true; notify(); //唤醒 } return true; } /** * 处理画布与绘制过程 要注意一定要保证是同步锁中才能执行 否则会出现 * * @param mCanvas 画布 */ private void processDrawCanvas(Canvas mCanvas) { try { mCanvas = mSurface.lockCanvas(); //加锁画布 if (mCanvas != null) { //防空保护 //清屏操作 mCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR); drawSurface(mCanvas); //真正开始画 SurfaceView 的地方 } }catch (Exception ignored){ }finally { if(mCanvas != null){ mSurface.unlockCanvasAndPost(mCanvas); //释放canvas锁,并显示视图 } } } /** * 真正的绘制 SurfaceView * * @param canvas 画布 */ private void drawSurface(Canvas canvas) { //防空保护 if (mWidth == 0 || mHeight == 0) { return; } //如果前一次绘制对象不为空 且 当前绘制者有透明效果的话 绘制前一次的对象即可 if (mPreDrawer != null && curDrawerAlpha < 1f) { mPreDrawer.setViewSize(mWidth, mHeight); mPreDrawer.drawBgAndBubble(canvas, 1f - curDrawerAlpha); } //直到当前绘制完全不透明时将上一次绘制的置空 if (curDrawerAlpha < 1f) { curDrawerAlpha += 0.5f; if (curDrawerAlpha > 1f) { curDrawerAlpha = 1f; mPreDrawer = null; } } //如果当前有绘制对象 直接绘制即可 先设置绘制宽高再绘制气泡 if (mCurDrawer != null) { mCurDrawer.setViewSize(mWidth, mHeight); mCurDrawer.drawBgAndBubble(canvas, curDrawerAlpha); } } /** * 处理线程需要的睡眠时间 * View通过刷新来重绘视图,在一些需要频繁刷新或执行大量逻辑操作时,超过16ms就会导致明显卡顿 * * @param drawTime 绘制时间 */ private void processDrawThreadSleep(long drawTime) { //需要睡眠时间 final long needSleepTime = 16 - drawTime; if (needSleepTime > 0) { try { Thread.sleep(needSleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } } }</code></pre> <p>知道看这种代码很枯燥,但不能急。首先这里有三种状态:正在绘制、活动、退出。其中活动是一种中间状态,指既没有活动又没有被销毁。在回调类中需要根据这种状态进行绘制线程的控制。</p> <p>那就来看回调方法:</p> <pre> <code class="language-java">/*========== Surface 回调方法 需要加同步锁 防止阻塞 START==========*/ @Override public void surfaceCreated(SurfaceHolder holder) { synchronized (mDrawThread) { mDrawThread.mSurface = holder; mDrawThread.notify(); //唤醒 } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { synchronized (mDrawThread) { mDrawThread.mSurface = holder; mDrawThread.notify(); //唤醒 while (mDrawThread.mActive) { try { mDrawThread.wait(); //等待 } catch (InterruptedException e) { e.printStackTrace(); } } } holder.removeCallback(this); } /*========== Surface 回调方法 需要加同步锁 防止阻塞 END==========*/</code></pre> <p>可以看到,在销毁的时候绘制线程是在等待状态。</p> <p>然后就是一些生命周期相关方法了,也很简单,就是设置相关状态:</p> <pre> <code class="language-java">/*========== 处理与 Activity 生命周期相关方法 需要加同步锁 防止阻塞 START==========*/ public void onDrawResume() { synchronized (mDrawThread) { mDrawThread.mRunning = true; //运行状态 mDrawThread.notify(); //唤醒线程 } } public void onDrawPause() { synchronized (mDrawThread) { mDrawThread.mRunning = false; //不运行状态 mDrawThread.notify(); //唤醒线程 } } public void onDrawDestroy() { synchronized (mDrawThread) { mDrawThread.mQuit = true; //退出状态 mDrawThread.notify(); //唤醒线程 } } /*========== 处理与 Activity 生命周期相关方法 需要加同步锁 防止阻塞 END==========*/</code></pre> <p>最后就是提供方法,给这个自定义的 SurfaceView 设置中间绘制者对象了:</p> <pre> <code class="language-java">/** * 设置绘制者 * * @param bubbleDrawer 气泡绘制 */ public void setDrawer(BubbleDrawer bubbleDrawer) { //防空保护 if (bubbleDrawer == null) { return; } curDrawerAlpha = 0f; //完全透明 //如果当前有正在绘制的对象 直接设置为前一次绘制对象 if (this.mCurDrawer != null) { this.mPreDrawer = mCurDrawer; } //当前绘制对象 为设置的对象 this.mCurDrawer = bubbleDrawer; }</code></pre> <p>到此,自定义 <strong>FloatBubbleView</strong> 就完成了,代码很长,建议直接看文末的源码。</p> <h3><strong>看结果</strong></h3> <p>好了, 现在只要在 Activity 中这样:</p> <pre> <code class="language-java">/** * 初始化Data */ private void initData() { //设置气泡绘制者 BubbleDrawer bubbleDrawer = new BubbleDrawer(this); //设置渐变背景 如果不需要渐变 设置相同颜色即可 bubbleDrawer.setBackgroundGradient(new int[]{0xffffffff, 0xffffffff}); //给SurfaceView设置一个绘制者 mDWView.setDrawer(bubbleDrawer); }</code></pre> <p>这样就大功告成了!效果图再贴一下吧,颜色大小位置都可以定义:</p> <p> </p> <p style="text-align:center"><img src="https://simg.open-open.com/show/068ca1f663462c4ffc1a4f5814ee430a.gif" alt="Android:会呼吸的悬浮气泡" width="414" height="679"></p> <p style="text-align:center">悬浮气泡演示图</p> <h2><strong>后话</strong></h2> <p>虽然效果实现了,但是我并没有将设置气泡的方法暴露出来,只写死在 BubbleDrawer 中:</p> <pre> <code class="language-java">if (mBubbles.size() == 0) { mBubbles.add(new CircleBubble(0.20f * width, -0.30f * width, 0.06f * width, 0.022f * width, 0.56f * width,0.0150f, 0x56ffc7c7)); //... }</code></pre> <p>开始我确实抽取了方法,提供给 <strong>Activity</strong> ,结果发现 <strong>Activity</strong> 中的代码太难看。另一方面因为 <strong>SurfaceView</strong> 消耗资源太多,我们应该不会在主要界面大量使用它,所以我觉得写死就够了,必要的时候动一动写死的数据就行了。</p> <p>还有一点就是,虽然效果很好看,但是确实消耗资源很大,有时候会很卡,不知道还有没有可以优化的地方,建议只在简单的页面,比如关于软件的页面用这样的效果,其他的主页面还是算了吧。</p> <h3><strong>参考资料</strong></h3> <p><a href="/misc/goto?guid=4959718199242533334" rel="nofollow,noindex">Weather - Mixiaoxiao</a></p> <p><a href="/misc/goto?guid=4959718199332533277" rel="nofollow,noindex">Android之SurfaceView简介(一)</a></p> <p><a href="/misc/goto?guid=4959718199416479116" rel="nofollow,noindex">Android SurfaceView入门学习 - 英勇青铜5</a></p> <h3> </h3> <p>来自:http://www.jianshu.com/p/5a672bac5ba9</p> <p> </p>