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>