textureView的两种使用场景

TextureView相关的SurfaceView

SurfaceView的工作方式是创建一个置于应用窗口之后的新窗口。这种方式的效率非常高,因为SurfaceView窗口刷新的时候不需要重绘应用程序的窗口(android普通窗口的视图绘制机制是一层一层的,任何一个子元素或者是局部的刷新都会导致整个视图结构全部重绘一次,因此效率非常低下,不过满足普通应用界面的需求还是绰绰有余),但是SurfaceView也有一些非常不便的限制。
因为SurfaceView的内容不在应用窗口上,所以不能使用变换(平移、缩放、旋转等)。也难以放在ListView或者ScrollView中,不能使用UI控件的一些特性比如View.setAlpha()。
为了解决这个问题 Android 4.0中引入了TextureView。

textureView使用方式

TextureView的使用非常简单,你唯一要做的就是获取用于渲染内容的SurfaceTexture。具体做法是先创建TextureView对象,然后实现SurfaceTextureListener接口:

@Override
public void onSurfaceTextureAvailable(SurfaceTexture arg0, int arg1, int arg2) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture arg0) {
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture arg0, int arg1,int arg2) {
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture arg0) {
}

主要说下onSurfaceTextureAvailable()。这个方法什么时候触发呢?在调用TextureView的draw方法时,如果还没有初始化SurfaceTexture。那么就会初始化它。初始化好时,就会回调这个接口。SurfaceTexture初始化好时,就表示可以接收外界的绘制指令了(可以异步接收)。然后SurfaceTexture会以GL纹理信息更新到TextureView对应的HardwareLayer中。然后就会在HardwareLayer中显示。看一下这个图:

[图片上传失败...(image-7d88ad-1513240599393)]

前面说了SurfaceTexture初始化好时,就表示可以接收外界的绘制指令了(可以异步接收)。接受的方式通常有两种,也就是图中的Surface进行接收。Surface提供dequeueBuffer/queueBuffer等硬件渲染接口,和lockCanvas/unlockCanvasAndPost等软件渲染接口,使内容流的源可以往BufferQueue中填graphic buffer。

在异步线程通过canvas来绘制

总体的流程:

  1. 继承TextureView
  2. 初始化时实现SurfaceTextureListener接口。
  3. 接口实现的onSurfaceTextureAvailable()方法中开启一个线程。
  4. 在该线程的run()方法中通过lockCanvas()方法获取到canvas。
  5. 调用canvas的api进行绘制。run()方法可以是一个循环,这样就可以不断的绘制。
  6. 通过unlockCanvasAndPost()来结束绘制。

举例子说明。

作为surface接收外界的视频等数据流。

我们如果想播放一个视频,那么player肯定需要一个surface来接收视频数据。
这个surface可以是SurfaceView,也可以是textureView对应surface。

那么textureView对应的surface在哪里呢?textureView中真正用来接收处理视频流的是SurfaceTexture。new Surface(textTureView.get SurfaceTexture())就得到了对应的surface。

说下总体流程:

  1. 创建textureView,设置和实现SurfaceTextureListener接口。(一个问题,为什么不一开始就把textureview放到界面中呢?因为如果一开始就放置了,SurfaceTextureListener回调时,可能player还没初始化好。也就没办法进行player.setSurface())
  2. 当准备播放时,这时候得保证player已经初始化好了(监听方法也设置了,比如项目中exoplayerView的setPlayer()方法)。把textureView添加到你的播放器界面view中。
  3. 等待SurfaceTextureListener的 回调,回调时,意味着SurfaceTexture准备好了,可以接收player的数据了。那么把对应的surface给到player。启动player的prepare(mediaSource)就可以了。

无缝衔接播放

首先注意一下,接收player产生的视频流的对象是SurfaceTexture,而不是textureView。textureView只是作为一个硬件加速层来展示。所以如果需要无缝衔接的播放(比如小屏播放),textureView复不复用没关系然用。一定要保证SurfaceTexture的复用。即,textureView可以new 一个新的,但textureView.setSurfaceText()中传入的需是老的SurfaceTexture。
(当然无缝衔接播放,需要的player是老的player。不然player中的source、下载渲染情况、surface都不一样,那还怎么无缝衔接。所以player最好做成单例。需要老的就直接重用,不需要老的,就player.release())

重用textureView—全屏/小屏幕播放

比如,当由于某种原因(需要小屏幕播放),我们会先把textureView从界面原来的ViewGroup中remove掉。放到其他的ViewGroup中继续原来位置播放,即把textureView添加到新的ViewGroup中。在remove textureView时,textureView 中的surfaceTexture成员变量会置为空。所以在textureView重新添加到页面view时,在draw()方法中会重新生成一个surfaceTexture,并回调onSurfaceTextureAvailable()接口方法。只有重新生成surfaceTexture时才会回调。注意如果在draw()方法之前,比如preDraw()中,把老的SurfaceTexture给到新的textureview,那么就不会生成新的surfaceTexture了,这个接口也就不会回调,下面会说)。前面说了,我们需要复用SurfaceTexture,才能保证无缝衔接。即当dispatchDraw()或onSurfaceTextureAvailable()方法回调时,我们就需要把目前player已关联的那个SurfaceTexture(老的)通过textureView.setSurfaceTexture(oldSurfaceTexture)给到textureView:

@Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
        if (savedSurfaceTexture == null) {
            savedSurfaceTexture = surfaceTexture;
            prepare();//把surfaceTexture给到player,然后 
//player.prepare()。
        } else {//表示之前存在savedSurfaceTexture。如果在preDraw()
//中已经把savedSurfaceTexture给到了textureView,那么
//addTextureView()到视图中时,并不会触发
//onSurfaceTextureAvailable()方法.
            textureView.setSurfaceTexture(savedSurfaceTexture);
        }
    }

上面说到了小屏幕播放的一个技巧。当需要小屏幕播放时,只需要把textureview和controllview(包括其他的一些必须的view,如果view比较多,甚至可以是整个playerView。因为同一个页面时,playerView外抛的监听器和player都不会变)拿出来放到其他viewGroup中即可,textureview和controllview大小都是可以调整的。其他都不需要改动,不需要重新创建playerview。

跨页面的无缝衔接播放

比如我们列表中正在播放一个视频,我点击这个item后进入这个视频的详情页。在详情页也需要这个视频的播放,并且希望播放进度和之前无缝衔接。那么应该怎么做呢?肯定不能只把textureview和controllview拿出来放到其他viewGroup中了。

那把整个playerView都复用?按理说playerView只是和view相关的东西。果真如此吗?看下exoplayerview,除了textureview、controllview、retryButton等view外,在setPlayer()方法中把player设置进来了。为什么需要设置进来player呢?因为player有一些回调,回调的实现有些需要操作view,需要在playerview内部实现(也有些需要外界的特定界面处理)。所以需要在这里设置player的一些监听器并实现。player设置进来了才能addTextureView(),这点前面讲了。addTextureView ()后,等到onSurfaceTextureAvailable()方法回调时,还需要player设置surface(如果player中已有surface,且需要复用,那么就不用设置了,需要保证老的surfaceTexture给到textureview)。所以playerView中肯定是需要player的。综上,对外界来说,一个playerView与另一个playerView可能在于:设置的player不同,外界设置的player的一些监听器实现不同,SurfaceTexture不同)(这里说的监听器不是playerView内部实现的与UI相关的监听器,这些UI相关的监听器属于playerview内部的,对外界透明,所以不用管。而是外界被某个特定页面实现的监听器,比如ExoplayerView的onPlayerStateChanged监听器,界面需要根据它来设置改变一些position等参数。这些需要考虑)。因此,对于跨页面无缝衔接播放时,player肯定不用变,但ExoplayerView向外抛出的监听器是与特定页面绑定的。所以跨页面复用ExoplayerView时,需要把ExoplayerView向外抛出的监听器设置成新页面的监听器设置。这样才能把其他页面的playerView拿到现在的页面复用。项目中playerView外抛的监听器一部分是在具体页面实现的,一部分是在ExoPlayerProvider中实现的。在项目中,外部设置监听时,都是通过ExoplayerView来进行监听的。虽然ExoplayerView外抛的监听接口也是来源于player,但项目中并没有直接对player设置监听。我觉得这样做还有待提高todo,因为有些监听跟UI没有关系,而是与player有关系。监听这些事件如果也通过ExoplayerView,就显得怪怪的。比如在ExoPlayerProvider的那些监听实现,其实跟具体页面无关,所以设置成player的监听岂不是更好。不过也有好处。在使用或者复用时,因为监听都来自于ExoplayerView,所以可以减少一些考虑。

不过,项目中跨页面的无缝衔接播放,并没有重用ExoplayerView。而是在新的页面重新new了一个ExoplayerView。前面讲了,对于一个新的ExoplayerView,外界需要setPlayer(),setPlayer时需要动态添加textureview,并设置SurfaceTexture。并设置一些外抛的监听器接口实现。前面讲了setPlayer()时,内部才会调用addTextureView方法,把ExoplayerView初始化时创建好的tetureView添加到view视图中,接着会在onSurfaceTextureAvailable()方法回调时(注意如果提前把老的SurfaceTexture给到新的textureview,那么这个接口不会回调,下面会说),会把老的SurfaceTexture给到新的textureview(前面代码中的else)。这里需要注意一下,因为player是老的(单例,下面会说),所以player中已经有老的surface了,所以不需要再设置。但是需要在这时候(或者dispatchDraw)把老的SurfaceTexture给到新的textureview,这样才能保证无缝衔接。前面说了,如果想无缝衔接播放,textureview可以不复用(比如这里就没有复用),但一定需要复用老的SurfaceTexture。最开始说了,再说一遍,注意setPlayer()时传入的player需要是老的player,不然player的source、播放情况、surface都没有,那还怎么无缝衔接啊,所以最好把player做成单例。 需要无缝衔接播放时就直接重用,如果是新的类,那么就player.release()。项目中就把player做成了单例的形式,存在ExoPlayerProvider中。
虽然ExoplayerView,Textureview(在ExoplayerView初始化时创建)都是新建的,但因为SurfaceTexture是老的,player是单例(老的),所以保证了无缝衔接播放。
不过项目中有个问题,虽然把textureview添加到了viewgroup照片中,但是onSurfaceTextureAvailable接口并没有回调。为什么呢?因为在draw()方法调用之前已经把老的SurfaceTexture给到新的textureview中了,这时候就不会重新创建SurfaceTexture了,也不会回调onSurfaceTextureAvailable接口了。前面有些地方分析不对,注意一下。我有空再改。textureView draw方法中的部分代码:

   boolean createNewSurface = (mSurface == null);
            if (createNewSurface) {
                // Create a new SurfaceTexture for the layer.
                mSurface = new SurfaceTexture(false);
                nCreateNativeWindow(mSurface);
            }
            mLayer.setSurfaceTexture(mSurface);
            mSurface.setDefaultBufferSize(getWidth(), getHeight());
            mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler);

            if (mListener != null && createNewSurface) {
                mListener.onSurfaceTextureAvailable(mSurface, getWidth(), getHeight());
            }
