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