diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index b0883904..19d83b57 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -1,5 +1,6 @@ package com.otaliastudios.cameraview; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.YuvImage; import android.hardware.Camera; @@ -692,9 +693,11 @@ class Camera1 extends CameraController { @Override - void startFocus(final float x, final float y) { - if (!isCameraOpened()) return; - List meteringAreas2 = computeMeteringAreas(x, y); + boolean startAutoFocus(@Nullable final Gesture gesture, PointF point) { + if (!isCameraOpened()) return false; + if (!mOptions.isAutoFocusSupported()) return false; + final PointF p = new PointF(point.x, point.y); // copy. + List meteringAreas2 = computeMeteringAreas(p.x, p.y); List meteringAreas1 = meteringAreas2.subList(0, 1); synchronized (mLock) { // At this point we are sure that camera supports auto focus... right? Look at CameraView.onTouchEvent(). @@ -705,15 +708,18 @@ class Camera1 extends CameraController { if (maxAE > 0) params.setMeteringAreas(maxAE > 1 ? meteringAreas2 : meteringAreas1); params.setFocusMode((String) mMapper.mapFocus(FOCUS_TAP)); // auto mCamera.setParameters(params); - mCameraCallbacks.dispatchOnFocusStart(x, y); + mCameraCallbacks.dispatchOnFocusStart(gesture, p); + // TODO this is not guaranteed to be called... Fix. mCamera.autoFocus(new Camera.AutoFocusCallback() { @Override public void onAutoFocus(boolean success, Camera camera) { - mCameraCallbacks.dispatchOnFocusEnd(success, x, y); + // TODO lock auto exposure and white balance for a while + mCameraCallbacks.dispatchOnFocusEnd(gesture, success, p); postResetFocus(); } }); } + return true; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java index e071adcd..b794c548 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java @@ -3,6 +3,7 @@ package com.otaliastudios.cameraview; import android.annotation.TargetApi; import android.content.Context; import android.graphics.ImageFormat; +import android.graphics.PointF; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; @@ -249,8 +250,8 @@ class Camera2 extends CameraController { } @Override - void startFocus(float x, float y) { - + boolean startAutoFocus(Gesture gesture, PointF point) { + return true; } @Nullable diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraConstants.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraConstants.java index 1bd0c909..fb1fb1d4 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraConstants.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraConstants.java @@ -1,12 +1,17 @@ package com.otaliastudios.cameraview; -/** - * Created by Mattia on 06/08/17. - */ + public class CameraConstants { public static final int PERMISSION_REQUEST_CODE = 16; + public static final int GESTURE_ACTION_NONE = 0; + public static final int GESTURE_ACTION_FOCUS = 1; + public static final int GESTURE_ACTION_FOCUS_WITH_MARKER = 2; + public static final int GESTURE_ACTION_CAPTURE = 3; + public static final int GESTURE_ACTION_ZOOM = 4; + public static final int GESTURE_ACTION_AE_CORRECTION = 5; + public static final int FACING_BACK = 0; public static final int FACING_FRONT = 1; @@ -20,9 +25,6 @@ public class CameraConstants { public static final int FOCUS_TAP = 2; public static final int FOCUS_TAP_WITH_MARKER = 3; - public static final int ZOOM_OFF = 0; - public static final int ZOOM_PINCH = 1; - public static final int GRID_OFF = 0; public static final int GRID_3X3 = 1; public static final int GRID_4X4 = 2; @@ -31,32 +33,14 @@ public class CameraConstants { public static final int SESSION_TYPE_PICTURE = 0; public static final int SESSION_TYPE_VIDEO = 1; - @Deprecated - public static final int CAPTURE_METHOD_STANDARD = 0; - @Deprecated - public static final int CAPTURE_METHOD_FRAME = 1; - - - /** - * This means the app will be requesting Camera and Audio permissions on Marshmallow+. - * With this configuration, it is mandatory that the developer adds - * - * to its manifest, or the app will crash. - * - * @deprecated this does nothing. - */ - @Deprecated - public static final int PERMISSIONS_VIDEO = 0; - - /** - * This means the app will be requesting the Camera permissions on Marshmallow+. - * On older APIs, this is not important, since the Camera permission is already - * declared in the manifest by the library. - * - * @deprecated this does nothing. - */ - @Deprecated - public static final int PERMISSIONS_PICTURE = 1; + @Deprecated public static final int ZOOM_OFF = 0; + @Deprecated public static final int ZOOM_PINCH = 1; + + @Deprecated public static final int CAPTURE_METHOD_STANDARD = 0; + @Deprecated public static final int CAPTURE_METHOD_FRAME = 1; + + @Deprecated public static final int PERMISSIONS_VIDEO = 0; + @Deprecated public static final int PERMISSIONS_PICTURE = 1; public static final int VIDEO_QUALITY_480P = 0; public static final int VIDEO_QUALITY_720P = 1; @@ -74,11 +58,16 @@ public class CameraConstants { static class Defaults { + static final int DEFAULT_GESTURE_ACTION_TAP = GESTURE_ACTION_NONE; + // static final int DEFAULT_GESTURE_ACTION_DOUBLE_TAP = GESTURE_ACTION_NONE; + static final int DEFAULT_GESTURE_ACTION_LONG_TAP = GESTURE_ACTION_NONE; + static final int DEFAULT_GESTURE_ACTION_PINCH = GESTURE_ACTION_NONE; + static final int DEFAULT_FACING = FACING_BACK; static final int DEFAULT_FLASH = FLASH_OFF; static final int DEFAULT_FOCUS = FOCUS_CONTINUOUS; - static final int DEFAULT_ZOOM = ZOOM_OFF; + @Deprecated static final int DEFAULT_ZOOM = ZOOM_OFF; @Deprecated static final int DEFAULT_METHOD = CAPTURE_METHOD_STANDARD; @Deprecated static final int DEFAULT_PERMISSIONS = PERMISSIONS_PICTURE; static final int DEFAULT_VIDEO_QUALITY = VIDEO_QUALITY_480P; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java index a0a74eca..4107aaf8 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java @@ -1,5 +1,6 @@ package com.otaliastudios.cameraview; +import android.graphics.PointF; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -49,7 +50,7 @@ abstract class CameraController implements Preview.SurfaceCallback { abstract boolean shouldFlipSizes(); // Wheter the Sizes should be flipped to match the view orientation. abstract boolean isCameraOpened(); - abstract void startFocus(float x, float y); + abstract boolean startAutoFocus(@Nullable Gesture gesture, PointF point); @Nullable abstract ExtraProperties getExtraProperties(); @Nullable abstract CameraOptions getCameraOptions(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java index 0a35248e..0de142ef 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java @@ -60,10 +60,9 @@ public abstract class CameraListener { * and the autofocus is trying to focus around that area. * This can be used to draw things on screen. * - * @param x coordinate with respect to CameraView.getWidth() - * @param y coordinate with respect to CameraView.getHeight() + * @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight() */ - public void onFocusStart(float x, float y) { + public void onFocusStart(PointF point) { } @@ -74,22 +73,35 @@ public abstract class CameraListener { * This might succeed or not. * * @param successful whether camera succeeded - * @param x coordinate with respect to CameraView.getWidth() - * @param y coordinate with respect to CameraView.getHeight() + * @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight() */ - public void onFocusEnd(boolean successful, float x, float y) { + public void onFocusEnd(boolean successful, PointF point) { } /** - * Noitifies that a finger pinch-to-zoom gesture just caused the camera zoom + * Noitifies that a finger gesture just caused the camera zoom * to be changed. This can be used to draw, for example, a seek bar. * - * @param zoom the new zoom value, 0...1 + * @param newValue the new zoom value + * @param bounds min and max bounds for newValue (fixed to 0 ... 1) * @param fingers finger positions that caused the event */ - public void onZoomChanged(float zoom, PointF[] fingers) { + public void onZoomChanged(float newValue, float[] bounds, PointF[] fingers) { + + } + + + /** + * Noitifies that a finger gesture just caused the camera exposure correction + * to be changed. This can be used to draw, for example, a seek bar. + * + * @param newValue the new correction value + * @param bounds min and max bounds for newValue, as returned by {@link CameraOptions} + * @param fingers finger positions that caused the event + */ + public void onExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers) { } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index 6a10a20a..b1ac1462 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -18,13 +18,14 @@ public class CameraOptions { private Set supportedWhiteBalance = new HashSet<>(5); private Set supportedFacing = new HashSet<>(2); private Set supportedFlash = new HashSet<>(4); - private Set supportedFocus = new HashSet<>(4); + @Deprecated private Set supportedFocus = new HashSet<>(4); private boolean zoomSupported; private boolean videoSnapshotSupported; private boolean exposureCorrectionSupported; private float exposureCorrectionMinValue; private float exposureCorrectionMaxValue; + private boolean autoFocusSupported; // Camera1 constructor. @@ -68,6 +69,7 @@ public class CameraOptions { zoomSupported = params.isZoomSupported(); videoSnapshotSupported = params.isVideoSnapshotSupported(); + autoFocusSupported = params.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_AUTO); // Exposure correction float step = params.getExposureCompensationStep(); @@ -164,6 +166,18 @@ public class CameraOptions { } + /** + * Whether auto focus is supported. This means you can map gestures to + * {@link CameraConstants#GESTURE_ACTION_FOCUS} or {@link CameraConstants#GESTURE_ACTION_FOCUS_WITH_MARKER} + * and focus will be changed on tap. + * + * @return whether auto focus is supported. + */ + public boolean isAutoFocusSupported() { + return autoFocusSupported; + } + + /** * Whether exposure correction is supported. If this is false, calling * {@link CameraView#setExposureCorrection(float)} has no effect. diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 67757872..089b95c9 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -31,6 +31,7 @@ import android.widget.FrameLayout; import java.io.ByteArrayOutputStream; import java.io.File; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import static com.otaliastudios.cameraview.CameraConstants.FACING_BACK; @@ -61,6 +62,11 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; * } * } * + * + * TODO: docs for gestures + * TODO: README for gestures + * TODO: deprecate setFocus, CONTINUOUS should be the default + * */ public class CameraView extends FrameLayout implements LifecycleObserver { @@ -92,12 +98,17 @@ public class CameraView extends FrameLayout implements LifecycleObserver { private Preview mPreviewImpl; private Lifecycle mLifecycle; - private FocusMarkerLayout mFocusMarkerLayout; private GridLinesLayout mGridLinesLayout; - private PinchToZoomLayout mPinchToZoomLayout; + private PinchGestureLayout mPinchGestureLayout; + private TapGestureLayout mTapGestureLayout; private boolean mIsStarted; private boolean mKeepScreenOn; + private float mZoomValue; + private float mExposureCorrectionValue; + + private HashMap mGestureMap = new HashMap<>(4); + public CameraView(@NonNull Context context) { super(context, null); init(context, null); @@ -118,30 +129,36 @@ public class CameraView extends FrameLayout implements LifecycleObserver { int whiteBalance = a.getInteger(R.styleable.CameraView_cameraWhiteBalance, CameraConstants.Defaults.DEFAULT_WHITE_BALANCE); int videoQuality = a.getInteger(R.styleable.CameraView_cameraVideoQuality, CameraConstants.Defaults.DEFAULT_VIDEO_QUALITY); int grid = a.getInteger(R.styleable.CameraView_cameraGrid, CameraConstants.Defaults.DEFAULT_GRID); - int zoom = a.getInteger(R.styleable.CameraView_cameraZoomMode, CameraConstants.Defaults.DEFAULT_ZOOM); mJpegQuality = a.getInteger(R.styleable.CameraView_cameraJpegQuality, CameraConstants.Defaults.DEFAULT_JPEG_QUALITY); mCropOutput = a.getBoolean(R.styleable.CameraView_cameraCropOutput, CameraConstants.Defaults.DEFAULT_CROP_OUTPUT); + int tapGesture = a.getInteger(R.styleable.CameraView_cameraGestureTap, CameraConstants.Defaults.DEFAULT_GESTURE_ACTION_TAP); + // int doubleTapGesture = a.getInteger(R.styleable.CameraView_cameraGestureDoubleTap, CameraConstants.Defaults.DEFAULT_GESTURE_ACTION_DOUBLE_TAP); + int longTapGesture = a.getInteger(R.styleable.CameraView_cameraGestureLongTap, CameraConstants.Defaults.DEFAULT_GESTURE_ACTION_LONG_TAP); + int pinchGesture = a.getInteger(R.styleable.CameraView_cameraGesturePinch, CameraConstants.Defaults.DEFAULT_GESTURE_ACTION_PINCH); a.recycle(); mCameraCallbacks = new CameraCallbacks(); mPreviewImpl = new TextureViewPreview(context, this); mCameraController = new Camera1(mCameraCallbacks, mPreviewImpl); - mFocusMarkerLayout = new FocusMarkerLayout(context); mGridLinesLayout = new GridLinesLayout(context); - mPinchToZoomLayout = new PinchToZoomLayout(context); - addView(mFocusMarkerLayout); + mPinchGestureLayout = new PinchGestureLayout(context); + mTapGestureLayout = new TapGestureLayout(context); addView(mGridLinesLayout); - addView(mPinchToZoomLayout); + addView(mPinchGestureLayout); + addView(mTapGestureLayout); mIsStarted = false; setFacing(facing); setFlash(flash); setFocus(focus); setSessionType(sessionType); - setZoomMode(zoom); setVideoQuality(videoQuality); setWhiteBalance(whiteBalance); setGrid(grid); + mapGesture(Gesture.TAP, tapGesture); + // mapGesture(Gesture.DOUBLE_TAP, doubleTapGesture); + mapGesture(Gesture.LONG_TAP, longTapGesture); + mapGesture(Gesture.PINCH, pinchGesture); if (!isInEditMode()) { mOrientationHelper = new OrientationHelper(context) { @@ -315,6 +332,52 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } + /** + * Maps a {@link Gesture} to a certain gesture action. + * For example, you can assign zoom control to the pinch gesture by just calling: + * + * cameraView.mapGesture(Gesture.PINCH, CameraConstants.GESTURE_ACTION_ZOOM); + * + * + * Not all actions can be assigned to a certain gesture. For example, zoom control can't be + * assigned to the Gesture.TAP gesture. Look at {@link Gesture} to know more. + * This method returns false if they are not assignable. + * + * @param gesture which gesture to map + * @param action which action should be assigned + * @return true if this action could be assigned to this gesture + */ + public boolean mapGesture(@NonNull Gesture gesture, @GestureAction int action) { + if (gesture.isAssignableTo(action)) { + mGestureMap.put(gesture, action); + switch (gesture) { + case PINCH: + mPinchGestureLayout.enable(mGestureMap.get(Gesture.PINCH) != CameraConstants.GESTURE_ACTION_NONE); + break; + case TAP: + // case DOUBLE_TAP: + case LONG_TAP: + mTapGestureLayout.enable(mGestureMap.get(Gesture.TAP) != CameraConstants.GESTURE_ACTION_NONE || + // mGestureMap.get(Gesture.DOUBLE_TAP) != CameraConstants.GESTURE_ACTION_NONE || + mGestureMap.get(Gesture.LONG_TAP) != CameraConstants.GESTURE_ACTION_NONE); + break; + } + return true; + } + mapGesture(gesture, CameraConstants.GESTURE_ACTION_NONE); + return false; + } + + + /** + * Clears any action mapped to the given gesture. + * @param gesture which gesture to clear + */ + public void clearGesture(@NonNull Gesture gesture) { + mGestureMap.put(gesture, CameraConstants.GESTURE_ACTION_NONE); + } + + @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return true; // Steal our own events. @@ -323,25 +386,48 @@ public class CameraView extends FrameLayout implements LifecycleObserver { @Override public boolean onTouchEvent(MotionEvent event) { - // Enable/Disable what needs to be enabled/disabled (because CameraOptions is involved). - // Pinch-to-zoom actually does not need it because it won't draw anything either way. - // Grid-lines does not need it because grid modes are always supported. - // Why don't we do it just once? Because shit happens when setFocus() is called before camera open. - // We don't know if focus is supported at that point. - CameraOptions options = mCameraController.getCameraOptions(); - mFocusMarkerLayout.enable(mCameraController.getFocus() == CameraConstants.FOCUS_TAP_WITH_MARKER); - mPinchToZoomLayout.enable(options != null && options.isZoomSupported()); - - // Pinch to zoom gesture... - if (mPinchToZoomLayout.onTouchEvent(event)) { - float zoom = mPinchToZoomLayout.getZoom(); - PointF[] fingers = mPinchToZoomLayout.getPoints(); - mCameraController.setZoom(zoom); // <- We know this is successful, due to mPinchToZoomLayout.enable() above. - mCameraCallbacks.dispatchOnZoomChanged(zoom, fingers); - - // Focus gesture... - } else if (mFocusMarkerLayout.onTouchEvent(event)) { - startFocus(event.getX(), event.getY()); + if (!mCameraController.isCameraOpened()) return true; + + CameraOptions options = mCameraController.getCameraOptions(); // Non null + if (mPinchGestureLayout.onTouchEvent(event)) { + int action = mGestureMap.get(Gesture.PINCH); + // This currently can be zoom or AE. + // Camera can either support these or not. + if (action == CameraConstants.GESTURE_ACTION_ZOOM) { + float oldValue = mZoomValue; + float newValue = mPinchGestureLayout.scaleValue(oldValue, 0, 1); + PointF[] points = mPinchGestureLayout.getPoints(); + if (mCameraController.setZoom(newValue)) { + mZoomValue = newValue; + mCameraCallbacks.dispatchOnZoomChanged(newValue, points); + } + + } else if (action == CameraConstants.GESTURE_ACTION_AE_CORRECTION) { + float oldValue = mExposureCorrectionValue; + float minValue = options.getExposureCorrectionMinValue(); + float maxValue = options.getExposureCorrectionMaxValue(); + float newValue = mPinchGestureLayout.scaleValue(oldValue, minValue, maxValue); + PointF[] points = mPinchGestureLayout.getPoints(); + float[] bounds = new float[]{minValue, maxValue}; + if (mCameraController.setExposureCorrection(newValue)) { + mExposureCorrectionValue = newValue; + mCameraCallbacks.dispatchOnExposureCorrectionChanged(newValue, bounds, points); + } + } + + } else if (mTapGestureLayout.onTouchEvent(event)) { + Gesture gesture = mTapGestureLayout.getGestureType(); + int action = mGestureMap.get(gesture); + // This currently can be capture, focus or focusWithMaker. + // Camera can either support these or not. + if (action == CameraConstants.GESTURE_ACTION_CAPTURE) { + capturePicture(); + + } else if (action == CameraConstants.GESTURE_ACTION_FOCUS || + action == CameraConstants.GESTURE_ACTION_FOCUS_WITH_MARKER) { + PointF point = mTapGestureLayout.getPoint(); + mCameraController.startAutoFocus(gesture, point); // This will call onFocusStart and onFocusEnd + } } return true; } @@ -424,7 +510,9 @@ public class CameraView extends FrameLayout implements LifecycleObserver { cameraCheck = PackageManager.PERMISSION_GRANTED; audioCheck = PackageManager.PERMISSION_GRANTED; } else { + //noinspection all cameraCheck = getContext().checkSelfPermission(Manifest.permission.CAMERA); + //noinspection all audioCheck = getContext().checkSelfPermission(Manifest.permission.RECORD_AUDIO); } switch (sessionType) { @@ -530,7 +618,16 @@ public class CameraView extends FrameLayout implements LifecycleObserver { * @param EVvalue exposure correction value. */ public void setExposureCorrection(float EVvalue) { - mCameraController.setExposureCorrection(EVvalue); + CameraOptions options = getCameraOptions(); + if (options != null) { + float min = options.getExposureCorrectionMinValue(); + float max = options.getExposureCorrectionMaxValue(); + if (EVvalue < min) EVvalue = min; + if (EVvalue > max) EVvalue = max; + if (mCameraController.setExposureCorrection(EVvalue)) { + mExposureCorrectionValue = EVvalue; + } + } } @@ -540,15 +637,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver { * This will have no effect if called before the camera is opened. * * Zoom value should be >= 0 and <= 1, where 1 will be the maximum available zoom. + * If it's not, it will be capped. * * @param zoom value in [0,1] */ public void setZoom(float zoom) { - if (zoom < 0 || zoom > 1) { - throw new IllegalArgumentException("Zoom value should be >= 0 and <= 1"); - } + if (zoom < 0) zoom = 0; + if (zoom > 1) zoom = 1; if (mCameraController.setZoom(zoom)) { - mPinchToZoomLayout.onExternalZoom(zoom); // Notify PinchToZoomLayout + mZoomValue = zoom; } } @@ -729,7 +826,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { public void startFocus(float x, float y) { if (x < 0 || x > getWidth()) throw new IllegalArgumentException("x should be >= 0 and <= getWidth()"); if (y < 0 || y > getHeight()) throw new IllegalArgumentException("y should be >= 0 and <= getHeight()"); - mCameraController.startFocus(x, y); + mCameraController.startAutoFocus(null, new PointF(x, y)); } @@ -821,20 +918,21 @@ public class CameraView extends FrameLayout implements LifecycleObserver { * @see CameraConstants#ZOOM_OFF * @see CameraConstants#ZOOM_PINCH * - * @param zoom the zoom mode + * @deprecated use {@link #mapGesture(Gesture, int)} to map zoom control to gestures */ + @Deprecated public void setZoomMode(@ZoomMode int zoom) { - mPinchToZoomLayout.setZoomMode(zoom); } /** * Gets the current zoom mode. - * @return the current zoom mode + * @deprecated use {@link #mapGesture(Gesture, int)} to map zoom control to gestures */ @ZoomMode + @Deprecated public int getZoomMode() { - return mPinchToZoomLayout.getZoomMode(); + return CameraConstants.ZOOM_OFF; } @@ -1221,36 +1319,59 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } - public void dispatchOnFocusStart(final float x, final float y) { + public void dispatchOnFocusStart(@Nullable final Gesture gesture, final PointF point) { + uiHandler.post(new Runnable() { + @Override + public void run() { + if (gesture != null && mGestureMap.get(gesture) == CameraConstants.GESTURE_ACTION_FOCUS_WITH_MARKER) { + mTapGestureLayout.onFocusStart(point); + } + + for (CameraListener listener : mListeners) { + listener.onFocusStart(point); + } + } + }); + } + + + public void dispatchOnFocusEnd(@Nullable final Gesture gesture, final boolean success, + final PointF point) { uiHandler.post(new Runnable() { @Override public void run() { + if (gesture != null && mGestureMap.get(gesture) == CameraConstants.GESTURE_ACTION_FOCUS_WITH_MARKER) { + mTapGestureLayout.onFocusEnd(success); + } + for (CameraListener listener : mListeners) { - listener.onFocusStart(x, y); + listener.onFocusEnd(success, point); } } }); } - public void dispatchOnFocusEnd(final boolean success, final float x, final float y) { + public void dispatchOnZoomChanged(final float newValue, final PointF[] fingers) { uiHandler.post(new Runnable() { @Override public void run() { for (CameraListener listener : mListeners) { - listener.onFocusEnd(success, x, y); + listener.onZoomChanged(newValue, new float[]{0, 1}, fingers); } } }); } - public void dispatchOnZoomChanged(final float zoom, final PointF[] fingers) { + public void dispatchOnExposureCorrectionChanged(final float newValue, + final float[] bounds, + final PointF[] fingers) { uiHandler.post(new Runnable() { @Override public void run() { for (CameraListener listener : mListeners) { - listener.onZoomChanged(zoom, fingers); + listener.onExposureCorrectionChanged(newValue, bounds, fingers); } } }); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Gesture.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Gesture.java new file mode 100644 index 00000000..848dae46 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Gesture.java @@ -0,0 +1,26 @@ +package com.otaliastudios.cameraview; + + +import java.util.Arrays; +import java.util.List; + +import static com.otaliastudios.cameraview.CameraConstants.*; + +public enum Gesture { + + PINCH(GESTURE_ACTION_ZOOM, GESTURE_ACTION_AE_CORRECTION), + TAP(GESTURE_ACTION_FOCUS, GESTURE_ACTION_FOCUS_WITH_MARKER, GESTURE_ACTION_CAPTURE), + // DOUBLE_TAP(GESTURE_ACTION_FOCUS, GESTURE_ACTION_FOCUS_WITH_MARKER, GESTURE_ACTION_CAPTURE), + LONG_TAP(GESTURE_ACTION_FOCUS, GESTURE_ACTION_FOCUS_WITH_MARKER, GESTURE_ACTION_CAPTURE); + + Gesture(@GestureAction Integer... controls) { + mControls = Arrays.asList(controls); + } + + private List mControls; + + boolean isAssignableTo(@GestureAction Integer control) { + return control == GESTURE_ACTION_NONE || mControls.contains(control); + } + +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Mapper.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Mapper.java index 78447cd4..30071d1d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Mapper.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Mapper.java @@ -34,6 +34,7 @@ abstract class Mapper { WB.put(CameraConstants.WHITE_BALANCE_FLUORESCENT, Camera.Parameters.WHITE_BALANCE_FLUORESCENT); WB.put(CameraConstants.WHITE_BALANCE_DAYLIGHT, Camera.Parameters.WHITE_BALANCE_DAYLIGHT); WB.put(CameraConstants.WHITE_BALANCE_CLOUDY, Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT); + // TODO FOCUS_MODE_FIXED is rarely supported. FOCUS.put(CameraConstants.FOCUS_FIXED, Camera.Parameters.FOCUS_MODE_FIXED); FOCUS.put(CameraConstants.FOCUS_CONTINUOUS, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); FOCUS.put(CameraConstants.FOCUS_TAP, Camera.Parameters.FOCUS_MODE_AUTO); diff --git a/cameraview/src/main/options/com/otaliastudios/cameraview/Focus.java b/cameraview/src/main/options/com/otaliastudios/cameraview/Focus.java index 820c5d3d..f1f09a83 100644 --- a/cameraview/src/main/options/com/otaliastudios/cameraview/Focus.java +++ b/cameraview/src/main/options/com/otaliastudios/cameraview/Focus.java @@ -12,5 +12,6 @@ import static com.otaliastudios.cameraview.CameraConstants.FOCUS_TAP_WITH_MARKER @Retention(RetentionPolicy.SOURCE) @IntDef({FOCUS_CONTINUOUS, FOCUS_TAP, FOCUS_FIXED, FOCUS_TAP_WITH_MARKER}) +@Deprecated public @interface Focus { } \ No newline at end of file diff --git a/cameraview/src/main/options/com/otaliastudios/cameraview/GestureAction.java b/cameraview/src/main/options/com/otaliastudios/cameraview/GestureAction.java new file mode 100644 index 00000000..e615c042 --- /dev/null +++ b/cameraview/src/main/options/com/otaliastudios/cameraview/GestureAction.java @@ -0,0 +1,14 @@ +package com.otaliastudios.cameraview; + +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static com.otaliastudios.cameraview.CameraConstants.*; + +@Retention(RetentionPolicy.SOURCE) +@IntDef({GESTURE_ACTION_NONE, GESTURE_ACTION_FOCUS, GESTURE_ACTION_FOCUS_WITH_MARKER, + GESTURE_ACTION_AE_CORRECTION, GESTURE_ACTION_CAPTURE, GESTURE_ACTION_ZOOM}) +public @interface GestureAction { +} diff --git a/cameraview/src/main/options/com/otaliastudios/cameraview/ZoomMode.java b/cameraview/src/main/options/com/otaliastudios/cameraview/ZoomMode.java index d51eb207..e657f77a 100644 --- a/cameraview/src/main/options/com/otaliastudios/cameraview/ZoomMode.java +++ b/cameraview/src/main/options/com/otaliastudios/cameraview/ZoomMode.java @@ -10,5 +10,6 @@ import static com.otaliastudios.cameraview.CameraConstants.ZOOM_PINCH; @Retention(RetentionPolicy.SOURCE) @IntDef({ZOOM_OFF, ZOOM_PINCH}) +@Deprecated public @interface ZoomMode { } diff --git a/cameraview/src/main/res/layout/layout_focus_marker.xml b/cameraview/src/main/res/layout/layout_focus_marker.xml index 81fdac7a..b16ee101 100644 --- a/cameraview/src/main/res/layout/layout_focus_marker.xml +++ b/cameraview/src/main/res/layout/layout_focus_marker.xml @@ -5,6 +5,7 @@ diff --git a/cameraview/src/main/res/values/attrs.xml b/cameraview/src/main/res/values/attrs.xml index 2ecffb0e..26db5d2e 100644 --- a/cameraview/src/main/res/values/attrs.xml +++ b/cameraview/src/main/res/values/attrs.xml @@ -3,6 +3,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/FocusMarkerLayout.java b/cameraview/src/main/views/com/otaliastudios/cameraview/FocusMarkerLayout.java index 207f0af9..56a162cf 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/FocusMarkerLayout.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/FocusMarkerLayout.java @@ -11,6 +11,7 @@ import android.view.MotionEvent; import android.widget.FrameLayout; import android.widget.ImageView; +@Deprecated class FocusMarkerLayout extends FrameLayout { private FrameLayout mFocusMarkerContainer; @@ -75,7 +76,6 @@ class FocusMarkerLayout extends FrameLayout { mFill.animate().alpha(0).setDuration(800).setListener(null).start(); } }).start(); - } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java b/cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java new file mode 100644 index 00000000..8906d874 --- /dev/null +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java @@ -0,0 +1,30 @@ +package com.otaliastudios.cameraview; + +import android.content.Context; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +/** + * Using Views to address the need to draw stuff, but think about it. + * Simple classes could be enough. + */ +abstract class GestureLayout extends FrameLayout { + + protected boolean mEnabled; + + public GestureLayout(Context context) { + super(context); + onInitialize(context); + } + + protected void onInitialize(Context context) { + } + + public void enable(boolean enable) { + mEnabled = enable; + } + + public abstract boolean onTouchEvent(MotionEvent event); +} diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/PinchGestureLayout.java b/cameraview/src/main/views/com/otaliastudios/cameraview/PinchGestureLayout.java new file mode 100644 index 00000000..8ba86a7b --- /dev/null +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/PinchGestureLayout.java @@ -0,0 +1,99 @@ +package com.otaliastudios.cameraview; + +import android.content.Context; +import android.graphics.PointF; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; + +class PinchGestureLayout extends GestureLayout { + + private final static float ADD_SENSITIVITY = 2f; + + private ScaleGestureDetector mDetector; + private boolean mNotify; + private float mAdditionFactor = 0; + private PointF[] mPoints = new PointF[]{ + new PointF(0, 0), + new PointF(0, 0) + }; + + + public PinchGestureLayout(Context context) { + super(context); + } + + + @Override + protected void onInitialize(Context context) { + super.onInitialize(context); + mDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() { + @Override + public boolean onScale(ScaleGestureDetector detector) { + mNotify = true; + mAdditionFactor = ((detector.getScaleFactor() - 1) * ADD_SENSITIVITY); + return true; + } + }); + + if (Build.VERSION.SDK_INT >= 19) { + mDetector.setQuickScaleEnabled(false); + } + } + + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!mEnabled) return false; + + // Reset the mNotify flag on a new gesture. + // This is to ensure that the mNotify flag stays on until the + // previous gesture ends. + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mNotify = false; + } + + // Let's see if we detect something. This will call onScale(). + mDetector.onTouchEvent(event); + + // Keep notifying CameraView as long as the gesture goes. + if (mNotify) { + mPoints[0].x = event.getX(0); + mPoints[0].y = event.getY(0); + if (event.getPointerCount() > 1) { + mPoints[1].x = event.getX(1); + mPoints[1].y = event.getY(1); + } + return true; + } + return false; + } + + public float scaleValue(float currValue, float minValue, float maxValue) { + float add = mAdditionFactor; + // ^ This works well if minValue = 0, maxValue = 1. + // Account for the different range: + add *= (maxValue - minValue); + + // ^ This works well if currValue = 0. + // Account for a different starting point: + /* if (add > 0) { + add *= (maxValue - currValue); + } else if (add < 0) { + add *= (currValue - minValue); + } Nope, I don't like this, it slows everything down. */ + + float newValue = currValue + add; + if (newValue < minValue) newValue = minValue; + if (newValue > maxValue) newValue = maxValue; + return newValue; + } + + public PointF[] getPoints() { + return mPoints; + } +} diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/PinchToZoomLayout.java b/cameraview/src/main/views/com/otaliastudios/cameraview/PinchToZoomLayout.java index a3bbb1b1..45b4412e 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/PinchToZoomLayout.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/PinchToZoomLayout.java @@ -10,6 +10,7 @@ import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; +@Deprecated class PinchToZoomLayout extends View { diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/TapGestureLayout.java b/cameraview/src/main/views/com/otaliastudios/cameraview/TapGestureLayout.java new file mode 100644 index 00000000..b552cb34 --- /dev/null +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/TapGestureLayout.java @@ -0,0 +1,166 @@ +package com.otaliastudios.cameraview; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.graphics.PointF; +import android.util.Log; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; + +class TapGestureLayout extends GestureLayout { + + + private GestureDetector mDetector; + private boolean mNotify; + private Gesture mType; + private PointF mPoint = new PointF(0, 0); + + private FrameLayout mFocusMarkerContainer; + private ImageView mFocusMarkerFill; + + public TapGestureLayout(Context context) { + super(context); + } + + + @Override + protected void onInitialize(Context context) { + super.onInitialize(context); + mDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + + @Override + public boolean onSingleTapUp(MotionEvent e) { + mNotify = true; + mType = Gesture.TAP; + return true; + } + + /* + TODO should use onSingleTapConfirmed and enable this. + public boolean onDoubleTap(MotionEvent e) { + mNotify = true; + mType = Gesture.DOUBLE_TAP; + return true; + } */ + + @Override + public void onLongPress(MotionEvent e) { + mNotify = true; + mType = Gesture.LONG_TAP; + } + }); + + mDetector.setIsLongpressEnabled(true); + + + // Views to draw the focus marker. + LayoutInflater.from(getContext()).inflate(R.layout.layout_focus_marker, this); + mFocusMarkerContainer = (FrameLayout) findViewById(R.id.focusMarkerContainer); + mFocusMarkerFill = (ImageView) findViewById(R.id.fill); + } + + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!mEnabled) return false; + + // Reset the mNotify flag on a new gesture. + // This is to ensure that the mNotify flag stays on until the + // previous gesture ends. + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mNotify = false; + } + + // Let's see if we detect something. + mDetector.onTouchEvent(event); + + // Keep notifying CameraView as long as the gesture goes. + if (mNotify) { + mPoint.x = event.getX(); + mPoint.y = event.getY(); + return true; + } + return false; + } + + + public Gesture getGestureType() { + return mType; + } + + + public PointF getPoint() { + return mPoint; + } + + + // Draw + + private final Runnable mFocusMarkerHideRunnable = new Runnable() { + @Override + public void run() { + onFocusEnd(false); + } + }; + + public void onFocusStart(PointF point) { + removeCallbacks(mFocusMarkerHideRunnable); + mFocusMarkerContainer.clearAnimation(); // animate().setListener(null).cancel(); + mFocusMarkerFill.clearAnimation(); // animate().setListener(null).cancel(); + + float x = (int) (point.x - mFocusMarkerContainer.getWidth() / 2); + float y = (int) (point.y - mFocusMarkerContainer.getWidth() / 2); + mFocusMarkerContainer.setTranslationX(x); + mFocusMarkerContainer.setTranslationY(y); + + mFocusMarkerContainer.setScaleX(1.36f); + mFocusMarkerContainer.setScaleY(1.36f); + mFocusMarkerContainer.setAlpha(1f); + mFocusMarkerFill.setScaleX(0); + mFocusMarkerFill.setScaleY(0); + mFocusMarkerFill.setAlpha(1f); + + // Since onFocusEnd is not guaranteed to be called, we post a hide runnable just in case. + animate(mFocusMarkerContainer, 1, 1, 300, 0, null); + animate(mFocusMarkerFill, 1, 1, 300, 0, new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + postDelayed(mFocusMarkerHideRunnable, 2000); + } + }); + } + + + public void onFocusEnd(boolean success) { + if (success) { + animate(mFocusMarkerContainer, 1, 0, 500, 0, null); + animate(mFocusMarkerFill, 1, 0, 500, 0, null); + } else { + animate(mFocusMarkerFill, 0, 0, 500, 0, null); + animate(mFocusMarkerContainer, 1.36f, 1, 500, 0, new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + animate(mFocusMarkerContainer, 1.36f, 0, 200, 1000, null); + } + }); + } + } + + + private static void animate(View view, float scale, float alpha, long duration, long delay, + Animator.AnimatorListener listener) { + view.animate().scaleX(scale).scaleY(scale) + .alpha(alpha) + .setDuration(duration) + .setStartDelay(delay) + .setListener(listener) + .start(); + } +} diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/MainActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/MainActivity.java index dd5b60a4..16ca25f9 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/MainActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/MainActivity.java @@ -75,8 +75,9 @@ public class MainActivity extends AppCompatActivity implements View.OnLayoutChan private boolean mCapturingPicture; private boolean mCapturingVideo; - private CameraListener mPictureListener; - private CameraListener mVideoListener; + // To show stuff in the callback + private Size mCaptureNativeSize; + private long mCaptureTime; @Override protected void onCreate(Bundle savedInstanceState) { @@ -92,8 +93,6 @@ public class MainActivity extends AppCompatActivity implements View.OnLayoutChan } }); - camera.addOnLayoutChangeListener(this); - sessionTypeRadioGroup.setOnCheckedChangeListener(sessionTypeChangedListener); cropModeRadioGroup.setOnCheckedChangeListener(cropModeChangedListener); widthModeRadioGroup.setOnCheckedChangeListener(widthModeChangedListener); @@ -101,6 +100,35 @@ public class MainActivity extends AppCompatActivity implements View.OnLayoutChan videoQualityRadioGroup.setOnCheckedChangeListener(videoQualityChangedListener); gridModeRadioGroup.setOnCheckedChangeListener(gridModeChangedListener); zoomModeRadioGroup.setOnCheckedChangeListener(zoomModeChangedListener); + + camera.addOnLayoutChangeListener(this); + camera.addCameraListener(new CameraListener() { + @Override + public void onPictureTaken(byte[] jpeg) { + super.onPictureTaken(jpeg); + mCapturingPicture = false; + long callbackTime = System.currentTimeMillis(); + if (mCapturingVideo) { + message("Captured while taking video. Size="+mCaptureNativeSize, false); + return; + } + PicturePreviewActivity.setImage(jpeg); + Intent intent = new Intent(MainActivity.this, PicturePreviewActivity.class); + intent.putExtra("delay", callbackTime - mCaptureTime); + intent.putExtra("nativeWidth", mCaptureNativeSize.getWidth()); + intent.putExtra("nativeHeight", mCaptureNativeSize.getHeight()); + startActivity(intent); + } + + @Override + public void onVideoTaken(File video) { + super.onVideoTaken(video); + mCapturingVideo = false; + Intent intent = new Intent(MainActivity.this, VideoPreviewActivity.class); + intent.putExtra("video", Uri.fromFile(video)); + startActivity(intent); + } + }); } private void message(String content, boolean important) { @@ -126,33 +154,14 @@ public class MainActivity extends AppCompatActivity implements View.OnLayoutChan camera.destroy(); } + @OnClick(R.id.capturePhoto) void capturePhoto() { if (mCapturingPicture) return; mCapturingPicture = true; - final long startTime = System.currentTimeMillis(); - final Size nativeSize = camera.getCaptureSize(); + mCaptureTime = System.currentTimeMillis(); + mCaptureNativeSize = camera.getCaptureSize(); message("Capturing picture...", false); - camera.removeCameraListener(mPictureListener); - mPictureListener = new CameraListener() { - @Override - public void onPictureTaken(byte[] jpeg) { - super.onPictureTaken(jpeg); - mCapturingPicture = false; - long callbackTime = System.currentTimeMillis(); - if (mCapturingVideo) { - message("Captured while taking video. Size="+nativeSize, false); - return; - } - PicturePreviewActivity.setImage(jpeg); - Intent intent = new Intent(MainActivity.this, PicturePreviewActivity.class); - intent.putExtra("delay", callbackTime-startTime); - intent.putExtra("nativeWidth", nativeSize.getWidth()); - intent.putExtra("nativeHeight", nativeSize.getHeight()); - startActivity(intent); - } - }; - camera.addCameraListener(mPictureListener); camera.capturePicture(); } @@ -164,18 +173,6 @@ public class MainActivity extends AppCompatActivity implements View.OnLayoutChan } if (mCapturingPicture || mCapturingVideo) return; mCapturingVideo = true; - camera.removeCameraListener(mVideoListener); - mVideoListener = new CameraListener() { - @Override - public void onVideoTaken(File video) { - super.onVideoTaken(video); - mCapturingVideo = false; - Intent intent = new Intent(MainActivity.this, VideoPreviewActivity.class); - intent.putExtra("video", Uri.fromFile(video)); - startActivity(intent); - } - }; - camera.addCameraListener(mVideoListener); message("Recording for 8 seconds...", true); camera.startCapturingVideo(null); camera.postDelayed(new Runnable() { diff --git a/demo/src/main/res/layout/activity_main.xml b/demo/src/main/res/layout/activity_main.xml index 8b2d74db..e7e4a796 100644 --- a/demo/src/main/res/layout/activity_main.xml +++ b/demo/src/main/res/layout/activity_main.xml @@ -20,19 +20,21 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> + android:id="@+id/camera" + android:layout_width="match_parent" + android:layout_height="400dp" + android:layout_gravity="center_horizontal" + android:keepScreenOn="true" + app:cameraZoomMode="off" + app:cameraGrid="off" + app:cameraCropOutput="false" + app:cameraFacing="back" + app:cameraFlash="off" + app:cameraGestureTap="focusWithMarker" + app:cameraGestureLongTap="none" + app:cameraGesturePinch="zoom" + app:cameraJpegQuality="100" + app:cameraSessionType="picture" />