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
允许我们监听到三个状态分别是:OnPrepared
、OnCompletion
和OnError
对应的就是:完成准备、播放完成和播放出错。如果之前了解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; } } } };
对MediaPlayer
OnCompletion的监听:
private MediaPlayer.OnCompletionListener mCompletionListener = new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { mCurrentState = STATE_PLAYBACK_COMPLETED; if (mOnCompletionListener != null) //这里我们向外暴露OnCompletion状态 mOnCompletionListener.onCompletion(mp); } };
对MediaPlayer
VideoSizeChange的监听:
OnVideoSizeChangedListener mVideoSizeChangedListener = new OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { mVideoWidth = width; mVideoHeight = height; ... };
对MediaPlayer
OnError的监听:
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; } }
以上是个大体的思路,非常有可能不完善,只是简单走完状态机中主要的状态。
然后,回到一开始说到的界面上的瑕疵,解决思路其实就是重写SurfaceView
的onMeasure()
方法,我们可以看到系统在重写’onMeasure()’方法是做了许多情况的判断,这里就不列举出来了。而针对我们这种固定的需求,因为前面获取到了mVideoWidth
、mVideoHeight
两个变量,运用起它们,大致是:
@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,播放功能先暂时就这样。
更多相关文章
- Android实现触摸校正功能
- Hbuilder android 在线更新功能 后端获取最新版本号和增量更新wg
- XBMC Romote:用 Android(安卓)手机控制 XBMC 媒体播放
- Launcher功能的修改及添加,本篇是一些小功能的展示,通知栏显隐,dock
- 给Android开发者的一封信
- 【Android】解决使用Dialog + EdiText 实现评论功能时,软键盘不协
- Android基础知识巩固系列 Android之四大组件——Activity(活动)
- 如何为Android应用程序添加社会化分享
- Android高仿微信图片多选功能