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状态机中三个重要的状态,这里附张图,给自己巩固和加深下印象:

详细介绍在这里: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,播放功能先暂时就这样。

更多相关文章

  1. Android实现触摸校正功能
  2. Hbuilder android 在线更新功能 后端获取最新版本号和增量更新wg
  3. XBMC Romote:用 Android(安卓)手机控制 XBMC 媒体播放
  4. Launcher功能的修改及添加,本篇是一些小功能的展示,通知栏显隐,dock
  5. 给Android开发者的一封信
  6. 【Android】解决使用Dialog + EdiText 实现评论功能时,软键盘不协
  7. Android基础知识巩固系列 Android之四大组件——Activity(活动)
  8. 如何为Android应用程序添加社会化分享
  9. Android高仿微信图片多选功能

随机推荐

  1. Android学习笔记---第一天---布局
  2. Android:控件GridView的使用
  3. Android(安卓)Studio--android:descendan
  4. Android TextView行间距解析
  5. Android下实现手机验证码
  6. (一)Android开发之安卓系统的前世今生
  7. Android各种资源引用的方法
  8. android的消息处理机制(图+源码分析)——Lo
  9. Android 中Edittext属性集合
  10. Android中Intent对应的category列表大全