定制Android原生相机支持连拍

Posted by KC on May 24, 2012

1、需求

之前基于Android 2.3原生相机做过定制,每拍完一张照片,会跳转到处理界面允许用户使用各种滤镜进行处理。现在需要增加一个分支流程,用户可以选择连拍模式,跳过滤镜,直接将照片保存或者进行情景模版叠加后保存。


## 2、正方向分析:

从拍照按钮开始分析。并在关键地方加入一些跟踪流程用的日志。

发现拍完一张照片之后,isCameraIdle()和mPreviewing都会被设置成false,从而令canTakePicture()返回false,这是导致不能连拍的原因。

canTakePicture()被autoFocus()函数调用,后者又被doFocus()调用。但是查找autoFocus()的调用方的时候,发现太多了,一时无法确定那个分支过来的是我想要的。

正向分析流程能得到很多信息,但是到后来分支多了,就容易混乱,于是尝试反过来看流程,从拍照那一刻开始往前推,看看逻辑能否和这个分支点重合。


## 3、反方向分析:

相机一定会调用的方法是camera.takePicture()方法。先找到这个方法。该方法的调用位于ImageCapture类中。

ImageCapture类中需要关注的方法预计会是:

1、capture()

1) 设置相机参数:mParameters对象:gps, Rotation,Timestamp等等。 最后调用mCameraDevice.takePicture()方法进行拍照。注意capture() 本身是private方法,它被initiate()共有方法简单包装。

2) initiate()方法被ImageCapture.onSnap()调用.

3) ImageCapture.onSnap()被以下两处调用:

  • AutoFocusCallback.onAutoFocus():在对焦完成后调用。该回调对象 被设置在mCameraDevice.autoFocus(mAutoFocusCallback)方法中。 mCameraDevice.autoFocus()被camera.autoFocus()调用

  • Camera.doSnap(): 当对焦模式为以下情况时回调用onSnap()进行拍照。

    – 无限远

    – 固定焦距

    – FOCUS_MODE_EDOF

    – 对焦已经成功

    – 对焦已经失败

    这些情况都是不需要对焦或者对焦已结束(不管成功还是失败),所以直接进行拍摄。

2、 storeImage():存储照片

mCameraDevice.takePicture()方法中的回调参数JpegPictureCallback类中定义了生成图片以及存储的相关代码。


4、关键

问题焦点落在autoFocus()。该方法调用了这两个关键方法: - canTakePicture():它决定了相机能否进行拍照。 - mCameraDevice.autoFocus(mAutoFocusCallback):调用它之后,相机会进行对焦(或者如果焦距设置为无限远则不对焦)然后拍摄。并且此方法是在canTakePicture()返回true的时候才执行的。

OK,要连拍,则必须让canTakePicture()方法返回true。


5、继续深入

1
2
3
private boolean canTakePicture() {
    return isCameraIdle() && mPreviewing && (mPicturesRemaining > 0);
}

根据前面的跟踪发现,每次拍照后,isCameraIdle() 和 mPreviewing 都会被设置为false。mPicturesRemaining我们不去管它,看过代码后发现这个是计算SD卡还足够容纳多少张照片的变量,目前我们存储卡容量肯定不会只能容下最后一张照片的,他肯定为true,无妨。

1
2
3
private boolean isCameraIdle() {
    return mStatus == IDLE && mFocusState == FOCUS_NOT_STARTED;
}

判断相机空闲是根据两个变量,mStatus设置为空闲(废话。。),相机未开始对焦。注意到在Camera中重写的startPreview()方法中设置了mStatus=IDLE。而且mstatus的状态只有两种:

1
2
private static final int IDLE                 = 1;
private static final int SNAPSHOT_IN_PROGRESS = 2;

那么估计就是在相机拍照过程中,mstatus被设置成SNAPSHOT_IN_PROGRESS了。确实,设置的地方就在ImageCapture.onSnap()方法中。

再找到mFocusState:

1
2
3
4
5
6
7
private static final int FOCUS_NOT_STARTED       = 0;
private static final int FOCUSING                = 1;
private static final int FOCUSING_SNAP_ON_FINISH = 2;
private static final int FOCUS_SUCCESS           = 3;
private static final int FOCUS_FAIL              = 4;
private int mFocusState                          = FOCUS_NOT_STARTED;

状态多了去了。看看需要的状态在何时出现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void clearFocusState() {
    mFocusState = FOCUS_NOT_STARTED;
    updateFocusIndicator();
}

