Android仿微信小视频录制功能(二)

Android仿微信小视频录制功能(二)

接着上一篇,在完成了录制功能后,伟大的哲学家沃兹基索德曾经说过:“有录就有放。”,那么紧接着就来实现播放功能,按照国际惯例,先上下效果图:
这里写图片描述
可以看到界面上存在着瑕疵,强迫症患者可能无法忍受,所以抓紧进入功能实现上来。

需求

简单分析下需求,需求很简单:因为我们录制的视频保存在本地,获取它不需要进行网络交互,但是仍然希望有一个进度条的展示,在进度条展示期间所呈现的是视频的预览图片,进度条加载完成后再将视频展示并播放,播放完成后再循环播放,并且提示点击可以关闭。

实现

功能实现上,提到视频播放首先想到的就是调用系统的VideoView控件来实现。所以,我们先用它来实现前面分析的需求上的功能,再来简单探究下这个VideoView
这里先给出界面布局吧:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/video_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    android:orientation="vertical" >
    <VideoView android:id="@+id/video_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
     />
    <ImageView android:id="@+id/video_thumb_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:scaleType="fitXY"
     />
    <com.example.activity.widget.movie.view.CircleProgressView
        android:id="@+id/circle_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
    />
</RelativeLayout>

简单暴力的把三个控件叠在一起。
功能实现上,由于采用VideoView的缘故,许多方法都是封装好的,所以实现起来也是非常的简单,就先给出代码:

public class MoviePlayerActivity extends BaseActivity implements OnPreparedListener,OnErrorListener,OnCompletionListener{
    private VideoView mVideoView;
    private CircleProgressView mProgressView;
    private ImageView mThumbView;
    private int completeCount = 0;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.movie_player_activity);
        initView();
        initData();
    }

    private void initView() {
        mVideoView = (VideoView) findViewById(R.id.video_view);
        mProgressView = (CircleProgressView) findViewById(R.id.circle_progress);
        mThumbView = (ImageView) findViewById(R.id.video_thumb_view);
        mVideoView.setOnPreparedListener(this);
        mVideoView.setOnErrorListener(this);
        mVideoView.setOnCompletionListener(this);
        mProgressView.setMax(100);
//      View contentView = getWindow().getDecorView().findViewById(R.id.content);
        RelativeLayout root = (RelativeLayout) findViewById(R.id.video_root);
        root.setOnTouchListener(mContentTouch);
    }

    private void initData() {
        MediaObject MediaObject = (MediaObject) getIntent().getSerializableExtra("MediaObj");
        PlayerTask task = new PlayerTask(MediaObject);
        task.execute();
    }

    private OnTouchListener mContentTouch = new OnTouchListener() {

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            int action = event.getAction();
            switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (completeCount >= 1)
                    finish();
                break;
            default:
                break;
            }
            return true;
        }
    };

    @Override
    protected void onResume() {
        // TODO Auto-generated method stub
        super.onResume();
        if(mVideoView != null)
            mVideoView.resume();
    }

    @Override
    protected void onPause() {
        // TODO Auto-generated method stub
        super.onPause();
        if(mVideoView != null)
            mVideoView.pause();
    }

    @Override
    protected void onDestroy() {
        // TODO Auto-generated method stub
        super.onDestroy();
        if(mVideoView != null)
            mVideoView.stopPlayback();
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        // TODO Auto-generated method stub
        completeCount ++;
        if(completeCount >= 1)
            Tools.showToast("点击关闭..");
        mVideoView.start();
    }


    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        return false;
    }


    @Override
    public void onPrepared(MediaPlayer mp) {}

    private class PlayerTask extends AsyncTask<Void, Integer, Void>{

        /* (non-Javadoc)
         */
        private int count = 0;
        private MediaObject mMediaObject;

        public PlayerTask(MediaObject obj){
            this.mMediaObject = obj;
        }

        private Bitmap decodeThumbBitmap(String path){
            BitmapFactory.Options options = new Options();
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            return BitmapFactory.decodeFile(path,options);
        }

        @Override
        protected void onPreExecute() {
            int screenWidth = DisplayUtil.getScreenWidth();
            mProgressView.setVisibility(View.VISIBLE);
            mThumbView.setVisibility(View.VISIBLE);
            if(!StringUtils.isEmpty(mMediaObject.getOutputVideoThumbPath())){
                Bitmap thumbBitmap = decodeThumbBitmap(mMediaObject.getOutputVideoThumbPath());
                int width = thumbBitmap.getWidth();
                int height = thumbBitmap.getHeight();
                mThumbView.getLayoutParams().width = screenWidth;
                mThumbView.getLayoutParams().height = (int)((width/height *1.0f) * screenWidth);
                mThumbView.setImageBitmap(thumbBitmap);
            }
            mVideoView.setVideoPath(mMediaObject.getOutputVideoPath());
        }

        /* (non-Javadoc)
         */
        @Override
        protected Void doInBackground(Void... params) {
            while(count <= 50){
                count += 2;
                publishProgress(count);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            return null;
        }

        /* (non-Javadoc)
         */
        @Override
        protected void onProgressUpdate(Integer... values) {
            mProgressView.setProgress(values[0]);
        }

        /* (non-Javadoc)
         */
        @Override
        protected void onPostExecute(Void result) {
            count = 0;
            mProgressView.setVisibility(View.GONE);
            mThumbView.setVisibility(View.GONE);
            mVideoView.start();
        }
    }
}

