稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Camera 2学习之拍照基础

前面一篇文章讲解了如何建立预览,下一步就是进行拍照了,这是相机类的核心业务,TL;DR。

拍照的基本流程

先简单的总结 一下,有个印象,后面会逐一详细的讲解:

  1. 需要一个目标输出,以接收camera的输出。Camera2的API界线划分的比较清楚,Camera只负责拍照成像,并且输出的结果不再像以前那样直接返回一个jpeg byte array,是一个中间结果,需要一个目标输出进行收集并处理。
  2. 需要一个CameraCaptureSession.CaptureCallback,以监听拍照流程状态
  3. 需要存储模块做最终的结果保存

整体架构图:

classDiagram OnImageAvailableListener <|-- PhotoSaveAgent PhotoWriteTask --|> Runnable PhotoResultObserver <|-- PhotoSaveAgent PhotoSaveAgent ..|> PhotoWriteTask CaptureCallback <|-- PhotoStillCapture PhotoSaveAgent *-- PhotoResult PhotoWriteTask *-- PhotoResult PhotoResultObserver --* PhotoStillCapture class PhotoSaveAgent { imageReader photoQueue executor +config(int width, int height, int format) +Surface getOutputTarget() onImageAvailable() onCaptureStart() onCaptureComplete() } class OnImageAvailableListener { <<interface>> onImageAvailable(Image image) } class Runnable { <<interface>> run() } class PhotoWriteTask { <<interface>> run() } class PhotoResultObserver { <<interface>> onCaptureStart(CaptureRequest request, long timestamp, long frame, String filename) onCaptureComplete(CaptureRequest request, CaptureResult result) } class PhotoResult { CaptureRequest captureRequest long timestamp long frameNumber String filename CaptureResult captureResult Image image } class CaptureCallback { <<interface>> onCaptureStarted() onCaptureProgressed() onCaptureCompleted() onCaptureFailed() } class PhotoStillCapture { Date dateTaken String filename Consumer consumer PhotoResultObserver resultObserver +generateRequest() +generateCaptureCallback() }

存储模块

存储模块的主要职责是提供目标输出给CameraAgent,收集拍照结果,并做后续的保存工作。它与CameraAgent是独立开来的,并没有直接的关系。

1
2
3
save
  |-- PhotoSaveAgent
  |-- PhotoWriteTask

关键的组件

ImageReader拍照结果收集器

PhotoSaveAgent用以封装和对外交互

PhotoWriteTask专门负责把jpeg结果进行文件保存和媒体库的记录创建

结果队列

因为结果会来源于ImageReader#OnImageAvailable和onCaptureComplete,需要合并两个异步回调的以合成最终结果,并且两个回调均是异步的,先后顺序 也不一样,因此需要一个队列。

因为查询比较多,所以,这里用一个哈希表来当作队列,键为CaptureRequest,而值为PhotoResult。

线程模型

会有一个HandlerThread,以保证存储模块相关的操作全部都发生在自己的HandlerThread里面,这里存储模块的主要的工作线程。当与外部交互 时,特别是有外部的回调过来时,都要及时的转入自己的工作线程进行后续处理,以保证线程的安全性。

ImageReader的回调OnImageAvailable也要放在自己的工作线程中。

还需要一个线程池专门用于jpeg结果的保存。因为文件的I/O是CPU密集型的耗时操作,如果放在主工作线程中,会造成阻塞,并且可能会多个拍照结果要处理,所以需要一个专门的线程池。

对外交互

接收camera size

因为创建ImageReader时需要指定一个尺寸,而这个尺寸必须 来自于camera,也就是说必须 是camera针对 某一配置所能支持的尺寸,并要符合一定的约束。这个尺寸最终会作用到目标输出Surface上面,从而影响拍照结果。

提供一个返回Surface的接口给外部

Camera 2的所有输出都是以Surface形式的中间结果(实质上都是一些buffer),所以需要提供一个Surface给CameraAgent。这个Surface可以从创建好了的ImageReader中直接获取。

实现一个拍照结果观察者

除了上述三个以外,不暴露任何公开的方法。

主要工作流程

此模块需要在Activity中进行初始化,和生命周期的管理,比如在onCreate时进行初始化,在onDestroy中进行销毁。

初始化时,要先准备工作线程HandlerThread,并启动。之后要把初始化工作都尽可能的放在工作线程中,以不阻塞外部调用线程。

之后是创建ImageReader,这个需要外部输入尺寸,一旦ImageReader创建完成,就会处于ready状态。

PhotoSaveAgent是对外交互的对象,在CameraContext中会使用此对象,主要是:

  1. 当CameraAgent对象创建好了后,计算出来了picture size后,通知camera size给PhotoSaveAgent,PhotoSaveAgent知道此size后便可以创建ImageReader
  2. 拍照时,获取目标输出Surface,来源是第1步创建好后的ImageReader
  3. 拍照时接收拍照状态
