Android仿微信小视频录制功能

作为开博的第一篇文章,正好最近在学习Android视频录制功能,所以决定趁热记录下来。

关于Android视频录制功能的实现流程以及相关API的介绍网上有很多,这里就不再赘述。在学习的过程中主要参考了 Vitamio公司的VCamera SDK 项目,因为该项目在Java层全开源,所以在调用Android系统的相机进行录制方面记录下一些学习体会。

具体关于VCamera项目,请移步VCamera

嗯,还是先看效果:

定义

首先是定义视频录制接口,接口很简单只包含两个方法,因为是调用系统的API实现录制,所以在方法上只用考虑到start和stop方法,出于对功能的拓展性上(比如自定义相机功能),接口的方法还可以更丰富。

public interface IMediaRecorder {    /**     * 开始录制      * @return 录制失败返回null     */    public MediaObject startRecord();    /**     * 停止录制     */    public void stopRecord();}

接着是bean:

    /** 视频最大时长,默认10秒 */    private int mMaxDuration;    /** 视频目录 */    private String mOutputDirectory;    /** 对象文件 */    private String mOutputObjectPath;    /** 视频码率 */    private int mVideoBitrate;    /** 最终视频输出路径 */    private String mOutputVideoPath;    /** 最终视频截图输出路径 */    private String mOutputVideoThumbPath;    /** 文件夹及文件名 */    private String mKey;    /** 开始时间 */    private long mStartTime;    /** 结束时间 */    private long mEndTime;    /** 视频移除标志位 */    private boolean mRemove;    /** 两个构造方法 */    public MediaObject(String key, String path) {        this(key, path, DEFAULT_VIDEO_BITRATE);    }    public MediaObject(String key, String path, int videoBitrate) {        this.mKey = key;        this.mOutputDirectory = path;        this.mVideoBitrate = videoBitrate;        this.mOutputObjectPath = mOutputDirectory + File.separator + mKey + ".obj";        this.mOutputVideoPath = mOutputDirectory + File.separator + mKey +".mp4";        this.mOutputVideoThumbPath = mOutputDirectory + File.separator + mKey + ".jpg";        this.mMaxDuration = DEFAULT_MAX_DURATION;    }

实现

在视频录制过程中分为两个部分,第一个就是我们在界面上看到的摄像头传来的预览画面Preview,另一个则是视频录制时使用的”录像机“Recorder,同样为了提高拓展性,在功能实现上我们先将预览画面提出来实现它。因为也许你有许多不同的Recorder,但是预览的方式只有那么一个。

那么抽象出一个录像父类非常重要,它实现了一些都需要用到的方法,其中当然包括我们的Preview

public abstract class MediaRecorderBase implements Callback, PreviewCallback, IMediaRecorder {...}

非常直观哈,Callback接口是SurfaceHolder的回调接口,我们所看到的预览画面就是呈现在SurfaceView上的,PreviewCallback预览画面的回调接口,至于我们先前定义的IMediaRecorder就留给后人去实现吧。

回到主线预览画面上,关于SurfaceView这里不做介绍了,我们要做的是先把它的SurfaceHolder拿过来

    public void setSurfaceHolder(SurfaceHolder sh) {        if (sh != null) {            sh.addCallback(this);            if (!DeviceUtils.hasHoneycomb()) {                sh.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);            }        }    }

然后就可以掌控这一切了。。

    public void prepare() {        mPrepared = true;        if (mSurfaceCreated)            startPreview();    }    @Override    public void surfaceCreated(SurfaceHolder holder) {        this.mSurfaceHolder = holder;        this.mSurfaceCreated = true;        if (mPrepared && !mStartPreview)            startPreview();    }    @Override    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {        this.mSurfaceHolder = holder;    }    @Override    public void surfaceDestroyed(SurfaceHolder holder) {        release();    }

startPreview()就是我们的预览方法:

/** 开始预览 */    public void startPreview() {        if (mStartPreview || mSurfaceHolder == null || !mPrepared)            return;        else            mStartPreview = true;        try {            //打开镜头            if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK)                camera = Camera.open();            else                camera = Camera.open(mCameraId);            try {                //为preview设置SurfaceHolder                camera.setPreviewDisplay(mSurfaceHolder);            } catch (IOException e) {                if (mOnErrorListener != null) {                mOnErrorListener.onVideoError(MEDIA_ERROR_CAMERA_SET_PREVIEW_DISPLAY, 0);                }                Log.e(" ", "setPreviewDisplay fail " + e.getMessage());            }            //获取摄像头参数            Parameters parameters = camera.getParameters();            prepareCameraParaments(parameters);            setPreviewCallback(parameters);            camera.setParameters(parameters);            camera.startPreview();            if (mOnPreparedListener != null)                mOnPreparedListener.onPrepared();        } catch (Exception e) {            e.printStackTrace();            if (mOnErrorListener != null) {                mOnErrorListener.onVideoError(MEDIA_ERROR_CAMERA_PREVIEW, 0);            }            Log.e(" ", "startPreview fail :" + e.getMessage());        }    }

这里有几个方法依次说一下,首先是protected void prepareCameraParaments(Parameters parameters)调用它会对相机进行一些参数上的处理,里面基本包括:

List<int[]> supportedPreviewFpsRange = parameters.getSupportedPreviewFpsRange();

这里返回一个数组列表,里面包含了设备所支持预览画面的FPS值,每个数组含有两个值,第一个值是最小FPS,另一个是最大FPS值,这样就构成了一个区间,可以检测你所期望的FPS值在不在设备支持中。那么:

parameters.setPreviewFrameRate(mFrameRate);

接着,

List supportedPreviewSizes = parameters.getSupportedPreviewSizes();

这个返回了设备支持的预览画面的尺寸,Size有很多,但是重点在比例,16:9、4:3 、11:9等等,比例非常关键,它直接关系到后来我们的SurfaceView的尺寸,选一个你想要的把它:

parameters.setPreviewSize(customSize.width, customSize.height);

我们知道小视频录制的时候是竖屏的,所以把它竖过来是必须的:

camera.setDisplayOrientation(90);

顺时针旋转90度,之所以这么做,API文档上有介绍,大意就是camera sensor没有转,但是我们可以把画面转过来。
这样就可以了,当然如果想设置更多,当然没问题,这里的Parameters设置的参数实际上都是以Map键值对的形式传入的,具体的源码也有大神解读。那么,比如加一个防抖功能(前提是你的设备支持):

        if ("true".equals(parameters.get("video-stabilization-supported")))            parameters.set("video-stabilization", "true");

再来看protected void setPreviewCallback(Parameters parameters)方法,作用呢就是给Camera对象设置我们一开始实现的那个PreviewCallback接口:

    protected void setPreviewCallback(Parameters parameters) {        Camera.Size size = parameters.getPreviewSize();        if (size != null) {            PixelFormat pf = new PixelFormat();            PixelFormat.getPixelFormatInfo(parameters.getPreviewFormat(), pf);            int buffSize = size.width * size.height * pf.bitsPerPixel / 8;            try {                camera.addCallbackBuffer(new byte[buffSize]);                camera.addCallbackBuffer(new byte[buffSize]);                camera.addCallbackBuffer(new byte[buffSize]);                camera.setPreviewCallbackWithBuffer(this);            } catch (OutOfMemoryError e) {                Log.e(" ", "startPreview...setPreviewCallback...", e);            }        } else {            camera.setPreviewCallback(this);        }    }

之前我们为parameter对象设置过previewSize参数,所以明显的这里用到了Camera.setPreviewCallbackWithBuffer(this)方法,实际上就是在视频预览的回调中加入缓冲区Buffer,怎么做文档上说的很清楚,调用Camera.addCallbackBuffer(new byte[buffSize]);而且Applications can add one or more buffers to the queue.可以加入多个到队列中,再看When a preview frame arrives and there is still at least one available buffer, the buffer will be used and removed from the queue.好吧用过了还要抛弃掉。。另外buffer大小,文档中也告诉了怎么去计算,就是呈现出来的每帧画面中的每个像素所占的bits的和除以8。那么被无情抛弃的buffer怎么办?队空了没buffer用了,这一帧的画面也会被系统舍弃。实际上,我们有回调PreviewCallback

    @Override    public void onPreviewFrame(byte[] data, Camera camera) {        camera.addCallbackBuffer(data);    }

再把它加回去。(以上全是个人理解,有偏差请见谅)
当设置完参数就可以开启预览了camera.startPreview()
停止预览很简单:Camera.stopPreview(),然后将相机回调解绑,资源释放掉,holder资源释放,定义的标志位重置就好。毕竟这些资源太重了。

回到录像部分,在这里需要做的首先是继承之前的MediaRecorderBase类并且实现未实现的IMediaRecorder接口方法。

public class MediaRecorderSystem extends MediaRecorderBase implements OnErrorListener {    @Override    public MediaObject startRecord() {...}    @Override    public void stopRecord() {...}    ...}

startRecord()会返回一个MediaObject对象,所以我们暴露出一个方法可以得到它

    /** 拍摄存储对象 */    private MediaObject mMediaObject;    public MediaObject setOutputDirectory(String key, String path) {        if (StringUtils.isNotEmpty(path)) {            File f = new File(path);            if (f != null) {                if (f.exists()) {                    //已经存在,删除                    FileUtils.deleteFile(f);                }                if (f.mkdirs()) {                    mMediaObject = new MediaObject(key, path, mVideoBitrate);                }            }        }        return mMediaObject;    }

之后在startRecord()中实现视频录制,直接上代码:

        if (mMediaRecorder == null) {            mMediaRecorder = new MediaRecorder();            mMediaRecorder.setOnErrorListener(this);        } else {            mMediaRecorder.reset();        }        // Step 1: Unlock and set camera to MediaRecorder        camera.unlock();        mMediaRecorder.setCamera(camera);        mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());        // Step 2: Set sources        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);//before setOutputFormat()        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//before setOutputFormat()        //设置视频输出的格式和编码        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);        CamcorderProfile mProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_TIME_LAPSE_CIF);        //after setVideoSource(),after setOutFormat()        mMediaRecorder.setVideoSize(mProfile.videoFrameWidth, mProfile.videoFrameHeight);        mMediaRecorder.setAudioEncodingBitRate(44100);        if (mProfile.videoBitRate > 2 * 1024 * 1024)                mMediaRecorder.setVideoEncodingBitRate(2 * 1024 * 1024);        else                mMediaRecorder.setVideoEncodingBitRate(mProfile.videoBitRate);        //after setVideoSource(),after setOutFormat();        mMediaRecorder.setVideoFrameRate(mProfile.videoFrameRate);        //after setOutputFormat()        mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);        //after setOutputFormat()        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);        // Step 3: Set output file        mMediaRecorder.setOutputFile(mMediaObject.getOutputVideoPath());        // Step 4: start and return        mMediaRecorder.prepare();        mMediaRecorder.start();        mMediaObject.setStartTime(System.currentTimeMillis());        mRecording = true;        return mMediaObject;

上面的一系列是调用系统API录制视频的基本流程,也可以去参考开发手册的讲解。
stopRecord()方法中,把录像停下来:

    ...    mMediaRecorder.setOnErrorListener(null);    mMediaRecorder.setPreviewDisplay(null);    ...    mMediaRecorder.stop();    ...    camera.lock();

重写父类的release()方法,释放掉MediaRecorder资源:

...super.release();mMediaRecorder.release();...

另外,在OnErrorListener监听中捕获到异常并且mMediaRecorder.reset()
视频录制功能基本上就完成了,但是我们需求的功能却不止这些,那么就再加点料。回到MediaRecorderBase类,实现需要添加的功能分别是:自动、手动对焦,变焦,摄像头切换以及闪光灯的开关。
这些功能需求很简单:
对焦:当我们一开启预览时会自动调节焦距,当获取到点击时则进入手动对焦;
变焦:双击预览区域zoom+,再次双击zoom-回到初始状态;
摄像头切换:默认开启后置摄像头,点击按钮切换至前置摄像头,再次点击切换到后置;
闪光灯:默认关闭,点击按钮打开闪光灯保持常亮,再次点击关闭;

首先是对焦,这里用到持续对焦这条参数

...//获取到设备支持的对焦模式List focusModes = parameters.getSupportedFocusModes();if(!CollectionUtils.isEmpty(focusModes)){    if(focusModes.list.contains("continuous-video"))        parameters.setFocusMode("continuous-video");}

这里以”continuous-video”参数为例,因为不同设备存在差异,实际上是需要判断focusModes列表中支持的类型,类型有:“continuous-video”、“continuous-picture”和“auto”当然你也可以直接使用Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
手动对焦方法如下:

    public boolean manualFocus(AutoFocusCallback cb, List focusAreas) {        //判断系统是否是4.0以上的版本        if (camera != null && focusAreas != null && DeviceUtils.hasICS()) {            try {                camera.cancelAutoFocus();                Parameters parameters = camera.getParameters();                if(parameters != null){                    // getMaxNumFocusAreas检测设备是否支持                    if (parameters.getMaxNumFocusAreas() > 0) {                        parameters.setFocusAreas(focusAreas);                    }                    // getMaxNumMeteringAreas检测设备是否支持                    if (parameters.getMaxNumMeteringAreas() > 0)                        parameters.setMeteringAreas(focusAreas);                    parameters.setFocusMode("macro");                    camera.setParameters(parameters);                    camera.autoFocus(cb);                    return true;                }            } catch (Exception e) {                if (mOnErrorListener != null) {                    mOnErrorListener.onVideoError(                            MEDIA_ERROR_CAMERA_AUTO_FOCUS, 0);                }                if (e != null)                    Log.e(" ", "autoFocus", e);            }        }        return false;    }

首先传入一个AutoFocusCallback回调,它会告诉我们对焦是否OK和当前Camera的对象,List是需要对焦的区域。流程很简单,先取消自动对焦,然后向相机参数中设置好对焦区域和对焦模式,再调用相机的Camera.autoFocus(...)方法,这样就会对指定的区域进行对焦。

变焦:

    public void setZoom(int zoomValue) {        Parameters parameters = camera.getParameters();        if (parameters.isZoomSupported()) {            final int MAX = parameters.getMaxZoom();            if (MAX == 0)                return;            if (zoomValue > MAX)                zoomValue = MAX;            parameters.setZoom(zoomValue); // value zoom value. The valid range                                            // is 0 to getMaxZoom.            camera.setParameters(parameters);        }        else            return;    }

传入一个焦距值,然后判断相机是否支持变焦,这里会出一个bug,对于有的设备,isZoomSupported()返回了true,但是仍然无法变焦,则需要判断下MaxZoom的值,如果为0,则仍然不支持变焦,因为是固定传入焦距,所以直接设置就好了。

摄像头切换我们在startPreview()方法中有这么一段:

        ...        try {            if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK)                camera = Camera.open();            else                camera = Camera.open(mCameraId);            try {                camera.setPreviewDisplay(mSurfaceHolder);            } catch (IOException e) {                if (mOnErrorListener != null) {                mOnErrorListener.onVideoError(MEDIA_ERROR_CAMERA_SET_PREVIEW_DISPLAY, 0);                }                Log.e(" ", "setPreviewDisplay fail " + e.getMessage());            }        ...

通过对mCameraId的改变,先调用stopPreview()再调用startPreview()就可以完成了。

最后,对于闪光的切换就不必多说了

    public void toggleFlashMode() {        Parameters parameters = camera.getParameters();        if (parameters != null) {            try {                final String mode = parameters.getFlashMode();                if (TextUtils.isEmpty(mode) || Camera.Parameters.FLASH_MODE_OFF.equals(mode))                    parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);                else                    parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);                camera.setParameters(parameters);            } catch (Exception e) {                Log.e(" ", "toggleFlashMode", e);            }        }    }

那么,录像功能上基本上就OK了,现在要把它用起来。

UI

先上xml:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent" >    <RelativeLayout        android:id="@+id/title_layout"        android:layout_width="match_parent"        android:layout_height="49dip"        android:background="@color/black"        android:gravity="center_vertical" >        <ImageView            android:id="@+id/title_back"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_marginLeft="10dip"            android:padding="10dip"            android:src="@drawable/arrow_left" />        <LinearLayout            android:layout_width="wrap_content"            android:layout_height="48dip"            android:layout_alignParentRight="true"            android:gravity="right|center_vertical"            android:orientation="horizontal" >            <CheckBox                android:id="@+id/record_camera_led"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:background="@drawable/record_camera_flash_led_selector"                android:button="@null"                android:textColor="@color/white" />            <CheckBox                android:id="@+id/record_camera_switcher"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_marginLeft="15dp"                android:layout_marginRight="10dp"                android:background="@drawable/record_camera_switch_selector"                android:button="@null" />        LinearLayout>    RelativeLayout>    <RelativeLayout        android:id="@+id/camera_layout"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:layout_below="@+id/title_layout" >        <SurfaceView            android:id="@+id/record_preview"            android:layout_width="match_parent"            android:layout_height="match_parent" />        <ImageView            android:id="@+id/record_focusing"            android:layout_width="40dp"            android:layout_height="40dp"            android:scaleType="fitXY"            android:src="@drawable/video_focus"            android:visibility="gone" />        <TextView android:id="@+id/record_tip"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_centerHorizontal="true"            android:textSize="12sp"            android:background="@drawable/recorder_tips"/>        <com.example.activity.widget.movie.view.ProgressView            android:id="@+id/record_progress"            android:layout_width="match_parent"            android:layout_height="3dp"/>    RelativeLayout>    <RelativeLayout        android:id="@+id/bottom_layout"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:layout_below="@+id/title_layout"        android:background="@color/black" >        <ImageView            android:id="@+id/record_controller"            android:layout_width="150dp"            android:layout_height="150dp"            android:layout_centerInParent="true"            android:src="@drawable/bg_movie_add_shoot" />        <TextView             android:layout_width="75dp"            android:layout_height="75dp"            android:layout_centerInParent="true"            android:text="按住拍"            android:gravity="center"            android:textColor="#FF45C01A"            android:textSize="20sp"/>    RelativeLayout>RelativeLayout>

不出意外的不太像。。蛤蛤
Activity:
onCreate中处理View:

private void initViews() {    ...    mWindowWidth = DeviceUtils.getScreenWidth(this);    int height = (int) (mWindowWidth * MediaRecorderBase.PREVIEW_RATIO);    ((RelativeLayout.LayoutParams)mBottomLayout.getLayoutParams()).topMargin = mWindowWidth;    ((RelativeLayout.LayoutParams)mRecordTipView.getLayoutParams()).topMargin = mWindowWidth - DisplayUtil.dip2px(this,(40));    ((RelativeLayout.LayoutParams)mProgressView.getLayoutParams()).topMargin = mWindowWidth - DisplayUtil.dip2px(this, 3);    RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mSurfaceView                .getLayoutParams();    lp.width = mWindowWidth;    lp.height = height;    mSurfaceView.setLayoutParams(lp);    mRecordTipView.setVisibility(View.GONE);}

这里我们对SurfaceView的的宽度和高度做了处理,而不是在xml中,之前的PreView中设置过camera.setDisplayOrientation(90)画面旋转的处理(下图),所以SurfaceView的尺寸也应做出相应的调整

onResume()中对MediaRecorder对象做处理:

...    private MediaRecorderBase mMediaRecorder;    private MediaObject mMediaObject;    @Override    protected void onResume() {        super.onResume();        if (mMediaRecorder == null) {            initMediaRecorder();        } else {            mRecordLed.setChecked(false);            mMediaRecorder.prepare();        }    }    private void initMediaRecorder() {        mMediaRecorder = new MediaRecorderSystem();        mMediaRecorder.setOnErrorListener(this);        File f = new File(CACHE_PATH);        if (!FileUtils.checkFile(f)) {            f.mkdirs();        }        String key = String.valueOf(System.currentTimeMillis());        mMediaObject = mMediaRecorder.setOutputDirectory(key,                CACHE_PATH + key);        mMediaObjList.add(mMediaObject);        mMediaRecorder.setSurfaceHolder(mSurfaceView.getHolder());        mMediaRecorder.prepare();    }...

这么做确保Activity从后台切换回来显示预览依旧正常;
Activity销毁时,处理等待删除缓存目录以及释放资源:

    @Override    protected void onDestroy() {        //activity 销毁时删除废弃的缓存目录        if (!CollectionUtils.isEmpty(mMediaObjList)) {            for (MediaObject obj : mMediaObjList) {                if(obj != null && obj.isRemove())                    FileUtils.deleteDir(obj.getOutputDirectory());            }        }        mMediaRecorder.release();        super.onDestroy();    }

录制按钮的Touch事件:

private View.OnTouchListener mOnVideoControllerTouchListener = new View.OnTouchListener() {        @Override        public boolean onTouch(View v, MotionEvent event) {            if (mMediaRecorder == null) {                return false;            }            if (mMediaObject == null){                String key = String.valueOf(System.currentTimeMillis());                mMediaObject = mMediaRecorder.setOutputDirectory(key,                        CACHE_PATH + key);                mMediaObjList.add(mMediaObject);            }            int action = event.getAction();            if(action == MotionEvent.ACTION_DOWN){                // 判断是否已经超时                if (mMediaObject.getDuration() >= RECORD_TIME_MAX) {                    return true;                }                mMediaObject.setStartTime(System.currentTimeMillis());                startRecord();            }            if(action == MotionEvent.ACTION_MOVE){                if(event.getY() < 0)                    mAtRemove = true;                else                    mAtRemove = false;                changeTip();                mProgressView.setRemove(mAtRemove);                mMediaObject.setRemove(mAtRemove);                mHandler.sendEmptyMessage(HANDLE_INVALIDATE_PROGRESS);            }            if(action == MotionEvent.ACTION_UP){                //停止录制                if (mPressedStatus){                    mRecordTipView.setVisibility(View.GONE);                    mCameraSwitch.setVisibility(View.VISIBLE);                    stopRecord();                }            }            return true;        }    };

按下去就开始录制,向上滑提示放开取消,下滑回来继续录,直到放开停止录制。下面是两个控制录制的方法:

    /**     * 开始录制      */    private void startRecord() {        if (mMediaRecorder != null) {            mMediaRecorder.startRecord();            // 使用系统录制环境,不能在中途切换前后摄像头,否则有问题            if (mMediaRecorder instanceof MediaRecorderSystem) {                mCameraSwitch.setVisibility(View.GONE);            }        }        mPressedStatus = true;        mRecordController.setImageResource(R.drawable.bg_movie_add_shoot);              mCameraSwitch.setEnabled(false);        mRecordLed.setEnabled(false);        mTimeCount = 0;// 时间计数器重新赋值        mTimer = new Timer();        mTimer.schedule(new TimerTask() {            @Override            public void run() {                mTimeCount++;                mProgressView.setProgress(mTimeCount * 100);// 设置进度条                mHandler.sendEmptyMessage(HANDLE_INVALIDATE_PROGRESS);                if (mTimeCount >= RECORD_TIME_MAX / 100) {// 达到指定时间                    this.cancel();                    mHandler.removeMessages(HANDLE_INVALIDATE_PROGRESS);                }            }        }, 0, 100);    }    /**     * 停止录制     */    private void stopRecord() {        resetTimer();        mPressedStatus = false;        if (mMediaRecorder != null && mMediaObject != null) {            long endTime = System.currentTimeMillis();            mMediaObject.setEndTime(endTime);            int duration = (int) mMediaObject.getDuration();            //录制时间小于最小值取消录制并返回            if(duration < RECORD_TIME_MIN || mMediaObject.isRemove()){                mMediaObject.setRemove(true);                mMediaObjList.add(mMediaObject);                if(duration < RECORD_TIME_MIN && !mAtRemove)                    Tools.showToast("视频时间太短");                mMediaRecorder.stopRecord();                mCameraSwitch.setEnabled(true);                mRecordLed.setEnabled(true);                mMediaObject = null;                return;            }            mMediaRecorder.stopRecord();        }        mCameraSwitch.setEnabled(true);        mRecordLed.setEnabled(true);        saveObj();        saveThumb();        //到下一个activity//      Intent intent = new Intent();//      intent.putExtra("MediaObj", mMediaObject);//      intent.setClass(this, MoviePreviewActivity.class);//      startActivity(intent);//      finish();    }    /**     * 重置计时器     */    private void resetTimer(){        if(mTimer != null){            mTimer.cancel();            mTimer.purge();            mProgressView.setProgress(0);            mProgressView.setRemove(false);            mHandler.sendEmptyMessage(HANDLE_INVALIDATE_PROGRESS);        }    }

Timer计时器用于更新进度条。
再看SurfaceView的触摸事件:

private GestureDetector mDetector;private View.OnTouchListener mOnSurfaveViewTouchListener = new View.OnTouchListener() {        @Override        public boolean onTouch(View v, MotionEvent event) {            if (mMediaRecorder == null || !mCreated) {                return false;            }            return mDetector.onTouchEvent(event);        }};...class ZoomGestureListener extends SimpleOnGestureListener{    @Override    public boolean onDoubleTap(MotionEvent e) {        if (mMediaRecorder == null || !mCreated) {            return false;        }        if(!mZoomIn){            mMediaRecorder.setZoom(8); //zoom in..            mZoomIn = true;        }else{            mMediaRecorder.setZoom(0); //zoom out..            mZoomIn = false;        }            return true;        }    @Override    public boolean onDown(MotionEvent e) {        checkCameraFocus(e);        return true;    }}

用GestureDetector 对单击和双击事件进行捕获处理。checkCameraFocus(...)方法就是用来手动对焦的:

private void checkCameraFocus(MotionEvent event) {        float x = event.getX();        float y = event.getY();        float touchMajor = event.getTouchMajor();        float touchMinor = event.getTouchMinor();        //触摸范围        Rect touchRect = new Rect((int) (x - touchMajor / 2),                (int) (y - touchMinor / 2), (int) (x + touchMajor / 2),                (int) (y + touchMinor / 2));        //坐标转换为focusArea范围        Rect focusRect = new Rect();        focusRect.set(touchRect.left * 2000 / mSurfaceView.getWidth() - 1000,                    touchRect.top * 2000 / mSurfaceView.getHeight() - 1000,                    touchRect.right * 2000 / mSurfaceView.getWidth() - 1000,                    touchRect.bottom * 2000 / mSurfaceView.getHeight() - 1000);        if (focusRect.left >= focusRect.right                || focusRect.top >= focusRect.bottom)            return;        ArrayList focusAreas = new ArrayList();        focusAreas.add(new Camera.Area(focusRect, 1000));        if (!mMediaRecorder.manualFocus(new Camera.AutoFocusCallback() {            @Override            public void onAutoFocus(boolean success, Camera camera) {                // if (success) {                mFocusImage.setVisibility(View.GONE);                System.out.println("onAutoFocus previewsize..width = " + camera.getParameters().getPreviewSize().width                         + "\nheight = " + camera.getParameters().getPreviewSize().height);                // }            }        }, focusAreas)) {            mFocusImage.setVisibility(View.GONE);        }        int focusWidth = mFocusImage.getWidth();        RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mFocusImage                .getLayoutParams();        int left = touchRect.left - (focusWidth / 2);        int top = touchRect.top - (focusWidth / 2);        if (left < 0)            left = 0;        else if (left >= mWindowWidth)            left = mWindowWidth - focusWidth;        if (top > mSurfaceView.getHeight())            top = mSurfaceView.getHeight() - focusWidth;        lp.leftMargin = left;        lp.topMargin = top;        mFocusImage.setLayoutParams(lp);        mFocusImage.setVisibility(View.VISIBLE);        mFocusImage.startAnimation(mFocusAnimation);        mHandler.sendEmptyMessageDelayed(HANDLE_HIDE_RECORD_FOCUS, 3500);// 最多3.5秒也要消失    }

这里的focusRect是通过touchRect计算映射过去的,之所以这么做,先看下图

简单来说Camera.Area对象的Rect字段是描述了一个矩形区域在一个2000 x 2000个单元格组成的区域中的映射位置。坐标-1000, -1000代表了top, left,并且坐标1000, 1000代表了bottom, right。并且即使使用Camera.setDisplayOrientation()旋转预览图像也不会改变该坐标系。

关于摄像头切换和闪光灯开关这里就简单说下,无非就是获取点击事件,然后调用之前在MediaRecorderBase类中封装好的方法即可。

最后差点忘了我们的进度条。。这是一个粗糙的进度条:

public class ProgressView extends View {    /** 进度条 */    private Paint mProgressPaint;    /** 回删 */    private Paint mRemovePaint;    /** 最长时长 */    private int mMax;    /** 进度*/    private int mProgress;    private boolean isRemove;    public ProgressView(Context Context, AttributeSet Attr) {        super(Context, Attr);        init();    }    private void init() {        mProgressPaint = new Paint();        mRemovePaint = new Paint();        setBackgroundColor(getResources().getColor(R.color.transparent));        mProgressPaint.setColor(Color.GREEN);        mProgressPaint.setStyle(Paint.Style.FILL);        mRemovePaint.setColor(getResources().getColor(                R.color.title_back));        mRemovePaint.setStyle(Paint.Style.FILL);;    }    @Override    protected void onDraw(Canvas canvas) {        canvas.save();        final int width = getMeasuredWidth(), height = getMeasuredHeight();        int progressLength = (int) ((mProgress / (mMax * 1.0f)) * (width / 2));        canvas.drawRect(progressLength, 0, width - progressLength, height, isRemove ? mRemovePaint : mProgressPaint);        canvas.restore();    }    public void setMax(int max){        this.mMax = max;    }    public void setProgress(int progress){        this.mProgress = progress;    }    public void setRemove(boolean isRemove){        this.isRemove = isRemove;    }}

结语

作为第一篇博,难免会啰嗦抓不到重点,其实也是自己在总结的时候没有做很好得精炼,后续在总结UI的实现上,贴了代码没有介绍,是因为发现这边可总结的确实不多,OK就这样。

更多相关文章

  1. 浅谈Java中Collections.sort对List排序的两种方法
  2. Python list sort方法的具体使用
  3. python list.sort()根据多个关键字排序的方法实现
  4. Android(安卓)源码解析-AsyncTask
  5. android:绘图
  6. Android(安卓)Layout布局文件里的android:layout_height等属性为
  7. Android(安卓)App开发基础篇—四大组件之Activity简介
  8. android的消息处理机制(图+源码分析)——Looper,Handler,Message
  9. Android输入法原理和疑云

随机推荐

  1. 活用Android的Message Queue(2)
  2. Android实现左右滑动效果
  3. Android属性动画(三) TimeInterpolator(插值
  4. Activity 的四种加载模式
  5. 在Android Studio中使用Android-PullToRe
  6. Android 开发经验传承
  7. Camera服务之--架构浅析
  8. Android(安卓)高通平台camera hal层调试
  9. 建立android新工程
  10. Android 用Mediacodec硬解码视频包AVpack