依次简单说下,VideoView允许我们监听到三个状态分别是:OnPreparedOnCompletionOnError对应的就是:完成准备、播放完成和播放出错。如果之前了解MediaPlayer状态机,那么会发现:VideoView这三个状态相对来说真是非常简洁。当然,正式的VideoView还会添加一个MediaPlayerControl用于控制视频的播放、暂停、控制播放进度。因为我们的需求很简单,所以就没有用到它。

那么,进入实现流程:在获取到控件以及MediaObject对象后,我们实现一个AsyncTask来模拟视频加载的任务:onPreExcute()中,我们先将之前保存好的视频预览图取出(实际上就是在视频录制完成后对视频文件第一帧的截图),并初始化到ImageView中去,然后将视频文件的path路径设置到VideoView中去。

doInBackground(...)中,就是简单模拟下加载进度了,不要忘了调用publishProgress()就行。

onProgressUpdate(...),更新我们的ProgressView

onPostExecute(...)中,将进度条和预览图都隐藏掉,调用VideoView.start()即可。

当然不要忘了执行我们的Task,循环播放就是在每次OnCompletion中重新start()就好,并且统计下播放完成的次数,如果大于等于1了就可以提示点击取消了,onTouch就好。

纵然这么简单,也不能忘记我们粗糙的进度条:

public class CircleProgressView extends View {

    private Paint mOutSidePaint;
    private Paint mInsidePaint;
    private int mMax = 1;
    private int mProgress;
    private float mCenterX;
    private float mCenterY;
    private RectF mOutSideCircleRectF = new RectF();
    private RectF mInSideCircleRectF = new RectF();
    private int mOutSideRadius = DisplayUtil.dip2px(getContext(), 30);
    private int mInSideRadius = DisplayUtil.dip2px(getContext(), 28);

    public CircleProgressView(Context context){
        this(context, null);
    }

    public CircleProgressView(Context context, AttributeSet attrs) {
        super(context, attrs);  
        initPaint();
    }

    private void initPaint() {
        // TODO Auto-generated method stub
        mOutSidePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mOutSidePaint.setColor(Color.LTGRAY);
        mOutSidePaint.setStrokeWidth(2.0f);
        mOutSidePaint.setStyle(Paint.Style.STROKE);

        mInsidePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mInsidePaint.setColor(Color.LTGRAY);
        mInsidePaint.setStyle(Paint.Style.FILL);
    }

    /* (non-Javadoc)
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//      super.measure(widthMeasureSpec, heightMeasureSpec);
//      setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false));
        calculateCircleCenter();
        calculateDrawRectF();

    }
    //
/*  private int measure(int measureSpec, boolean isWidth) {
        int result;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        int padding = isWidth ? getPaddingLeft() + getPaddingRight()
                : getPaddingTop() + getPaddingBottom();
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight();
            result += padding;
            if (mode == MeasureSpec.AT_MOST) {
                result = Math.min(result, size);
            }
        }
        return result;
    }*/