stateDiagram-v2 [*] --> Setup Setup --> Config Config --> ReadyForImage ReadyForImage --> CollectPhotoResult CollectPhotoResult --> SavePhoto SavePhoto --> ReadyForImage SavePhoto --> TearDown TearDown --> [*]

拍照结果收集

一旦PhotoSaveAgent处于ready状态后,就可以随时收集拍照结果,拍照的结果全部是以回调的形式被动通知的,二个是来自于拍照模块,一个是ImageReader:

  1. 拍照开始
  2. 拍照结束
  3. ImageReader通知onImageAvailable

需要注意,前面的三个回调拍照开始肯定 最先来,但后面的拍照结束和onImageAvailable谁先谁后真不一定,理论上来说是拍照结束来的早一些,但也不绝对,因此在逻辑处理上不能强依赖这两者的午后顺序。

主要的流程是:

  1. 拍照开始时,在结果队列中添加记录,以CaptureRequest为key,并创建PhotoResult对象;
  2. 拍照结束回调中,以CaptureRequest为key,查询结果,向PhotoResult对象添加CaptureResult,并检查PhotoResult的状态,如果PhotoResult的CaptureResult和Image对象都齐全了,就说明拍照结果已集齐,这时就可以移除队列并进行保存了;
  3. 在onImageAvailable中,流程稍复杂些,因为这个回调只有一个Image对象,需要与队列中的PhotoResult对象进行匹配,就是通过拍照时的timestamp进行匹配,这是唯一的标识了,遍历队列,如果找到了就更新PhotoResult,并检查是否集齐,如已集齐则移除队列并进行结果保存。

另外需要注意的是,拍照开始和拍照结束两个回调的调用线程不确定,所以需要进行转换,先转到PhotoSaveAgent的工作线程中再去处理,这样可以保证PhotoSaveAgent的所有操作都在自己的线程中,不需要再做额外的线程安全性保护。

拍照结果保存

在拍照结果回调中,以及onImageAvailable回调中检查PhotoResult是否集齐,集齐后,就可以进行结果保存。

