前面一篇文章 讲解了如何建立预览,下一步就是进行拍照了,这是相机类的核心业务,TL;DR。
拍照的基本流程
先简单的总结 一下,有个印象,后面会逐一详细的讲解:
需要一个目标输出,以接收camera的输出。Camera2的API界线划分的比较清楚,Camera只负责拍照成像,并且输出的结果不再像以前那样直接返回一个jpeg byte array,是一个中间结果,需要一个目标输出进行收集并处理。
需要一个CameraCaptureSession.CaptureCallback,以监听拍照流程状态
需要存储模块做最终的结果保存
整体架构图:
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中会使用此对象,主要是:
当CameraAgent对象创建好了后,计算出来了picture size后,通知camera size给PhotoSaveAgent,PhotoSaveAgent知道此size后便可以创建ImageReader
拍照时,获取目标输出Surface,来源是第1步创建好后的ImageReader
拍照时接收拍照状态
stateDiagram-v2
[*] --> Setup
Setup --> Config
Config --> ReadyForImage
ReadyForImage --> CollectPhotoResult
CollectPhotoResult --> SavePhoto
SavePhoto --> ReadyForImage
SavePhoto --> TearDown
TearDown --> [*]
拍照结果收集
一旦PhotoSaveAgent处于ready状态后,就可以随时收集拍照结果,拍照的结果全部是以回调的形式被动通知的,二个是来自于拍照模块,一个是ImageReader:
拍照开始
拍照结束
ImageReader通知onImageAvailable
需要注意,前面的三个回调拍照开始肯定 最先来,但后面的拍照结束和onImageAvailable谁先谁后真不一定,理论上来说是拍照结束来的早一些,但也不绝对,因此在逻辑处理上不能强依赖这两者的午后顺序。
主要的流程是:
拍照开始时,在结果队列中添加记录,以CaptureRequest为key,并创建PhotoResult对象;
拍照结束回调中,以CaptureRequest为key,查询结果,向PhotoResult对象添加CaptureResult,并检查PhotoResult的状态,如果PhotoResult的CaptureResult和Image对象都齐全了,就说明拍照结果已集齐,这时就可以移除队列并进行保存了;
在onImageAvailable中,流程稍复杂些,因为这个回调只有一个Image对象,需要与队列中的PhotoResult对象进行匹配,就是通过拍照时的timestamp进行匹配,这是唯一的标识了,遍历队列,如果找到了就更新PhotoResult,并检查是否集齐,如已集齐则移除队列并进行结果保存。
另外需要注意的是,拍照开始和拍照结束两个回调的调用线程不确定,所以需要进行转换,先转到PhotoSaveAgent的工作线程中再去处理,这样可以保证PhotoSaveAgent的所有操作都在自己的线程中,不需要再做额外的线程安全性保护。
拍照结果保存
在拍照结果回调中,以及onImageAvailable回调中检查PhotoResult是否集齐,集齐后,就可以进行结果保存。
主要流程如下:
创建PhotoWriteTask,它实现了Runnable接口,以方便在线程池中使用
向线程池提交任务
在创建任务的时候,要把需要的信息都保存下来。特别需要注意的是,需要从Image中把jpeg byte array拷贝出来。因为ImageReader内部会用Image池子去不断的接收从外部输入(拍照时就是camera sensor)的结果,所以onImageAvailable中传过来的Image需要尽快的释放(即Image#close),以保证ImageReader能正常工作。PhotoWriteTask虽然是单独的线程池,但文件的I/O过程不可控,可能长也可能短,假如简单的持有Image对象,可能会导致Image对象无法及时被释放,从而导致ImageReader不能正常工作。因此需要在创建任务的时候就赶紧把jpeg bytes从Image中拷贝出来。
具体的写入过程比较清晰,向MediaProvider中写入文件即可。
拍照模块
这是最为核心的一部分。
1
2
3
4
5
snapshot
|-- PhotoCaptureStatus
|-- PhotoResult
|-- PhotoResultObserver
|-- PhotoStillCapture
关键组件
PhotoResultObserver,拍照结果观察者接口,用于向外部通知拍照结果,两个方法,一个是拍照开始,一个是拍照结束。注意拍照失败也是调用拍照结束,只不过没有CaptureResult。
PhotoResult,这是合成后的拍照结果对象,里面包含着拍照结果的一切信息,如文件名,参数,CaptureRequest,CaptureResult和Image。是一个POJO,仅做状态和数据的保存,无逻辑。
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的处理只以在主线程中。
缩略图处理
缩短略图是一个非常重要的拍照结果展示,用以告诉用户拍照成功了,并且是预览结果成片的入口,因此需要做二个事情:
监听拍照结果,注意这里并不是说要监听拍照状态,而是要监听拍照结果,因为只有得到最终结果jpeg array后,才可以从这里创建缩略图,并展示 出来。
点击缩略图时要能够进行结果成片的展示,所以,还需要知道拍照结果的文件路径,或者说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 ( 1 f , 0.75f , 1 f , 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 );
}
};