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