Android开发视频播放器,一般都是使用MediaPlayer+SurfaceView来实现,VideoView也是使用了MediaPlayer+SurfaceView方式(不信看源码)。所以,我打算使用MediaPlayer+SurfaceView封装自己的视频播放库。
本章打算用之前的例子:视频播放器开发 - MediaPlayer
在例子中,使用的是原生的SurfaceView,现将SurfaceView的高度改为wrap_content,播放一段网络小视频(http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4):
可以看出SurfaceView占满整个屏幕,和match_parent的效果是一样的,而且视频被拉伸,严重变形!!这显然不是我想要的。
之前看过VideoView的源码,里面有这种变形的解决办法。所以,开发视频播放器,第一步,就是自定义一个适配视频的SurfaceView。
代码如下:
public class VideoSurfaceView extends SurfaceView {
// 视频宽度
private int videoWidth;
// 视频高度
private int videoHeight;
public VideoSurfaceView(Context context) {
this(context, null);
}
public VideoSurfaceView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
videoWidth = 0;
videoHeight = 0;
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
}
/**
* 根据视频的宽高设置SurfaceView的宽高
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getDefaultSize(videoWidth, widthMeasureSpec);
int height = getDefaultSize(videoHeight, heightMeasureSpec);
if (videoWidth > 0 && videoHeight > 0) {
// 获取测量模式和测量大小
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 分情况设置大小
if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {
// layout_width = 确定值或match_parent
// layout_height = 确定值或match_parent
// the size is fixed
width = widthSpecSize;
height = heightSpecSize;
// 做适配,不让视频拉伸,保持原来宽高的比例
// for compatibility, we adjust size based on aspect ratio
if ( videoWidth * height < width * videoHeight) {
//Log.i("@@@", "image too wide, correcting");
width = height * videoWidth / videoHeight;
} else if ( videoWidth * height > width * videoHeight) {
//Log.i("@@@", "image too tall, correcting");
height = width * videoHeight / videoWidth;
}
} else if (widthSpecMode == MeasureSpec.EXACTLY) {
// layout_width = 确定值或match_parent
// layout_height = wrap_content
// only the width is fixed, adjust the height to match aspect ratio if possible
width = widthSpecSize;
// 计算高多少,保持原来宽高的比例
height = width * videoHeight / videoWidth;
if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
// couldn't match aspect ratio within the constraints
height = heightSpecSize;
}
} else if (heightSpecMode == MeasureSpec.EXACTLY) {
// layout_width = wrap_content
// layout_height = 确定值或match_parent
// only the height is fixed, adjust the width to match aspect ratio if possible
height = heightSpecSize;
// 计算宽多少,保持原来宽高的比例
width = height * videoWidth / videoHeight;
if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
// couldn't match aspect ratio within the constraints
width = widthSpecSize;
}
} else {
// layout_width = wrap_content
// layout_height = wrap_content
// neither the width nor the height are fixed, try to use actual video size
width = videoWidth;
height = videoHeight;
if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
// too tall, decrease both width and height
height = heightSpecSize;
width = height * videoWidth / videoHeight;
}
if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
// too wide, decrease both width and height
width = widthSpecSize;
height = width * videoHeight / videoWidth;
}
}
} else {
// no size yet, just adopt the given spec sizes
}
// 设置SurfaceView的宽高
setMeasuredDimension(width, height);
}
/**
* 调整大小
* @param videoWidth
* @param videoHeight
*/
public void adjustSize(int videoWidth, int videoHeight) {
if (videoWidth == 0 || videoHeight == 0) return;
// 赋值自己的宽高
this.videoWidth = videoWidth;
this.videoHeight = videoHeight;
// 设置Holder固定的大小
getHolder().setFixedSize(videoWidth, videoHeight);
// 重新设置自己的大小
requestLayout();
}
}
代码注释比较清晰,相信大家看得明白!!
然后需要有个时机去调用自定义VideoSurfaceView的adjustSize方法来调整大小,还是参照VideoView源码,时机是MediaPlayer监听到OnVideoSizeChangedListener事件:
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener,
MediaPlayer.OnSeekCompleteListener, MediaPlayer.OnVideoSizeChangedListener {
// 自定义的SurfaceView
private VideoSurfaceView surfaceView;
/**
* 视频大小改变的时候调用
* @param player
* @param width
* @param height
*/
@Override
public void onVideoSizeChanged(MediaPlayer player, int width, int height) {
// 调整SurfaceView的宽高
surfaceView.adjustSize(player.getVideoWidth(), player.getVideoHeight());
}
...
// 初始化MediaPlayer
player = new MediaPlayer();
// 设置视频大小改变监听
player.setOnVideoSizeChangedListener(this);
}
效果图:
如图,VideoSurfaceView做到了适配视频的大小。
但是还有个小问题,如果我们一开始把VideoSurfaceView设置高度为wrap_content,当MediaPlayer没有准备好,视频还没有被解析成功时,VideoSurfaceView的高度还是黑黑的全屏,所以我们一开始可以把VideoSurfaceView设为一个固定值。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.johan.video.VideoSurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="250dp"
/>
...
LinearLayout>
而且注意一点,这个值应该比视频的高度高一点,为什么呢?我们分析一下VideoSurfaceView的onMeasure方法:
if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {
// layout_width = 确定值或match_parent
// layout_height = 确定值或match_parent
// the size is fixed
width = widthSpecSize;
height = heightSpecSize;
// 做适配,不让视频拉伸,保持原来宽高的比例
// for compatibility, we adjust size based on aspect ratio
if ( videoWidth * height < width * videoHeight) {
//Log.i("@@@", "image too wide, correcting");
width = height * videoWidth / videoHeight;
} else if ( videoWidth * height > width * videoHeight) {
//Log.i("@@@", "image too tall, correcting");
height = width * videoHeight / videoWidth;
}
}
如果我们设置的高度比视频的高度低的话,为了不让视频变形,会将width设值为height * videoWidth / videoHeight,也就是width变小,导致SurfaceView不能占满整个屏幕的宽度。
当然你也可以把判断去掉,这样无论什么情况都可以占满整个屏幕的宽度。不知道之后会出现什么情况,所以暂时不改这里的逻辑。
好了,自定义SurfaceView到这里!!