一、前言

之前已经介绍过过时的旧 Camera 的使用了,毕竟在从 Android 5.0 后推荐使用 Camera2 了,所以现在开始介绍 Camera2 相关使用。老规矩还是从 SurfaceView 说起。

如果你对 Camera2 的相关类和接口还不熟悉,可以先看看下面这些介绍:

  • CameraManager详解
  • CameraDevice详解
  • CameraCharacteristics详解
  • CameraCaptureSession详解
  • CaptureRequest和CaptureResult

为什么选择 SurfaceView

SurfaceView 在自己独立的线程中绘制,不会影响到主线程,内部使用双缓冲机制,画面更流畅。相比于 TextureView,它内存占用低,绘制更及时,耗时也更低,但不支持动画和截图。

下面是该应用的简要截图:

二、相机开发步骤

我们选择将 Camera 和 View 分开,Camera 的相关操作由 Camera2Proxy 类完成,而 View 持有一个 Camera2Proxy 对象。这样 Camera2Proxy 也是可以重复利用的。

注意: 避免篇幅过长,下面每个小模块的示例代码在最后统一给出。

1. 打开相机

通过 CameraManager 的 openCamera() 方法打开相机,并在 CameraDevice.StateCallback 回调中获取 CameraDevice 对象。需要指定打开的相机 cameraId。
注意:
CameraCharacteristics.LENS_FACING_FRONT 通常表示后置摄像头,CameraCharacteristics.LENS_FACING_BACK 通常表示前置摄像头。

2. 相机配置

在 Camera2 API 中,相机的一些通用配置是通过 CameraCharacteristics 类完成,针对不同的请求(预览&拍照等),我们还可以通过 CaptureRequest 类单独配置。

我们可以设置 闪光模式聚焦模式曝光强度预览图片格式和大小拍照图片格式和大小 等等信息。

3. 设置相机预览时的显示方向

设置好了预览的显示方向和大小,预览的画面才不会产生拉伸等现象。

4. 开始预览、停止预览

可以通过 CameraCaptureSessionsetRepeatingRequest() 重复发送预览的请求来实现预览,通过 stopRepeating() 方法来停止发送。

5. 释放相机

相机是很耗费系统资源的东西,用完一定要释放。

6. 点击聚焦

简单的说,就是根据用户在 view 上的触摸点,映射到相机坐标系中对应的点,然后通过 CaptureRequest.BuilderCaptureRequest.CONTROL_AF_REGIONS 字段设置聚焦的区域。

7. 双指放大缩小

通过 View 的点击事件,获取到双指之间的间距,并通过 CaptureRequest.BuilderCaptureRequest.SCALER_CROP_REGION 字段设置缩放。

8. 拍照

新建一个 ImageReader 对象作为拍照的输出目标,通过创建一个拍照的 CaptureRequest,并通过 CameraCaptureSessioncapture() 方法来发送单次请求。

注意,预览的时候是通过 CameraCaptureSessionsetRepeatingRequest() 来发送重复请求,注意区分。

9. Camera2Proxy 类

下面代码还用到了 OrientationEventListener,这里之前没介绍,是通过传感器来获取当前手机的方向的,用于 拍照 的时候设置图片的选择使用,后面会介绍。

