TextureView相關(guān)的SurfaceView
SurfaceView的工作方式是創(chuàng)建一個(gè)置于應(yīng)用窗口之后的新窗口实昨。這種方式的效率非常高,因?yàn)镾urfaceView窗口刷新的時(shí)候不需要重繪應(yīng)用程序的窗口(android普通窗口的視圖繪制機(jī)制是一層一層的,任何一個(gè)子元素或者是局部的刷新都會導(dǎo)致整個(gè)視圖結(jié)構(gòu)全部重繪一次,因此效率非常低下,不過滿足普通應(yīng)用界面的需求還是綽綽有余)愕贡,但是SurfaceView也有一些非常不便的限制。
因?yàn)镾urfaceView的內(nèi)容不在應(yīng)用窗口上巷屿,所以不能使用變換(平移固以、縮放、旋轉(zhuǎn)等)嘱巾。也難以放在ListView或者ScrollView中憨琳,不能使用UI控件的一些特性比如View.setAlpha()。
為了解決這個(gè)問題 Android 4.0中引入了TextureView旬昭。
textureView使用方式
TextureView的使用非常簡單篙螟,你唯一要做的就是獲取用于渲染內(nèi)容的SurfaceTexture。具體做法是先創(chuàng)建TextureView對象问拘,然后實(shí)現(xiàn)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()遍略。這個(gè)方法什么時(shí)候觸發(fā)呢?在調(diào)用TextureView的draw方法時(shí)骤坐,如果還沒有初始化SurfaceTexture绪杏。那么就會初始化它艘款。初始化好時(shí),就會回調(diào)這個(gè)接口谬返。SurfaceTexture初始化好時(shí)低矮,就表示可以接收外界的繪制指令了(可以異步接收)。然后SurfaceTexture會以GL紋理信息更新到TextureView對應(yīng)的HardwareLayer中色难。然后就會在HardwareLayer中顯示蝴簇∈涝看一下這個(gè)圖:
[圖片上傳失敗...(image-7d88ad-1513240599393)]
前面說了SurfaceTexture初始化好時(shí)辖佣,就表示可以接收外界的繪制指令了(可以異步接收)霹抛。接受的方式通常有兩種,也就是圖中的Surface進(jìn)行接收卷谈。Surface提供dequeueBuffer/queueBuffer等硬件渲染接口杯拐,和lockCanvas/unlockCanvasAndPost等軟件渲染接口,使內(nèi)容流的源可以往BufferQueue中填graphic buffer世蔗。
在異步線程通過canvas來繪制
總體的流程:
- 繼承TextureView
- 初始化時(shí)實(shí)現(xiàn)SurfaceTextureListener接口端逼。
- 接口實(shí)現(xiàn)的onSurfaceTextureAvailable()方法中開啟一個(gè)線程。
- 在該線程的run()方法中通過lockCanvas()方法獲取到canvas污淋。
- 調(diào)用canvas的api進(jìn)行繪制顶滩。run()方法可以是一個(gè)循環(huán),這樣就可以不斷的繪制寸爆。
- 通過unlockCanvasAndPost()來結(jié)束繪制礁鲁。
舉例子說明。
作為surface接收外界的視頻等數(shù)據(jù)流赁豆。
我們?nèi)绻氩シ乓粋€(gè)視頻仅醇,那么player肯定需要一個(gè)surface來接收視頻數(shù)據(jù)。
這個(gè)surface可以是SurfaceView魔种,也可以是textureView對應(yīng)surface析二。
那么textureView對應(yīng)的surface在哪里呢?textureView中真正用來接收處理視頻流的是SurfaceTexture务嫡。new Surface(textTureView.get SurfaceTexture())就得到了對應(yīng)的surface甲抖。
說下總體流程:
- 創(chuàng)建textureView,設(shè)置和實(shí)現(xiàn)SurfaceTextureListener接口心铃。(一個(gè)問題准谚,為什么不一開始就把textureview放到界面中呢?因?yàn)槿绻婚_始就放置了去扣,SurfaceTextureListener回調(diào)時(shí)柱衔,可能player還沒初始化好。也就沒辦法進(jìn)行player.setSurface())
- 當(dāng)準(zhǔn)備播放時(shí)愉棱,這時(shí)候得保證player已經(jīng)初始化好了(監(jiān)聽方法也設(shè)置了唆铐,比如項(xiàng)目中exoplayerView的setPlayer()方法)。把textureView添加到你的播放器界面view中奔滑。
- 等待SurfaceTextureListener的 回調(diào)艾岂,回調(diào)時(shí),意味著SurfaceTexture準(zhǔn)備好了朋其,可以接收player的數(shù)據(jù)了王浴。那么把對應(yīng)的surface給到player脆炎。啟動player的prepare(mediaSource)就可以了。
無縫銜接播放
首先注意一下氓辣,接收player產(chǎn)生的視頻流的對象是SurfaceTexture秒裕,而不是textureView。textureView只是作為一個(gè)硬件加速層來展示钞啸。所以如果需要無縫銜接的播放(比如小屏播放)几蜻,textureView復(fù)不復(fù)用沒關(guān)系然用。一定要保證SurfaceTexture的復(fù)用体斩。即梭稚,textureView可以new 一個(gè)新的,但textureView.setSurfaceText()中傳入的需是老的SurfaceTexture絮吵。
(當(dāng)然無縫銜接播放哨毁,需要的player是老的player。不然player中的source源武、下載渲染情況扼褪、surface都不一樣,那還怎么無縫銜接粱栖。所以player最好做成單例话浇。需要老的就直接重用,不需要老的闹究,就player.release())
重用textureView—全屏/小屏幕播放
比如幔崖,當(dāng)由于某種原因(需要小屏幕播放),我們會先把textureView從界面原來的ViewGroup中remove掉渣淤。放到其他的ViewGroup中繼續(xù)原來位置播放赏寇,即把textureView添加到新的ViewGroup中。在remove textureView時(shí)价认,textureView 中的surfaceTexture成員變量會置為空嗅定。所以在textureView重新添加到頁面view時(shí),在draw()方法中會重新生成一個(gè)surfaceTexture用踩,并回調(diào)onSurfaceTextureAvailable()接口方法渠退。只有重新生成surfaceTexture時(shí)才會回調(diào)。注意如果在draw()方法之前脐彩,比如preDraw()中碎乃,把老的SurfaceTexture給到新的textureview,那么就不會生成新的surfaceTexture了惠奸,這個(gè)接口也就不會回調(diào)梅誓,下面會說)。前面說了,我們需要復(fù)用SurfaceTexture梗掰,才能保證無縫銜接删豺。即當(dāng)dispatchDraw()或onSurfaceTextureAvailable()方法回調(diào)時(shí),我們就需要把目前player已關(guān)聯(lián)的那個(gè)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()
//中已經(jīng)把savedSurfaceTexture給到了textureView拥坛,那么
//addTextureView()到視圖中時(shí),并不會觸發(fā)
//onSurfaceTextureAvailable()方法.
textureView.setSurfaceTexture(savedSurfaceTexture);
}
}
上面說到了小屏幕播放的一個(gè)技巧尘分。當(dāng)需要小屏幕播放時(shí)猜惋,只需要把textureview和controllview(包括其他的一些必須的view,如果view比較多培愁,甚至可以是整個(gè)playerView著摔。因?yàn)橥粋€(gè)頁面時(shí),playerView外拋的監(jiān)聽器和player都不會變)拿出來放到其他viewGroup中即可定续,textureview和controllview大小都是可以調(diào)整的谍咆。其他都不需要改動,不需要重新創(chuàng)建playerview私股。
跨頁面的無縫銜接播放
比如我們列表中正在播放一個(gè)視頻摹察,我點(diǎn)擊這個(gè)item后進(jìn)入這個(gè)視頻的詳情頁。在詳情頁也需要這個(gè)視頻的播放倡鲸,并且希望播放進(jìn)度和之前無縫銜接供嚎。那么應(yīng)該怎么做呢?肯定不能只把textureview和controllview拿出來放到其他viewGroup中了峭状。
那把整個(gè)playerView都復(fù)用克滴?按理說playerView只是和view相關(guān)的東西。果真如此嗎优床?看下exoplayerview劝赔,除了textureview、controllview胆敞、retryButton等view外望忆,在setPlayer()方法中把player設(shè)置進(jìn)來了。為什么需要設(shè)置進(jìn)來player呢竿秆?因?yàn)閜layer有一些回調(diào)启摄,回調(diào)的實(shí)現(xiàn)有些需要操作view,需要在playerview內(nèi)部實(shí)現(xiàn)(也有些需要外界的特定界面處理)幽钢。所以需要在這里設(shè)置player的一些監(jiān)聽器并實(shí)現(xiàn)歉备。player設(shè)置進(jìn)來了才能addTextureView(),這點(diǎn)前面講了匪燕。addTextureView ()后蕾羊,等到onSurfaceTextureAvailable()方法回調(diào)時(shí)喧笔,還需要player設(shè)置surface(如果player中已有surface,且需要復(fù)用龟再,那么就不用設(shè)置了书闸,需要保證老的surfaceTexture給到textureview)。所以playerView中肯定是需要player的利凑。綜上浆劲,對外界來說,一個(gè)playerView與另一個(gè)playerView可能在于:設(shè)置的player不同哀澈,外界設(shè)置的player的一些監(jiān)聽器實(shí)現(xiàn)不同牌借,SurfaceTexture不同)(這里說的監(jiān)聽器不是playerView內(nèi)部實(shí)現(xiàn)的與UI相關(guān)的監(jiān)聽器,這些UI相關(guān)的監(jiān)聽器屬于playerview內(nèi)部的割按,對外界透明膨报,所以不用管。而是外界被某個(gè)特定頁面實(shí)現(xiàn)的監(jiān)聽器适荣,比如ExoplayerView的onPlayerStateChanged監(jiān)聽器,界面需要根據(jù)它來設(shè)置改變一些position等參數(shù)现柠。這些需要考慮)。因此弛矛,對于跨頁面無縫銜接播放時(shí)晒旅,player肯定不用變,但ExoplayerView向外拋出的監(jiān)聽器是與特定頁面綁定的汪诉。所以跨頁面復(fù)用ExoplayerView時(shí)废恋,需要把ExoplayerView向外拋出的監(jiān)聽器設(shè)置成新頁面的監(jiān)聽器設(shè)置。這樣才能把其他頁面的playerView拿到現(xiàn)在的頁面復(fù)用扒寄。項(xiàng)目中playerView外拋的監(jiān)聽器一部分是在具體頁面實(shí)現(xiàn)的鱼鼓,一部分是在ExoPlayerProvider中實(shí)現(xiàn)的。在項(xiàng)目中该编,外部設(shè)置監(jiān)聽時(shí)迄本,都是通過ExoplayerView來進(jìn)行監(jiān)聽的。雖然ExoplayerView外拋的監(jiān)聽接口也是來源于player课竣,但項(xiàng)目中并沒有直接對player設(shè)置監(jiān)聽嘉赎。我覺得這樣做還有待提高todo,因?yàn)橛行┍O(jiān)聽跟UI沒有關(guān)系于樟,而是與player有關(guān)系公条。監(jiān)聽這些事件如果也通過ExoplayerView,就顯得怪怪的迂曲。比如在ExoPlayerProvider的那些監(jiān)聽實(shí)現(xiàn)靶橱,其實(shí)跟具體頁面無關(guān),所以設(shè)置成player的監(jiān)聽豈不是更好。不過也有好處关霸。在使用或者復(fù)用時(shí)传黄,因?yàn)楸O(jiān)聽都來自于ExoplayerView,所以可以減少一些考慮队寇。
不過膘掰,項(xiàng)目中跨頁面的無縫銜接播放,并沒有重用ExoplayerView佳遣。而是在新的頁面重新new了一個(gè)ExoplayerView识埋。前面講了,對于一個(gè)新的ExoplayerView苍日,外界需要setPlayer(),setPlayer時(shí)需要?jiǎng)討B(tài)添加textureview窗声,并設(shè)置SurfaceTexture相恃。并設(shè)置一些外拋的監(jiān)聽器接口實(shí)現(xiàn)。前面講了setPlayer()時(shí)笨觅,內(nèi)部才會調(diào)用addTextureView方法拦耐,把ExoplayerView初始化時(shí)創(chuàng)建好的tetureView添加到view視圖中,接著會在onSurfaceTextureAvailable()方法回調(diào)時(shí)(注意如果提前把老的SurfaceTexture給到新的textureview见剩,那么這個(gè)接口不會回調(diào)杀糯,下面會說),會把老的SurfaceTexture給到新的textureview(前面代碼中的else)苍苞。這里需要注意一下固翰,因?yàn)閜layer是老的(單例,下面會說)羹呵,所以player中已經(jīng)有老的surface了骂际,所以不需要再設(shè)置。但是需要在這時(shí)候(或者dispatchDraw)把老的SurfaceTexture給到新的textureview冈欢,這樣才能保證無縫銜接歉铝。前面說了,如果想無縫銜接播放凑耻,textureview可以不復(fù)用(比如這里就沒有復(fù)用)太示,但一定需要復(fù)用老的SurfaceTexture。最開始說了香浩,再說一遍类缤,注意setPlayer()時(shí)傳入的player需要是老的player,不然player的source邻吭、播放情況呀非、surface都沒有,那還怎么無縫銜接啊,所以最好把player做成單例岸裙。 需要無縫銜接播放時(shí)就直接重用猖败,如果是新的類,那么就player.release()降允。項(xiàng)目中就把player做成了單例的形式恩闻,存在ExoPlayerProvider中。
雖然ExoplayerView剧董,Textureview(在ExoplayerView初始化時(shí)創(chuàng)建)都是新建的幢尚,但因?yàn)镾urfaceTexture是老的,player是單例(老的)翅楼,所以保證了無縫銜接播放尉剩。
不過項(xiàng)目中有個(gè)問題,雖然把textureview添加到了viewgroup照片中毅臊,但是onSurfaceTextureAvailable接口并沒有回調(diào)理茎。為什么呢?因?yàn)樵赿raw()方法調(diào)用之前已經(jīng)把老的SurfaceTexture給到新的textureview中了管嬉,這時(shí)候就不會重新創(chuàng)建SurfaceTexture了皂林,也不會回調(diào)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的復(fù)用問題及無縫銜接播放
列表中的playerView最好進(jìn)行復(fù)用胎挎。因?yàn)榱斜碇械膒layerView是在同一個(gè)頁面沟启,所以playerView被界面設(shè)置的一些監(jiān)聽不需要變。前面說了一個(gè)playerView中除了監(jiān)聽器犹菇,還有player美浦、textureView、surfaceTexture這些東西项栏。因?yàn)椴サ臇|西不一樣了浦辨,這些東西中的一些需要做一些調(diào)整。哪些作調(diào)整呢沼沈?怎么調(diào)整呢流酬?我們具體分析一下。
對于player列另,我們需要把這次的進(jìn)度保存起來芽腾,這樣下面再播這個(gè)視頻時(shí),可以從原來的地方續(xù)播页衙。(通過player.getPosition()保存long型的postion值摊滔,然后當(dāng)再次播放時(shí)player.seekToPosition(position)就可以了)player還需要release一下原來持有的surface()阴绢,可能還需要release player中的一些其他東西。因?yàn)閾Q成了新的視頻艰躺,所以playerView內(nèi)部設(shè)置并實(shí)現(xiàn)的player的一些監(jiān)聽需要置空呻袭,重新再設(shè)置。把老的textureView remove掉腺兴,置空左电。保存的surfaceTexture也置空。重新創(chuàng)建一個(gè)textureView页响,添加到視圖中篓足。因?yàn)閠extureView中是新創(chuàng)建的,所以如果preDraw()不給textureView添加surfaceTexture的話闰蚕,那么textureView添加到視圖后栈拖,等onSurfaceTextureAvailable接口好了之后會調(diào)用這個(gè)接口。前面說了没陡,這時(shí)候會把onSurfaceTextureAvailable接口返回的surfaceTexture保存成成員變量涩哟,然后包成surface給到player。這時(shí)候player進(jìn)行prepare()诗鸭。
項(xiàng)目中在list列表中重用playerView時(shí)染簇,并沒有“手動將老的textureView remove掉参滴,置空强岸,然后重新創(chuàng)建一個(gè)textureView,添加到視圖中”砾赔。playView中使用的還是老的textureView蝌箍。** 這證明了,不管是復(fù)用不復(fù)用playerView時(shí)候暴心,textureView用老的新的都沒關(guān)系妓盲。這個(gè)不用在意,需要考慮的是surfaceTexture专普。list列表中重用playerView時(shí)悯衬,甚至都沒有走exoplayerView的setPlayer()方法。所以playerView中還是復(fù)用的原來的player(就算進(jìn)行了setPlayer()方法檀夹,設(shè)置的其實(shí)也是一起的player筋粗,因?yàn)閜layer是單例。不過會做一些player的狀態(tài)重置)炸渡。貌似player監(jiān)聽器也都沒有重置娜亿。這個(gè)確實(shí)不是很清楚,什么時(shí)候需要player的監(jiān)聽器重置蚌堵,什么時(shí)候不需要重置买决。我能知道的是沛婴,頁面換了,肯定和頁面相關(guān)的監(jiān)聽接口實(shí)現(xiàn)要換督赤。exoplayerView中的player換了嘁灯,那么需要進(jìn)行setPlayer(),把老的player置空够挂,監(jiān)聽置空旁仿。新的player設(shè)置exoplayerView內(nèi)部的接口實(shí)現(xiàn)。這里重用整個(gè)exoplayerView孽糖,所以監(jiān)聽器都不用變枯冈。實(shí)踐來看,這樣是可以的雖然playerView里面的textureView沒有動办悟,但是在列表頁面中尘奏,肯定會把playerView從一個(gè)item view中remove掉,然后放到新的item view中病蛉。這樣exoplayerView中textureView的surfaceTexture就會變成空了炫加。如果不在preDraw()中把一個(gè)surfaceTexture給到textureView。那么接下來就會回調(diào)onSurfaceTextureAvailable接口铺然。在preDraw()中把一個(gè)surfaceTexture給到textureView俗孝。這個(gè)surfaceTexture是什么呢?還是老的surfaceTexture魄健。其實(shí)我試了試項(xiàng)目中列表的視頻不斷切換播放赋铝,發(fā)現(xiàn)各個(gè)地方使用的surfaceTexture和surface都是同一個(gè)實(shí)例。這是為什么呢沽瘦?因?yàn)槭褂胏learSurface()清理了surface中攜帶的數(shù)據(jù)革骨。可以認(rèn)為surfaceTexture也煥然一新了(其實(shí)surfaceTexture是通過surface來接收數(shù)據(jù)析恋,surface清理了就ok了)良哲。所以,其實(shí)復(fù)用最主要的核心是surface的數(shù)據(jù)有沒有被清理助隧。其他的復(fù)用不復(fù)用都不那么重要筑凫。** 在清理surface之前,需要player釋放surface并村。清理完成后巍实,再添加進(jìn)來。
player.clearVideoSurface();
clearSurface(mSavedSurface);
player.setVideoSurface(mSavedSurface);
總結(jié)一下橘霎,列表復(fù)用playerView時(shí)蔫浆,只是把surface清理了一下,再放回player中姐叁。并且瓦盛,因?yàn)閜layerView有個(gè)detatch和tatch過程洗显,playerView中textureview的surfaceTexture置空了,所以需要在preDraw()時(shí)原环,再把老的surfaceTexture給到textureview挠唆。為什么老的可以,因?yàn)樗呛蛃urface關(guān)聯(lián)的嘱吗,surface清理了就可以了玄组。
然后就是準(zhǔn)備mediaSource,然后player.prepare(mediaSource);如果需要從以前的位置續(xù)播谒麦,那么獲取上一次的position俄讹,執(zhí)行player.seekTo(resumePosition);