近期学习了解Android相机的使用,如何预览及切图,看了一些资料,在github上找到了google对于Camera2使用的Demo,就这份Demo做一个个人分析理解。
示例项目中有两份代码,一份Java编写,一份Kotlin编写,将分别做分析记录。
一、Java示例
目录结构非常简单,只有一个Activity,一个Fragment,一个自定义View。
Activity里面只有一个Fragment,这里不做解释。
接下来我们看这个自定义View是什么样的。
/**
* A {@link TextureView} that can be adjusted to a specified aspect ratio.
*/
public class AutoFitTextureView extends TextureView {
private int mRatioWidth = 0;
private int mRatioHeight = 0;
public AutoFitTextureView(Context context) {
this(context, null);
}
public AutoFitTextureView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AutoFitTextureView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
* calculated from the parameters. Note that the actual sizes of parameters don't matter, that
* is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.
*
* @param width Relative horizontal size
* @param height Relative vertical size
*/
public void setAspectRatio(int width, int height) {
if (width < 0 || height < 0) {
throw new IllegalArgumentException("Size cannot be negative.");
}
mRatioWidth = width;
mRatioHeight = height;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (0 == mRatioWidth || 0 == mRatioHeight) {
setMeasuredDimension(width, height);
} else {
if (width < height * mRatioWidth / mRatioHeight) {
setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
} else {
setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
}
}
}
}
可以看到,这是一个自定义的TextureView,在相机使用的时候,TextureView将作为载体去展示相机获取的内容,就是我们展示使用的控件。
同时,这个自定义TextureView重写了onMeasure方法,大家都知道这个方法是改变控件大小使用的,接下来我们看内部实现。mRatioHeight和mRatioWidth是一个从外部设置的宽高,当我们的默认要展示的宽高比和设定的宽高比不一样的时候,重新计算,让最短的(宽或高)充满控件,另一个(高或宽)按比例确定其长度。这样做的目的是让相机获取的比例和我们控件所一致,否则会造成图片拉伸的效果。另外,当我们每次设置新的数据进来的时候,会调用requestLayout方法,此方法会重新测量控件宽高,保证显示的准确性。
在看Fragment中使用Camera2进行开发之前,我们先简单了解一下Camera2如何使用。
1、Camera2的显示需要一个TextureView作为载体,所以我们需要一个TextureView。(SurfaceView也可以)
2、当TextureView创建完成后,6.0之后需要先做相机的权限请求验证(Manifest.permission.CAMERA),验证通过后,获取CameraManager对象activity.getSystemService(Context.CAMARA_SERVICE),然后用CameraManager打开摄像头,调用openCamera(@NotNull String : cameraId, @NotNull final CameraDevice.StateCallback callback,
@Nullable Handler : handler)方法打开摄像头。
该方法第一个参数为相机id(通常0为后置摄像头,1为前置摄像头),第二个参数为相机状态的回调,后面详细解析,第三个参数为接收消息使用的Handler,作用是表示在哪个线程调用,如果为null,就在当前线程使用,附上源码注释:
* @param cameraId
* The unique identifier of the camera device to open
* @param callback
* The callback which is invoked once the camera is opened
* @param handler
* The handler on which the callback should be invoked, or
* {@code null} to use the current thread's {@link android.os.Looper looper}.
3、第二步我们需要一个CameraDevice.StateCallback对象,接下来我们就需要创建这个对象,这个抽象类有三个实现,分别是onOpen、onDisconnect、onError,按名称就知道其含义了。
4、在回调onOpen的时候,就证明是相机打开成功了,此时还需要创建CameraCaptureSession,调用CameraDevice的creatCaptureSession方法,源码和部分注释如下
/**
* <p>Create a new camera capture session by providing the target output set of Surfaces to the
* camera device.</p>
* @param outputs The new set of Surfaces that should be made available as
* targets for captured image data.
* @param callback The callback to notify about the status of the new capture session.
* @param handler The handler on which the callback should be invoked, or {@code null} to use
* the current thread's {@link android.os.Looper looper}.
*
* @throws IllegalArgumentException if the set of output Surfaces do not meet the requirements,
* the callback is null, or the handler is null but the current
* thread has no looper.
* @throws CameraAccessException if the camera device is no longer connected or has
* encountered a fatal error
* @throws IllegalStateException if the camera device has been closed
*/
public abstract void createCaptureSession(@NonNull List<Surface> outputs,
@NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)
throws CameraAccessException;
第一个参数是一个Surface的List,其作用是需要将要显示在这个Surface上面,此处我们需要两个Surface,一个是显示在页面上,另一个是拍照的时候保存用的,先通过我们前面的TextureView获取显示用的Surface。
SurfaceTexture texture = mTextureView.getSurfaceTexture();
assert texture != null;
// We configure the size of default buffer to be the size of camera preview we want.
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
// This is the output Surface we need to start preview.
Surface surface = new Surface(texture);
在拍照截图的时候,我们还需要一个ImageReader对象,此时将ImageReader的Surface也传入进去。
第二个参数是CameraCaptureSession.StateCallback,看名称即知道是状态回调。
第三个参数是handler,和之前的作用一样,不做解释了。
五、在CameraCaptureSession.StateCallback的回调onConfigured中,我们可以取到CameraCaptureSession的实例,
接下来,我们需要调用CaptureSession的setRepeatingRequest方法,部分注释如下
/**
* Request endlessly repeating capture of images by this capture session.
*
* <p>With this method, the camera device will continually capture images
* using the settings in the provided {@link CaptureRequest}, at the maximum
* rate possible.</p>
*
* @param request the request to repeat indefinitely
* @param listener The callback object to notify every time the
* request finishes processing. If null, no metadata will be
* produced for this stream of requests, although image data will
* still be produced.
* @param handler the handler on which the listener should be invoked, or
* {@code null} to use the current thread's {@link android.os.Looper
* looper}.
*
* @return int A unique capture sequence ID used by
* {@link CaptureCallback#onCaptureSequenceCompleted}.
*
* @throws CameraAccessException if the camera device is no longer connected or has
* encountered a fatal error
* @throws IllegalStateException if this session is no longer active, either because the session
* was explicitly closed, a new session has been created
*