##寫在前面
這個標題看起來玄乎玄乎的,其實一張圖就明白了:
最早看到這個效果是 MIUI6 系統升級介面,有很多五顏六色的氣泡懸浮著,覺得很好看。可惜現在找不到動態圖了。而且雖然 MIUI8 更新介面也有類似的氣泡,但是不過是靜態的,不咋好看。
再次見到這個效果是在 Pure 天氣這款軟體中,可惜開發者不開源。不過萬能的 Github 上有類似的實現,於是果斷把自定義 View 部分抽出來學習學習。
懷著敬意放上原專案地址,很好看的一款天氣 APP:
還是那句話,學習自定義 View 沒有什麼捷徑,就是看原始碼、模仿、動手。
##具體實現
###先思考
在看原始碼之前,我自己想了一下該怎樣去實現,思路如下:
- 自定義一個圓形 View ,支援大小、顏色、位置等屬性
- 浮動利用最簡單的平移動畫來實現
- 平移的範圍通過自定義圓心的移動範圍來確定
- 最後給動畫一個迴圈就行了
雖然看起來比較簡單,但是實現起來還是遇到不少坑。首先畫圓一點問題都沒有,問題出在動畫上。動畫看起來很遲鈍,根本就不是呼吸效果,像哮喘一樣。
所以不能用動畫,就想到了不斷重繪。於是仍然給圓心設定一個小圓,讓圓心在小圓上移動,在這個過程中不斷重繪,結果直接 Crash 了,看了看 Log ,發現是執行緒阻塞了,但是這裡並沒有開啟子執行緒啊,一看,我去,主執行緒。
那這條路行不通,又想到用貝塞爾去做,結果突然想起來之前繪製阻塞了主執行緒,那開子執行緒繪製不就完了,Android View 裡面能開子執行緒繪製的不就是 SurfaceView 。於是看了看作者原始碼,果然是自定義 SurfaceView 。
關於 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 類看起來太亂了,這也是原始碼中的實現方式。
###後動手
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.2f 和 curDrawerAlpha += 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 消耗資源太多,我們應該不會在主要介面大量使用它,所以我覺得寫死就夠了,必要的時候動一動寫死的資料就行了。
還有一點就是,雖然效果很好看,但是確實消耗資源很大,有時候會很卡,不知道還有沒有可以優化的地方,建議只在簡單的頁面,比如關於軟體的頁面用這樣的效果,其他的主頁面還是算了吧。
###參考資料
Android SurfaceView入門學習 - 英勇青銅5
###專案原始碼
個人部落格:www.iamxiarui.com 原文連結:http://www.iamxiarui.com/?p=912