本文的思路将按照我的项目实际过程来展开。

 

第一部分:问题的出现

需求:页面有很多布局,其中有一块区域播放视频,当我单击这块区域的时候,全屏播放。

我使用MediaPlayer + SurfaceView,当全屏时,改变SurfaceView的大小,然后MediaPlayer.setHolder(mSurfaceViewHolder),但是 ,只有SurfaceView改变了,但是视频大小仍然没有改变。而且在不设置surfaceHolder的情况下,视频仍以奇特的方式播放,具体方法可以参看我之前的例子,链接是:。大家可以尝试,我在后面会进行一些测试来一步一步来排除问题。

 

第二部分:转换思路,解决问题

解决问题优先,换了一种思路解决问题,使用Android框架封装好的VideoView解决,很快实现。

思路:当需要全屏时,gone掉无关紧要的其他布局。让VideoView的父组件(的父组件)FILL_PARENT。当要回到默认的显示布局时,将之前保存的原始尺寸设置回来,原来gone掉的布局显示出来即可。

关键代码如下:

  1. public void onClick(View v) { 
  2.         switch (v.getId()) { 
  3.         case R.id.videoView: 
  4.             if (!isFullScreen) { 
  5.                 toFullScreen(); 
  6.             } else { 
  7.                 toDefaultScreen(); 
  8.             } 
  9.             break
  10.         } 
  11.     } 
  12.  
  13.  
  14.     private void toFullScreen() { 
  15.         // TODO Auto-generated method stub 
  16.  
  17.         mVideoBarDefaultWidth = mVideoView.getWidth(); 
  18.         mVideoBarDefaultHeight = mVideoView.getHeight(); 
  19.  
  20.         mTitleBar.setVisibility(View.GONE); 
  21.         mAppBar.setVisibility(View.GONE); 
  22.         mCategoryBar.setVisibility(View.GONE); 
  23.         mIndicatorBar.setVisibility(View.GONE); 
  24.  
  25.         mContentLayout.setLayoutParams(new LinearLayout.LayoutParams( 
  26.                 LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); 
  27.  
  28.         mVideoViewLayout.setLayoutParams(new LinearLayout.LayoutParams( 
  29.                 LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); 
  30.  
  31.         mVideoView.setLayoutParams(new LinearLayout.LayoutParams( 
  32.                 LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); 
  33.  
  34.         isFullScreen = true
  35.  
  36.     } 
  37.  
  38.     private void toDefaultScreen() { 
  39.         // TODO Auto-generated method stub 
  40.         mTitleBar.setVisibility(View.VISIBLE); 
  41.         mAppBar.setVisibility(View.VISIBLE); 
  42.         mCategoryBar.setVisibility(View.VISIBLE); 
  43.         mIndicatorBar.setVisibility(View.VISIBLE); 
  44.          
  45.         mContentLayout.setLayoutParams(new LinearLayout.LayoutParams( 
  46.                 LayoutParams.FILL_PARENT, mVideoBarDefaultHeight)); 
  47.         mVideoViewLayout.setLayoutParams(new LinearLayout.LayoutParams( 
  48.                 mVideoBarDefaultWidth, mVideoBarDefaultHeight)); 
  49.         mVideoView.setLayoutParams(new LinearLayout.LayoutParams( 
  50.                 mVideoBarDefaultWidth, mVideoBarDefaultHeight)); 
  51.          
  52.         isFullScreen = false
  53.     } 

 

第三部分: 原因 + 设计测试

VideoView本身是SurfaceView的子类,既然VideoView没有问题,显然问题出在我对SurfaceView的处理上。因为出现了上面提到的不设置SurfaceViewHolder,MediaPlayer也能播放的问题。So,

第一个问题:MediaPlayer播放视频的依赖条件是什么?

查找到android的视频播放框架,见下图(双击图片可以放大)。

显然,视频播放需要通过:

(1) JAVA Framework(即Android Framework),通过VideoView,经过Surface,

(2) 再到Native Framework,

(3) 最后到达Driver.

在这里要重申一句:我们的问题是,为什么 SurfaceView没能实现全屏? 那么go on !!!

我们只需要关注到Android Framework之间的关联关系即可。显然,VideoView的父类是SurfaceView,最终通过其与Surface产生关联。

即VideoView?----------------SurfaceView?----------------------Surface

SurfaceView和Surface:

分析:文档

首先,SurfaceView的父类是View,最终父类是Object,但是Surface的父类是Object。类的层级见下图所示(双击可放大)。

所以两者是对等的关系,那么这两个是如何产生关系的。

SurfaceView的Class Overview见下图(双击图片可放大):

蓝色选中部分为关键内容:SurfaceView在它的Window上凿出一个洞,以便让Surface层的内容显示出来。本身它只提供一个显示Surface的窗口。

为方便期间,在此提供SurfaceView类的翻译,来自:http://www.cnblogs.com/xuling/archive/2011/06/06/android.html

在此贴出来供参考:

SurfaceView是视图(View)的继承类,这个视图里内嵌了一个专门用于绘制的Surface。你可以控制这个Surface的格式和尺寸。Surfaceview控制这个Surface的绘制位置。
        surface是纵深排序(Z-ordered)的,这表明它总在自己所在窗口的后面。surfaceview提供了一个可见区域,只有在这个可见区域内 的surface部分内容才可见,可见区域外的部分不可见。surface的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者 surface的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件)。注意,如果surface上面 有透明控件,那么它的每次变化都会引起框架重新计算它和顶层控件的透明效果,这会影响性能。
你可以通过SurfaceHolder接口访问这个surface,getHolder()方法可以得到这个接口。
        surfaceview变得可见时,surface被创建;surfaceview隐藏前,surface被销毁。这样能节省资源。如果你要查看 surface被创建和销毁的时机,可以重载surfaceCreated(SurfaceHolder)和 surfaceDestroyed(SurfaceHolder)。
        surfaceview的核心在于提供了两个线程:UI线程和渲染线程。这里应注意:
        1> 所有SurfaceView和SurfaceHolder.Callback的方法都应该在UI线程里调用,一般来说就是应用程序主线程。渲染线程所要访问的各种变量应该作同步处理。
        2> 由于surface可能被销毁,它只在SurfaceHolder.Callback.surfaceCreated()和 SurfaceHolder.Callback.surfaceDestroyed()之间有效,所以要确保渲染线程访问的是合法有效的surface。

第一个问题已经解决: Android的视频播放必须通过SurfaceView(或其封装类VideoView)实现。

第二个问题:视频播放中,可以切换到不同的SurfaceView上吗?

测试。

测试之前有必要上一张图,就是MediaPlayer的状态机(State-Machine),状态时刻指导代码。见下图。(双击图片可放大)。

 

建立测试Demo,如下图所示(双击图片可以放大):

 

Test1指定初始SurfaceView的大小(demo中有左小右大两个SurfaceView),播放视频

显然,MediaPlayer和显示是独立的两个部分,为简化起见,MediaPlayer直接准备好。代码如下,

  1. public void onClick(View v) { 
  2.  
  3.         switch (v.getId()) { 
  4.         case R.id.buttonToSmall: 
  5.             Log.d("testSurfaceView""onClick buttonToSmall"); 
  6.             try { 
  7.                 if (mMediaPlayer.isPlaying()) { 
  8.                     mMediaPlayer.release(); 
  9.                     mMediaPlayer.setDataSource(mPath); 
  10.                     mMediaPlayer.prepare(); 
  11.                 } 
  12.                 mMediaPlayer.setDisplay(mSurfaceView1.getHolder()); 
  13.                 mMediaPlayer.start(); 
  14.  
  15.             } catch (IllegalArgumentException e) { 
  16.                 // TODO Auto-generated catch block 
  17.                 e.printStackTrace(); 
  18.             } catch (IllegalStateException e) { 
  19.                 // TODO Auto-generated catch block 
  20.                 e.printStackTrace(); 
  21.             } catch (IOException e) { 
  22.                 // TODO Auto-generated catch block 
  23.                 e.printStackTrace(); 
  24.             } 
  25.             break
  26.         case R.id.buttonToBig: 
  27.             Log.d("testSurfaceView""onClick buttonToBig"); 
  28.                 try { 
  29.                     if (mMediaPlayer.isPlaying()) { 
  30.                         mMediaPlayer.release(); 
  31.                     mMediaPlayer.setDataSource(mPath); 
  32.                     mMediaPlayer.prepare(); 
  33.                     } 
  34.                 mMediaPlayer.setDisplay(mSurfaceView2.getHolder()); 
  35.                 mMediaPlayer.start(); 
  36.                 } catch (IllegalArgumentException e) { 
  37.                     // TODO Auto-generated catch block 
  38.                     e.printStackTrace(); 
  39.                 } catch (IllegalStateException e) { 
  40.                     // TODO Auto-generated catch block 
  41.                     e.printStackTrace(); 
  42.                 } catch (IOException e) { 
  43.                     // TODO Auto-generated catch block 
  44.                     e.printStackTrace(); 
  45.                 } 
  46.             break

测试结果:初始的surfaceview将决定视频的播放大小。系统会自动适配SurfaceView和Video的大小比例,使之恰当,但是在实际的显示效果并不佳,所以最好能通过运算计算一下。

 

Test2既然两个SurfaceView都能单独播放,那么能否从一个切换到另一个上呢?

MediaPlayer通过SurfaceView显示出来,而SurfaceView由SurfaceViewHolder控制。MediaPlayer通过方法setDisplay(SurfaceViewHolder sh)实现。

参看SurfaceHolder的文档,见下图(双击图片可放大)。

参看setDisplay()方法的文档,见下图(双击图片可放大):

要点:在非播放状况下,使用此方法。

2-2-1代码如下: 

  1. public void onClick(View v) { 
  2.  
  3.         switch (v.getId()) { 
  4.         case R.id.buttonToSmall: 
  5.             Log.d("testSurfaceView""onClick buttonToSmall"); 
  6.             mMediaPlayer.setDisplay(mSurfaceView1.getHolder()); 
  7.             mMediaPlayer.start(); 
  8.             break
  9.         case R.id.buttonToBig: 
  10.             Log.d("testSurfaceView""onClick buttonToBig"); 
  11.             mMediaPlayer.pause(); 
  12.             mMediaPlayer.setDisplay(mSurfaceView2.getHolder()); 
  13.             mMediaPlayer.start(); 
  14.             break

测试结果:根本没变,还是在原来的Surface上显示。

2-2-2参考之前写的代码,做联想,如果重新Prepare,是否可以播放?

代码如下:

  1. public void start() { 
  2.         // TODO Auto-generated method stub 
  3.         mMediaPlayer.start(); 
  4.     } 
  5.  
  6.     public void onClick(View v) { 
  7.  
  8.         switch (v.getId()) { 
  9.         case R.id.buttonToSmall: 
  10.             Log.d("testSurfaceView""onClick buttonToSmall"); 
  11.             mMediaPlayer.setDisplay(mSurfaceView1.getHolder()); 
  12.             mMediaPlayer.start(); 
  13.             break
  14.         case R.id.buttonToBig: 
  15.             Log.d("testSurfaceView""onClick buttonToBig"); 
  16.             try { 
  17.                 mMediaPlayer.stop(); 
  18.                 mMediaPlayer.setDisplay(mSurfaceView2.getHolder()); 
  19.                 mMediaPlayer.prepare(); 
  20.                 mMediaPlayer.start(); 
  21.             } catch (IllegalStateException e1) { 
  22.                 // TODO Auto-generated catch block 
  23.                 e1.printStackTrace(); 
  24.             } catch (IOException e1) { 
  25.                 // TODO Auto-generated catch block 
  26.                 e1.printStackTrace(); 
  27.             } 
  28.             break

测试结果:不能切换,有一个暂停,即是停止和重新做准备的所得。

结论:第一次设置holder之后,似乎就绑定了,无法切换到其他holder所控制的SurfaceView对应的display surface。

 More为什么setDisplay()方法不能使得显示视频的SurfaceView进行切换,查找源码:如下图所示:

 

因为我暂时对Native Framework不是很了解,所以只提到这里。原因就在:_setVideoSurface();

updateSurfaceScreenOn();

这两个方法中,可以去查找相关内容。

 

Test3既然不能再两个SurfaceView之间切换,那么更改其中一个大小,可以变化吗?

点击Enlarge1按钮,gone掉Big SurfaceView,增大small SurfaceView

代码如下:

  1. public void onClick(View v) { 
  2.          
  3.         switch (v.getId()) { 
  4.         case R.id.buttonToSmall: 
  5.             Log.d("testSurfaceView""onClick buttonToSmall"); 
  6.             try { 
  7.                 mMediaPlayer.setDisplay(mSurfaceView1.getHolder()); 
  8.                 mMediaPlayer.prepare(); 
  9.                 mMediaPlayer.start(); 
  10.             } catch (IllegalStateException e) { 
  11.                 // TODO Auto-generated catch block 
  12.                 e.printStackTrace(); 
  13.             } catch (IOException e) { 
  14.                 // TODO Auto-generated catch block 
  15.                 e.printStackTrace(); 
  16.             } 
  17.              
  18.             break
  19.         case R.id.buttonToBig: 
  20.             Log.d("testSurfaceView""onClick buttonToBig"); 
  21.             Log.d("testSurfaceView""has finished buttonToBig"); 
  22.             break
  23.              
  24.         case R.id.buttonEnlarge1: 
  25.             Log.d("testSurfaceView""onClick buttonEnlarge1"); 
  26.             try { 
  27.                  
  28.                 mMediaPlayer.pause(); 
  29.                 mSurfaceView2.setVisibility(View.GONE); 
  30.                 mSurfaceView1.setLayoutParams(new LinearLayout.LayoutParams(500500)); 
  31.                 mSurfaceView1.getHolder().setFixedSize(500500);            
  32.                 mMediaPlayer.setDisplay(mSurfaceView1.getHolder()); 
  33.                 mMediaPlayer.start(); 
  34.             } catch (IllegalStateException e) { 
  35.                 // TODO Auto-generated catch block 
  36.                 e.printStackTrace(); 
  37.             } catch (IllegalArgumentException e) { 
  38.                 // TODO Auto-generated catch block 
  39.                 e.printStackTrace(); 
  40.             } 
  41.             break
  42.         } 
  43.      
  44.     } 

测试结果:我靠!居然ok了。再写此篇博文的时候,这个问题并没有解决。我是准备按照我的思路一步一步测试。

原因分析:很简单的道理。真正显示的是surface,通过SurfaceView.getHolder()获得SurfaceView对应下的Surface的控制器SurfaceHolder,获得SurfaceView的初始大小,但是,SurfaceHolder并未与SurfaceView动态关联(即虽然凿出的洞很大,但是真正播放的大小仍然是初始大小)。我们需要使用方法SurfaceHolder让真正播放的大小与凿的洞的大小相适应。这样视频才会大。

解决了MediaPlayer +SurafaceView 全屏的问题。