package com.afei.camerademo.camera;import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.MeteringRectangle;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.ImageReader;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.NonNull;
import android.util.Log;
import android.util.Size;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.SurfaceHolder;import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;public class Camera2Proxy {private static final String TAG = "Camera2Proxy";private Activity mActivity;private int mCameraId = CameraCharacteristics.LENS_FACING_FRONT; // 要打开的摄像头IDprivate Size mPreviewSize; // 预览大小private CameraManager mCameraManager; // 相机管理者private CameraCharacteristics mCameraCharacteristics; // 相机属性private CameraDevice mCameraDevice; // 相机对象private CameraCaptureSession mCaptureSession;private CaptureRequest.Builder mPreviewRequestBuilder; // 相机预览请求的构造器private CaptureRequest mPreviewRequest;private Handler mBackgroundHandler;private HandlerThread mBackgroundThread;private ImageReader mImageReader;private Surface mPreviewSurface;private OrientationEventListener mOrientationEventListener;private int mDisplayRotate = 0;private int mDeviceOrientation = 0; // 设备方向,由相机传感器获取private int mZoom = 0; // 缩放/*** 打开摄像头的回调*/private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {@Overridepublic void onOpened(@NonNull CameraDevice camera) {Log.d(TAG, "onOpened");mCameraDevice = camera;initPreviewRequest();}@Overridepublic void onDisconnected(@NonNull CameraDevice camera) {Log.d(TAG, "onDisconnected");releaseCamera();}@Overridepublic void onError(@NonNull CameraDevice camera, int error) {Log.e(TAG, "Camera Open failed, error: " + error);releaseCamera();}};@TargetApi(Build.VERSION_CODES.M)public Camera2Proxy(Activity activity) {mActivity = activity;mCameraManager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE);mOrientationEventListener = new OrientationEventListener(mActivity) {@Overridepublic void onOrientationChanged(int orientation) {mDeviceOrientation = orientation;}};}@SuppressLint("MissingPermission")public void openCamera(int width, int height) {Log.v(TAG, "openCamera");startBackgroundThread(); // 对应 releaseCamera() 方法中的 stopBackgroundThread()mOrientationEventListener.enable();try {mCameraCharacteristics = mCameraManager.getCameraCharacteristics(Integer.toString(mCameraId));StreamConfigurationMap map = mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);// 拍照大小,选择能支持的一个最大的图片大小Size largest = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)), new CompareSizesByArea());Log.d(TAG, "picture size: " + largest.getWidth() + "*" + largest.getHeight());mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), ImageFormat.JPEG, 2);// 预览大小,根据上面选择的拍照图片的长宽比,选择一个和控件长宽差不多的大小mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), width, height, largest);Log.d(TAG, "preview size: " + mPreviewSize.getWidth() + "*" + mPreviewSize.getHeight());// 打开摄像头mCameraManager.openCamera(Integer.toString(mCameraId), mStateCallback, mBackgroundHandler);} catch (CameraAccessException e) {e.printStackTrace();}}public void releaseCamera() {Log.v(TAG, "releaseCamera");if (null != mCaptureSession) {mCaptureSession.close();mCaptureSession = null;}if (mCameraDevice != null) {mCameraDevice.close();mCameraDevice = null;}if (mImageReader != null) {mImageReader.close();mImageReader = null;}mOrientationEventListener.disable();stopBackgroundThread(); // 对应 openCamera() 方法中的 startBackgroundThread()}public void setImageAvailableListener(ImageReader.OnImageAvailableListener onImageAvailableListener) {if (mImageReader == null) {Log.w(TAG, "setImageAvailableListener: mImageReader is null");return;}mImageReader.setOnImageAvailableListener(onImageAvailableListener, null);}public void setPreviewSurface(SurfaceHolder holder) {mPreviewSurface = holder.getSurface();}public void setPreviewSurface(SurfaceTexture surfaceTexture) {surfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());mPreviewSurface = new Surface(surfaceTexture);}private void initPreviewRequest() {try {mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);mPreviewRequestBuilder.addTarget(mPreviewSurface); // 设置预览输出的 SurfacemCameraDevice.createCaptureSession(Arrays.asList(mPreviewSurface, mImageReader.getSurface()),new CameraCaptureSession.StateCallback() {@Overridepublic void onConfigured(@NonNull CameraCaptureSession session) {mCaptureSession = session;// 设置连续自动对焦mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);// 设置自动曝光mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);// 设置完后自动开始预览mPreviewRequest = mPreviewRequestBuilder.build();startPreview();}@Overridepublic void onConfigureFailed(@NonNull CameraCaptureSession session) {Log.e(TAG, "ConfigureFailed. session: mCaptureSession");}}, mBackgroundHandler); // handle 传入 null 表示使用当前线程的 Looper} catch (CameraAccessException e) {e.printStackTrace();}}public void startPreview() {Log.v(TAG, "startPreview");if (mCaptureSession == null || mPreviewRequestBuilder == null) {Log.w(TAG, "startPreview: mCaptureSession or mPreviewRequestBuilder is null");return;}try {// 开始预览,即一直发送预览的请求mCaptureSession.setRepeatingRequest(mPreviewRequest, null, mBackgroundHandler);} catch (CameraAccessException e) {e.printStackTrace();}}public void stopPreview() {Log.v(TAG, "stopPreview");if (mCaptureSession == null || mPreviewRequestBuilder == null) {Log.w(TAG, "stopPreview: mCaptureSession or mPreviewRequestBuilder is null");return;}try {mCaptureSession.stopRepeating();} catch (CameraAccessException e) {e.printStackTrace();}}public void captureStillPicture() {try {CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);captureBuilder.addTarget(mImageReader.getSurface());captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(mDeviceOrientation));// 预览如果有放大,拍照的时候也应该保存相同的缩放Rect zoomRect = mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION);if (zoomRect != null) {captureBuilder.set(CaptureRequest.SCALER_CROP_REGION, zoomRect);}mCaptureSession.stopRepeating();mCaptureSession.abortCaptures();final long time = System.currentTimeMillis();mCaptureSession.capture(captureBuilder.build(), new CameraCaptureSession.CaptureCallback() {@Overridepublic void onCaptureCompleted(@NonNull CameraCaptureSession session,@NonNull CaptureRequest request,@NonNull TotalCaptureResult result) {Log.w(TAG, "onCaptureCompleted, time: " + (System.currentTimeMillis() - time));try {mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);mCaptureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler);} catch (CameraAccessException e) {e.printStackTrace();}startPreview();}}, mBackgroundHandler);} catch (CameraAccessException e) {e.printStackTrace();}}private int getJpegOrientation(int deviceOrientation) {if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) return 0;int sensorOrientation = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);// Round device orientation to a multiple of 90deviceOrientation = (deviceOrientation + 45) / 90 * 90;// Reverse device orientation for front-facing camerasboolean facingFront = mCameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT;if (facingFront) deviceOrientation = -deviceOrientation;// Calculate desired JPEG orientation relative to camera orientation to make// the image upright relative to the device orientationint jpegOrientation = (sensorOrientation + deviceOrientation + 360) % 360;Log.d(TAG, "jpegOrientation: " + jpegOrientation);return jpegOrientation;}public boolean isFrontCamera() {return mCameraId == CameraCharacteristics.LENS_FACING_BACK;}public Size getPreviewSize() {return mPreviewSize;}public void switchCamera(int width, int height) {mCameraId ^= 1;Log.d(TAG, "switchCamera: mCameraId: " + mCameraId);releaseCamera();openCamera(width, height);}private Size chooseOptimalSize(Size[] sizes, int viewWidth, int viewHeight, Size pictureSize) {int totalRotation = getRotation();boolean swapRotation = totalRotation == 90 || totalRotation == 270;int width = swapRotation ? viewHeight : viewWidth;int height = swapRotation ? viewWidth : viewHeight;return getSuitableSize(sizes, width, height, pictureSize);}private int getRotation() {int displayRotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();switch (displayRotation) {case Surface.ROTATION_0:displayRotation = 90;break;case Surface.ROTATION_90:displayRotation = 0;break;case Surface.ROTATION_180:displayRotation = 270;break;case Surface.ROTATION_270:displayRotation = 180;break;}int sensorOrientation = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);mDisplayRotate = (displayRotation + sensorOrientation + 270) % 360;return mDisplayRotate;}private Size getSuitableSize(Size[] sizes, int width, int height, Size pictureSize) {int minDelta = Integer.MAX_VALUE; // 最小的差值,初始值应该设置大点保证之后的计算中会被重置int index = 0; // 最小的差值对应的索引坐标float aspectRatio = pictureSize.getHeight() * 1.0f / pictureSize.getWidth();Log.d(TAG, "getSuitableSize. aspectRatio: " + aspectRatio);for (int i = 0; i < sizes.length; i++) {Size size = sizes[i];// 先判断比例是否相等if (size.getWidth() * aspectRatio == size.getHeight()) {int delta = Math.abs(width - size.getWidth());if (delta == 0) {return size;}if (minDelta > delta) {minDelta = delta;index = i;}}}return sizes[index];}public void handleZoom(boolean isZoomIn) {if (mCameraDevice == null || mCameraCharacteristics == null || mPreviewRequestBuilder == null) {return;}// maxZoom 表示 active_rect 宽度除以 crop_rect 宽度的最大值float maxZoom = mCameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);Log.d(TAG, "handleZoom: maxZoom: " + maxZoom);int factor = 100; // 放大/缩小的一个因素,设置越大越平滑,相应放大的速度也越慢if (isZoomIn && mZoom < factor) {mZoom++;} else if (mZoom > 0) {mZoom--;}Log.d(TAG, "handleZoom: mZoom: " + mZoom);Rect rect = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);int minW = (int) ((rect.width() - rect.width() / maxZoom) / (2 * factor));int minH = (int) ((rect.height() - rect.height() / maxZoom) / (2 * factor));int cropW = minW * mZoom;int cropH = minH * mZoom;Log.d(TAG, "handleZoom: cropW: " + cropW + ", cropH: " + cropH);Rect zoomRect = new Rect(cropW, cropH, rect.width() - cropW, rect.height() - cropH);mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, zoomRect);mPreviewRequest = mPreviewRequestBuilder.build();startPreview(); // 需要重新 start preview 才能生效}public void focusOnPoint(double x, double y, int width, int height) {if (mCameraDevice == null || mPreviewRequestBuilder == null) {return;}// 1. 先取相对于view上面的坐标int previewWidth = mPreviewSize.getWidth();int previewHeight = mPreviewSize.getHeight();if (mDisplayRotate == 90 || mDisplayRotate == 270) {previewWidth = mPreviewSize.getHeight();previewHeight = mPreviewSize.getWidth();}// 2. 计算摄像头取出的图像相对于view放大了多少,以及有多少偏移double tmp;double imgScale;double verticalOffset = 0;double horizontalOffset = 0;if (previewHeight * width > previewWidth * height) {imgScale = width * 1.0 / previewWidth;verticalOffset = (previewHeight - height / imgScale) / 2;} else {imgScale = height * 1.0 / previewHeight;horizontalOffset = (previewWidth - width / imgScale) / 2;}// 3. 将点击的坐标转换为图像上的坐标x = x / imgScale + horizontalOffset;y = y / imgScale + verticalOffset;if (90 == mDisplayRotate) {tmp = x;x = y;y = mPreviewSize.getHeight() - tmp;} else if (270 == mDisplayRotate) {tmp = x;x = mPreviewSize.getWidth() - y;y = tmp;}// 4. 计算取到的图像相对于裁剪区域的缩放系数,以及位移Rect cropRegion = mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION);if (cropRegion == null) {Log.w(TAG, "can't get crop region");cropRegion = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);}int cropWidth = cropRegion.width();int cropHeight = cropRegion.height();if (mPreviewSize.getHeight() * cropWidth > mPreviewSize.getWidth() * cropHeight) {imgScale = cropHeight * 1.0 / mPreviewSize.getHeight();verticalOffset = 0;horizontalOffset = (cropWidth - imgScale * mPreviewSize.getWidth()) / 2;} else {imgScale = cropWidth * 1.0 / mPreviewSize.getWidth();horizontalOffset = 0;verticalOffset = (cropHeight - imgScale * mPreviewSize.getHeight()) / 2;}// 5. 将点击区域相对于图像的坐标,转化为相对于成像区域的坐标x = x * imgScale + horizontalOffset + cropRegion.left;y = y * imgScale + verticalOffset + cropRegion.top;double tapAreaRatio = 0.1;Rect rect = new Rect();rect.left = clamp((int) (x - tapAreaRatio / 2 * cropRegion.width()), 0, cropRegion.width());rect.right = clamp((int) (x + tapAreaRatio / 2 * cropRegion.width()), 0, cropRegion.width());rect.top = clamp((int) (y - tapAreaRatio / 2 * cropRegion.height()), 0, cropRegion.height());rect.bottom = clamp((int) (y + tapAreaRatio / 2 * cropRegion.height()), 0, cropRegion.height());// 6. 设置 AF、AE 的测光区域,即上述得到的 rectmPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{new MeteringRectangle(rect, 1000)});mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{new MeteringRectangle(rect, 1000)});mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START);mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_START);try {// 7. 发送上述设置的对焦请求,并监听回调mCaptureSession.capture(mPreviewRequestBuilder.build(), mAfCaptureCallback, mBackgroundHandler);} catch (CameraAccessException e) {e.printStackTrace();}}private final CameraCaptureSession.CaptureCallback mAfCaptureCallback = new CameraCaptureSession.CaptureCallback() {private void process(CaptureResult result) {Integer state = result.get(CaptureResult.CONTROL_AF_STATE);if (null == state) {return;}Log.d(TAG, "process: CONTROL_AF_STATE: " + state);if (state == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || state == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {Log.d(TAG, "process: start normal preview");mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.FLASH_MODE_OFF);startPreview();}}@Overridepublic void onCaptureProgressed(@NonNull CameraCaptureSession session,@NonNull CaptureRequest request,@NonNull CaptureResult partialResult) {process(partialResult);}@Overridepublic void onCaptureCompleted(@NonNull CameraCaptureSession session,@NonNull CaptureRequest request,@NonNull TotalCaptureResult result) {process(result);}};private void startBackgroundThread() {if (mBackgroundThread == null || mBackgroundHandler == null) {Log.v(TAG, "startBackgroundThread");mBackgroundThread = new HandlerThread("CameraBackground");mBackgroundThread.start();mBackgroundHandler = new Handler(mBackgroundThread.getLooper());}}private void stopBackgroundThread() {Log.v(TAG, "stopBackgroundThread");if (mBackgroundThread != null) {mBackgroundThread.quitSafely();try {mBackgroundThread.join();mBackgroundThread = null;mBackgroundHandler = null;} catch (InterruptedException e) {e.printStackTrace();}}}private int clamp(int x, int min, int max) {if (x > max) return max;if (x < min) return min;return x;}/*** Compares two {@code Size}s based on their areas.*/static class CompareSizesByArea implements Comparator<Size> {@Overridepublic int compare(Size lhs, Size rhs) {// We cast here to ensure the multiplications won't overflowreturn Long.signum((long) lhs.getWidth() * lhs.getHeight() -(long) rhs.getWidth() * rhs.getHeight());}}}

三、Camera2SurfaceView

通过上面的介绍,对于相机的操作应该有了一定的了解了,接下来完成 View 这部分。

需求分析:

  1. Camera2SurfaceView 是要继承 SurfaceView 的。
  2. 我们需要重写 onMeasure 使得 Camera2SurfaceView 的宽高可以和相机预览尺寸相匹配,这样就不会有画面被拉伸的感觉了。
  3. 我们需要在 Camera2SurfaceView 中完成对相机的打开、关闭等操作,值得庆幸的是我们可以通过上面的 Camera2Proxy 很容易的做到。
  4. 我们需要重写 onTouchEvent 方法,来实现单点聚焦,双指放大缩小的功能。

实现:

主要是在 SurfaceHolder.Callback 的几个回调方法中打开和释放相机,另外就是重写 onMeasureonTouchEvent 那几个方法。

package com.afei.camerademo.surfaceview;import android.app.Activity;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;import com.afei.camerademo.camera.Camera2Proxy;public class Camera2SurfaceView extends SurfaceView {private static final String TAG = "Camera2SurfaceView";private Camera2Proxy mCameraProxy;private int mRatioWidth = 0;private int mRatioHeight = 0;private float mOldDistance;public Camera2SurfaceView(Context context) {this(context, null);}public Camera2SurfaceView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public Camera2SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {this(context, attrs, defStyleAttr, 0);}public Camera2SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);init(context);}private void init(Context context) {getHolder().addCallback(mSurfaceHolderCallback);mCameraProxy = new Camera2Proxy((Activity) context);}private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {@Overridepublic void surfaceCreated(SurfaceHolder holder) {mCameraProxy.setPreviewSurface(holder);mCameraProxy.openCamera(getWidth(), getHeight());}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {Log.d(TAG, "surfaceChanged: width: " + width + ", height: " + height);int previewWidth = mCameraProxy.getPreviewSize().getWidth();int previewHeight = mCameraProxy.getPreviewSize().getHeight();if (width > height) {setAspectRatio(previewWidth, previewHeight);} else {setAspectRatio(previewHeight, previewWidth);}}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {mCameraProxy.releaseCamera();}};public void setAspectRatio(int width, int height) {if (width < 0 || height < 0) {throw new IllegalArgumentException("Size cannot be negative.");}mRatioWidth = width;mRatioHeight = height;requestLayout();}public Camera2Proxy getCameraProxy() {return mCameraProxy;}@Overrideprotected 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);}}}@Overridepublic boolean onTouchEvent(MotionEvent event) {if (event.getPointerCount() == 1) {mCameraProxy.focusOnPoint(event.getX(), event.getY(), getWidth(), getHeight());return true;}switch (event.getAction() & MotionEvent.ACTION_MASK) {case MotionEvent.ACTION_POINTER_DOWN:mOldDistance = getFingerSpacing(event);break;case MotionEvent.ACTION_MOVE:float newDistance = getFingerSpacing(event);if (newDistance > mOldDistance) {mCameraProxy.handleZoom(true);} else if (newDistance < mOldDistance) {mCameraProxy.handleZoom(false);}mOldDistance = newDistance;break;default:break;}return super.onTouchEvent(event);}private static float getFingerSpacing(MotionEvent event) {float x = event.getX(0) - event.getX(1);float y = event.getY(0) - event.getY(1);return (float) Math.sqrt(x * x + y * y);}}

