package com.zhongyun.zxing.journeyapps.barcodescanner; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Rect; import android.os.Build; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.ViewGroup; import android.view.WindowManager; import java.util.ArrayList; import java.util.List; import com.zhongyun.viewer.R; import com.zhongyun.zxing.journeyapps.barcodescanner.camera.CameraInstance; import com.zhongyun.zxing.journeyapps.barcodescanner.camera.CameraSettings; import com.zhongyun.zxing.journeyapps.barcodescanner.camera.DisplayConfiguration; /** * CameraPreview is a view that handles displaying of a camera preview on a SurfaceView. It is * intended to be used as a base for realtime processing of camera images, e.g. barcode decoding * or OCR, although none of this happens in CameraPreview itself. * * The camera is managed on a separate thread, using CameraInstance. * * Two methods MUST be called on CameraPreview to manage its state: * 1. resume() - initialize the camera and start the preview. Call from the Activity's onResume(). * 2. pause() - stop the preview and release any resources. Call from the Activity's onPause(). * * Startup sequence: * * 1. Create SurfaceView. * 2. open camera. * 2. layout this container, to get size * 3. set display config, according to the container size * 4. configure() * 5. wait for preview size to be ready * 6. set surface size according to preview size * 7. set surface and start preview */ public class CameraPreview extends ViewGroup { public interface StateListener { /** * Preview and frame sizes are determined. */ void previewSized(); /** * Preview has started. */ void previewStarted(); /** * Preview has stopped. */ void previewStopped(); /** * The camera has errored, and cannot display a preview. * * @param error the error */ void cameraError(Exception error); } private static final String TAG = CameraPreview.class.getSimpleName(); private CameraInstance cameraInstance; private WindowManager windowManager; private Handler stateHandler; private SurfaceView surfaceView; private boolean previewActive = false; private RotationListener rotationListener; private List<StateListener> stateListeners = new ArrayList<StateListener>(); private DisplayConfiguration displayConfiguration; private CameraSettings cameraSettings = new CameraSettings(); // Size of this container, non-null after layout is performed private Size containerSize; // Size of the preview resolution private Size previewSize; // Rect placing the preview surface private Rect surfaceRect; // Size of the current surface. non-null if the surface is ready private Size currentSurfaceSize; // Framing rectangle relative to this view private Rect framingRect = null; // Framing rectangle relative to the preview resolution private Rect previewFramingRect = null; // Size of the framing rectangle. If null, defaults to using a margin percentage. private Size framingRectSize = null; // Fraction of the width / heigth to use as a margin. This fraction is used on each size, so // must be smaller than 0.5; private double marginFraction = 0.1d; private final SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { currentSurfaceSize = null; } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (holder == null) { Log.e(TAG, "*** WARNING *** surfaceChanged() gave us a null surface!"); return; } currentSurfaceSize = new Size(width, height); startPreviewIfReady(); } }; private final Handler.Callback stateCallback = new Handler.Callback() { @Override public boolean handleMessage(Message message) { if (message.what == R.id.zxing_prewiew_size_ready) { previewSized((Size) message.obj); return true; } else if (message.what == R.id.zxing_camera_error) { Exception error = (Exception) message.obj; if (isActive()) { // This check prevents multiple errors from begin passed through. pause(); fireState.cameraError(error); } } return false; } }; private RotationCallback rotationCallback = new RotationCallback() { @Override public void onRotationChanged(int rotation) { // Make sure this is run on the main thread. stateHandler.post(new Runnable() { @Override public void run() { rotationChanged(); } }); } }; public CameraPreview(Context context) { super(context); initialize(context, null, 0, 0); } public CameraPreview(Context context, AttributeSet attrs) { super(context, attrs); initialize(context, attrs, 0, 0); } public CameraPreview(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialize(context, attrs, defStyleAttr, 0); } private void initialize(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { if (getBackground() == null) { // Default to SurfaceView colour, so that there are less changes. setBackgroundColor(Color.BLACK); } TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.zxing_camera_preview); int framingRectWidth = (int) attributes.getDimension(R.styleable.zxing_camera_preview_zxing_framing_rect_width, -1); int framingRectHeight = (int) attributes.getDimension(R.styleable.zxing_camera_preview_zxing_framing_rect_height, -1); attributes.recycle(); if (framingRectWidth > 0 && framingRectHeight > 0) { this.framingRectSize = new Size(framingRectWidth, framingRectHeight); } windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); stateHandler = new Handler(stateCallback); setupSurfaceView(); rotationListener = new RotationListener(); } private void rotationChanged() { pause(); resume(); } private void setupSurfaceView() { surfaceView = new SurfaceView(getContext()); if (Build.VERSION.SDK_INT < 11) { surfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); } surfaceView.getHolder().addCallback(surfaceCallback); addView(surfaceView); } /** * Add a listener to be notified of changes to the preview state, as well as camera errors. * * @param listener the listener */ public void addStateListener(StateListener listener) { stateListeners.add(listener); } private final StateListener fireState = new StateListener() { @Override public void previewSized() { for (StateListener listener : stateListeners) { listener.previewSized(); } } @Override public void previewStarted() { for (StateListener listener : stateListeners) { listener.previewStarted(); } } @Override public void previewStopped() { for (StateListener listener : stateListeners) { listener.previewStopped(); } } @Override public void cameraError(Exception error) { for (StateListener listener : stateListeners) { listener.cameraError(error); } } }; private void calculateFrames() { if (containerSize == null || previewSize == null || displayConfiguration == null) { previewFramingRect = null; framingRect = null; surfaceRect = null; throw new IllegalStateException("containerSize or previewSize is not set yet"); } int previewWidth = previewSize.width; int previewHeight = previewSize.height; int width = containerSize.width; int height = containerSize.height; surfaceRect = displayConfiguration.scalePreview(previewSize); Rect container = new Rect(0, 0, width, height); framingRect = calculateFramingRect(container, surfaceRect); Rect frameInPreview = new Rect(framingRect); frameInPreview.offset(-surfaceRect.left, -surfaceRect.top); previewFramingRect = new Rect(frameInPreview.left * previewWidth / surfaceRect.width(), frameInPreview.top * previewHeight / surfaceRect.height(), frameInPreview.right * previewWidth / surfaceRect.width(), frameInPreview.bottom * previewHeight / surfaceRect.height()); if (previewFramingRect.width() <= 0 || previewFramingRect.height() <= 0) { previewFramingRect = null; framingRect = null; Log.w(TAG, "Preview frame is too small"); } else { fireState.previewSized(); } } /** * Call this on the main thread, while the preview is running. * * @param on true to turn on the torch */ public void setTorch(boolean on) { if (cameraInstance != null) { cameraInstance.setTorch(on); } } private void containerSized(Size containerSize) { this.containerSize = containerSize; if (cameraInstance != null) { if (cameraInstance.getDisplayConfiguration() == null) { displayConfiguration = new DisplayConfiguration(getDisplayRotation(), containerSize); cameraInstance.setDisplayConfiguration(displayConfiguration); cameraInstance.configureCamera(); } } } private void previewSized(Size size) { this.previewSize = size; if (containerSize != null) { calculateFrames(); requestLayout(); startPreviewIfReady(); } } private void startPreviewIfReady() { if (currentSurfaceSize != null && previewSize != null && surfaceRect != null) { if (currentSurfaceSize.equals(new Size(surfaceRect.width(), surfaceRect.height()))) { startCameraPreview(surfaceView.getHolder()); } else { // Surface is not the correct size yet } } } @SuppressLint("DrawAllocation") @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { containerSized(new Size(r - l, b - t)); if (surfaceRect == null) { // Match the container, to reduce the risk of issues. The preview should never be drawn // while the surface has this size. surfaceView.layout(0, 0, getWidth(), getHeight()); } else { surfaceView.layout(surfaceRect.left, surfaceRect.top, surfaceRect.right, surfaceRect.bottom); } } /** * The framing rectangle, relative to this view. Use to draw the rectangle. * * Will never be null while the preview is active. * * @return the framing rect, or null * @see #isPreviewActive() */ public Rect getFramingRect() { return framingRect; } /** * The framing rect, relative to the camera preview resolution. * * Will never be null while the preview is active. * * @return the preview rect, or null * @see #isPreviewActive() */ public Rect getPreviewFramingRect() { return previewFramingRect; } /** * @return the CameraSettings currently in use */ public CameraSettings getCameraSettings() { return cameraSettings; } /** * Set the CameraSettings. Use this to select a different camera, change exposure and torch * settings, and some other options. * * This has no effect if the camera is already open. * * @param cameraSettings the new settings */ public void setCameraSettings(CameraSettings cameraSettings) { this.cameraSettings = cameraSettings; } /** * Start the camera preview and decoding. Typically this should be called from the Activity's * onResume() method. * * Call from UI thread only. */ public void resume() { // This must be safe to call multiple times Util.validateMainThread(); Log.d(TAG, "resume()"); // initCamera() does nothing if called twice, but does log a warning initCamera(); if (currentSurfaceSize != null) { // The activity was paused but not stopped, so the surface still exists. Therefore // surfaceCreated() won't be called, so init the camera here. startPreviewIfReady(); } else { // Install the callback and wait for surfaceCreated() to init the camera. surfaceView.getHolder().addCallback(surfaceCallback); } // To trigger surfaceSized again requestLayout(); rotationListener.listen(getContext(), rotationCallback); } /** * Pause scanning and the camera preview. Typically this should be called from the Activity's * onPause() method. * * Call from UI thread only. */ public void pause() { // This must be safe to call multiple times. Util.validateMainThread(); Log.d(TAG, "pause()"); if (cameraInstance != null) { cameraInstance.close(); cameraInstance = null; previewActive = false; } if (currentSurfaceSize == null) { SurfaceHolder surfaceHolder = surfaceView.getHolder(); surfaceHolder.removeCallback(surfaceCallback); } this.containerSize = null; this.previewSize = null; this.previewFramingRect = null; rotationListener.stop(); fireState.previewStopped(); } public Size getFramingRectSize() { return framingRectSize; } /** * Set an exact size for the framing rectangle. It will be centered in the view. * * @param framingRectSize the size */ public void setFramingRectSize(Size framingRectSize) { this.framingRectSize = framingRectSize; } public double getMarginFraction() { return marginFraction; } /** * The the fraction of the width/height of view to be used as a margin for the framing rect. * This is ignored if framingRectSize is specified. * * @param marginFraction the fraction */ public void setMarginFraction(double marginFraction) { if(marginFraction >= 0.5d) { throw new IllegalArgumentException("The margin fraction must be less than 0.5"); } this.marginFraction = marginFraction; } /** * Considered active if between resume() and pause(). * * @return true if active */ protected boolean isActive() { return cameraInstance != null; } private int getDisplayRotation() { return windowManager.getDefaultDisplay().getRotation(); } private void initCamera() { if (cameraInstance != null) { Log.w(TAG, "initCamera called twice"); return; } cameraInstance = new CameraInstance(getContext()); cameraInstance.setCameraSettings(cameraSettings); cameraInstance.setReadyHandler(stateHandler); cameraInstance.open(); } private void startCameraPreview(SurfaceHolder holder) { if (!previewActive) { Log.i(TAG, "Starting preview"); cameraInstance.setSurfaceHolder(holder); cameraInstance.startPreview(); previewActive = true; previewStarted(); fireState.previewStarted(); } } /** * Called when the preview is started. Override this to start decoding work. */ protected void previewStarted() { } /** * Get the current CameraInstance. This may be null, and may change when * pausing / resuming the preview. * * While the preview is active, getCameraInstance() will never be null. * * @return the current CameraInstance * @see #isPreviewActive() */ public CameraInstance getCameraInstance() { return cameraInstance; } /** * The preview typically starts being active a while after calling resume(), and stops * when calling pause(). * * @return true if the preview is active */ public boolean isPreviewActive() { return previewActive; } /** * Calculate framing rectangle, relative to the preview frame. * * Note that the SurfaceView may be larger than the container. * * Override this for more control over the framing rect calculations. * * @param container this container, with left = top = 0 * @param surface the SurfaceView, relative to this container * @return the framing rect, relative to this container */ protected Rect calculateFramingRect(Rect container, Rect surface) { // intersection is the part of the container that is used for the preview Rect intersection = new Rect(container); boolean intersects = intersection.intersect(surface); if(framingRectSize != null) { // Specific size is specified. Make sure it's not larger than the container or surface. int horizontalMargin = Math.max(0, (intersection.width() - framingRectSize.width) / 2); int verticalMargin = Math.max(0, (intersection.height() - framingRectSize.height) / 2); intersection.inset(horizontalMargin, verticalMargin); return intersection; } // margin as 10% (default) of the smaller of width, height int margin = (int)Math.min(intersection.width() * marginFraction, intersection.height() * marginFraction); intersection.inset(margin, margin); if (intersection.height() > intersection.width()) { // We don't want a frame that is taller than wide. intersection.inset(0, (intersection.height() - intersection.width()) / 2); } return intersection; } }