需求
最近有个需求要求界面上使用圆形相机预览进行面部检测 , 具体需求如下图
关于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 , 并且裁剪显示的区域为圆形实现的 , 写下来记录一下 , 后面如果要用到的话方便自己到这里来查看.