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。