四、SurfaceCamera2Activity

接下来,我们把写好的 Camera2SurfaceView 放在 Activity 或者 Fragment 中使用就行了。

注意相机使用前,需要申请相关权限,以及权限的动态申请。

1. AndroidManifest.xml

相机相关权限如下,动态权限的申请代码很多,这里不详细介绍了,不清楚的可以看这篇博客:Android动态权限申请

    <uses-permission android:name="android.permission.CAMERA"/><uses-feature android:name="android.hardware.camera"/><uses-feature android:name="android.hardware.camera.autofocus"/>

2. 拍照功能

需要注意的是,前置摄像头是存在左右镜像的,因此针对前置摄像头我们需要手机进行一个左右镜像的操作。

下面是完整的 SurfaceCamera2Activity 代码:

package com.afei.camerademo.surfaceview;import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.Image;
import android.media.ImageReader;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;import com.afei.camerademo.ImageUtils;
import com.afei.camerademo.R;
import com.afei.camerademo.camera.Camera2Proxy;import java.nio.ByteBuffer;public class SurfaceCamera2Activity extends AppCompatActivity implements View.OnClickListener {private static final String TAG = "SurfaceCamera2Activity";private ImageView mCloseIv;private ImageView mSwitchCameraIv;private ImageView mTakePictureIv;private ImageView mPictureIv;private Camera2SurfaceView mCameraView;private Camera2Proxy mCameraProxy;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_surface_camera2);initView();}private void initView() {mCloseIv = findViewById(R.id.toolbar_close_iv);mCloseIv.setOnClickListener(this);mSwitchCameraIv = findViewById(R.id.toolbar_switch_iv);mSwitchCameraIv.setOnClickListener(this);mTakePictureIv = findViewById(R.id.take_picture_iv);mTakePictureIv.setOnClickListener(this);mPictureIv = findViewById(R.id.picture_iv);mPictureIv.setOnClickListener(this);mPictureIv.setImageBitmap(ImageUtils.getLatestThumbBitmap());mCameraView = findViewById(R.id.camera_view);mCameraProxy = mCameraView.getCameraProxy();}@Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.toolbar_close_iv:finish();break;case R.id.toolbar_switch_iv:mCameraProxy.switchCamera(mCameraView.getWidth(), mCameraView.getHeight());mCameraProxy.startPreview();break;case R.id.take_picture_iv:mCameraProxy.setImageAvailableListener(mOnImageAvailableListener);mCameraProxy.captureStillPicture(); // 拍照break;case R.id.picture_iv:Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);startActivity(intent);break;}}private ImageReader.OnImageAvailableListener mOnImageAvailableListener =new ImageReader.OnImageAvailableListener() {@Overridepublic void onImageAvailable(ImageReader reader) {new ImageSaveTask().execute(reader.acquireNextImage()); // 保存图片}};private class ImageSaveTask extends AsyncTask<Image, Void, Void> {@Overrideprotected Void doInBackground(Image ... images) {ByteBuffer buffer = images[0].getPlanes()[0].getBuffer();byte[] bytes = new byte[buffer.remaining()];buffer.get(bytes);long time = System.currentTimeMillis();if (mCameraProxy.isFrontCamera()) {Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);Log.d(TAG, "BitmapFactory.decodeByteArray time: " + (System.currentTimeMillis() - time));time = System.currentTimeMillis();// 前置摄像头需要左右镜像Bitmap rotateBitmap = ImageUtils.rotateBitmap(bitmap, 0, true, true);Log.d(TAG, "rotateBitmap time: " + (System.currentTimeMillis() - time));time = System.currentTimeMillis();ImageUtils.saveBitmap(rotateBitmap);Log.d(TAG, "saveBitmap time: " + (System.currentTimeMillis() - time));rotateBitmap.recycle();} else {ImageUtils.saveImage(bytes);Log.d(TAG, "saveBitmap time: " + (System.currentTimeMillis() - time));}images[0].close();return null;}@Overrideprotected void onPostExecute(Void aVoid) {mPictureIv.setImageBitmap(ImageUtils.getLatestThumbBitmap());}}
}

附上 ImageUtils 代码:

package com.afei.camerademo;import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;public class ImageUtils {private static final String TAG = "ImageUtils";private static final String GALLERY_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + File.separator + "Camera";private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss");public static Bitmap rotateBitmap(Bitmap source, int degree, boolean flipHorizontal, boolean recycle) {if (degree == 0) {return source;}Matrix matrix = new Matrix();matrix.postRotate(degree);if (flipHorizontal) {matrix.postScale(-1, 1); // 前置摄像头存在水平镜像的问题,所以有需要的话调用这个方法进行水平镜像}Bitmap rotateBitmap = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);if (recycle) {source.recycle();}return rotateBitmap;}public static void saveBitmap(Bitmap bitmap) {String fileName = DATE_FORMAT.format(new Date(System.currentTimeMillis())) + ".jpg";File outFile = new File(GALLERY_PATH, fileName);Log.d(TAG, "saveImage. filepath: " + outFile.getAbsolutePath());FileOutputStream os = null;try {os = new FileOutputStream(outFile);boolean success = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);if (success) {insertToDB(outFile.getAbsolutePath());}} catch (IOException e) {e.printStackTrace();} finally {if (os != null) {try {os.close();} catch (IOException e) {e.printStackTrace();}}}}public static void insertToDB(String picturePath) {ContentValues values = new ContentValues();ContentResolver resolver = MyApp.getInstance().getContentResolver();values.put(MediaStore.Images.ImageColumns.DATA, picturePath);values.put(MediaStore.Images.ImageColumns.TITLE, picturePath.substring(picturePath.lastIndexOf("/") + 1));values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, System.currentTimeMillis());values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/jpeg");resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);}}

