Android:會呼吸的懸浮氣泡

iamxiarui_發表於2018-07-02

##寫在前面

這個標題看起來玄乎玄乎的,其實一張圖就明白了:

懸浮氣泡演示圖

最早看到這個效果是 MIUI6 系統升級介面,有很多五顏六色的氣泡懸浮著,覺得很好看。可惜現在找不到動態圖了。而且雖然 MIUI8 更新介面也有類似的氣泡,但是不過是靜態的,不咋好看。

MIUI8

再次見到這個效果是在 Pure 天氣這款軟體中,可惜開發者不開源。不過萬能的 Github 上有類似的實現,於是果斷把自定義 View 部分抽出來學習學習。

Pure

懷著敬意放上原專案地址,很好看的一款天氣 APP:

Weather - Mixiaoxiao

還是那句話,學習自定義 View 沒有什麼捷徑,就是看原始碼、模仿、動手。

##具體實現

###先思考

在看原始碼之前,我自己想了一下該怎樣去實現,思路如下:

  • 自定義一個圓形 View ,支援大小、顏色、位置等屬性
  • 浮動利用最簡單的平移動畫來實現
  • 平移的範圍通過自定義圓心的移動範圍來確定
  • 最後給動畫一個迴圈就行了

雖然看起來比較簡單,但是實現起來還是遇到不少坑。首先畫圓一點問題都沒有,問題出在動畫上。動畫看起來很遲鈍,根本就不是呼吸效果,像哮喘一樣。

所以不能用動畫,就想到了不斷重繪。於是仍然給圓心設定一個小圓,讓圓心在小圓上移動,在這個過程中不斷重繪,結果直接 Crash 了,看了看 Log ,發現是執行緒阻塞了,但是這裡並沒有開啟子執行緒啊,一看,我去,主執行緒。

那這條路行不通,又想到用貝塞爾去做,結果突然想起來之前繪製阻塞了主執行緒,那開子執行緒繪製不就完了,Android View 裡面能開子執行緒繪製的不就是 SurfaceView 。於是看了看作者原始碼,果然是自定義 SurfaceView

早已看穿一切

關於 SurfaceView 我只在以前學習的視訊案例、撕MM衣服案例、還有手寫板案例中遇到過,學的不是很深,加上本文它不是重點,所以就不詳細說了,如果不瞭解這個或者想深入瞭解一下的話,可以點選文末的相關連結,這裡只簡單提一下比較重要的一點,也就是 SurfaceViewView 的主要區別:

SurfaceView 在一個新起的單獨執行緒中重新繪製畫面,而 View 必須在 UI 執行緒中更新畫面。

這就決定了 SurfaceView 的一些特定使用場景:

  • 需要介面迅速更新;
  • 對幀率要求較高的情況;
  • 渲染 UI 需要較長的時間。

所以綜合來看,SurfaceView 無疑是實現這類效果的最佳選擇。

###再分析

廢話不多說,來分析一下思路。

1、首先光從介面上能看到就是圓,且是能浮動的圓,所以不管能不能動,先得把圓畫出來。要是我的話,我直接就拿著 PaintCanvas 上開畫了。在原始碼中開發者單獨抽取了繪製圓的類,但這個類的作用不僅僅是繪製圓,後面我們再說。

2、其次就是自定義 SurfaceView ,我們需要把畫出來的圓放到 SurfaceView 中。而自定義 SurfaceView 需要實現 SurfaceHolder.Callback 介面,就是一些回撥方法。同時需要開子執行緒去不斷重新整理介面,因為這些圓是需要動起來的.

3、另外重要的一點就是,SurfaceView 在渲染過程中需要消耗大量資源,比如記憶體啊、CPU 啊之類的,所以最好提供一個生命週期相關的方法,讓它和 Activity 的生命週期保持一致,儘量保證及時回收資源,減少消耗。

4、最後需要提一點的是,SurfaceView 本身並不需要繪製內容,或者說在這裡它的主要作用就是重新整理介面就行了。就好像在放視訊的時候,只需要重新整理視訊頁面就行,它並不參與視訊具體內容的繪製。

所以這樣來說的話,我們最好定義一個繪製過程的中間者,主要作用就是把繪製出來的圓放在 SurfaceView 上,同時也能做一些其他的工作,比如繪製背景、設定尺寸等。這樣做的好處就是能讓 SurfaceView 專心的做一件事:不斷重新整理,這就夠了。

OK,總結一下我們到底需要哪些東西:

  • 專門繪製圓的類
  • 重新整理過程中的子執行緒
  • 實現 SurfaceHolder.Callback 介面方法
  • 提供生命週期相關方法
  • 一個繪製過程的中間物件

