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,播放功能先暂时就这样。