五、项目地址

部分没有贴出来的代码,可在下面地址中找到。

地址:

https://github.com/afei-cn/CameraDemo/tree/master/app/src/main/java/com/afei/camerademo/surfaceview

其它:

自定义Camera系列之:SurfaceView + Camera

自定义Camera系列之:TextureView + Camera

自定义Camera系列之:GLSurfaceViewView + Camera

自定义Camera系列之:TextureView + Camera2

自定义Camera系列之:GLSurfaceView + Camera2

自定义Camera系列之:SurfaceView + Camera2相关推荐

  1. 自定义Camera系列之:TextureView + Camera2

    一.前言 之前已经介绍过过时的旧 Camera 的使用了,毕竟在从 Android 5.0 后推荐使用 Camera2 了,所以现在开始介绍 Camera2 相关使用.该篇介绍 TextureView ...

  2. 自定义Camera系列之:SurfaceView + Camera

    一.前言 之前一直想把 Camera 系列的写一下,拖了很久,现在慢慢填坑吧. 首先介绍 SurfaceView + Camera 的组合.虽然从 Android 5.0 后推荐使用 Camera2 ...

  3. 自定义Camera系列之:GLSurfaceView + Camera2

    一.前言 假如你要使用 OpenGL ES 来渲染相机的话,使用 GLSurfaceView 将是一个很常用的选择. 这里介绍 GLSurfaceView + Camera2 的组合. 如果你对 Ca ...

  4. 自定义Camera系列之:TextureView + Camera

    一.前言 上一篇介绍了 自定义Camera系列之:SurfaceView + Camera,接着我们介绍使用 TextureView + Camera 的组合. 为什么选择 TextureView ? ...

  5. 自定义Camera系列之:GLSurfaceView + Camera

    一.前言 假如你要使用 OpenGL ES 来渲染相机的话,使用 GLSurfaceView 将是一个很常用的选择. 这里介绍 GLSurfaceView + Camera 的组合.虽然从 Andro ...

  6. Android Camera开发系列(下)——自定义Camera实现拍照查看图片等功能

    Android Camera开发系列(下)--自定义Camera实现拍照查看图片等功能 Android Camera开发系列(上)--Camera的基本调用与实现拍照功能以及获取拍照图片加载大图片 上 ...

  7. Android自定义camera相机 系列(一)

    该文章 主要使用 自定义 surfaceview 及 camera 知识点,来实现一个自定义的拍照 .切换闪光灯 和 前后摄像头的功能.阅读需要消耗时间 :15分钟+ .内容比较简单算是 开发相机的过 ...

  8. Android仿IOS滑动关机-自定义view系列(6)

    Android仿IOS滑动关机-自定义view系列 功能简介 GIf演示 主要实现步骤-具体内容看github项目里的代码 Android技术生活交流 更多其他页面-自定义View-实用功能合集:点击 ...

  9. 自定义Camera实现头像框效果,并裁剪指定区域合成

    需要一个带框的相机,并且拍好后能合成框和人脸,不过需要人自己凑过去哈哈哈 这两天看了很多博客,然后自己根据自己的要求改了改,基本可以用,调节参数可以获得想要的效果 参考链接在最后面,要是看不懂我的,可 ...

