使用 SurfaceView 實現一個下雨的天氣效果
介紹 SurfaceView 和 View 的區別,以及一些需要使用到 SurfaceView 的場景。
章之前,先上一張本文要最終實現的效果圖:
先分析一下雨滴的實現:
- 每個雨滴其實就是一條線,通過 canvas.drawLine() 繪制
- 線(雨滴)的長度、寬度、下落速度、透明度以及位置都是在一定范圍內隨機生成
- 每 draw 一次然后改變雨滴的位置然后重繪即可實現雨滴的下落效果
分析完了,那么可以直接寫一個類直接繼承 View ,然后重寫 onDraw() 嗎?可以看到效果圖中的雨滴的下落速度很快,那么意味著每一幀都要調用 onDraw() 一次使其重新繪制一次,假如你的 onDraw() 方法里面的渲染代碼稍微有點費時,而 View 的 onDraw() 方法調用是在 UI 線程中,那么繪制出來的效果就不會那么流暢,甚至還會阻塞 UI 線程,所以為了更流暢的效果并且不阻塞 UI 線程,我們這里使用 SurfaceView 來實現。
初識 SurfaceView
SurfaceView 直接繼承自 View,View 必須在 UI 線程中繪制,而 SurfaceView 不同于 View,它可以在非 UI 線程中繪制并顯示在界面上,這意味著你可以自己新開一個線程,然后把繪制渲染的代碼放在該線程中。
Surface 是 Z 軸排序的,SurfaceView 的 Z 軸位置小于它的宿主 Window,代表它總是在自己所在 Window 的后面,既然在后面,那么是怎么顯示的呢?SurfaceView 在其 Window 中打出一個“孔”(其實就是在其宿主 Window 上設置了一塊透明區域來使其能夠顯示),意味著他的兄弟節點的 View 會覆蓋它,例如你可以在 SurfaceView 上方放置按鈕,文本等控件。
要想訪問下面的 Surface ,可以通過 Android 提供給我們的 SurfaceHolder 接口。可以調用 SurfaceView 的 getHolder() 來獲取。
SurfaceView 是有生命周期的,我們必須在它生命周期期間進行執行繪制代碼,所以我們需要監聽 SurfaceView 的狀態(例如創建以及銷毀),這里 Android 為我們提供了 SurfaceHolder.Callback 這個接口來可以讓我們方便的監聽 SurfaceView 的狀態。
那么下面看下 SurfaceHolder.Callback 接口
public interface Callback {
// SurfaceView 創建時調用(SurfaceView的窗口可見時)
public void surfaceCreated(SurfaceHolder holder);
// SurfaceView 改變時調用
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height);
// SurfaceView 銷毀時調用(SurfaceView的窗口不可見時)
public void surfaceDestroyed(SurfaceHolder holder);
}
我們的繪制代碼需要在 surfaceCreated 和 surfaceDestroyed 之間執行,否則無效,SurfaceHolder.Callback的回調方法是執行在 UI 線程中的,繪制線程需要我們自己手動創建。
View 和 SurfaceView 的使用場景
- View 適合那些與用戶交互并且渲染時間不是很長的控件,因為 View 的繪制和用戶交互都處在 UI 線程中。
- SurfaceView 適合迅速的更新界面或者渲染時間比較長以至于影響到用戶體驗的場景。
使用 SurfaceView(實現)
這里我們和自定義 View 類似,寫一個類 DynamicWeatherView 繼承自 SurfaceView,然后為了監聽 SurfaceView 的狀態,所以我們還需要實現 SurfaceHolder.Callback 接口來監聽 SurfaceView 的狀態,接口的回調具體時機上面也已經介紹過了。
public class DynamicWeatherView extends SurfaceView implements SurfaceHolder.Callback{
public DynamicWeatherView(Context context) {
this(context, null);
}
public DynamicWeatherView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DynamicWeatherView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// SurfaceView 創建時調用(可見)
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
// SurfaceView 銷毀時調用(不可見)
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
}
上面也提到了,控制 Surface 我們需要 SurfaceHolder 對象,調用 SurfaceView 的 getHolder() 即可獲得,然后為這個 SurfaceHolder 添加一個 SurfaceHolder.Callback 回調,這里就是 DynamicWeatherView 當前對象
private SurfaceHolder mHolder;
mHolder = getHolder();
mHolder.addCallback(this);
mHolder.setFormat(PixelFormat.TRANSPARENT);
然后實現我們的繪制線程:
private class DrawThread extends Thread {
// 用來停止線程的標記
private boolean isRunning = false;
public void setRunning(boolean running) {
isRunning = running;
}
@Override
public void run() {
Canvas canvas;
// 無限循環繪制
while (isRunning) {
if (mType != null && mViewWidth != 0 && mViewHeight != 0) {
canvas = mHolder.lockCanvas();
if (canvas != null) {
mType.onDraw(canvas);
if (isRunning) {
mHolder.unlockCanvasAndPost(canvas);
} else {
// 停止線程
break;
}
SystemClock.sleep(1);
}
}
}
}
}
從上面的代碼可以看出 SurfaceView 的更新流程具體為:
// 鎖定畫布并獲得 canvas
canvas = mHolder.lockCanvas();
// 在 canvas 上進行繪制
mType.onDraw(canvas);
// 解除鎖定并提交更改
mHolder.unlockCanvasAndPost(canvas);
繪制線程代碼量不多,因為具體的繪制代碼在 mType.onDraw(canvas) 中,mType 是我們自己定義的一個接口,代表一種天氣類型:
public interface WeatherType {
void onDraw(Canvas canvas);
void onSizeChanged(Context context, int w, int h);
}
這樣要想實現不同的天氣類型,只要實現這個接口重寫 onDraw 和 onSizeChanged 方法即可,這里我們實現的是下雨的效果,所以實現了一個 RainTypeImpl 類:
public class RainTypeImpl extends BaseType {
// 背景
private Drawable mBackground;
// 雨滴集合
private ArrayList<RainHolder> mRains;
// 畫筆
private Paint mPaint;
public RainTypeImpl(Context context, DynamicWeatherView dynamicWeatherView) {
super(context, dynamicWeatherView);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.WHITE);
// 這里雨滴的寬度統一為3
mPaint.setStrokeWidth(3);
mRains = new ArrayList<>();
}
@Override
public void generate() {
mBackground = getContext().getResources().getDrawable(R.drawable.rain_sky_night);
mBackground.setBounds(0, 0, getWidth(), getHeight());
for (int i = 0; i < 60; i++) {
RainHolder rain = new RainHolder(
getRandom(1, getWidth()),
getRandom(1, getHeight()),
getRandom(dp2px(9), dp2px(15)),
getRandom(dp2px(5), dp2px(9)),
getRandom(20, 100)
);
mRains.add(rain);
}
}
private RainHolder r;
@Override
public void onDraw(Canvas canvas) {
clearCanvas(canvas);
// 畫背景
mBackground.draw(canvas);
// 畫出集合中的雨點
for (int i = 0; i < mRains.size(); i++) {
r = mRains.get(i);
mPaint.setAlpha(r.a);
canvas.drawLine(r.x, r.y, r.x, r.y + r.l, mPaint);
}
// 將集合中的點按自己的速度偏移
for (int i = 0; i < mRains.size(); i++) {
r = mRains.get(i);
r.y += r.s;
if (r.y > getHeight()) {
r.y = -r.l;
}
}
}
private class RainHolder {
/**
* 雨點 x 軸坐標
*/
int x;
/**
* 雨點 y 軸坐標
*/
int y;
/**
* 雨點長度
*/
int l;
/**
* 雨點移動速度
*/
int s;
/**
* 雨點透明度
*/
int a;
public RainHolder(int x, int y, int l, int s, int a) {
this.x = x;
this.y = y;
this.l = l;
this.s = s;
this.a = a;
}
}
}
代碼不難,基本都有注釋,RainHolder 對象代表一個雨滴,每繪制一次然后改變雨滴的位置,然后準備下一次繪制,來實現雨滴的移動。
BaseType 類是我們的一個抽象基類,實現了 DynamicWeatherView.WeatherType 接口,內部有一些公共方法,具體可以看 Demo 中的代碼。
最后我們的 Activity 代碼:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DynamicWeatherView mDynamicWeatherView = (DynamicWeatherView) findViewById(R.id.dynamic_weather_view);
mDynamicWeatherView.setType(new RainTypeImpl(this, mDynamicWeatherView));
}
}
今后要想實現不同的天氣類型,只需要繼承 BaseType 類重寫相關方法即可。
來自:http://melodyxxx.com/2017/03/07/use_surfaceview/