簡述SurfaceView及常見問題


  在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繪制,且不會影響應用程序的主線程響應用戶輸入。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM