在Android開發中,SurfaceView平常並不常用,但是遇到一些視頻播放或者拍照等情況,就需要用到。下面對該控件進行簡單的介紹,並列舉出使用過程中遇到的問題進行QA形式的解答!
聲明: 關於SurfaceView的原理的介紹主要參考文章:https://blog.csdn.net/luoshengyang/article/details/8661317
一、運用場景:
普通的Android控件,它們的UI都是在應用程序的主線程中進行繪制的。而應用程序除了繪制外,還需要及時響應用戶輸入,否則,會引起ANR。為了避免ANR,對於一些游戲畫面,攝像預覽、視頻播放等UI比較復雜,而且要求能夠進行高效繪制的視圖,它們的UI就不適合在應用程序的主線程中進行繪制。這時需要給此類視圖生成一個獨立的繪圖表面,並使用一個獨立的線程來繪制這些視圖的UI,此時就必須使用SurfaceView進行開發。
二、SurfaceView是什么?
1、定義:
繼承自View,該類內嵌了一個專門用於繪制的Surface。你可以控制這個Surface的格式和尺寸。SurfaceView控制這個Surface的繪制位置。
Surface是縱深排序(Z-ordered)的,這表明它總在自己所在窗口的后面。Surfaceview提供了一個可見區域,只有在這個可見區域內 的surface部分內容才可見,可見區域外的部分不可見。Surface的排版顯示受到視圖層級關系的影響,它的兄弟視圖結點會在頂端顯示。這意味者 surface的內容會被它的兄弟視圖遮擋,這一特性可以用來放置遮蓋物(overlays)(例如,文本和按鈕等控件)。注意,如果surface上面 有透明控件,那么它的每次變化都會引起框架重新計算它和頂層控件的透明效果,這會影響性能。
2、負責繪制UI的SurfaceFlinger:
講到Surface,在此需要簡要介紹一下SurfaceFlinger,一個負責繪制Android應用程序UI的服務。SurfaceFlinger服務運行在Android系統的System進程中,它負責管理Android系統的幀緩沖區(Frame Buffer)。Android應用程序為了能夠將自己的UI繪制在系統的幀緩沖區上,就必須要與SurfaceFlinger服務進行通信.具體流程概括為:
(1) Android應用程序請求SurfaceFlinger服務創建Surface;
(2) Surface創建后,Android應用程序在上面繪制自己的UI;
(3) 再請求SurfaceFlinger服務將已經繪制好UI的Surface渲染到設備顯示屏上。
一般來說,每一個窗口在SurfaceFlinger服務中都對應有一個Layer,用來描述它的繪圖表面。對於那些具有SurfaceView的窗口來說,每一個SurfaceView在SurfaceFlinger服務中還對應有一個獨立的Layer或者LayerBuffer,用來單獨描述它的繪圖表面,以區別於它的宿主窗口的繪圖表面。
由於擁有獨立的繪圖表面,因此SurfaceView的UI就可以在一個獨立的線程中進行繪制。又由於不會占用主線程資源,SurfaceView一方面 可以實現復雜而高效的UI,另一方面又不會導致用戶輸入得不到及時響應。
3、單獨介紹SurfaceView的一個成員變量:(僅做了解)
SurfaceView類的成員變量mRequestedType描述的是SurfaceView的繪圖表面的類型,有以下四個值:摘自源碼。
SURFACE_TYPE_NORMAL:用RAM緩存原生數據的普通Surface
SURFACE_TYPE_HARDWARE:適用於DMA(Direct memory access )引擎和硬件加速的Surface
SURFACE_TYPE_GPU:適用於GPU加速的Surface
SURFACE_TYPE_PUSH_BUFFERS:表明該Surface不包含原生數據,Surface用到的數據由其他對象提供,在Camera圖像預覽中就使用該類型的Surface,有Camera負責提供給預覽Surface數據,這樣圖像預覽會比較流暢。如果設置這種類型則就不能調用lockCanvas來獲取Canvas對象了。
需要說明的是,雖然SurfaceHolder中已經不建議使用setType()方法了,我們自己寫demo也可以看到該方法已經被聲明為@Deprecated
/** * Sets the surface's type. * * @deprecated this is ignored, this value is set automatically when needed. */ @Deprecated public void setType(int type);
但是,在使用自定義的SurfaceView實現視頻播放時,還為了兼容低版本,仍需要設置setType:
//設置SurfaceHolder類型,為了兼容低版本,需要設置此類型,否則播放時,只有聲音,而沒有圖像 getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
4、關於View的setWillNotDraw():
查看SurfaceView的源碼可知,在其構造函數中調用了setWillNotDraw(true);該方法會導致draw()、onDraw()都不執行。
/** * If this view doesn't do any drawing on its own, set this flag to * allow further optimizations. By default, this flag is not set on * View, but could be set on some View subclasses such as ViewGroup. * * Typically, if you override {@link #onDraw(android.graphics.Canvas)} * you should clear this flag. * * @param willNotDraw whether or not this View draw on its own */ public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); }
自定義SurfaceView,運行結果如下:
(1)默認執行結果:
(2)在CommSurfaceView的構造函數中,手動設置setWillNotDraw(false),運行結果如下:
三、SurfaceView的實現過程:(詳情請參看原文)
SurfaceView的整個實現過程分為三步:繪圖表面的創建、在宿主窗口設置一塊透明區域用於顯示、在獨立線程中進行自己UI的繪制。
1.、創建獨立的繪圖表面;
2、在宿主窗口上設置一塊透明區域來顯示自己(使繪制的UI可見);【SurfaceView的窗口類型所表示的Z軸位置小於Activity窗口的Z軸位置】
3、在獨立的線程中進行其UI繪制。
四、SurfaceView的使用:
由於SurfaceView的底層實現過程已經進行了封裝,並為開發者提供了上層使用接口,即:系統給SurfaceView提供了一個專門繪圖的Surface,嵌入在了SurfaceView視圖層中,畫面在Surface中繪制完成,在SurfaceView中通過獲得SurfaceHolder的對象,管理並展示Surface的數據內容。所以,開發時只需按照下述步驟即可:
1、一般都是重新自定義SurfaceView,起到封裝的作用:
(1)繼承SurfaceView;
(2)利用getHolder()獲取SurfaceHolder對象;
(3)給SurfaceHolder對象添加實現SurfaceHolder.Callback接口的對象,即添加回調函數SurfaceHolder.addCallback(callback);
(4)重寫Callback的三個方法:surfaceChanged,surfaceCreated,surfaceDestroyed;
(5)利用SurfaceHolder對象設置其類型、格式、大小等。
(一)在SurfaceView上進行UI繪制的流程如下:
(1). 在繪圖表面的基礎上建立一塊畫布,即獲得一個Canvas對象。
(2). 利用Canvas類提供的繪圖接口在前面獲得的畫布上繪制任意的UI。
(3). 將已經填充好UI數據的畫布緩沖區提交給SurfaceFlinger服務,以便SurfaceFlinger服務可以將它合成到屏幕上去。
代碼格式: Canvas c=surfaceHolder.lockCanvas(); .....具體畫圖操作...... surfaceHolder.unlockCanvasAndPost(c);
(二)利用SurfaceView播放視頻:
Android中有三種實現形式,均可實現視頻的播放:
-
- 原生VideoView(繼承SurfaceView)
- 直接使用SurfaceView
- 自定義播放控件(繼承SurfaceView)
具體視頻的播放均是使用的MediaPlayer,只是在自定義及VideoView中,被封裝在各自的類中,對調用者不可見。
視頻的畫面一般由兩部分組成:視頻內容畫面+上層操作布局(標題、播放進度、時長、暫停等控件)。
為了配合上層操作布局,一般都會實現MediaPlayerControl接口(當然,完全可以自定義),獲取視頻播放相關信息。
以原生VideoView為例,展現代碼格式: VideoView nativeVideoView = (VideoView) findViewById(R.id.nativeVideoView); MediaController nativeMediaController = new MediaController(this);
//設置播放路徑:實際內部封裝了MediaPlayer的創建及數據源、監聽事件、播放類型、畫面顯示等的設置,即視頻播放前的准備工作,
//其中 mMediaPlayer.setDisplay(mSurfaceHolder);即是畫面相關的設置 nativeVideoView.setVideoPath(path); nativeVideoView.setMediaController(nativeMediaController); nativeVideoView.requestFocus(); nativeVideoView.start();//內部執行mediaPlayer.start()
五、常見問題:(以VideoView為例)
1、在播放視頻時,滑動進度,會出現閃屏(畫面一直在變)?
原因:原生VideoView使用SeekBar,在滑動進度的過程中,在OnSeekBarChangeListener的onProgressChanged()中調用了MediaPlayerControl的seekTo(),即實時更新視頻畫面,出現閃屏。
1 源碼:frameworks\base\core\java\android\widget\MediaController.java 2 3 private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { 4 @Override 5 public void onStartTrackingTouch(SeekBar bar) { 6 show(3600000); 7 8 mDragging = true; 9 10 // By removing these pending progress messages we make sure 11 // that a) we won't update the progress while the user adjusts 12 // the seekbar and b) once the user is done dragging the thumb 13 // we will post one of these messages to the queue again and 14 // this ensures that there will be exactly one message queued up. 15 removeCallbacks(mShowProgress); 16 } 17 18 @Override 19 public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { 20 if (!fromuser) { 21 // We're not interested in programmatically generated changes to 22 // the progress bar's position. 23 return; 24 } 25 26 long duration = mPlayer.getDuration(); 27 long newposition = (duration * progress) / 1000L; 28 mPlayer.seekTo( (int) newposition); //實時更新播放畫面 29 if (mCurrentTime != null) 30 mCurrentTime.setText(stringForTime( (int) newposition)); 31 } 32 33 @Override 34 public void onStopTrackingTouch(SeekBar bar) { 35 mDragging = false; 36 setProgress(); 37 updatePausePlay(); 38 show(sDefaultTimeout); 39 40 // Ensure that progress is properly updated in the future, 41 // the call to show() does not guarantee this because it is a 42 // no-op if we are already showing. 43 post(mShowProgress); 44 } 45 };
處理:解決此問題,可將視頻畫面的更新放在滑動結束(即onStopTrackingTouch()中)即可。
引入新的問題:若用戶看視頻,需要從30分鍾開始播放,但是具體位置不確定,因此需要在30分鍾附近查看,此時則需要隨着用戶的滑動,畫面切換,
但是,又不能出現上述閃屏問題。
新問題的解決方案:可以監聽兩次滑動的時間間隔,然后畫面更新(例:兩次間隔500ms更新一次畫面)
測試結果:證實可以。具體為在滑動過程中即onProgressChanged()中進行如下操作:
//startTouchTime:首次為拖動開始的時間,之后為每次更新時重新賦值更新時的時間 //changeTouchTime:時刻獲取滑動變化的時間
if(mDragging && changeTouchTime - startTouchTime >= 500){ Log.e(TAG, "prepare fromuser onProgressChanged ++++++++++++++ "); startTouchTime = changeTouchTime; //重置,最近一次修改
mPlayer.seekTo(newPosition); if (currentTime != null) { currentTime.setText(stringForTime(newPosition)); } }
2、橫豎屏切換時,視頻從頭播放,不會連續。
原因:默認情況下, Activity在橫豎屏切換時會重新創建,因此視頻重播.結合是否設置屬性對Activity的生命周期的影響,即可明白。
解決方案:橫豎屏切換必須在AndroidManifest.xml中設置屬性android:configChanges="screenSize|orientation"。
3、播放完成,停止時,播放進度未顯示在最后?(該問題是我的項目中出現的,並非通用問題)
現象:由於視頻播放完成,做退出處理,一般看不到此現象。
原因:根據視頻“剪輯”時,進度可以完全顯示。查找原因,發現剪輯視頻時,整個過程中進度條可見,一直在實時更新界面進度,因此可完全顯示;而普通播放視頻時,進度條等播放布局是可隱藏的,而代碼中在MoviePlayer的setProgress()中,判斷當進度等布局不可見時,直接返回,而不更新界面顯示進度,因此,在播放完成時,界面顯示的是最近一次布局可見時的進度(一般不在最后)。
4、視頻播放過程中,操作暫停播放后點擊Home鍵退回桌面,再次進入會出現黑屏?【注:播放情況下正常是由於播放再次進入執行了繼續播放,即刷新了界面】
原因:點擊Home鍵時,SurfaceView中的Surface被銷毀(從執行回調函數surfaceDestroy()即可看出),播放畫面顯示為黑色。
測試現象:
(1) 看到黑屏效果,是因為Activity背景被設置為黑色,在去掉背景色之后,發現僅有播放視頻區域顯示黑色;
(2)將窗體設置為白色,按上述操作,發現僅視頻播放區域為黑色;將主題設置為android:Theme.Translucent,按上述操作,發現僅視頻播放區域為透明,即可看見MainActivity。這也可以解釋SurfaceView的實現原理中的第二條,在宿主窗口中設置一塊透明區域顯示自己。運行效果圖如下:MainActivity界面(左側)、右側為視頻播放界面。
得出結論:Surface被銷毀后,播放區域顯示的背景為當前主題Theme中的配置。
5、其他問題:SurfaceView、GLSurfaceView、SurfaceTexture、TextureView的區別:
SurfaceView:Android1.0就有,繼承自View,因為有單獨的Surface【不在View hierachy】中,它的顯示也不受View的屬性控制,所以不能進行平移,縮放等變換,也不能放在其它ViewGroup中,一些View中的特性也無法使用。
GLSurfaceView:Android1.5引入,繼承自SurfaceView,在SurfaceView的基礎上,它加入了EGL的管理,並自帶了渲染線程。另外它定義了用戶需要實現的Render接口,提供了用Strategy pattern更改具體Render行為的靈活性。作為GLSurfaceView的Client,只需要將實現了渲染函數的Renderer的實現類設置給GLSurfaceView即可。
SurfaceTexture:Android 3.0(API level 11)加入。單獨的類,和SurfaceView不同的是,它對圖像流的處理並不直接顯示,而是轉為GL外部紋理,因此可用於圖像流數據的二次處理(如Camera濾鏡,桌面特效等)。比如Camera的預覽數據,變成紋理后可以交給GLSurfaceView直接顯示,也可以通過SurfaceTexture交給TextureView作為View heirachy中的一個硬件加速層來顯示。
TextureView:在4.0(API level 14)中引入。繼承自View,它可以將內容流直接投影到View中,可以用於實現Live preview【實時預覽】等功能.和SurfaceView不同,它不會在WMS中單獨創建窗口,而是作為View hierachy中的一個普通View,因此可以和其它普通View一樣進行移動,旋轉,縮放,動畫等變化。值得注意的是TextureView必須在硬件加速的窗口中。它顯示的內容流數據可以來自App進程或是遠端進程。
六、總結:
SurfaceView是android系統中特殊的視圖,繼承自View。它具有獨立的繪圖表面,但是,由於其窗口類型所表示的Z軸位置小於Activity窗口的Z軸位置,因此,為了使其可見,需要在宿主窗口申請設置一塊透明區域來顯示。一切條件就緒后,就可以在獨立的線程中進行復雜的UI繪制,且不會影響應用程序的主線程響應用戶輸入。