Android使用 Camera API + SurfaceView 方式进行预览拍照。
SurfaceView 的创建以及回调
创建一个SurfaceView,并实现SurfaceHolder的回调。
由于Camera在SurfaceView中是通过SurfaceHolder 使得Surfaceview能够预览Camera返回的数据,因此我们需要实现SurfaceHolder 的回调,实现代码如下:
public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback { private static final String TAG = CameraSurfaceView.class.getSimpleName(); private SurfaceHolder mSurfaceHolder; public CameraSurfaceView(Context context) { super(context); init(); } public CameraSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public CameraSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this); } @Override public void surfaceCreated(SurfaceHolder holder) { CameraUtils.openFrontalCamera(CameraUtils.DESIRED_PREVIEW_FPS); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { CameraUtils.startPreviewDisplay(holder); } @Override public void surfaceDestroyed(SurfaceHolder holder) { CameraUtils.releaseCamera(); }}
Camera 操作的辅助类
CameraUtils 辅助类主要是Camera API 的一些操作,比如打开相机、开始预览、停止预览、切换相机、设置预览参数等操作,具体实现如下:
public class CameraUtils { // 相机默认宽高,相机的宽度和高度跟屏幕坐标不一样,手机屏幕的宽度和高度是反过来的。 public static final int DEFAULT_WIDTH = 1280; public static final int DEFAULT_HEIGHT = 720; public static final int DESIRED_PREVIEW_FPS = 30; private static int mCameraID = Camera.CameraInfo.CAMERA_FACING_FRONT; private static Camera mCamera; private static int mCameraPreviewFps; private static int mOrientation = 0; /** * 打开相机,默认打开前置相机 * @param expectFps */ public static void openFrontalCamera(int expectFps) { if (mCamera != null) { throw new RuntimeException("camera already initialized!"); } Camera.CameraInfo info = new Camera.CameraInfo(); int numCameras = Camera.getNumberOfCameras(); for (int i = 0; i < numCameras; i++) { Camera.getCameraInfo(i, info); if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { mCamera = Camera.open(i); mCameraID = info.facing; break; } } // 如果没有前置摄像头,则打开默认的后置摄像头 if (mCamera == null) { mCamera = Camera.open(); mCameraID = Camera.CameraInfo.CAMERA_FACING_BACK; } // 没有摄像头时,抛出异常 if (mCamera == null) { throw new RuntimeException("Unable to open camera"); } Camera.Parameters parameters = mCamera.getParameters(); mCameraPreviewFps = CameraUtils.chooseFixedPreviewFps(parameters, expectFps * 1000); parameters.setRecordingHint(true); mCamera.setParameters(parameters); setPreviewSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT); setPictureSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT); mCamera.setDisplayOrientation(mOrientation); } /** * 根据ID打开相机 * @param cameraID * @param expectFps */ public static void openCamera(int cameraID, int expectFps) { if (mCamera != null) { throw new RuntimeException("camera already initialized!"); } mCamera = Camera.open(cameraID); if (mCamera == null) { throw new RuntimeException("Unable to open camera"); } mCameraID = cameraID; Camera.Parameters parameters = mCamera.getParameters(); mCameraPreviewFps = CameraUtils.chooseFixedPreviewFps(parameters, expectFps * 1000); parameters.setRecordingHint(true); mCamera.setParameters(parameters); setPreviewSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT); setPictureSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT); mCamera.setDisplayOrientation(mOrientation); } /** * 开始预览 * @param holder */ public static void startPreviewDisplay(SurfaceHolder holder) { if (mCamera == null) { throw new IllegalStateException("Camera must be set when start preview"); } try { mCamera.setPreviewDisplay(holder); mCamera.startPreview(); } catch (IOException e) { e.printStackTrace(); } } /** * 切换相机 * @param cameraID */ public static void switchCamera(int cameraID, SurfaceHolder holder) { if (mCameraID == cameraID) { return; } mCameraID = cameraID; // 释放原来的相机 releaseCamera(); // 打开相机 openCamera(cameraID, CameraUtils.DESIRED_PREVIEW_FPS); // 打开预览 startPreviewDisplay(holder); } /** * 释放相机 */ public static void releaseCamera() { if (mCamera != null) { mCamera.stopPreview(); mCamera.release(); mCamera = null; } } /** * 开始预览 */ public static void startPreview() { if (mCamera != null) { mCamera.startPreview(); } } /** * 停止预览 */ public static void stopPreview() { if (mCamera != null) { mCamera.stopPreview(); } } /** * 拍照 */ public static void takePicture(Camera.ShutterCallback shutterCallback, Camera.PictureCallback rawCallback, Camera.PictureCallback pictureCallback) { if (mCamera != null) { mCamera.takePicture(shutterCallback, rawCallback, pictureCallback); } } /** * 设置预览大小 * @param camera * @param expectWidth * @param expectHeight */ public static void setPreviewSize(Camera camera, int expectWidth, int expectHeight) { Camera.Parameters parameters = camera.getParameters(); Camera.Size size = calculatePerfectSize(parameters.getSupportedPreviewSizes(), expectWidth, expectHeight); parameters.setPreviewSize(size.width, size.height); camera.setParameters(parameters); } /** * 获取预览大小 * @return */ public static Camera.Size getPreviewSize() { if (mCamera != null) { return mCamera.getParameters().getPreviewSize(); } return null; } /** * 设置拍摄的照片大小 * @param camera * @param expectWidth * @param expectHeight */ public static void setPictureSize(Camera camera, int expectWidth, int expectHeight) { Camera.Parameters parameters = camera.getParameters(); Camera.Size size = calculatePerfectSize(parameters.getSupportedPictureSizes(), expectWidth, expectHeight); parameters.setPictureSize(size.width, size.height); camera.setParameters(parameters); } /** * 获取照片大小 * @return */ public static Camera.Size getPictureSize() { if (mCamera != null) { return mCamera.getParameters().getPictureSize(); } return null; } /** * 计算最完美的Size * @param sizes * @param expectWidth * @param expectHeight * @return */ public static Camera.Size calculatePerfectSize(List<Camera.Size> sizes, int expectWidth, int expectHeight) { sortList(sizes); // 根据宽度进行排序 Camera.Size result = sizes.get(0); boolean widthOrHeight = false; // 判断存在宽或高相等的Size // 辗转计算宽高最接近的值 for (Camera.Size size: sizes) { // 如果宽高相等,则直接返回 if (size.width == expectWidth && size.height == expectHeight) { result = size; break; } // 仅仅是宽度相等,计算高度最接近的size if (size.width == expectWidth) { widthOrHeight = true; if (Math.abs(result.height - expectHeight) > Math.abs(size.height - expectHeight)) { result = size; } } // 高度相等,则计算宽度最接近的Size else if (size.height == expectHeight) { widthOrHeight = true; if (Math.abs(result.width - expectWidth) > Math.abs(size.width - expectWidth)) { result = size; } } // 如果之前的查找不存在宽或高相等的情况,则计算宽度和高度都最接近的期望值的Size else if (!widthOrHeight) { if (Math.abs(result.width - expectWidth) > Math.abs(size.width - expectWidth) && Math.abs(result.height - expectHeight) > Math.abs(size.height - expectHeight)) { result = size; } } } return result; } /** * 排序 * @param list */ private static void sortList(List<Camera.Size> list) { Collections.sort(list, new Comparator<Camera.Size>() { @Override public int compare(Camera.Size pre, Camera.Size after) { if (pre.width > after.width) { return 1; } else if (pre.width < after.width) { return -1; } return 0; } }); } /** * 选择合适的FPS * @param parameters * @param expectedThoudandFps 期望的FPS * @return */ public static int chooseFixedPreviewFps(Camera.Parameters parameters, int expectedThoudandFps) { List<int[]> supportedFps = parameters.getSupportedPreviewFpsRange(); for (int[] entry : supportedFps) { if (entry[0] == entry[1] && entry[0] == expectedThoudandFps) { parameters.setPreviewFpsRange(entry[0], entry[1]); return entry[0]; } } int[] temp = new int[2]; int guess; parameters.getPreviewFpsRange(temp); if (temp[0] == temp[1]) { guess = temp[0]; } else { guess = temp[1] / 2; } return guess; } /** * 设置预览角度,setDisplayOrientation本身只能改变预览的角度 * previewFrameCallback以及拍摄出来的照片是不会发生改变的,拍摄出来的照片角度依旧不正常的 * 拍摄的照片需要自行处理 * 这里Nexus5X的相机简直没法吐槽,后置摄像头倒置了,切换摄像头之后就出现问题了。 * @param activity */ public static int calculateCameraPreviewOrientation(Activity activity) { Camera.CameraInfo info = new Camera.CameraInfo(); Camera.getCameraInfo(mCameraID, info); int rotation = activity.getWindowManager().getDefaultDisplay() .getRotation(); int degrees = 0; switch (rotation) { case Surface.ROTATION_0: degrees = 0; break; case Surface.ROTATION_90: degrees = 90; break; case Surface.ROTATION_180: degrees = 180; break; case Surface.ROTATION_270: degrees = 270; break; } int result; if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { result = (info.orientation + degrees) % 360; result = (360 - result) % 360; } else { result = (info.orientation - degrees + 360) % 360; } mOrientation = result; return result; } /** * 获取当前的Camera ID * @return */ public static int getCameraID() { return mCameraID; } /** * 获取当前预览的角度 * @return */ public static int getPreviewOrientation() { return mOrientation; } /** * 获取FPS(千秒值) * @return */ public static int getCameraPreviewThousandFps() { return mCameraPreviewFps; }}
权限申请
在Activity中使用CameraSurfaceview,有Android6.0动态权限申请问题,需要我们判断相机和存储权限是否申请了:
public class CameraSurfaceViewActivity extends AppCompatActivity implements View.OnClickListener { private static final int REQUEST_CAMERA = 0x01; private CameraSurfaceView mCameraSurfaceView; private Button mBtnTake; private Button mBtnSwitch; private int mOrientation; // CameraSurfaceView 容器包装类 private FrameLayout mAspectLayout; private boolean mCameraRequested; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.activity_camera_surface); // Android 6.0相机动态权限检查 if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { initView(); } else { ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE }, REQUEST_CAMERA); } } /** * 初始化View */ private void initView() { mAspectLayout = (FrameLayout) findViewById(R.id.layout_aspect);; mCameraSurfaceView = new CameraSurfaceView(this); mAspectLayout.addView(mCameraSurfaceView); mOrientation = CameraUtils.calculateCameraPreviewOrientation(CameraSurfaceViewActivity.this); mBtnTake = (Button) findViewById(R.id.btn_take); mBtnTake.setOnClickListener(this); mBtnSwitch = (Button) findViewById(R.id.btn_switch); mBtnSwitch.setOnClickListener(this); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) { // 相机权限 case REQUEST_CAMERA: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { mCameraRequested = true; initView(); } break; } } @Override protected void onResume() { super.onResume(); if (mCameraRequested) { CameraUtils.startPreview(); } } @Override protected void onPause() { super.onPause(); CameraUtils.stopPreview(); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_take: takePicture(); break; case R.id.btn_switch: switchCamera(); break; } } /** * 拍照 */ private void takePicture() { CameraUtils.takePicture(new Camera.ShutterCallback() { @Override public void onShutter() { } }, null, new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { CameraUtils.startPreview(); Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); if (bitmap != null) { bitmap = ImageUtils.getRotatedBitmap(bitmap, mOrientation); String path = Environment.getExternalStorageDirectory() + "/DCIM/Camera/" + System.currentTimeMillis() + ".jpg"; try { FileOutputStream fout = new FileOutputStream(path); BufferedOutputStream bos = new BufferedOutputStream(fout); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos); bos.flush(); bos.close(); fout.close(); } catch (IOException e) { e.printStackTrace(); } } CameraUtils.startPreview(); } }); } /** * 切换相机 */ private void switchCamera() { if (mCameraSurfaceView != null) { CameraUtils.switchCamera(1 - CameraUtils.getCameraID(), mCameraSurfaceView.getHolder()); // 切换相机后需要重新计算旋转角度 mOrientation = CameraUtils.calculateCameraPreviewOrientation(CameraSurfaceViewActivity.this); } }}
由于用到了相机和存储权限,我们需要在manifest中注册相机和存储权限,这里要说明的是,manifest用use-permission只是声明了需要使用哪些权限,而我们实际项目中在使用到这两项权限时,需要你检查权限是否已经被授权,如果没授权,则需要请求授权:
<!-- 存储权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 相机权限 --> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.FLASHLIGHT" />
图像处理的辅助类
另外,ImageUtils类的实现如下:
public class ImageUtils { /** * 旋转图片 * @param bitmap * @param rotation * @Return */ public static Bitmap getRotatedBitmap(Bitmap bitmap, int rotation) { Matrix matrix = new Matrix(); matrix.postRotate(rotation); return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false); } /** * 镜像翻转图片 * @param bitmap * @Return */ public static Bitmap getFlipBitmap(Bitmap bitmap) { Matrix matrix = new Matrix(); matrix.setScale(-1, 1); matrix.postTranslate(bitmap.getWidth(), 0); return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false); }}
layout如下:
<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.cgfay.camerasample.CameraSurfaceViewActivity"> <FrameLayout android:id="@+id/layout_aspect" android:layout_width="match_parent" android:layout_height="wrap_content"> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="bottom" android:gravity="center"> <Button android:id="@+id/btn_take" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="拍照" /> <Button android:id="@+id/btn_switch" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="切换相机" /> </LinearLayout></FrameLayout>
至此,通过SurfaceView + Camera API 预览拍照功能已经实现。
备注
Camera API 在打开相机是在哪个线程,那么onPreviewFrame回调执行就在哪个线程。
因此,如果要通过onPreviewFrame回调使用预览数据,则可以通过HandlerThread 异步调用Camera进行操作。
另外一个问题,onPreviewFrame方法中不要执行过于复杂的逻辑操作,这样会阻塞Camera,无法获取新的Frame,导致帧率下降。
网友评论