多提一句,最後的繪製中間者也可以不定義,全部封裝到自定義 SurfaceView 中,但是從我實踐來看,我最後不得不單獨抽取出來,因為 SurfaceView 類看起來太亂了,這也是原始碼中的實現方式。

23333

###後動手

Talk is cheap,Show me the code .

####1、畫圓

既然要畫圓,我們肯定要設定一些圓的基本屬性:

  • 圓心座標
  • 圓的半徑
  • 圓的顏色

由於需要圓動起來,也就是說它會偏移,所以要確定一個範圍。範圍確定了,就需要指定它該怎麼變化,因為我們要求它緩慢而順暢的呼吸,不能瞬間大喘氣,也就是它不能瞬間移動偏移量那麼多,所以最好指定它每一步變化多少,那就需要下面這兩樣東西:

  • 圓心偏移範圍
  • 每一幀的變化量

額外的,因為移動是每次都需要變的,下一次變化時不能重新開始,所以我們要記錄當前已經偏移的距離,然後根據一個標誌位不斷呼氣...吐氣...呼氣...吐氣,所以需要:

  • 當前幀變化量
  • 標誌位

好了,看建構函式吧:

/**
 * @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;
    }

	//...畫圓方法先省略
}
複製程式碼

好了,構造好了圓就要開始繪製圓了。之前說到,這個類的作用不僅僅是繪製圓,還要不斷更新圓的位置,也就是不斷重繪圓。更直接地說,我們需要繪製出不斷偏移的每一幀的圓。

步驟如下:

  • 確定當前幀偏移位置
  • 根據當前幀偏移位置計算圓心座標
  • 設定圓的顏色透明度等屬性
  • 真正的開始繪製圓

程式碼如下,結合上面的步驟和程式碼中的註釋應該很容易看懂:

/**
 * 更新位置並重新繪製
 *
 * @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);
}
複製程式碼

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

/**
 * 轉成透明顏色
 *
 * @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);
}
複製程式碼

到此,畫每一幀圓的工作我們就完成了。

####2、繪製中間者物件

現在來說這個特殊的中間者物件,前文說了,單獨抽取這個類不是必須的。但最好抽取一下,讓 SurfaceView 專心做自己的事情。在這個中間者物件中我們做兩件事情:

  • 繪製背景
  • 繪製懸浮氣泡

先來看繪製背景。為什麼需要繪製背景呢,因為 SurfaceView 本身其實是個黑色,從我們日常看視訊的軟體中也能發現,視訊播放時周圍都是黑色的。有人問為什麼不能直接在佈局中設定呢?當然可以直接設定啊,不過要記得新增一句 setZOrderOnTop(true) ,不然會把之後繪製的懸浮氣泡遮擋住。

在這裡就來繪製一下吧,因為原始碼中給出了一個漸變色的繪製,我覺得挺好玩,學一學。直接看程式碼吧,都是模板程式碼,沒啥好解釋的,簡單的 get/set 再畫一下就好了:

/**
 * @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);
    }

	//...暫時省略圓的繪製方法
}
複製程式碼

上面程式碼就一點需要注意,漸變最少需要兩種顏色,不然沒法漸變,這個很好理解吧,不再多解釋了。現在我們來畫氣泡,步驟如下:

  • 設定一下圓的範圍,一般都為全屏
  • 根據圓的構造方法新增多個圓
  • 繪製新增的這些圓

直接來看程式碼,其實也很簡單:

/*===== 圖形相關 =====*/
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);
    }
}
複製程式碼

從程式碼中看出,已經將所有新增的圓放到集合裡,然後遍歷集合去畫,這就不用新增一個畫一個了,只需統一新增再統一繪製即可。

既然背景繪製好了,氣泡也繪製好了,那就到了最後一步,需要提供方法讓 SurfaceView 去新增背景和氣泡:

/**
 * 畫背景 畫所有的氣泡
 *
 * @param canvas 畫布
 * @param alpha  透明值
 */
void drawBgAndBubble(Canvas canvas, float alpha) {
    drawGradientBackground(canvas, alpha);
    drawCircleBubble(canvas, alpha);
}
複製程式碼

到此,這個繪製中間者物件就完成了。

####3、自定義 SurfaceView

終於到了重要的 SurfaceView 部分了,這部分不太好描述,因為最好的解釋方式就是看程式碼。

首先自定義 FloatBubbleView 繼承於 SurfaceView ,看一下簡單的變數定義、構造方法:

