圆形Camera预览实现
-
需求
最近有个需求要求界面上使用圆形相机预览进行面部检测 , 具体需求如下图
关于Camera之前接触得比较多 , 主要就是通过SurfaceView显示预览视图 , 因此需要展示圆形预览界面, 只需要控制SurfaceView的显示范围就可以了.
-
实现
由于较为简单 , 下面我们直接给出实现代码:
import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.hardware.Camera; import android.util.DisplayMetrics; import android.util.Log; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import java.io.IOException; import java.util.List; public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { private static final String TAG = "CameraPreview"; private Camera mCamera; private SurfaceHolder mHolder; private Activity mContext; private CameraListener listener; private int cameraId = Camera.CameraInfo.CAMERA_FACING_FRONT; private int displayDegree = 90; public CameraPreview(Activity context) { super(context); mContext = context; mCamera = Camera.open(cameraId); mHolder = getHolder(); mHolder.addCallback(this); mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); } public void setCameraListener(CameraListener listener) { this.listener = listener; } /** * 拍照获取bitmap */ public void captureImage() { try { mCamera.takePicture(null, null, new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { if (null != listener) { Bitmap bitmap = rotateBitmap(BitmapFactory.decodeByteArray(data, 0, data.length), displayDegree); listener.onCaptured(bitmap); } } }); } catch (Exception e) { e.printStackTrace(); if (null != listener) { listener.onCaptured(null); } } } /** * 预览拍照 */ public void startPreview() { mCamera.startPreview(); } @Override public boolean onTouchEvent(MotionEvent event) { if (null != mCamera) { mCamera.autoFocus(null); } return super.onTouchEvent(event); } @Override public void surfaceCreated(SurfaceHolder holder) { try { startCamera(holder); } catch (IOException e) { e.printStackTrace(); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (mHolder.getSurface() == null) { return; } try { mCamera.stopPreview(); } catch (Exception e) { e.printStackTrace(); } try { startCamera(mHolder); } catch (Exception e) { Log.e(TAG, e.toString()); } } private void startCamera(SurfaceHolder holder) throws IOException { mCamera.setPreviewDisplay(holder); setCameraDisplayOrientation(mContext, cameraId, mCamera); Camera.Size preSize = getCameraSize(); Camera.Parameters parameters = mCamera.getParameters(); parameters.setPreviewSize(preSize.width, preSize.height); parameters.setPictureSize(preSize.width, preSize.height); parameters.setJpegQuality(100); mCamera.setParameters(parameters); mCamera.startPreview(); } public Camera.Size getCameraSize() { if (null != mCamera) { Camera.Parameters parameters = mCamera.getParameters(); DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); Camera.Size preSize = Util.getCloselyPreSize(true, metrics.widthPixels, metrics.heightPixels, parameters.getSupportedPreviewSizes()); return preSize; } return null; } @Override public void surfaceDestroyed(SurfaceHolder holder) { releaseCamera(); } /** * Android API: Display Orientation Setting * Just change screen display orientation, * the rawFrame data never be changed. */ private void setCameraDisplayOrientation(Activity activity, int cameraId, Camera camera) { Camera.CameraInfo info = new Camera.CameraInfo(); Camera.getCameraInfo(cameraId, 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; } if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { displayDegree = (info.orientation + degrees) % 360; displayDegree = (360 - displayDegree) % 360; // compensate the mirror } else { displayDegree = (info.orientation - degrees + 360) % 360; } camera.setDisplayOrientation(displayDegree); } /** * 将图片按照某个角度进行旋转 * * @param bm 需要旋转的图片 * @param degree 旋转角度 * @return 旋转后的图片 */ private Bitmap rotateBitmap(Bitmap bm, int degree) { Bitmap returnBm = null; // 根据旋转角度,生成旋转矩阵 Matrix matrix = new Matrix(); matrix.postRotate(degree); try { // 将原始图片按照旋转矩阵进行旋转,并得到新的图片 returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true); } catch (OutOfMemoryError e) { e.printStackTrace(); } if (returnBm == null) { returnBm = bm; } if (bm != returnBm) { bm.recycle(); } return returnBm; } /** * 释放资源 */ public synchronized void releaseCamera() { try { if (null != mCamera) { mCamera.setPreviewCallback(null); mCamera.stopPreview();//停止预览 mCamera.release(); // 释放相机资源 mCamera = null; } if (null != mHolder) { mHolder.removeCallback(this); mHolder = null; } } catch (Exception e) { e.printStackTrace(); } } }
封装了一个CameraPreview ,与相机预览相关的逻辑全部放在里面了 , 同时对外暴露了一个CameraListener 可以提供拍照、预览等回调(取决于自己定义)
接下来就是控制CameraPreview的显示了, 用一个RelativeLayout包裹起来, 并且切割成圆形
import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Outline; import android.graphics.Rect; import android.hardware.Camera; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.support.annotation.RequiresApi; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.widget.FrameLayout; import android.widget.RelativeLayout; import com.dong.circlecamera.R; import java.util.Timer; import java.util.TimerTask; public class CircleCameraLayout extends RelativeLayout { public CircleCameraLayout(Context context) { super(context); init(context, null, -1, -1); } public CircleCameraLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, -1, -1); } public CircleCameraLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr, -1); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public CircleCameraLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs, defStyleAttr, defStyleRes); } private Timer timer; private TimerTask pressTask; private Context mContext; private int circleWidth = 0;//指定半径 private int borderWidth = 0;//指定边框 private CameraPreview cameraPreview;//摄像预览 private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { mContext = context; timer = new Timer(); if (attrs != null && defStyleAttr == -1 && defStyleRes == -1) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleCameraLayout, defStyleAttr, defStyleRes); circleWidth = (int) typedArray.getDimension(R.styleable.CircleCameraLayout_circle_camera_width, ViewGroup.LayoutParams.WRAP_CONTENT); borderWidth = (int) typedArray.getDimension(R.styleable.CircleCameraLayout_border_width, 5); typedArray.recycle(); } startView(); } /** * 设置照相预览 * * @param cameraPreview */ public void setCameraPreview(CameraPreview cameraPreview) { this.cameraPreview = cameraPreview; } /** * 释放回收 */ public void release() { if (null != pressTask) { pressTask.cancel(); pressTask = null; } if (null != timer) { timer.cancel(); timer = null; } } //延时启动摄像头 public void startView() { pressTask = new TimerTask() { @Override public void run() { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { pressTask.cancel(); pressTask = null; if (null != cameraPreview) { show(); } else { startView(); } } }); } }; timer.schedule(pressTask, 50); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void show() { //cmaera根view--layout RelativeLayout cameraRoot = new RelativeLayout(mContext); RelativeLayout.LayoutParams rootParams = new RelativeLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); rootParams.addRule(CENTER_IN_PARENT, TRUE); cameraRoot.setBackgroundColor(Color.TRANSPARENT); cameraRoot.setClipChildren(false); //camera--layout FrameLayout cameraLayout = new FrameLayout(mContext); Camera.Size preSize = cameraPreview.getCameraSize(); int cameraHeight = (int) ((float) preSize.width / (float) preSize.height * circleWidth); RelativeLayout.LayoutParams cameraParams = new RelativeLayout.LayoutParams(circleWidth, cameraHeight); cameraParams.addRule(CENTER_IN_PARENT, TRUE); cameraLayout.setLayoutParams(cameraParams); cameraLayout.addView(cameraPreview); cameraLayout.setOutlineProvider(viewOutlineProvider);//把自定义的轮廓提供者设置给imageView cameraLayout.setClipToOutline(true);//开启裁剪 //circleView--layout // CircleView circleView = new CircleView(mContext); CircleView2 circleView = new CircleView2(mContext); circleView.setBorderWidth(circleWidth, borderWidth); //设置margin值---隐藏超出部分布局 int margin = (cameraHeight - circleWidth) / 2 - borderWidth / 2; rootParams.setMargins(0, -margin, 0, -margin); cameraRoot.setLayoutParams(rootParams); //添加camera cameraRoot.addView(cameraLayout); //添加circle cameraRoot.addView(circleView); //添加根布局 this.addView(cameraRoot); } //自定义一个轮廓提供者 public ViewOutlineProvider viewOutlineProvider = new ViewOutlineProvider() { @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void getOutline(View view, Outline outline) { //裁剪成一个圆形 int left0 = 0; int top0 = (view.getHeight() - view.getWidth()) / 2; int right0 = view.getWidth(); int bottom0 = (view.getHeight() - view.getWidth()) / 2 + view.getWidth(); outline.setOval(left0, top0, right0, bottom0); } }; }
接着再看一下如何在MainActivity使用的
package com.dong.circlecamera; import android.Manifest; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.ImageView; import android.widget.Toast; import com.dong.circlecamera.view.CameraListener; import com.dong.circlecamera.view.CameraPreview; import com.dong.circlecamera.view.CircleCameraLayout; import com.dong.circlecamera.view.Util; /** * @create 2018/12/1 * @Describe 自定义圆形拍照、解决非全屏(竖屏)下预览相机拉伸问题。 */ public class MainActivity extends AppCompatActivity implements View.OnClickListener { private static final int PERMISSION_REQUEST_CODE = 10; private String[] mPermissions = {Manifest.permission.CAMERA}; private CircleCameraLayout rootLayout; private ImageView imageView; private CameraPreview cameraPreview; private boolean hasPermissions; private boolean resume = false;//解决home键黑屏问题 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.bt_take_photo).setOnClickListener(this); findViewById(R.id.bt_re_take_photo).setOnClickListener(this); rootLayout = findViewById(R.id.rootLayout); imageView = findViewById(R.id.image); //权限检查 if (Util.checkPermissionAllGranted(this, mPermissions)) { hasPermissions = true; } else { ActivityCompat.requestPermissions(this, mPermissions, PERMISSION_REQUEST_CODE); } } @Override protected void onResume() { super.onResume(); if (hasPermissions) { startCamera(); resume = true; } } private void startCamera() { if (null != cameraPreview) cameraPreview.releaseCamera(); cameraPreview = new CameraPreview(this); rootLayout.removeAllViews(); rootLayout.setCameraPreview(cameraPreview); if (!hasPermissions || resume) { rootLayout.startView(); } cameraPreview.setCameraListener(new CameraListener() { @Override public void onCaptured(Bitmap bitmap) { if (null != bitmap) { imageView.setImageBitmap(bitmap); Toast.makeText(MainActivity.this, "拍照成功", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this, "拍照失败", Toast.LENGTH_SHORT).show(); } } }); } @Override public void onClick(View v) { if (null == cameraPreview) return; switch (v.getId()) { case R.id.bt_take_photo: cameraPreview.captureImage();//抓取照片 break; case R.id.bt_re_take_photo: cameraPreview.startPreview(); break; } } @Override protected void onDestroy() { super.onDestroy(); if (null != cameraPreview) { cameraPreview.releaseCamera(); } rootLayout.release(); } /** * 申请权限结果返回处理 */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { boolean isAllGranted = true; for (int grant : grantResults) { // 判断是否所有的权限都已经授予了 if (grant != PackageManager.PERMISSION_GRANTED) { isAllGranted = false; break; } } if (isAllGranted) { // 所有的权限都授予了 startCamera(); } else {// 提示需要权限的原因 AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage("拍照需要允许权限, 是否再次开启?") .setTitle("提示") .setPositiveButton("确认", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ActivityCompat.requestPermissions(MainActivity.this, mPermissions, PERMISSION_REQUEST_CODE); } }) .setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); finish(); } }); builder.create().show(); } } } }
-
最后
圆形预览框 , 主要是通过一个relativelayout包裹住封装好的surfaceview , 并且裁剪显示的区域为圆形实现的 , 写下来记录一下 , 后面如果要用到的话方便自己到这里来查看.