列表中playerView的复用问题及无缝衔接播放

列表中的playerView最好进行复用。因为列表中的playerView是在同一个页面,所以playerView被界面设置的一些监听不需要变。前面说了一个playerView中除了监听器,还有player、textureView、surfaceTexture这些东西。因为播的东西不一样了,这些东西中的一些需要做一些调整。哪些作调整呢?怎么调整呢?我们具体分析一下。
对于player,我们需要把这次的进度保存起来,这样下面再播这个视频时,可以从原来的地方续播。(通过player.getPosition()保存long型的postion值,然后当再次播放时player.seekToPosition(position)就可以了)player还需要release一下原来持有的surface(),可能还需要release player中的一些其他东西。因为换成了新的视频,所以playerView内部设置并实现的player的一些监听需要置空,重新再设置。把老的textureView remove掉,置空。保存的surfaceTexture也置空。重新创建一个textureView,添加到视图中。因为textureView中是新创建的,所以如果preDraw()不给textureView添加surfaceTexture的话,那么textureView添加到视图后,等onSurfaceTextureAvailable接口好了之后会调用这个接口。前面说了,这时候会把onSurfaceTextureAvailable接口返回的surfaceTexture保存成成员变量,然后包成surface给到player。这时候player进行prepare()。

项目中在list列表中重用playerView时,并没有“手动将老的textureView remove掉,置空,然后重新创建一个textureView,添加到视图中”。playView中使用的还是老的textureView。** 这证明了,不管是复用不复用playerView时候,textureView用老的新的都没关系。这个不用在意,需要考虑的是surfaceTexture。list列表中重用playerView时,甚至都没有走exoplayerView的setPlayer()方法。所以playerView中还是复用的原来的player(就算进行了setPlayer()方法,设置的其实也是一起的player,因为player是单例。不过会做一些player的状态重置)。貌似player监听器也都没有重置。这个确实不是很清楚,什么时候需要player的监听器重置,什么时候不需要重置。我能知道的是,页面换了,肯定和页面相关的监听接口实现要换。exoplayerView中的player换了,那么需要进行setPlayer(),把老的player置空,监听置空。新的player设置exoplayerView内部的接口实现。这里重用整个exoplayerView,所以监听器都不用变。实践来看,这样是可以的虽然playerView里面的textureView没有动,但是在列表页面中,肯定会把playerView从一个item view中remove掉,然后放到新的item view中。这样exoplayerView中textureView的surfaceTexture就会变成空了。如果不在preDraw()中把一个surfaceTexture给到textureView。那么接下来就会回调onSurfaceTextureAvailable接口。在preDraw()中把一个surfaceTexture给到textureView。这个surfaceTexture是什么呢?还是老的surfaceTexture。其实我试了试项目中列表的视频不断切换播放,发现各个地方使用的surfaceTexture和surface都是同一个实例。这是为什么呢?因为使用clearSurface()清理了surface中携带的数据。可以认为surfaceTexture也焕然一新了(其实surfaceTexture是通过surface来接收数据,surface清理了就ok了)。所以,其实复用最主要的核心是surface的数据有没有被清理。其他的复用不复用都不那么重要。** 在清理surface之前,需要player释放surface。清理完成后,再添加进来。

  player.clearVideoSurface();
  clearSurface(mSavedSurface);
  player.setVideoSurface(mSavedSurface);

总结一下,列表复用playerView时,只是把surface清理了一下,再放回player中。并且,因为playerView有个detatch和tatch过程,playerView中textureview的surfaceTexture置空了,所以需要在preDraw()时,再把老的surfaceTexture给到textureview。为什么老的可以,因为它是和surface关联的,surface清理了就可以了。

然后就是准备mediaSource,然后player.prepare(mediaSource);如果需要从以前的位置续播,那么获取上一次的position,执行player.seekTo(resumePosition);

推荐阅读更多精彩内容