/**
 * @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;
    }

	//...省略其他方法
}
複製程式碼

這裡其他的內容都比較好理解,重點提兩個變數:

private BubbleDrawer mPreDrawer;    //上一次繪製物件
private BubbleDrawer mCurDrawer;    //當前繪製物件
複製程式碼

這是什麼意思呢,開始我也不太理解,那換個思路,大家還記得 ListView 中的 ViewHolder 麼,這個 ViewHolder 其實就是用來複用的。那 SurfaceView 中也有個 SurfaceHolder ,作用可以看做是相同的,就是用來不斷複用不斷重新整理介面的。

那這裡的這兩個變數是幹什麼的呢?就是相當於 當前重新整理的中間者物件上一次重新整理的中間者物件

那獲得這兩個物件有什麼用呢?注意看,還有個 curDrawerAlpha 變數,顧名思義,當前的透明度。

三者結合在一起,再加上一個這樣的小迴圈:

if (curDrawerAlpha < 1f) {
	curDrawerAlpha += 0.5f;
	if (curDrawerAlpha > 1f) {
		curDrawerAlpha = 1f;
		mPreDrawer = null;
	}
}
複製程式碼

那這又有什麼作用呢,別急,先看下面兩張對比圖,分別設定 curDrawerAlpha += 0.2fcurDrawerAlpha += 0.8f

模擬器太卡,將就著看

0.2f

再看 0.8f ,從暗到明顯然快了點:

0.8f

現在知道作用了麼,就是實現介面從暗到明的效果。那為什麼需要這樣的效果呢,我嘗試過去掉這個,發現繪製的時候會偶爾出現閃黑屏的現象,黑色剛好是 SurfaceView 的本身顏色,加上這個效果就不會出現了。

好,接下來看重中之重的繪製執行緒方法,為了方便我單獨抽取了執行緒類,並將 run 方法按照不同的功能分成好幾個方法,註釋寫的很清晰:

/**
 * 繪製執行緒 必須開啟子執行緒繪製 防止出現阻塞主執行緒的情況
 */
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();
            }
        }
    }
}
複製程式碼

知道看這種程式碼很枯燥,但不能急。首先這裡有三種狀態:正在繪製、活動、退出。其中活動是一種中間狀態,指既沒有活動又沒有被銷燬。在回撥類中需要根據這種狀態進行繪製執行緒的控制。

那就來看回撥方法:

/*========== 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==========*/
複製程式碼

可以看到,在銷燬的時候繪製執行緒是在等待狀態。

然後就是一些生命週期相關方法了,也很簡單,就是設定相關狀態:

/*========== 處理與 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==========*/
複製程式碼

最後就是提供方法,給這個自定義的 SurfaceView 設定中間繪製者物件了:

/**
 * 設定繪製者
 *
 * @param bubbleDrawer 氣泡繪製
 */
public void setDrawer(BubbleDrawer bubbleDrawer) {
    //防空保護
    if (bubbleDrawer == null) {
        return;
    }
    curDrawerAlpha = 0f;        //完全透明
    //如果當前有正在繪製的物件 直接設定為前一次繪製物件
    if (this.mCurDrawer != null) {
        this.mPreDrawer = mCurDrawer;
    }
    //當前繪製物件 為設定的物件
    this.mCurDrawer = bubbleDrawer;
}
複製程式碼

到此,自定義 FloatBubbleView 就完成了,程式碼很長,建議直接看文末的原始碼。

###看結果

好了, 現在只要在 Activity 中這樣:

/**
 * 初始化Data
 */
private void initData() {
    //設定氣泡繪製者
    BubbleDrawer bubbleDrawer = new BubbleDrawer(this);
    //設定漸變背景 如果不需要漸變 設定相同顏色即可
    bubbleDrawer.setBackgroundGradient(new int[]{0xffffffff, 0xffffffff});
    //給SurfaceView設定一個繪製者
    mDWView.setDrawer(bubbleDrawer);
}
複製程式碼

這樣就大功告成了!效果圖再貼一下吧,顏色大小位置都可以定義:

懸浮氣泡演示圖

##後話

雖然效果實現了,但是我並沒有將設定氣泡的方法暴露出來,只寫死在 BubbleDrawer 中:

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));
	//...
}
複製程式碼

開始我確實抽取了方法,提供給 Activity ,結果發現 Activity 中的程式碼太難看。另一方面因為 SurfaceView 消耗資源太多,我們應該不會在主要介面大量使用它,所以我覺得寫死就夠了,必要的時候動一動寫死的資料就行了。

還有一點就是,雖然效果很好看,但是確實消耗資源很大,有時候會很卡,不知道還有沒有可以優化的地方,建議只在簡單的頁面,比如關於軟體的頁面用這樣的效果,其他的主頁面還是算了吧。

###參考資料

Weather - Mixiaoxiao

Android之SurfaceView簡介(一)

Android SurfaceView入門學習 - 英勇青銅5

###專案原始碼

FloatBubbleView - IamXiaRui


個人部落格:www.iamxiarui.com 原文連結:http://www.iamxiarui.com/?p=912

相關文章