主要流程如下:

  1. 创建PhotoWriteTask,它实现了Runnable接口,以方便在线程池中使用
  2. 向线程池提交任务
  3. 在创建任务的时候,要把需要的信息都保存下来。特别需要注意的是,需要从Image中把jpeg byte array拷贝出来。因为ImageReader内部会用Image池子去不断的接收从外部输入(拍照时就是camera sensor)的结果,所以onImageAvailable中传过来的Image需要尽快的释放(即Image#close),以保证ImageReader能正常工作。PhotoWriteTask虽然是单独的线程池,但文件的I/O过程不可控,可能长也可能短,假如简单的持有Image对象,可能会导致Image对象无法及时被释放,从而导致ImageReader不能正常工作。因此需要在创建任务的时候就赶紧把jpeg bytes从Image中拷贝出来。
  4. 具体的写入过程比较清晰,向MediaProvider中写入文件即可。

拍照模块

这是最为核心的一部分。

1
2
3
4
5
snapshot
    |-- PhotoCaptureStatus
    |-- PhotoResult
    |-- PhotoResultObserver
    |-- PhotoStillCapture

关键组件

  1. PhotoResultObserver,拍照结果观察者接口,用于向外部通知拍照结果,两个方法,一个是拍照开始,一个是拍照结束。注意拍照失败也是调用拍照结束,只不过没有CaptureResult。
  2. PhotoResult,这是合成后的拍照结果对象,里面包含着拍照结果的一切信息,如文件名,参数,CaptureRequest,CaptureResult和Image。是一个POJO,仅做状态和数据的保存,无逻辑。
  3. PhotoStillCapture,可理解为拍照对象,用以封装拍照参数,生成CaptureRequest和处理CaptureCallback。

线程模型

这属于核心业务,所以它是在CameraContext的工作线程中的。

对外交互

通过PhotoResultObserver来通知外部拍照的结果状态。

工作流程

CameraContext中拉回拍照接口,并转入到自己的工作线程中。CameraAgent增加拍照接口,这是主要的功能入口。

sequenceDiagram CameraActivity ->> CameraAgent: takePhoto CameraAgent ->> PhotoSaveAgent: getOutputTarget CameraAgent -->> CameraActivity: started CameraAgent -->> CameraActivity: ongoing CameraAgent -->> PhotoSaveAgent: onCaptureStart CameraAgent -->> CameraActivity: completed CameraAgent -->> PhotoSaveAgent: onCaptureComplete PhotoSaveAgent ->> PhotoWriteTask: savePhoto PhotoWriteTask -->> CameraActivity: thumbnailArrived

CameraAgent会在其capturePhoto方法,创建一个PhotoStillCapture对象,然后调用CameraCaptureSession#capture方法进行拍照。拍照请求由PhotoStillCapture生成,CaptureCallback亦由PhotoStillCapture对象处理。

创建PhotoStillCapture对象是会锁定一些参数,如当前的旋转,文件名字(通常以时间戳为文件名字), 以及PhotoResultObserver。

生成拍照请求CaptureRequest时,会传入一些拍照需要的参数,如旋转,如图片质量等等。

在CatpureCallback中,做一些简单处理,然后回调PhotoResultObserver。

到此,拍照模块的事情 就做完,它就要是拍照的前期工作,与外部做连接,下发请求就基本上完整了。拍照结果的收集则是存储模块的事情 了。

状态反馈

从整个拍照交互来说,也是需要状态反馈的,最为基础的交互逻辑是,为了保证拍照的成功率,当下发拍照请求后,到拍照结束前,也就是CaptureCallback#onCaptureCompleted或者CaptureCallback#onCaptureFailed这两个回调回来之前,是不可以下发新的拍照请求的。

classDiagram CameraActivity --|> Consumer Consumer --* PhotoStillCapture CaptureCallback <|-- PhotoStillCapture class Consumer { <<interface>> +accept(PhotoCaptureStatus status) } class CameraActivity { } class PhotoStillCapture { consumer } class CaptureCallback { <<interface>> onCaptureStarted() onCaptureProgressed() onCaptureCompleted() onCaptureFailed() }

那么交互 层面也需要知道拍照状态,以方便进行UI管控,比如说当下发了拍照请求后,就把快门shutter设置为disabled,在拍照结束后再恢复为enabled的;此外还有拍照动画,也需要知道状态。

但UI交互层只知道状态就可以了,并不需要特别的数据,所以 这一路的状态通过简单的Consumer即可实现,仅在拍照模块中定义一些简单的状态就可以了:

1
2
3
4
5
6
public enum PhotoCaptureStatus {
    STARTED,
    ONGOING,
    FAILED,
    COMPLETED
}

因为拍照是由UI层触发的,所以在调用CameraContext时,就提供一个接收状态的Consumer就可以了,这样就把UI层和逻辑层分离开了,逻辑层接收Consumer作为参数,在关键的节点就回报状态;UI层负责处理感兴趣的状态就可以了:

1
2
3
4
5
public void takePhoto(View view) {
        shutterView.setEnabled(false);

        cameraFactory.takePhoto(status -> mainHandler.post(() -> captureStatusListener.accept(status)));
    }

当然别忘记了,在Consumer中要做线程切换,UI的处理只以在主线程中。

缩略图处理

缩短略图是一个非常重要的拍照结果展示,用以告诉用户拍照成功了,并且是预览结果成片的入口,因此需要做二个事情:

  1. 监听拍照结果,注意这里并不是说要监听拍照状态,而是要监听拍照结果,因为只有得到最终结果jpeg array后,才可以从这里创建缩略图,并展示 出来。
  2. 点击缩略图时要能够进行结果成片的展示,所以,还需要知道拍照结果的文件路径,或者说Uri

以上两个信息,都在PhotoSaveAgent里面,因此需要建立UI层与存储模块之间的连接,但其实存储层只是一个数据来源,它并不负责缩略图的业务,而且缩略图的来源可能的不止存储模块,还能来源于直接的数据库查询。

classDiagram ThumbnailObserver <|-- CameraActivity CameraActivity --* Thumbnail ThumbnailObserver --* PhotoSaveAgent class CameraActivity { thumbnailArrived(Thumbnail thumbnaill) } class PhotoSaveAgent { +addThumbnailObserver() } class Thumbnail { bitmap uri } class ThumbnailObserver { <<interface>> thumbnailArrived(Thumbnail thumbnail) }

为此,新建一个缩略图模块,里面有一个Thumbnail对象,是一个POJO用以代表缩略图的相关信息,如Bitmap和Uri;还有一个ThumbnailObserver的接口,用以让数据源告诉观察者,一个新的缩略图对象生成了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Thumbnail {
    private final Uri uri;
    private final String mime;
    private final Bitmap bitmap;

    public Thumbnail(Uri uri, String mime, Bitmap bitmap) {
        this.uri = uri;
        this.mime = mime;
        this.bitmap = bitmap;
    }

    public Bitmap getBitmap() {
        return bitmap;
    }

    public Intent generateAction() {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(uri, mime);
        return intent;
    }
}
1
2
3
public interface ThumbnailObserver {
    void thumbnailArrived(Thumbnail thumbnail);
}

如此,UI层与PhotoSaveAgent就分离开来了,它们之间的联系只有ThumbnailObserver接口,UI层就实现接口,负责收到缩略图后的展示工作:

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
   private final ThumbnailObserver thumbnailObserver = thumbnail -> mainHandler.post(() -> updateThumbnail(thumbnail));

    private void updateThumbnail(Thumbnail thumbnail) {
        thumbnailSwitcher.setEnabled(true);
        thumbnailSwitcher.setTag(thumbnail);

        ((ImageView) thumbnailSwitcher.getNextView()).setImageBitmap(thumbnail.getBitmap());

        thumbnailSwitcher.showNext();
    }

    // when init PhotoSaveAgent, register the thumbnail observer
    imageSaveAgent.addThumbnailObserver(thumbnailObserver);

    public void viewLastPhoto(View view) {
        if (thumbnailSwitcher.getTag() == null) {
            Log.d(LOG_TAG, "No thumbnail, you should take photo first.");
            return;
        }
        Thumbnail thumbnail = (Thumbnail) thumbnailSwitcher.getTag();
        Intent action = thumbnail.generateAction();
        try {
            startActivity(action);
        } catch (ActivityNotFoundException e) {
            Log.d(LOG_TAG, "Unfortunately, we cannot view the photo.");
        }
    }

而对于PhotoSaveAgent则需要在PhotoWriteTask里面,最后一步,也即文件保存完毕后,生成缩略图,因为是需要Uri的,所以必须是要在最后才能生成缩略图,并通过ThumbnailObserver回调:

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
   // in PhotoWriteTask
   public void run() {
        ///// other codes

        Thumbnail t = generateThumbnail(fileUri);
        for (ThumbnailObserver to : thumbnailObservers) {
            to.thumbnailArrived(t);
        }
    }

    private Thumbnail generateThumbnail(Uri uri) {
        return new Thumbnail(uri, mime, extractThumbnailBitmap(jpeg));
    }

    private Bitmap extractThumbnailBitmap(byte[] jpegArray) {
        int target = 480;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeByteArray(jpegArray, 0, jpegArray.length, options);
        options.inSampleSize = Math.min(options.outWidth, options.outHeight) / target;
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeByteArray(jpegArray, 0, jpegArray.length, options);
        Matrix matrix = new Matrix();
        matrix.postRotate(jpegOrientation);
        Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        if (rotated != bitmap) {
            bitmap.recycle();
        }
        return rotated;
    }

拍照动画

拍照动画的目的就在于给用户一个直观 的感觉,特别是拍照开始了,至于拍照结束倒是不用特别的,这人一般在缩略图那里体现。

主要做两个动画,一个是快门shutter的动画,这个是在快门点击时就可以去做了,主要是一个缩放的动画,把缩放的动画正着放一遍(缩小0.75倍),再reverse(放大到正常大小)即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void takePhoto(View view) {
        shutterView.setEnabled(false);

        ScaleAnimation anim = new ScaleAnimation(1f, 0.75f, 1f, 0.75f,
                Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        anim.setDuration(Config.CAPTURE_ANIM_DURATION / 2);
        anim.setInterpolator(new AccelerateDecelerateInterpolator());
        anim.setFillAfter(true);
        anim.setRepeatMode(Animation.REVERSE);
        anim.setRepeatCount(1);
        shutterView.startAnimation(anim);

        cameraFactory.takePhoto(status -> mainHandler.post(() -> captureStatusListener.accept(status)));
    }

另外一个就是在拍照开始时的动画,这个动画的时机是在硬件真的开始拍照时做,也即是要在CaptureCallback#onCaptureStarted时去做,需要特别注意的是,这个时间是由硬件决定的,所以只有在onCaptureStarted回调时去做才是最恰当的。

至于动画的形式,一般是通过预览区域的闪动实现,比如可以用一个与预览Surface一样大小的View,改变它的颜色,给用户一种预览闪动的效果即可。这里的overlayView是一个盖在预览上面的透明的View,在做动画时给它设置为半透明的白色:

1
2
3
4
5
6
7
8
9
10
11
12
 private final Consumer<PhotoCaptureStatus> captureStatusListener = status -> {
    Log.d(LOG_TAG, "capture status " + status);
    statusView.setText("Capture Status: " + status);
    if (status == PhotoCaptureStatus.STARTED) {
        overlayView.setBackground(new ColorDrawable(Color.argb(150, 255, 255, 255)));
        mainHandler.postDelayed(() -> overlayView.setBackground(null), Config.CAPTURE_ANIM_DURATION);
    } else if (status == PhotoCaptureStatus.COMPLETED) {
        shutterView.setEnabled(true);
    } else if (status == PhotoCaptureStatus.FAILED) {
        shutterView.setEnabled(true);
    }
};

Comments