package com.camnter.newlife.utils.camera;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.support.annotation.DrawableRes;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Layout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import com.camnter.newlife.R;
import com.camnter.newlife.utils.BitmapUtils;
import com.camnter.newlife.utils.DeviceUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import static com.camnter.newlife.utils.camera.IdCardCameraActivity.PROMPT_FRONT;
/**
* Description:CameraPreviewView
* Created by:CaMnter
*/
public class CameraPreviewView extends SurfaceView implements SurfaceHolder.Callback {
// 1080.0f / 792.0f
private static final float SCREEN_RECT_WIDTH_RATIO = 1.3636364f;
// 1488.0f / 1224.0f
private static final float SCREEN_RECT_HEIGHT_RATIO = 1.2156863f;
// 1080.0f / 100.8f
private static final float SCREEN_WIDTH_RECT_MARGIN_LEFT_RATIO = 10.714286f;
// 1080.0f / 64.9f
private static final float SCREEN_WIDTH_RECT_MARGIN_RIGHT_RATIO = 16.640985f;
// 1224.0f / 634.97f
private static final float RECT_HEIGHT_FRONT_IMAGE_MARGIN_TOP_RATIO = 1.9276502f;
// 1224.0f / 165.6f
private static final float RECT_HEIGHT_REVERSE_IMAGE_MARGIN_TOP_RATIO = 7.391304f;
// 1080.0f / 421.92f
private static final float RECT_HEIGHT_REVERSE_IMAGE_MARGIN_LEFT_RATIO = 2.559727f;
private static final String PROMPT_CONTENT_FRONT = "将身份证正面 对准边框和头像";
private static final String PROMPT_CONTENT_REVERSE = "将身份证背面 对准边框和国徽";
private static final float PROMPT_SIZE = 13.4f;
private static final int DEFAULT_PROMPT_COLOR = 0xffFFFFFF;
private static final int DEFAULT_CORNER_COLOR = 0xffFFFFFF;
public static final int DRAW_MODE_BY_SELF = 0x261;
public static final int DRAW_MODE_BY_DRAWABLE = 0x262;
@IntDef({ DRAW_MODE_BY_SELF, DRAW_MODE_BY_DRAWABLE })
@Retention(RetentionPolicy.SOURCE)
private @interface DrawMode {
}
// corner dp
private static final float DEFAULT_CORNER_STROKE = 2.4f;
private static final float DEFAULT_CORNER_LENGTH = 15.4f;
private float screenWidth;
private float screenHeight;
private Paint rectPaint;
private Paint cornerPaint;
private TextPaint promptPaint;
private DisplayMetrics metrics;
// corner px
private float cornerStroke;
private float cornerLength;
private float promptWidth;
private int[] rectWidthHeight;
private Rect rect;
private volatile boolean runningState = false;
private SurfaceThread surfaceThread;
private SurfaceHolder surfaceHolder;
@IdCardCameraActivity.PromptViewType
private int promptViewType;
private String promptTipContent = "";
@DrawMode
private int drawMode;
private DrawProxy drawProxy;
private PreviewListener previewListener;
public CameraPreviewView(Context context) {
this(context, null);
}
public CameraPreviewView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CameraPreviewView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.init();
}
public void setPromptViewType(@IdCardCameraActivity.PromptViewType final int promptViewType) {
this.promptViewType = promptViewType;
this.promptTipContent = this.promptViewType == PROMPT_FRONT ?
PROMPT_CONTENT_FRONT : PROMPT_CONTENT_REVERSE;
this.initPromptPaint();
}
public void setDrawMode(@DrawMode final int drawMode) {
this.drawMode = drawMode;
if (this.drawProxy == null) {
this.drawProxy = new DrawProxy(this.getContext(), this.drawMode, this.promptViewType);
return;
}
this.drawProxy.setDrawMode(this.drawMode);
}
private void init() {
this.metrics = this.getResources().getDisplayMetrics();
this.cornerStroke = this.dp2px(DEFAULT_CORNER_STROKE);
this.cornerLength = this.dp2px(DEFAULT_CORNER_LENGTH);
this.initSurfaceHolder();
this.initRectPaint();
this.initCornerPaint();
this.initPromptPaint();
}
private void initSurfaceHolder() {
this.surfaceHolder = this.getHolder();
this.surfaceHolder.addCallback(this);
this.surfaceHolder.setFormat(PixelFormat.TRANSPARENT);
this.setZOrderOnTop(true);
this.setKeepScreenOn(true);
}
/**
* 透过 CLEAR 将 SurfaceView 的 background 和 矩形范围颜色消除
*/
private void initRectPaint() {
this.rectPaint = new Paint();
this.rectPaint.setAntiAlias(true);
this.rectPaint.setStyle(Paint.Style.FILL_AND_STROKE);
this.rectPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
private void initCornerPaint() {
this.cornerPaint = new Paint();
this.cornerPaint.setColor(DEFAULT_CORNER_COLOR);
this.cornerPaint.setStrokeWidth(this.cornerStroke);
this.cornerPaint.setStrokeCap(Paint.Cap.ROUND);
}
private void initPromptPaint() {
this.promptPaint = new TextPaint();
final float promptSize = DeviceUtils.dp2px(this.getContext(), PROMPT_SIZE);
this.promptPaint.setAntiAlias(true);
this.promptPaint.setColor(DEFAULT_PROMPT_COLOR);
this.promptPaint.setTextSize(promptSize);
this.promptPaint.setStyle(Paint.Style.FILL_AND_STROKE);
this.promptWidth = Layout.getDesiredWidth(this.promptTipContent, promptPaint);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
this.screenWidth = this.getWidth();
this.screenHeight = this.getHeight();
this.rectWidthHeight = this.drawProxy.getRectWidthHeightProxy();
this.start();
}
private void handlePreviewListener(@NonNull final PreviewListener previewListener,
final int[] rectWidthHeight) {
switch (this.promptViewType) {
case PROMPT_FRONT:
final int frontImageViewWidth = this.getResources()
.getDimensionPixelOffset(R.dimen.id_card_front_image_width);
final int frontImageMarginTop = this.rect.top +
(int) (rectWidthHeight[1] / RECT_HEIGHT_FRONT_IMAGE_MARGIN_TOP_RATIO);
final int frontImageMarginLeft = this.rect.left +
(int) (rectWidthHeight[0] / 2.0f - frontImageViewWidth / 2.0f);
previewListener.notificationFrontImageView(frontImageMarginTop,
frontImageMarginLeft);
break;
case IdCardCameraActivity.PROMPT_REVERSE:
final int reverseImageMarginTop = this.rect.top +
(int) (rectWidthHeight[1] / RECT_HEIGHT_REVERSE_IMAGE_MARGIN_TOP_RATIO);
final int reverseImageMarginLeft = this.rect.left +
(int) (rectWidthHeight[1] / RECT_HEIGHT_REVERSE_IMAGE_MARGIN_LEFT_RATIO);
previewListener.notificationReverseImageView(reverseImageMarginTop,
reverseImageMarginLeft);
break;
}
}
public void setPreviewListener(PreviewListener previewListener) {
this.previewListener = previewListener;
}
private void start() {
if (this.runningState) return;
this.surfaceThread = new SurfaceThread();
this.surfaceThread.start();
this.runningState = true;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
this.stop();
}
private void stop() {
if (!this.runningState || this.surfaceThread == null) return;
this.surfaceThread.interrupt();
this.surfaceThread = null;
this.runningState = false;
}
/**
* Dp to px
*
* @param dp dp
* @return px
*/
private float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.metrics);
}
public interface PreviewListener {
void notificationFrontImageView(final int frontImageMarginTop,
final int frontImageMarginLeft);
void notificationReverseImageView(final int reverseImageMarginTop,
final int reverseImageMarginLeft);
}
private class SurfaceThread extends Thread {
@Override
public void run() {
Canvas canvas = null;
try {
canvas = surfaceHolder.lockCanvas();
/***************
* 抽离透明区域 *
***************/
canvas.save();
canvas.drawARGB(100, 0, 0, 0);
canvas.drawRect(rect, rectPaint);
canvas.restore();
/*************
* 身份证图片 *
*************/
if (drawMode == DRAW_MODE_BY_DRAWABLE) {
canvas.save();
drawProxy.drawIdCardContourProxy(canvas);
canvas.save(Canvas.ALL_SAVE_FLAG);
canvas.restore();
}
/********
* 边框 *
********/
canvas.save();
float cornerHalfStroke = cornerStroke / 2;
// leftTop >> h + v
canvas.drawLine(rect.left - cornerHalfStroke, rect.top - cornerHalfStroke,
rect.left - cornerHalfStroke + cornerLength, rect.top - cornerHalfStroke,
cornerPaint);
canvas.drawLine(rect.left - cornerHalfStroke, rect.top - cornerHalfStroke,
rect.left - cornerHalfStroke, rect.top - cornerHalfStroke + cornerLength,
cornerPaint);
// leftBottom >> h + v
canvas.drawLine(rect.left - cornerHalfStroke, rect.bottom + cornerHalfStroke,
rect.left - cornerHalfStroke + cornerLength, rect.bottom + cornerHalfStroke,
cornerPaint);
canvas.drawLine(rect.left - cornerHalfStroke, rect.bottom + cornerHalfStroke,
rect.left - cornerHalfStroke, rect.bottom + cornerHalfStroke - cornerLength,
cornerPaint);
// rightTop >> h + v
canvas.drawLine(rect.right + cornerHalfStroke, rect.top - cornerHalfStroke,
rect.right + cornerHalfStroke - cornerLength, rect.top - cornerHalfStroke,
cornerPaint);
canvas.drawLine(rect.right + cornerHalfStroke, rect.top - cornerHalfStroke,
rect.right + cornerHalfStroke, rect.top - cornerHalfStroke + cornerLength,
cornerPaint);
// rightBottom >> h + v
canvas.drawLine(rect.right + cornerHalfStroke, rect.bottom + cornerHalfStroke,
rect.right + cornerHalfStroke - cornerLength, rect.bottom + cornerHalfStroke,
cornerPaint);
canvas.drawLine(rect.right + cornerHalfStroke, rect.bottom + cornerHalfStroke,
rect.right + cornerHalfStroke, rect.bottom + cornerHalfStroke - cornerLength,
cornerPaint);
canvas.restore();
/********
* 文字 *
********/
canvas.save();
// X = rect.marginLeft + rectWidth + rect.marginLeft
// Y = screenHeight / 2 - 文字宽度 / 2
drawTextRotate(canvas,
promptTipContent,
rect.left + (int) (screenWidth / SCREEN_WIDTH_RECT_MARGIN_RIGHT_RATIO) +
rectWidthHeight[0],
screenHeight / 2.0f - promptWidth / 2.0f,
promptPaint, 90);
canvas.restore();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
surfaceHolder.unlockCanvasAndPost(canvas);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void drawTextRotate(@NonNull final Canvas canvas,
@NonNull final String text,
final float x,
final float y,
@NonNull final Paint paint,
final float angle) {
if (angle != 0) {
canvas.rotate(angle, x, y);
}
canvas.drawText(text, x, y, paint);
if (angle != 0) {
canvas.rotate(-angle, x, y);
}
}
}
private final class DrawProxy {
@DrawMode
private int drawMode;
@IdCardCameraActivity.PromptViewType
private final int promptViewType;
@DrawableRes
private static final int ID_CARD_FRONT_CONTOUR = R.drawable.bg_id_card_front_contour;
@DrawableRes
private static final int ID_CARD_REVERSE_CONTOUR = R.drawable.bg_id_card_reverse_contour;
private static final int DEFAULT_RETRY_COUNT = 3;
private WeakReference<Context> contextReference;
private Bitmap expectBitmap;
public DrawProxy(@NonNull final Context context,
@DrawMode final int drawMode,
@IdCardCameraActivity.PromptViewType final int promptViewType) {
this.drawMode = drawMode;
this.promptViewType = promptViewType;
this.contextReference = new WeakReference<>(context);
}
private void setDrawMode(@DrawableRes final int drawMode) {
this.drawMode = drawMode;
}
@Nullable
private int[] getRectWidthHeightProxy() {
final int[] rectWidthHeight = new int[2];
if (screenWidth <= 0) return rectWidthHeight;
// width
rectWidthHeight[0] = (int) (screenWidth / SCREEN_RECT_WIDTH_RATIO);
rect = new Rect();
final int rectMarginLeft = (int) (screenWidth / SCREEN_WIDTH_RECT_MARGIN_LEFT_RATIO);
switch (this.drawMode) {
case DRAW_MODE_BY_SELF: {
// height
rectWidthHeight[1] = (int) (screenHeight / SCREEN_RECT_HEIGHT_RATIO);
final int rectMarginTop = (int) (screenHeight / 2 -
((float) rectWidthHeight[1]) / 2);
rect.left = rectMarginLeft;
rect.top = rectMarginTop;
rect.right = rectMarginLeft + rectWidthHeight[0];
rect.bottom = rectMarginTop + rectWidthHeight[1];
if (previewListener != null) {
handlePreviewListener(previewListener, rectWidthHeight);
}
break;
}
case DRAW_MODE_BY_DRAWABLE: {
this.initExpectBitmap(rectWidthHeight[0]);
if (this.expectBitmap == null) return rectWidthHeight;
final int expectBitmapWidth = this.expectBitmap.getWidth();
final int expectBitmapHeight = this.expectBitmap.getHeight();
// height
rectWidthHeight[1] = expectBitmapHeight;
final int rectMarginTop = (int) (screenHeight / 2 -
((float) expectBitmapHeight) / 2);
rect.left = rectMarginLeft;
rect.top = rectMarginTop;
rect.right = rectMarginLeft + expectBitmapWidth;
rect.bottom = rectMarginTop + expectBitmapHeight;
break;
}
}
return rectWidthHeight;
}
private void drawIdCardContourProxy(@NonNull final Canvas canvas) {
canvas.drawBitmap(this.expectBitmap, rect.left, rect.top, null);
}
private void initExpectBitmap(final int newWidth) {
@DrawableRes
final int idCardContour = this.promptViewType == PROMPT_FRONT ?
ID_CARD_FRONT_CONTOUR :
ID_CARD_REVERSE_CONTOUR;
try {
final Context context = this.contextReference.get();
if (context == null) return;
final Bitmap originalBitmap = this.decodeResourceSafely(
context.getResources(),
idCardContour,
DEFAULT_RETRY_COUNT
);
if (originalBitmap == null) return;
this.expectBitmap = this.getBitmapCompressedByWidthSafely(
originalBitmap,
newWidth,
DEFAULT_RETRY_COUNT
);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
@Nullable
private Bitmap decodeResourceSafely(@NonNull final Resources res,
final int drawableRes,
final int retryCount) {
try {
return BitmapFactory.decodeResource(res, drawableRes);
} catch (OutOfMemoryError outOfMemoryError) {
outOfMemoryError.printStackTrace();
System.gc();
if (retryCount <= 0) return null;
return this.decodeResourceSafely(res, drawableRes, retryCount - 1);
}
}
@Nullable
private Bitmap getBitmapCompressedByWidthSafely(@NonNull final Bitmap bitmap,
final double newWidth,
final int retryCount) {
try {
return BitmapUtils.getBitmapCompressedByWidth(bitmap, newWidth);
} catch (OutOfMemoryError outOfMemoryError) {
outOfMemoryError.printStackTrace();
System.gc();
if (retryCount <= 0) return null;
return this.getBitmapCompressedByWidthSafely(bitmap, newWidth, retryCount - 1);
}
}
}
}