MediaPlayer+TextureView實現小視頻居中(不拉伸)播放

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

github下載源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章