private void stopPreview() {
    if (mCameraDevice != null && mPreviewing) {
        Log.v(TAG, "stopPreview");
        mCameraDevice.stopPreview();
    }
    mPreviewing = false;
    // If auto focus was in progress, it would have been canceled.
    clearFocusState();
}

停止预览的时候会重置该状态,按照我们对android相机的理解,拍照结束后应该会停止预览,我们也看到ImageCapture.capture()的最后一行mPreviewing = false;(虽然不是直接调用stopPreview())


6、解决

其实回头来看,这个定制也是比较简单的,在拍照结束后直接存储起来。然后再调用一次restartPreview()方法,该方法其实还是会去调用startPreview()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void startPreview() throws CameraHardwareException {
    if (mPausing || isFinishing())
        return;

    try {
        ensureCameraDevice();
    } catch (CameraHardwareException e) {
        MobclickAgent.reportError(Camera.this, AppUtils.joinArray(e.getStackTrace(), "\n"));
        throw e;
    }

    // If we're previewing already, stop the preview first (this will blank
    // the screen).
    if (mPreviewing)
        stopPreview();

    Util.setCameraDisplayOrientation(this, mCameraId, mCameraDevice);
    setCameraParameters(UPDATE_PARAM_ALL);
    setPreviewDisplay(mSurfaceHolder);

    mCameraDevice.setErrorCallback(mErrorCallback);

    try {
        Log.v(TAG, "startPreview");
        mCameraDevice.startPreview();
    } catch (Throwable ex) {
        closeCamera();
        MobclickAgent.reportError(Camera.this, AppUtils.joinArray(ex.getStackTrace(), "\n"));
        throw new RuntimeException("startPreview failed", ex);
    }
    mPreviewing = true;
    mZoomState = ZOOM_STOPPED;
    mStatus = IDLE;
}

7、完善

连拍已经打开了,但方案并不完善。我们的需求和原生相机的模式并不完全一直,多出来的其中一个重要的部分是需要配合情景相机,如果用户使用情景相机进行连拍,则后台要处理更多的事情,这会使后台花费更多的时间和更大的内存和CPU资源消耗。

所有的保存使用后台的异步线程进行,但是页面线程还是保留可操作状态,只是把相机的Preview设置暂时暂停,否则用户可能在前一张照片未处理完成就继续拍摄下一张照片,如果用户采用拍摄照片尺寸过大,可能导致后台内存溢出。这点的做法其实是和系统相机一致的。拍摄完成到开启下一张拍摄的时候有一个画面定格,可以认为是给用户看看刚刚所拍的照片,同时也是留出时间给后台进行图片的生成和存储。这比使用一堆复杂的排队机制要来得更纯粹些。

这里我们通过一个saveInstant()方法来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
 * 后台立即保存图片,保存好后会调用前台传入的callback接口。
 * 这里我新建了一个相机辅助类来做这个事,所以涉及到比较多的参数,
 * 我们统一封装到Bundle中。
 * 
 * @param activity
 * @param bundle
 * @param callback
 */
public static void instantSave(final Activity activity, final Bundle bundle, final PictureSaveCallBack callback) {
    new Thread(new Runnable() {
        //开启线程处理,不阻塞UI
        @Override
        public void run() {
            //这里要做的事情很多,判断前后摄像头,图片旋转角度,是否情景相机模式,是否需要合成情景模版等等。
            //把原始图片最终生成好。
            //生成相机左下角的缩略图。
            //调用回调接口通知相机图片保存已经OK(相机界面要做“善后”:更新左下角缩略图)
            activity.runOnUiThread(new Runnable() {
                //这个回调是设计到UI的,所以我们在UI线程中执行。也可以采取通知的形式。
                @Override
                public void run() {
                    callback.callback(thumbFilePath);
                }
            });
        }
    }).start()
}

在onPictureTaken()方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    if (burstEnabled) {
    // 连拍模式
    Bundle b = new Bundle();
    //构造参数放到Bundle中。
    CameraUtils.instantSave(Camera.this, b, new CameraUtils.PictureSaveCallBack() {
     @Override
        public void callback(String thumbFilePath) {
            Bitmap bmp = BitmapFactory.decodeFile(thumbFilePath);
            mLastPictureButton.setScaleType(ScaleType.FIT_XY);
            if (bmp != null) {
                mLastPictureButton.setImageBitmap(bmp);
            }
            updateThumbnailButton();
            progressBar.setVisibility(View.INVISIBLE);
            restartPreview();
        }
});