引子:16年手机小视频功能可以说是井喷式发展,我们公司也有这样的需求,android自带的有VideoView可以实现视频的播放,但是封装的太死,有些业务需求不能满足,所以自己写一个,在这里记下来,权当练手。
我的思路是用MediaPlayer和TextureView来结合实现。(VideoView底层用的也是MediaPlayer,至于为什么不用SurfaceView而用TextureView,是因为SurfaceView不能放在可滑动的控件中,至于具体原因和缺点如果不清楚可自行百度之,TextureView正是为了解决这个问题而存在的
首先我们要继承自TextureView并实现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方法中初始化mediaplayer,代码如下,我都有详尽的注释:
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
Log.e(TEXTUREVIDEO_TAG,"onsurfacetexture available");
if (mMediaPlayer==null){
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
//当MediaPlayer对象处于Prepared状态的时候,可以调整音频/视频的属性,如音量,播放时是否一直亮屏,循环播放等。
mMediaPlayer.setVolume(1f,1f);
}
});
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
return false;
}
});
mMediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
//此方法获取的是缓冲的状态
Log.e(TEXTUREVIDEO_TAG,"缓冲中:"+percent);
}
});
//播放完成的监听
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mState = VideoState.init;
if (listener!=null) listener.onPlayingFinish();
}
});
}
//拿到要展示的图形界面
Surface mediaSurface = new Surface(surface);
//把surface设置给MediaPlayer
mMediaPlayer.setSurface(mediaSurface);
mState = VideoState.palying;
}
在onSurfaceTextureDestroyed方法中,停止mediaplayer:
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
mMediaPlayer.pause();
mMediaPlayer.stop();
mMediaPlayer.reset();
if (listener!=null)listener.onTextureDestory();
return false;
}
做到这一步我们基本上就可以简单的播放视频了,调用以下代码进行播放:
mMediaPlayer.reset();
mMediaPlayer.setDataSource(url);
mMediaPlayer.prepare();
mMediaPlayer.start();
解决播放时候视图拉伸的问题:
但是在播放的时候我发现视频是拉伸的,就像这样:
相当于ImageView的FIT_XY的形式,导致整个看起来拉伸变形,而我们的要求是铺满但不变形拉伸,就相当于ImageView的CenterCrop形式,所以还应该对视图进行缩放处理,所以又写了一个方法:
//重新计算video的显示位置,裁剪后全屏显示
private void updateTextureViewSizeCenterCrop(){
float sx = (float) getWidth() / (float) mVideoWidth;
float sy = (float) getHeight() / (float) mVideoHeight;
Matrix matrix = new Matrix();
float maxScale = Math.max(sx, sy);
//第1步:把视频区移动到View区,使两者中心点重合.
matrix.preTranslate((getWidth() - mVideoWidth) / 2, (getHeight() - mVideoHeight) / 2);
//第2步:因为默认视频是fitXY的形式显示的,所以首先要缩放还原回来.
matrix.preScale(mVideoWidth / (float) getWidth(), mVideoHeight / (float) getHeight());
//第3步,等比例放大或缩小,直到视频区的一边超过View一边, 另一边与View的另一边相等. 因为超过的部分超出了View的范围,所以是不会显示的,相当于裁剪了.
matrix.postScale(maxScale, maxScale, getWidth() / 2, getHeight() / 2);//后两个参数坐标是以整个View的坐标系以参考的
setTransform(matrix);
postInvalidate();
}
这个方法需要在我们得知小视频的具体宽高后调用,MediaPlayer已经给我们提供好了接口,我们只需要在初始化的时候给MediaPlayer设置,代码如下:
mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
mVideoHeight = mMediaPlayer.getVideoHeight();
mVideoWidth = mMediaPlayer.getVideoWidth();
updateTextureViewSize(mVideoMode);
if (listener!=null){
listener.onVideoSizeChanged(mVideoWidth,mVideoHeight);
}
}
});
到此视频就能用CenterCrop形式播放,但是我还想实现微博小视频那样,居中播放,剩余的位置留白,所以我又写了一个方法,和上边那个类似,缩放比例计算方式不同,代码如下:
//重新计算video的显示位置,让其全部显示并据中
private void updateTextureViewSizeCenter(){
float sx = (float) getWidth() / (float) mVideoWidth;
float sy = (float) getHeight() / (float) mVideoHeight;
Matrix matrix = new Matrix();
//第1步:把视频区移动到View区,使两者中心点重合.
matrix.preTranslate((getWidth() - mVideoWidth) / 2, (getHeight() - mVideoHeight) / 2);
//第2步:因为默认视频是fitXY的形式显示的,所以首先要缩放还原回来.
matrix.preScale(mVideoWidth / (float) getWidth(), mVideoHeight / (float) getHeight());
//第3步,等比例放大或缩小,直到视频区的一边和View一边相等.如果另一边和view的一边不相等,则留下空隙
if (sx >= sy){
matrix.postScale(sy, sy, getWidth() / 2, getHeight() / 2);
}else{
matrix.postScale(sx, sx, getWidth() / 2, getHeight() / 2);
}
setTransform(matrix);
postInvalidate();
}
然后就得到了我想要的样子,效果如图:
扩展:如果以上两种加上再默认的一种视频缩放方式还不能满足你的需求,那你可以自己写,自己实现缩放的比例,里边涉及到一些矩阵Matrix的知识,如果不知道百度之;
以上代码你可能会发现,有两个参数listener和mState很多地方都有用到,listener是我自己定义的方便外部调用的接口,里边的方法可以根据自己的需求自行增改,mState是监听播放状态的枚举,也可以自行增减,代码如下:
//回调监听
public interface OnVideoPlayingListener {
void onVideoSizeChanged(int vWidth,int vHeight);
void onStart();
void onPlaying(int duration, int percent);
void onPause();
void onRestart();
void onPlayingFinish();
void onTextureDestory();
}
//播放状态
public enum VideoState{
init,palying,pause
}
最后一点,播放进度获取:
我在这里写了一个handler,每当调用start()方法的时候就启动handler,每隔100毫秒获取一次播放进度:
//播放进度获取
private void getPlayingProgress(){
mProgressHandler.sendEmptyMessage(0);
}
private Handler mProgressHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 0){
if (listener!=null && mState == VideoState.palying){
listener.onPlaying(mMediaPlayer.getDuration(),
mMediaPlayer.getCurrentPosition());
sendEmptyMessageDelayed(0,100);
}
}
}
};
以上基本上就是这个播放控件的所有代码了,总共300行不到,实现起来还是挺轻松的,以下是全部代码:
package com.ylh.textureplayer.videoview;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import java.io.IOException;
/**
* Created by yangLiHai on 2016/11/3.
*/
public class TextureVideoPlayer extends TextureView implements TextureView.SurfaceTextureListener{
private String TEXTUREVIDEO_TAG = "yangLiHai_video";
private String url;
public VideoState mState;
private MediaPlayer mMediaPlayer;
private int mVideoWidth;//视频宽度
private int mVideoHeight;//视频高度
public static final int CENTER_CROP_MODE = 1;//中心裁剪模式
public static final int CENTER_MODE = 2;//一边中心填充模式
public int mVideoMode = 0;
//回调监听
public interface OnVideoPlayingListener {
void onVideoSizeChanged(int vWidth,int vHeight);
void onStart();
void onPlaying(int duration, int percent);
void onPause();
void onRestart();
void onPlayingFinish();
void onTextureDestory();
}
//播放状态
public enum VideoState{
init,palying,pause
}
private OnVideoPlayingListener listener;
public void setOnVideoPlayingListener(OnVideoPlayingListener listener){
this.listener = listener;
}
public TextureVideoPlayer(Context context) {
super(context);
init();
}
public TextureVideoPlayer(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TextureVideoPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
setSurfaceTextureListener(this);
}
public void setUrl(String url){
this.url = url;
}
public void play(){
if (mMediaPlayer==null ) return;
try {
mMediaPlayer.reset();
mMediaPlayer.setDataSource(url);
mMediaPlayer.prepare();
mMediaPlayer.start();
mState = VideoState.palying;
if (listener!=null) listener.onStart();
getPlayingProgress();
} catch (IOException e) {
e.printStackTrace();
Log.e(TEXTUREVIDEO_TAG , e.toString());
}
}
public void pause(){
if (mMediaPlayer==null) return;
if (mMediaPlayer.isPlaying()){
mMediaPlayer.pause();
mState = VideoState.pause;
if (listener!=null) listener.onPause();
}else{
mMediaPlayer.start();
mState = VideoState.palying;
if (listener!=null) listener.onRestart();
getPlayingProgress();
}
}
public void stop(){
if (mMediaPlayer.isPlaying()){
mMediaPlayer.stop();
// mMediaPlayer.release();
}
}
//播放进度获取
private void getPlayingProgress(){
mProgressHandler.sendEmptyMessage(0);
}
private Handler mProgressHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 0){
if (listener!=null && mState == VideoState.palying){
listener.onPlaying(mMediaPlayer.getDuration(),mMediaPlayer.getCurrentPosition());
sendEmptyMessageDelayed(0,100);
}
}
}
};
public boolean isPlaying(){
return mMediaPlayer.isPlaying();
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
Log.e(TEXTUREVIDEO_TAG,"onsurfacetexture available");
if (mMediaPlayer==null){
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
//当MediaPlayer对象处于Prepared状态的时候,可以调整音频/视频的属性,如音量,播放时是否一直亮屏,循环播放等。
mMediaPlayer.setVolume(1f,1f);
}
});
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
return false;
}
});
mMediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
//此方法获取的是缓冲的状态
Log.e(TEXTUREVIDEO_TAG,"缓冲中:"+percent);
}
});
//播放完成的监听
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mState = VideoState.init;
if (listener!=null) listener.onPlayingFinish();
}
});
mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
mVideoHeight = mMediaPlayer.getVideoHeight();
mVideoWidth = mMediaPlayer.getVideoWidth();
updateTextureViewSize(mVideoMode);
if (listener!=null){
listener.onVideoSizeChanged(mVideoWidth,mVideoHeight);
}
}
});
}
//拿到要展示的图形界面
Surface mediaSurface = new Surface(surface);
//把surface
mMediaPlayer.setSurface(mediaSurface);
mState = VideoState.palying;
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
updateTextureViewSize(mVideoMode);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
mMediaPlayer.pause();
mMediaPlayer.stop();
mMediaPlayer.reset();
if (listener!=null)listener.onTextureDestory();
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
public void setVideoMode(int mode){
mVideoMode=mode;
}
/**
*
* @param mode Pass {@link #CENTER_CROP_MODE} or {@link #CENTER_MODE}. Default
* value is 0.
*/
public void updateTextureViewSize(int mode){
if (mode==CENTER_MODE){
updateTextureViewSizeCenter();
}else if (mode == CENTER_CROP_MODE){
updateTextureViewSizeCenterCrop();
}
}
//重新计算video的显示位置,裁剪后全屏显示
private void updateTextureViewSizeCenterCrop(){
float sx = (float) getWidth() / (float) mVideoWidth;
float sy = (float) getHeight() / (float) mVideoHeight;
Matrix matrix = new Matrix();
float maxScale = Math.max(sx, sy);
//第1步:把视频区移动到View区,使两者中心点重合.
matrix.preTranslate((getWidth() - mVideoWidth) / 2, (getHeight() - mVideoHeight) / 2);
//第2步:因为默认视频是fitXY的形式显示的,所以首先要缩放还原回来.
matrix.preScale(mVideoWidth / (float) getWidth(), mVideoHeight / (float) getHeight());
//第3步,等比例放大或缩小,直到视频区的一边超过View一边, 另一边与View的另一边相等. 因为超过的部分超出了View的范围,所以是不会显示的,相当于裁剪了.
matrix.postScale(maxScale, maxScale, getWidth() / 2, getHeight() / 2);//后两个参数坐标是以整个View的坐标系以参考的
setTransform(matrix);
postInvalidate();
}
//重新计算video的显示位置,让其全部显示并据中
private void updateTextureViewSizeCenter(){
float sx = (float) getWidth() / (float) mVideoWidth;
float sy = (float) getHeight() / (float) mVideoHeight;
Matrix matrix = new Matrix();
//第1步:把视频区移动到View区,使两者中心点重合.
matrix.preTranslate((getWidth() - mVideoWidth) / 2, (getHeight() - mVideoHeight) / 2);
//第2步:因为默认视频是fitXY的形式显示的,所以首先要缩放还原回来.
matrix.preScale(mVideoWidth / (float) getWidth(), mVideoHeight / (float) getHeight());
//第3步,等比例放大或缩小,直到视频区的一边和View一边相等.如果另一边和view的一边不相等,则留下空隙
if (sx >= sy){
matrix.postScale(sy, sy, getWidth() / 2, getHeight() / 2);
}else{
matrix.postScale(sx, sx, getWidth() / 2, getHeight() / 2);
}
setTransform(matrix);
postInvalidate();
}
}
调用方式也很简单,我会在demo里边写清楚,如果不想用fitxy形式播放视频,记得在play之前调用setVideoMode(int mode);