    private void calculateCircleCenter() {
        mCenterX = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2.0f
                + getPaddingLeft();
        mCenterY = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2.0f
                + getPaddingTop();
    }


    private void calculateDrawRectF() {
        mOutSideCircleRectF.left = mCenterX - mOutSideRadius;
        mOutSideCircleRectF.top = mCenterY - mOutSideRadius;
        mOutSideCircleRectF.right = mCenterX + mOutSideRadius;
        mOutSideCircleRectF.bottom = mCenterY + mOutSideRadius;

        mInSideCircleRectF.left = mCenterX - mInSideRadius;
        mInSideCircleRectF.top = mCenterY - mInSideRadius;
        mInSideCircleRectF.right = mCenterX + mInSideRadius;
        mInSideCircleRectF.bottom = mCenterY + mInSideRadius;
    }
    /* 
     * 
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();
        //画外圈
        canvas.drawCircle(mCenterX, mCenterY, mOutSideRadius, mOutSidePaint);
//      //画内圈
        canvas.drawArc(mInSideCircleRectF, -90,
                mProgress * 360 / mMax, true, mInsidePaint);
        canvas.restore();
    }

    public void setMax(int max){
        this.mMax = max;
    }

    public void setProgress(int progress){
        this.mProgress = progress;
        invalidate();
    }
}

OK,大功告成。

优化

关于优化,这里先说下我的思路,因为还没有具体实现… 等具体实现好了,我再回来补充。

说是优化,其实是换一种实现方式,因为在使用VideoView时,发现其许多的功能点在我们需求分析中都运用不上,类似MediaPlayerControl以及seekTo()等等都是可以“咔擦”掉的。

那么就来看看VideoView是怎么实现的,实际上他就是一个SurfaceView的子类,内部视频播放功能其实是靠MediaPlayer来完成的,暴露出的那三个状态也正是MediaPlayer状态机中三个重要的状态,这里附张图,给自己巩固和加深下印象:
MediaPlayer状态机
详细介绍在这里:Android MediaPlayer状态机
通过自己的实现,可以更多的去监听状态从而处理一些业务。

大体的思路如下:
继承SurfaceView并且实现其Callback接口
一样的定义一些状态码:

    // all possible internal states
    private static final int STATE_ERROR              = -1;
    private static final int STATE_IDLE               = 0;
    private static final int STATE_PREPARING          = 1;
    private static final int STATE_PREPARED           = 2;
    private static final int STATE_PLAYING            = 3;
    private static final int STATE_PAUSED             = 4;
    private static final int STATE_PLAYBACK_COMPLETED = 5;

同样用两个变量来控制状态:

    private int mCurrentState = STATE_IDLE;
    private int mTargetState  = STATE_IDLE;

以及其他的变量:

...
    private int mVideoWidth;
    private int mVideoHeight;
...

初始化View:

    protected void initVideoView() {
        mVideoWidth = 0;
        mVideoHeight = 0;
        ...
        mCurrentState = STATE_IDLE;
        mTargetState = STATE_IDLE;
    }

在设置Path的时候进入到Prepared状态:

    public void setVideoPath(String path) {
    ...
        if (StringUtils.isNotEmpty(path)) {
            mTargetState = STATE_PREPARED;
            openVideo(Uri.parse(path));
        }
    }
public void openVideo(Uri uri) {

        Exception exception = null;
        try {
            if (mMediaPlayer == null) {
            ...
            //这里初始化设置我们的mMediaPlayer,设置监听
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.setOnPreparedListener(mPreparedListener);
            mMediaPlayer.setOnCompletionListener(mCompletionListener);
            mMediaPlayer.setOnErrorListener(mErrorListener);
            mMediaPlayer.setOnVideoSizeChangedListener(mVideoSizeChangedListener);
            ...
            } else {
                mMediaPlayer.reset();
            }
            mMediaPlayer.setDataSource(getContext(), uri);
            mMediaPlayer.prepareAsync();
            mCurrentState = STATE_PREPARING;
        } 
        ...
        } catch (Exception ex) {
            exception = ex;
        }
        if (exception != null) {
        //捕获到异常,切换状态
            mCurrentState = STATE_ERROR;
            if (mErrorListener != null)
                mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
        }
    }

MediaPlayer的Prepared状态监听:

    MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
        @Override
        public void onPrepared(MediaPlayer mp) {
            //必须是正常状态
            if (mCurrentState == STATE_PREPARING) {
            ...
                mCurrentState = STATE_PREPARED;
                mVideoWidth = mp.getVideoWidth();
                mVideoHeight = mp.getVideoHeight();
            ...
                switch (mTargetState) {
                case STATE_PREPARED:
                //这里是我们向外暴露的Prepared状态
                    if (mOnPreparedListener != null)
                        mOnPreparedListener.onPrepared(mMediaPlayer);
                    break;
                case STATE_PLAYING:
                    start();
                    break;
                }
            }
        }
    };

MediaPlayerOnCompletion的监听:

    private MediaPlayer.OnCompletionListener mCompletionListener = new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mp) {
            mCurrentState = STATE_PLAYBACK_COMPLETED;
            if (mOnCompletionListener != null)
            //这里我们向外暴露OnCompletion状态
                mOnCompletionListener.onCompletion(mp);
        }
    };

MediaPlayerVideoSizeChange的监听:

    OnVideoSizeChangedListener mVideoSizeChangedListener = new OnVideoSizeChangedListener() {
        @Override
        public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
            mVideoWidth = width;
            mVideoHeight = height;
            ...
    };

MediaPlayerOnError的监听:

    private MediaPlayer.OnErrorListener mErrorListener = new MediaPlayer.OnErrorListener() {
        @Override
        public boolean onError(MediaPlayer mp, int framework_err, int impl_err) {
        ...
            mCurrentState = STATE_ERROR;
            if (mOnErrorListener != null)
            //这里我们向外暴露onError状态
                mOnErrorListener.onError(mp, framework_err, impl_err);
            return true;
        ...
        }
    };

start()方法:

    public void start() {
        mTargetState = STATE_PLAYING;
        //这里可用状态包括{Prepared, Started, Paused, PlaybackCompleted}
        if (mMediaPlayer != null && (mCurrentState == STATE_PREPARED || mCurrentState == STATE_PAUSED || mCurrentState == STATE_PLAYING || mCurrentState == STATE_PLAYBACK_COMPLETED)) {
            try {
                if (!isPlaying())
                    mMediaPlayer.start();
                mCurrentState = STATE_PLAYING;
                ...
            } catch (IllegalStateException e) {
                ...
            } catch (Exception e) {
                ...
            }
        }
    }

最后release()方法:

    public void release() {
        mTargetState = STATE_IDLE;
        mCurrentState = STATE_IDLE;
        if (mMediaPlayer != null) {
            try {
                ...
                mMediaPlayer.stop();
                mMediaPlayer.release();
            } catch (IllegalStateException e) {
                ...
            } catch (Exception e) {
                ...
            }
            mMediaPlayer = null;
        }
    }

以上是个大体的思路,非常有可能不完善,只是简单走完状态机中主要的状态。

然后,回到一开始说到的界面上的瑕疵,解决思路其实就是重写SurfaceViewonMeasure()方法,我们可以看到系统在重写’onMeasure()’方法是做了许多情况的判断,这里就不列举出来了。而针对我们这种固定的需求,因为前面获取到了mVideoWidthmVideoHeight两个变量,运用起它们,大致是:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        //这里省去了对MeasureSpec三个状态的判断
        int width = DisplayUtil.getScreenWidth();
        int height = (int)((mVideoWidth/mVideoHeight *1.0f) * width);
        setMeasuredDimension(width, height);
        ...
    }

结语

总的来说,整个的实现过程中必然会存在多少瑕疵,也许以上的优化方面并不会为整个功能带来性能上的改变,反而在调用系统的控件获得的是稳定的体验。但是了解其实现原理,则是必不可少的,蛤蛤。OK,播放功能先暂时就这样。

猜你喜欢

转载自blog.csdn.net/u012227600/article/details/50906541