Android利用SurfaceView實現(xiàn)下雨的天氣動畫效果
首先是最終實現(xiàn)的效果圖:

先分析一下雨滴的實現(xiàn):
- 每個雨滴其實就是一條線,通過
canvas.drawLine()繪制 - 線(雨滴)的長度、寬度、下落速度、透明度以及位置都是在一定范圍內(nèi)隨機生成
- 每 draw 一次然后改變雨滴的位置然后重繪即可實現(xiàn)雨滴的下落效果
分析完了,那么可以直接寫一個類直接繼承 View ,然后重寫 onDraw() 嗎?可以看到效果圖中的雨滴的下落速度很快,那么意味著每一幀都要調(diào)用 onDraw() 一次使其重新繪制一次,假如你的 onDraw() 方法里面的渲染代碼稍微有點費時,而 View 的 onDraw() 方法調(diào)用是在 UI 線程中,那么繪制出來的效果就不會那么流暢,甚至還會阻塞 UI 線程,所以為了更流暢的效果并且不阻塞 UI 線程,我們這里使用 SurfaceView 來實現(xiàn)。
初識 SurfaceView
SurfaceView 直接繼承自 View,View 必須在 UI 線程中繪制,而 SurfaceView 不同于 View,它可以在非 UI 線程中繪制并顯示在界面上,這意味著你可以自己新開一個線程,然后把繪制渲染的代碼放在該線程中。
Surface 是 Z 軸排序的,SurfaceView 的 Z 軸位置小于它的宿主 Window,代表它總是在自己所在 Window 的后面,既然在后面,那么是怎么顯示的呢?SurfaceView 在其 Window 中打出一個“孔”(其實就是在其宿主 Window 上設(shè)置了一塊透明區(qū)域來使其能夠顯示),意味著他的兄弟節(jié)點的 View 會覆蓋它,例如你可以在 SurfaceView 上方放置按鈕,文本等控件。
要想訪問下面的 Surface ,可以通過 Android 提供給我們的 SurfaceHolder 接口??梢哉{(diào)用 SurfaceView 的 getHolder() 來獲取。
SurfaceView 是有生命周期的,我們必須在它生命周期期間進行執(zhí)行繪制代碼,所以我們需要監(jiān)聽 SurfaceView 的狀態(tài)(例如創(chuàng)建以及銷毀),這里 Android 為我們提供了 SurfaceHolder.Callback 這個接口來可以讓我們方便的監(jiān)聽 SurfaceView 的狀態(tài)。
那么下面看下 SurfaceHolder.Callback 接口
public interface Callback {
// SurfaceView 創(chuàng)建時調(diào)用(SurfaceView的窗口可見時)
public void surfaceCreated(SurfaceHolder holder);
// SurfaceView 改變時調(diào)用
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height);
// SurfaceView 銷毀時調(diào)用(SurfaceView的窗口不可見時)
public void surfaceDestroyed(SurfaceHolder holder);
}
我們的繪制代碼需要在 surfaceCreated 和 surfaceDestroyed 之間執(zhí)行,否則無效,SurfaceHolder.Callback的回調(diào)方法是執(zhí)行在 UI 線程中的,繪制線程需要我們自己手動創(chuàng)建。
具體可看官方文檔:https://developer.android.google.cn/reference/android/view/SurfaceView.html
View 和 SurfaceView 的使用場景
- View 適合那些與用戶交互并且渲染時間不是很長的控件,因為 View 的繪制和用戶交互都處在 UI 線程中。
- SurfaceView 適合迅速的更新界面或者渲染時間比較長以至于影響到用戶體驗的場景。
使用 SurfaceView(實現(xiàn))
這里我們和自定義 View 類似,寫一個類 DynamicWeatherView 繼承自 SurfaceView,然后為了監(jiān)聽 SurfaceView 的狀態(tài),所以我們還需要實現(xiàn) SurfaceHolder.Callback 接口來監(jiān)聽 SurfaceView 的狀態(tài),接口的回調(diào)具體時機上面也已經(jīng)介紹過了。
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 創(chuàng)建時調(diào)用(可見)
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
// SurfaceView 銷毀時調(diào)用(不可見)
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
}
上面也提到了,控制 Surface 我們需要 SurfaceHolder 對象,調(diào)用 SurfaceView 的 getHolder() 即可獲得,然后為這個 SurfaceHolder 添加一個 SurfaceHolder.Callback 回調(diào),這里就是 DynamicWeatherView 當(dāng)前對象
private SurfaceHolder mHolder;
mHolder = getHolder(); mHolder.addCallback(this); mHolder.setFormat(PixelFormat.TRANSPARENT);
然后實現(xiàn)我們的繪制線程:
private class DrawThread extends Thread {
// 用來停止線程的標(biāo)記
private boolean isRunning = false;
public void setRunning(boolean running) {
isRunning = running;
}
@Override
public void run() {
Canvas canvas;
// 無限循環(huán)繪制
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);
}
這樣要想實現(xiàn)不同的天氣類型,只要實現(xiàn)這個接口重寫 onDraw 和 onSizeChanged 方法即可,這里我們實現(xiàn)的是下雨的效果,所以實現(xiàn)了一個 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);
// 這里雨滴的寬度統(tǒng)一為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 軸坐標(biāo)
*/
int x;
/**
* 雨點 y 軸坐標(biāo)
*/
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 對象代表一個雨滴,每繪制一次然后改變雨滴的位置,然后準(zhǔn)備下一次繪制,來實現(xiàn)雨滴的移動。
BaseType 類是我們的一個抽象基類,實現(xiàn)了 DynamicWeatherView.WeatherType 接口,內(nèi)部有一些公共方法,具體可以看 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));
}
}
今后要想實現(xiàn)不同的天氣類型,只需要繼承 BaseType 類重寫相關(guān)方法即可。
源碼下載:點擊這里
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對各位Android開發(fā)者們能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
CoordinatorLayout的使用如此簡單(Android)
這篇文章主要為大家詳細介紹了Android CoordinatorLayout的使用,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-09-09
AndroidStudio 配置 AspectJ 環(huán)境實現(xiàn)AOP的方法
本篇文章主要介紹了AndroidStudio 配置 AspectJ 環(huán)境實現(xiàn)AOP的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02
Android模擬器中窗口截圖存成文件實現(xiàn)思路及代碼
Android模擬器內(nèi)容是用OpenGL渲染的,所以用一般的編程截圖(如PrintWindow()等)會是黑屏。這是因為畫的東西放在framebuffer里 接下來介紹如何實現(xiàn)Android模擬器中窗口截圖存成文件,感興趣的朋友可以了解下哦2013-01-01
Android編程設(shè)計模式之Builder模式實例詳解
這篇文章主要介紹了Android編程設(shè)計模式之Builder模式,結(jié)合實例形式詳細分析了Android設(shè)計模式之Builder模式概念、功能、使用場景、用法及相關(guān)注意事項,需要的朋友可以參考下2017-12-12
Android studio4.1更新后出現(xiàn)的問題詳解
這篇文章主要介紹了Android studio4.1更新后出現(xiàn)的問題詳解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
Android AlertDialog六種創(chuàng)建方式案例詳解
這篇文章主要介紹了Android AlertDialog六種創(chuàng)建方式案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-08-08
android實現(xiàn)http中請求訪問添加cookie的方法
這篇文章主要介紹了android實現(xiàn)http中請求訪問添加cookie的方法,實例分析了兩種添加cookie的技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-10-10
android開發(fā)基礎(chǔ)教程—SharedPreferences讀寫
本文介紹SharedPreferences的讀與寫的實現(xiàn)思路,感興趣的朋友可以了解下2013-01-01