最新文章

  1. ehcache导致Tomcat重启出错
  2. 35岁前务必成功的12级跳(男女通用) 转
  3. 事件源event.target
  4. bzoj4383(拓扑排序)
  5. alm系统的使用流程_840D sl系统授权管理
  6. docker 部署springboot容器日志处理
  7. php的文件包含总结 include require include_once require_once
  8. 求python一个类与对象的代码_Python基础系列(五)类和对象,让你更懂你的python代码...
  9. Microsoft .NET Framework 2.0对文件传输协议(FTP)操作(上传,下载,新建,删除,FTP间传送文件等)实现汇总1...
  10. 怎么使用Vegas制作炫彩灯光效果?
  11. MAC完全卸载/删除Parallels Desktop虚拟机和PD虚拟机文件的方法
  12. 暴风影音xp版本_暴风影音黯然退市!怀念那些年用过的播放器
  13. Python爬虫基本代码附解析
  14. Packet Tracer 思科模拟器入门教程 实验报告1
  15. 2015-2016 Petrozavodsk Winter Training Camp, Moscow SU Trinity Contest
  16. 解决电脑某个盘可用容量小于该盘总容量减去盘内所有文件大小总和
  17. 从简历被拒,到斩获 BAT offer,全靠这些吊炸天的公众号!
  18. Bitstream Vera Sans Mono 编程字体安装
  19. Xilinx Zynq mpsoc 的 pcie Tandem 配置
  20. 8.3 有效工作量证明

热门文章

  1. ROLAP,MOLAP和HOLAP之间的区别
  2. allenNLP入门记录
  3. A2开发版简介 ----学习笔记
  4. 在Win7下通过SecureCRT 远程配置DynamipsGUI中的路由器
  5. 强制删除文件 lockdir
  6. Function究竟是什么?
  7. windows server 2008 英文版安装中文vs2008 sp1补丁失败的解决办法
  8. 【Docker】从 Docker 镜像中下载内容到本地
  9. 神器!五分钟完成大型爬虫项目!
  10. 【Windows】一键自动设置IP及DNS的批处理脚本