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下载源码

发布了17 篇原创文章 · 获赞 12 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_24295537/article/details/53997098