引子: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);