-
需求
最近有個需求要求界面上使用圓形相機預覽進行面部檢測 , 具體需求如下圖
關於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 , 並且裁剪顯示的區域為圓形實現的 , 寫下來記錄一下 , 后面如果要用到的話方便自己到這里來查看.