Android播放器開發第一步——自定義VideoView繼承SurfaceView
上文介紹了開發Android播發器的簡單流程:利用Vitamio開發視頻播放器(一)
這裏直接進入第一步:
首先我們來看下官方文檔對surface的介紹:
SurfaceView是視圖(View)的繼承類,這個視圖裏內嵌了一個專門用於繪製的Surface。
你可以控制這個Surface的格式和尺寸。Surfaceview控制這個Surface的繪製位置。
surface是縱深排序(Z-ordered)的,這表明它總在自己所在窗口的後面。
surfaceview提供了一個可見區域,只有在這個可見區域內 的surface部分內容纔可見,可見區域外的部分不可見。
surface的排版顯示受到視圖層級關係的影響,它的兄弟視圖結點會在頂端顯示。這意味者 surface的內容會被它的兄弟視圖遮擋,這一特性可以用來放置遮蓋物(overlays)(例如,文本和按鈕等控件)。
注意,如果surface上面 有透明控件,那麼它的每次變化都會引起框架重新計算它和頂層控件的透明效果,這會影響性能。
SurfaceView和View最本質的區別在於:SurfaceView是在一個新起的單獨線程中可以重新繪製畫面而View必須在UI的主線程中更新畫面。所以surface這些特性正好滿足了我們作爲視頻容器的要求,下面代碼寫起來:
1.定義一個類VideoView繼承SurfaceView
2.定義一個接口SurfaceCallback
private SurfaceCallback mListener;
public interface SurfaceCallback {
public void onSurfaceCreated(SurfaceHolder holder);
public void onSurfaceChanged(SurfaceHolder holder, int format,int width, int height);
public void onSurfaceDestroyed(SurfaceHolder holder);
}
這個接口幹什麼等會在分析
3.初始化SurfaceHolder和它的接口mCallback
private SurfaceHolder mSurfaceHolder;
private SurfaceHolder.Callback mCallback = new SurfaceHolder.Callback() {
//surface大小或者格式改變時調用
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
holder.setKeepScreenOn(true);
if (mListener != null)
mListener.onSurfaceChanged(holder, format, width, height);
}
//surface創建時調用,一般在這裏調用畫面
@Override
public void surfaceCreated(SurfaceHolder holder) {
mSurfaceHolder = holder;
if (mListener != null)
mListener.onSurfaceCreated(holder);
}
//surface銷燬時調用,一般在這裏將畫面的停止
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mListener != null)
mListener.onSurfaceDestroyed(holder);
}
};
萬物都有它的生命週期,就像Activity,一個surface也不例外
這個SurfaceHolder類似一個監聽器(其實就是個接口),重寫了三個方法可以看出它監聽了surface的創建,銷燬和改變。
到時候我們要讓播放Activity實現我們的監聽,所以我們不在這三個方法裏做具體實現而是交給mListener,這個mListener就是第2步中我們定義的SurfaceCallback接口了,到時候我們只需在Activity重寫SurfaceCallback下的三個方法就可以監聽這個surface了。
4.記得在構造方法中addCallback,爲SurfaceHolder添加mCallback回調接口
public VideoView(Context context, AttributeSet attrs) {
super(context, attrs);
getHolder().addCallback(mCallback); // 爲SurfaceHolder添加mCallback回調接口
getHolder().setFormat(PixelFormat.RGBA_8888);
}
通過SurfaceHolder接口訪問這個surface,getHolder()方法可以得到這個接口
5.再寫一個初始化方法initialize
private Activity mActivity;
public void initialize(Activity activity, SurfaceCallback l, boolean push) {
mActivity = activity;
mListener = l;//拿到回調
if (mSurfaceHolder != null) {
mSurfaceHolder = getHolder();
}
if (push)
//設置Surface不維護自己的緩衝區,而是等待屏幕的渲染引擎將內容推送到用戶面前
getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
else
getHolder().setType(SurfaceHolder.SURFACE_TYPE_NORMAL);
}
傳兩個參數,activity後面要用,SurfaceCallback作爲回調拿到,其實在構造方法裏就可以做這些事了 -,- 但是第三個參數push爲後面是否開啓硬件加速做準備。
6.設置屏幕尺寸setVideoLayout
private int mSurfaceWidth, mSurfaceHeight;
private int mVideoMode = VIDEO_LAYOUT_SCALE;//默認全屏
public static final int VIDEO_LAYOUT_ORIGIN = 0;//100%
public static final int VIDEO_LAYOUT_SCALE = 1;//全屏
public static final int VIDEO_LAYOUT_STRETCH = 2;//拉伸
public static final int VIDEO_LAYOUT_ZOOM = 3;//裁剪
public void setVideoLayout(int mode, float userRatio, int videoWidth,
int videoHeight, float videoRatio) {
mVideoMode = mode;
setSurfaceLayout(userRatio, videoWidth, videoHeight, videoRatio);
}
// 屏幕適配
private void setSurfaceLayout(float userRatio, int videoWidth,
int videoHeight, float videoAspectRatio) {
LayoutParams lp = getLayoutParams();
//拿到屏幕的寬高,display.getWidth
int windowWidth = DeviceUtils.getScreenWidth(mActivity);
int windowHeight = DeviceUtils.getScreenHeight(mActivity);
//屏幕寬高比
float windowRatio = windowWidth / (float) windowHeight;
//視頻寬高比
float videoRatio = userRatio <= 0.01f ? videoAspectRatio : userRatio;
mSurfaceHeight = videoHeight;
mSurfaceWidth = videoWidth;
//100%,視頻原始尺寸顯示在屏幕上
if (VIDEO_LAYOUT_ORIGIN == mVideoMode && mSurfaceWidth < windowWidth
&& mSurfaceHeight < windowHeight) {
lp.width = (int) (mSurfaceHeight * videoRatio);
lp.height = mSurfaceHeight;
} else if (mVideoMode == VIDEO_LAYOUT_ZOOM) {
//裁剪,通過視頻寬高比和屏幕寬高比比較來判斷是否裁剪視頻寬高
lp.width = windowRatio > videoRatio ? windowWidth
: (int) (videoRatio * windowHeight);
lp.height = windowRatio < videoRatio ? windowHeight
: (int) (windowWidth / videoRatio);
} else {
//伸縮
boolean full = mVideoMode == VIDEO_LAYOUT_STRETCH;
lp.width = (full || windowRatio < videoRatio) ? windowWidth
: (int) (videoRatio * windowHeight);
lp.height = (full || windowRatio > videoRatio) ? windowHeight
: (int) (windowWidth / videoRatio);
}
setLayoutParams(lp);
getHolder().setFixedSize(mSurfaceWidth, mSurfaceHeight); // 設置分辨率,必須的
}
代碼註釋的很詳細了,這裏做了幾個簡單的視頻適配,對外暴露了一個setVideoLayout,拿到了視頻的寬高和比,還有視頻的模式,把這幾個參數傳給私有的setSurfaceLayout去判斷:
如果爲VIDEO_LAYOUT_ORIGIN模式,視頻原始模式,那麼視頻surface高=視頻高,surface寬=視頻高*視頻寬高比=視頻寬
如果爲VIDEO_LAYOUT_ZOOM模式,裁剪模式,那麼判斷:
- 如果屏幕寬高比>視頻視頻寬高比(以寬做參考系),那麼surface的寬爲屏幕的寬,surface的高爲視頻放大到寬正好爲屏幕寬時高的大小,就是windowWidth / videoRatio(注意這裏是÷),此時可以肯定的是當視頻寬正好適配屏幕寬時,視頻高肯定會大於屏幕的高(前提我們已經控制分辨率不變了),多出來的部分我們看不到了,所以達到了裁剪多餘高的效果
- 反之屏幕寬高比<視頻視頻寬高比(以高爲參考系),那麼就是裁剪寬了。
如果爲VIDEO_LAYOUT_STRETCH模式,那麼surface寬高就等於屏幕寬高,起到了伸縮的效果。
最後提幾點:
- 所有SurfaceView和SurfaceHolder.Callback的方法都應該在UI線程裏調用,一般來說就是應用程序主線程。渲染線程所要訪問的各種變量應該作同步處理。
- 由於surface可能被銷燬,它只在SurfaceHolder.Callback.surfaceCreated()和SurfaceHolder.Callback.surfaceDestroyed()之間有效,所以要確保渲染線程訪問的是合法有效的surface。