From ecd2cdba134d04cf6af35ad0f2084fe06d595bae Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Mon, 24 Jun 2019 13:10:50 -0500 Subject: [PATCH] AutoFocusMarker (#484) * Create AutoFocusMarker and DefaultAutoFocusMarker * Ensure onFocusEnd is called * Add cameraAutoFocusMarker XML tag * Update docs * Fix changelog and migration guide * Fix tests --- .../cameraview/CameraOptions1Test.java | 5 +- .../cameraview/CameraViewCallbacksTest.java | 16 ++-- .../cameraview/CameraViewTest.java | 14 +--- .../cameraview/engine/IntegrationTest.java | 4 +- .../cameraview/CameraListener.java | 4 +- .../cameraview/CameraOptions.java | 5 +- .../otaliastudios/cameraview/CameraView.java | 46 ++++++++--- .../cameraview/engine/Camera1Engine.java | 40 +++++++--- .../cameraview/gesture/Gesture.java | 10 +-- .../cameraview/gesture/GestureAction.java | 19 ++--- .../cameraview/gesture/TapGestureLayout.java | 71 ----------------- .../cameraview/markers/AutoFocusMarker.java | 38 +++++++++ .../cameraview/markers/AutoFocusTrigger.java | 20 +++++ .../markers/DefaultAutoFocusMarker.java | 79 +++++++++++++++++++ .../cameraview/markers/Marker.java | 31 ++++++++ .../cameraview/markers/MarkerLayout.java | 69 ++++++++++++++++ .../cameraview/markers/MarkerParser.java | 41 ++++++++++ ...l.xml => cameraview_focus_marker_fill.xml} | 0 ...ml => cameraview_focus_marker_outline.xml} | 0 .../layout/cameraview_layout_focus_marker.xml | 40 ++++------ cameraview/src/main/res/values/attrs.xml | 24 +++--- cameraview/src/main/res/values/strings.xml | 4 + .../cameraview/demo/Control.java | 5 +- demo/src/main/res/layout/activity_camera.xml | 5 +- docs/_posts/2018-12-20-camera-events.md | 4 +- docs/_posts/2018-12-20-changelog.md | 11 ++- docs/_posts/2018-12-20-controls.md | 6 +- docs/_posts/2018-12-20-gestures.md | 12 +-- docs/_posts/2018-12-20-more-features.md | 18 +++++ docs/_posts/2018-12-20-v1-migration-guide.md | 15 +++- 30 files changed, 464 insertions(+), 192 deletions(-) create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusMarker.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusTrigger.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarker.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/markers/Marker.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/markers/MarkerLayout.java create mode 100644 cameraview/src/main/java/com/otaliastudios/cameraview/markers/MarkerParser.java rename cameraview/src/main/res/drawable/{focus_marker_fill.xml => cameraview_focus_marker_fill.xml} (100%) rename cameraview/src/main/res/drawable/{focus_marker_outline.xml => cameraview_focus_marker_outline.xml} (100%) create mode 100644 cameraview/src/main/res/values/strings.xml diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java index f40b8a0b..9d13e999 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java @@ -216,9 +216,8 @@ public class CameraOptions1Test extends BaseTest { when(params.getMinExposureCompensation()).thenReturn(0); CameraOptions o = new CameraOptions(params, false); - assertFalse(o.supports(GestureAction.FOCUS)); - assertFalse(o.supports(GestureAction.FOCUS_WITH_MARKER)); - assertTrue(o.supports(GestureAction.CAPTURE)); + assertFalse(o.supports(GestureAction.AUTO_FOCUS)); + assertTrue(o.supports(GestureAction.TAKE_PICTURE)); assertTrue(o.supports(GestureAction.NONE)); assertTrue(o.supports(GestureAction.ZOOM)); assertFalse(o.supports(GestureAction.EXPOSURE_CORRECTION)); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java index caf0b45e..7f4333f8 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java @@ -189,31 +189,31 @@ public class CameraViewCallbacksTest extends BaseTest { public void testDispatchOnFocusStart() { // Enable tap gesture. // Can't mock package protected. camera.mTapGestureLayout = mock(TapGestureLayout.class); - camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); + camera.mapGesture(Gesture.TAP, GestureAction.AUTO_FOCUS); PointF point = new PointF(); - completeTask().when(listener).onFocusStart(point); + completeTask().when(listener).onAutoFocusStart(point); camera.mCameraCallbacks.dispatchOnFocusStart(Gesture.TAP, point); assertNotNull(task.await(200)); - verify(listener, times(1)).onFocusStart(point); - // Can't mock package protected. verify(camera.mTapGestureLayout, times(1)).onFocusStart(point); + verify(listener, times(1)).onAutoFocusStart(point); + // Can't mock package protected. verify(camera.mTapGestureLayout, times(1)).onAutoFocusStart(point); } @Test public void testDispatchOnFocusEnd() { // Enable tap gesture. // Can't mock package protected. camera.mTapGestureLayout = mock(TapGestureLayout.class); - camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); + camera.mapGesture(Gesture.TAP, GestureAction.AUTO_FOCUS); PointF point = new PointF(); boolean success = true; - completeTask().when(listener).onFocusEnd(success, point); + completeTask().when(listener).onAutoFocusEnd(success, point); camera.mCameraCallbacks.dispatchOnFocusEnd(Gesture.TAP, success, point); assertNotNull(task.await(200)); - verify(listener, times(1)).onFocusEnd(success, point); - // Can't mock package protected. verify(camera.mTapGestureLayout, times(1)).onFocusEnd(success); + verify(listener, times(1)).onAutoFocusEnd(success, point); + // Can't mock package protected. verify(camera.mTapGestureLayout, times(1)).onAutoFocusEnd(success); } @Test diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index dc247caf..98bffc05 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -173,7 +173,7 @@ public class CameraViewTest extends BaseTest { assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.ZOOM); // Not assignable: This is like clearing - cameraView.mapGesture(Gesture.PINCH, GestureAction.CAPTURE); + cameraView.mapGesture(Gesture.PINCH, GestureAction.TAKE_PICTURE); assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.NONE); // Test clearing @@ -197,7 +197,7 @@ public class CameraViewTest extends BaseTest { assertFalse(cameraView.mPinchGestureLayout.isActive()); // TapGestureLayout - cameraView.mapGesture(Gesture.TAP, GestureAction.CAPTURE); + cameraView.mapGesture(Gesture.TAP, GestureAction.TAKE_PICTURE); assertTrue(cameraView.mTapGestureLayout.isActive()); cameraView.clearGesture(Gesture.TAP); assertFalse(cameraView.mPinchGestureLayout.isActive()); @@ -230,7 +230,7 @@ public class CameraViewTest extends BaseTest { }; } }); - cameraView.mapGesture(Gesture.TAP, GestureAction.CAPTURE); + cameraView.mapGesture(Gesture.TAP, GestureAction.TAKE_PICTURE); cameraView.dispatchTouchEvent(event); assertTrue(mockController.mPictureCaptured); } @@ -253,13 +253,7 @@ public class CameraViewTest extends BaseTest { } }); mockController.mFocusStarted = false; - cameraView.mapGesture(Gesture.TAP, GestureAction.FOCUS); - cameraView.dispatchTouchEvent(event); - assertTrue(mockController.mFocusStarted); - - // Try with FOCUS_WITH_MARKER - mockController.mFocusStarted = false; - cameraView.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); + cameraView.mapGesture(Gesture.TAP, GestureAction.AUTO_FOCUS); cameraView.dispatchTouchEvent(event); assertTrue(mockController.mFocusStarted); } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/IntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/IntegrationTest.java index 555611e1..e50fc87e 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/IntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/IntegrationTest.java @@ -19,8 +19,6 @@ import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.WhiteBalance; -import com.otaliastudios.cameraview.engine.Camera1Engine; -import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.FrameProcessor; import com.otaliastudios.cameraview.internal.utils.Task; @@ -466,7 +464,7 @@ public class IntegrationTest extends BaseTest { CameraOptions o = waitForOpen(true); final Task focus = new Task<>(true); - doEndTask(focus, 0).when(listener).onFocusStart(any(PointF.class)); + doEndTask(focus, 0).when(listener).onAutoFocusStart(any(PointF.class)); camera.startAutoFocus(1, 1); PointF point = focus.await(300); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java index ad80e405..8a51ef36 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java @@ -88,7 +88,7 @@ public abstract class CameraListener { * @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight() */ @UiThread - public void onFocusStart(@NonNull PointF point) { } + public void onAutoFocusStart(@NonNull PointF point) { } /** @@ -101,7 +101,7 @@ public abstract class CameraListener { * @param point coordinates with respect to CameraView.getWidth() and CameraView.getHeight() */ @UiThread - public void onFocusEnd(boolean successful, @NonNull PointF point) { } + public void onAutoFocusEnd(boolean successful, @NonNull PointF point) { } /** diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index 2d7bd434..2fcd1000 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -150,10 +150,9 @@ public class CameraOptions { */ public boolean supports(@NonNull GestureAction action) { switch (action) { - case FOCUS: - case FOCUS_WITH_MARKER: + case AUTO_FOCUS: return isAutoFocusSupported(); - case CAPTURE: + case TAKE_PICTURE: case NONE: return true; case ZOOM: diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 70e3416c..e5896c50 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -35,6 +35,7 @@ import com.otaliastudios.cameraview.controls.Control; import com.otaliastudios.cameraview.controls.ControlParser; import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Flash; +import com.otaliastudios.cameraview.markers.MarkerLayout; import com.otaliastudios.cameraview.engine.Camera1Engine; import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.frame.Frame; @@ -56,6 +57,7 @@ import com.otaliastudios.cameraview.internal.GridLinesLayout; import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.utils.OrientationHelper; import com.otaliastudios.cameraview.internal.utils.WorkerHandler; +import com.otaliastudios.cameraview.markers.MarkerParser; import com.otaliastudios.cameraview.preview.CameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.SurfaceCameraPreview; @@ -65,6 +67,8 @@ import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.SizeSelector; import com.otaliastudios.cameraview.size.SizeSelectorParser; import com.otaliastudios.cameraview.size.SizeSelectors; +import com.otaliastudios.cameraview.markers.AutoFocusMarker; +import com.otaliastudios.cameraview.markers.AutoFocusTrigger; import java.io.File; import java.util.ArrayList; @@ -103,6 +107,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { private OrientationHelper mOrientationHelper; private CameraEngine mCameraEngine; private MediaActionSound mSound; + private AutoFocusMarker mAutoFocusMarker; @VisibleForTesting List mListeners = new CopyOnWriteArrayList<>(); @VisibleForTesting List mFrameProcessors = new CopyOnWriteArrayList<>(); private Lifecycle mLifecycle; @@ -112,6 +117,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { PinchGestureLayout mPinchGestureLayout; TapGestureLayout mTapGestureLayout; ScrollGestureLayout mScrollGestureLayout; + private MarkerLayout mMarkerLayout; private boolean mKeepScreenOn; @SuppressWarnings({"FieldCanBeLocal", "unused"}) private boolean mExperimental; @@ -154,6 +160,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver { // Size selectors and gestures SizeSelectorParser sizeSelectors = new SizeSelectorParser(a); GestureParser gestures = new GestureParser(a); + MarkerParser markers = new MarkerParser(a); a.recycle(); @@ -168,10 +175,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver { mPinchGestureLayout = new PinchGestureLayout(context); mTapGestureLayout = new TapGestureLayout(context); mScrollGestureLayout = new ScrollGestureLayout(context); + mMarkerLayout = new MarkerLayout(context); addView(mGridLinesLayout); addView(mPinchGestureLayout); addView(mTapGestureLayout); addView(mScrollGestureLayout); + addView(mMarkerLayout); // Apply self managed setPlaySounds(playSounds); @@ -201,6 +210,9 @@ public class CameraView extends FrameLayout implements LifecycleObserver { mapGesture(Gesture.SCROLL_HORIZONTAL, gestures.getHorizontalScrollAction()); mapGesture(Gesture.SCROLL_VERTICAL, gestures.getVerticalScrollAction()); + // Apply markers + setAutoFocusMarker(markers.getAutoFocusMarker()); + if (!isInEditMode()) { mOrientationHelper = new OrientationHelper(context, mCameraCallbacks); } @@ -527,12 +539,11 @@ public class CameraView extends FrameLayout implements LifecycleObserver { //noinspection ConstantConditions switch (action) { - case CAPTURE: + case TAKE_PICTURE: takePicture(); break; - case FOCUS: - case FOCUS_WITH_MARKER: + case AUTO_FOCUS: mCameraEngine.startAutoFocus(gesture, points[0]); break; @@ -1041,6 +1052,18 @@ public class CameraView extends FrameLayout implements LifecycleObserver { } + /** + * Sets an {@link AutoFocusMarker} to be notified of autofocus start, end and fail events + * so that it can draw elements on screen. + * + * @param autoFocusMarker the marker, or null + */ + public void setAutoFocusMarker(@Nullable AutoFocusMarker autoFocusMarker) { + mAutoFocusMarker = autoFocusMarker; + mMarkerLayout.onMarker(MarkerLayout.TYPE_AUTOFOCUS, autoFocusMarker); + } + + /** * Sets the current delay in milliseconds to reset the focus after an autofocus process. * @@ -1721,12 +1744,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver { mUiHandler.post(new Runnable() { @Override public void run() { - if (gesture != null && mGestureMap.get(gesture) == GestureAction.FOCUS_WITH_MARKER) { - mTapGestureLayout.onFocusStart(point); + mMarkerLayout.onEvent(MarkerLayout.TYPE_AUTOFOCUS, new PointF[]{ point }); + if (mAutoFocusMarker != null) { + AutoFocusTrigger trigger = gesture != null ? + AutoFocusTrigger.GESTURE : AutoFocusTrigger.METHOD; + mAutoFocusMarker.onAutoFocusStart(trigger, point); } for (CameraListener listener : mListeners) { - listener.onFocusStart(point); + listener.onAutoFocusStart(point); } } }); @@ -1744,12 +1770,14 @@ public class CameraView extends FrameLayout implements LifecycleObserver { playSound(MediaActionSound.FOCUS_COMPLETE); } - if (gesture != null && mGestureMap.get(gesture) == GestureAction.FOCUS_WITH_MARKER) { - mTapGestureLayout.onFocusEnd(success); + if (mAutoFocusMarker != null) { + AutoFocusTrigger trigger = gesture != null ? + AutoFocusTrigger.GESTURE : AutoFocusTrigger.METHOD; + mAutoFocusMarker.onAutoFocusEnd(trigger, success, point); } for (CameraListener listener : mListeners) { - listener.onFocusEnd(success, point); + listener.onAutoFocusEnd(success, point); } } }); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java index 46c249d2..c9619453 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java @@ -52,11 +52,13 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac private static final String TAG = Camera1Engine.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); + private static final int AUTOFOCUS_END_DELAY_MILLIS = 2500; private Camera mCamera; private boolean mIsBound = false; - private Runnable mPostFocusResetRunnable = new Runnable() { + private Runnable mFocusEndRunnable; + private final Runnable mFocusResetRunnable = new Runnable() { @Override public void run() { if (!isCameraAvailable()) return; @@ -307,7 +309,10 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac @Override protected void onStop() { LOG.i("onStop:", "About to clean up."); - mHandler.get().removeCallbacks(mPostFocusResetRunnable); + mHandler.get().removeCallbacks(mFocusResetRunnable); + if (mFocusEndRunnable != null) { + mHandler.get().removeCallbacks(mFocusEndRunnable); + } if (mVideoRecorder != null) { mVideoRecorder.stop(); mVideoRecorder = null; @@ -911,24 +916,41 @@ public class Camera1Engine extends CameraEngine implements Camera.PreviewCallbac params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); mCamera.setParameters(params); mCallback.dispatchOnFocusStart(gesture, p); - // TODO this is not guaranteed to be called... Fix. + + // The auto focus callback is not guaranteed to be called, but we really want it to be. + // So we remove the old runnable if still present and post a new one. + if (mFocusEndRunnable != null) mHandler.get().removeCallbacks(mFocusEndRunnable); + mFocusEndRunnable = new Runnable() { + @Override + public void run() { + if (isCameraAvailable()) { + mCallback.dispatchOnFocusEnd(gesture, false, p); + } + } + }; + mHandler.get().postDelayed(mFocusEndRunnable, AUTOFOCUS_END_DELAY_MILLIS); + + // Wrapping autoFocus in a try catch to handle some device specific exceptions, + // see See https://github.com/natario1/CameraView/issues/181. try { mCamera.autoFocus(new Camera.AutoFocusCallback() { @Override public void onAutoFocus(boolean success, Camera camera) { - // TODO lock auto exposure and white balance for a while + if (mFocusEndRunnable != null) { + mHandler.get().removeCallbacks(mFocusEndRunnable); + mFocusEndRunnable = null; + } mCallback.dispatchOnFocusEnd(gesture, success, p); - mHandler.get().removeCallbacks(mPostFocusResetRunnable); + mHandler.get().removeCallbacks(mFocusResetRunnable); if (shouldResetAutoFocus()) { - mHandler.get().postDelayed(mPostFocusResetRunnable, getAutoFocusResetDelay()); + mHandler.get().postDelayed(mFocusResetRunnable, getAutoFocusResetDelay()); } } }); } catch (RuntimeException e) { - // Handling random auto-focus exception on some devices - // See https://github.com/natario1/CameraView/issues/181 LOG.e("startAutoFocus:", "Error calling autoFocus", e); - mCallback.dispatchOnFocusEnd(gesture, false, p); + // Let the mFocusEndRunnable do its job. (could remove it and quickly dispatch + // onFocusEnd here, but let's make it simpler). } } }); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java index 26420628..0599f60f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java @@ -30,9 +30,8 @@ public enum Gesture { * Single tap gesture, typically assigned to the focus control. * This gesture can be mapped to one shot actions: * - * - {@link GestureAction#FOCUS} - * - {@link GestureAction#FOCUS_WITH_MARKER} - * - {@link GestureAction#CAPTURE} + * - {@link GestureAction#AUTO_FOCUS} + * - {@link GestureAction#TAKE_PICTURE} * - {@link GestureAction#NONE} */ TAP(GestureType.ONE_SHOT), @@ -41,9 +40,8 @@ public enum Gesture { * Long tap gesture. * This gesture can be mapped to one shot actions: * - * - {@link GestureAction#FOCUS} - * - {@link GestureAction#FOCUS_WITH_MARKER} - * - {@link GestureAction#CAPTURE} + * - {@link GestureAction#AUTO_FOCUS} + * - {@link GestureAction#TAKE_PICTURE} * - {@link GestureAction#NONE} */ LONG_TAP(GestureType.ONE_SHOT), diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java index 453674d5..61294b96 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java @@ -2,6 +2,7 @@ package com.otaliastudios.cameraview.gesture; import com.otaliastudios.cameraview.CameraView; +import com.otaliastudios.cameraview.markers.AutoFocusMarker; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -27,18 +28,10 @@ public enum GestureAction { * * - {@link Gesture#TAP} * - {@link Gesture#LONG_TAP} - */ - FOCUS(1, GestureType.ONE_SHOT), - - /** - * Auto focus control, typically assigned to the tap gesture. - * On top of {@link #FOCUS}, this will draw a default marker on screen. - * This action can be mapped to one shot gestures: * - * - {@link Gesture#TAP} - * - {@link Gesture#LONG_TAP} + * To control marker drawing, please see {@link CameraView#setAutoFocusMarker(AutoFocusMarker)} */ - FOCUS_WITH_MARKER(2, GestureType.ONE_SHOT), + AUTO_FOCUS(1, GestureType.ONE_SHOT), /** * When triggered, this action will fire a picture shoot. @@ -47,7 +40,7 @@ public enum GestureAction { * - {@link Gesture#TAP} * - {@link Gesture#LONG_TAP} */ - CAPTURE(3, GestureType.ONE_SHOT), + TAKE_PICTURE(2, GestureType.ONE_SHOT), /** * Zoom control, typically assigned to the pinch gesture. @@ -57,7 +50,7 @@ public enum GestureAction { * - {@link Gesture#SCROLL_HORIZONTAL} * - {@link Gesture#SCROLL_VERTICAL} */ - ZOOM(4, GestureType.CONTINUOUS), + ZOOM(3, GestureType.CONTINUOUS), /** * Exposure correction control. @@ -67,7 +60,7 @@ public enum GestureAction { * - {@link Gesture#SCROLL_HORIZONTAL} * - {@link Gesture#SCROLL_VERTICAL} */ - EXPOSURE_CORRECTION(5, GestureType.CONTINUOUS); + EXPOSURE_CORRECTION(4, GestureType.CONTINUOUS); final static GestureAction DEFAULT_PINCH = NONE; diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureLayout.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureLayout.java index ddf40bd2..2e37785f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureLayout.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureLayout.java @@ -24,9 +24,6 @@ public class TapGestureLayout extends GestureLayout { private GestureDetector mDetector; private boolean mNotify; - private FrameLayout mFocusMarkerContainer; - private ImageView mFocusMarkerFill; - public TapGestureLayout(@NonNull Context context) { super(context, 1); mDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { @@ -54,12 +51,6 @@ public class TapGestureLayout extends GestureLayout { }); mDetector.setIsLongpressEnabled(true); - - - // Views to draw the focus marker. - LayoutInflater.from(getContext()).inflate(R.layout.cameraview_layout_focus_marker, this); - mFocusMarkerContainer = findViewById(R.id.focusMarkerContainer); - mFocusMarkerFill = findViewById(R.id.fill); } @Override @@ -88,66 +79,4 @@ public class TapGestureLayout extends GestureLayout { return 0; } -// Draw - - private final Runnable mFocusMarkerHideRunnable = new Runnable() { - @Override - public void run() { - onFocusEnd(false); - } - }; - - public void onFocusStart(@NonNull 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(@NonNull View view, float scale, float alpha, long duration, long delay, - @Nullable Animator.AnimatorListener listener) { - view.animate().scaleX(scale).scaleY(scale) - .alpha(alpha) - .setDuration(duration) - .setStartDelay(delay) - .setListener(listener) - .start(); - } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusMarker.java b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusMarker.java new file mode 100644 index 00000000..35f5f9de --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusMarker.java @@ -0,0 +1,38 @@ +package com.otaliastudios.cameraview.markers; + +import android.graphics.PointF; + +import com.otaliastudios.cameraview.CameraView; + +import androidx.annotation.NonNull; + +/** + * A marker for the autofocus operations. Receives callback when focus starts, + * ends successfully or failed, and can be used to draw on screen. + * + * The point coordinates are meant with respect to {@link CameraView} width and height, + * so a 0, 0 point means that focus is happening on the top-left visible corner. + */ +public interface AutoFocusMarker extends Marker { + + /** + * Called when the autofocus process has started. + * + * @param trigger the autofocus trigger + * @param point coordinates + */ + void onAutoFocusStart(@NonNull AutoFocusTrigger trigger, @NonNull PointF point); + + + /** + * Called when the autofocus process has ended, and the camera converged + * to a new focus or failed while trying to do so. + * + * @param trigger the autofocus trigger + * @param successful whether the operation succeeded + * @param point coordinates + */ + void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, boolean successful, @NonNull PointF point); + + +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusTrigger.java b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusTrigger.java new file mode 100644 index 00000000..8303aa79 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusTrigger.java @@ -0,0 +1,20 @@ +package com.otaliastudios.cameraview.markers; + +import com.otaliastudios.cameraview.CameraView; +import com.otaliastudios.cameraview.gesture.GestureAction; + +/** + * Gives information about what triggered the autofocus operation. + */ +public enum AutoFocusTrigger { + + /** + * Autofocus was triggered by {@link GestureAction#AUTO_FOCUS}. + */ + GESTURE, + + /** + * Autofocus was triggered by the {@link CameraView#startAutoFocus(float, float)} method. + */ + METHOD +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarker.java b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarker.java new file mode 100644 index 00000000..ef7b5ffa --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarker.java @@ -0,0 +1,79 @@ +package com.otaliastudios.cameraview.markers; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.graphics.PointF; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.otaliastudios.cameraview.R; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * A default implementation of {@link AutoFocusMarker}. + * You can call {@link com.otaliastudios.cameraview.CameraView#setAutoFocusMarker(AutoFocusMarker)} + * passing in this class to have basic marker drawing. + */ +public class DefaultAutoFocusMarker implements AutoFocusMarker { + + private View mContainer; + private View mFill; + + @Nullable + @Override + public View onAttach(@NonNull Context context, @NonNull ViewGroup container) { + View view = LayoutInflater.from(context).inflate(R.layout.cameraview_layout_focus_marker, container, false); + mContainer = view.findViewById(R.id.focusMarkerContainer); + mFill = view.findViewById(R.id.focusMarkerFill); + return view; + } + + @Override + public void onAutoFocusStart(@NonNull AutoFocusTrigger trigger, @NonNull PointF point) { + if (trigger == AutoFocusTrigger.METHOD) return; + mContainer.clearAnimation(); + mFill.clearAnimation(); + mContainer.setScaleX(1.36f); + mContainer.setScaleY(1.36f); + mContainer.setAlpha(1f); + mFill.setScaleX(0); + mFill.setScaleY(0); + mFill.setAlpha(1f); + animate(mContainer, 1, 1, 300, 0, null); + animate(mFill, 1, 1, 300, 0, null); + } + + @Override + public void onAutoFocusEnd(@NonNull AutoFocusTrigger trigger, boolean successful, @NonNull PointF point) { + if (trigger == AutoFocusTrigger.METHOD) return; + if (successful) { + animate(mContainer, 1, 0, 500, 0, null); + animate(mFill, 1, 0, 500, 0, null); + } else { + animate(mFill, 0, 0, 500, 0, null); + animate(mContainer, 1.36f, 1, 500, 0, new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + animate(mContainer, 1.36f, 0, 200, 1000, null); + } + }); + } + } + + private static void animate(@NonNull View view, float scale, float alpha, long duration, + long delay, @Nullable Animator.AnimatorListener listener) { + view.animate() + .scaleX(scale) + .scaleY(scale) + .alpha(alpha) + .setDuration(duration) + .setStartDelay(delay) + .setListener(listener) + .start(); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/markers/Marker.java b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/Marker.java new file mode 100644 index 00000000..db3baa0e --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/Marker.java @@ -0,0 +1,31 @@ +package com.otaliastudios.cameraview.markers; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import com.otaliastudios.cameraview.CameraView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * A marker is an overlay over the {@link CameraView} preview, which should be drawn + * at specific times during the camera lifecycle. + * Currently only {@link AutoFocusMarker} is available. + */ +public interface Marker { + + /** + * Marker is being attached to the CameraView. If a {@link View} is returned, + * it becomes part of the hierarchy and is automatically translated (if possible) + * to match the event place on screen, for example the point where autofocus was started + * by the user finger. + * + * @param context a context + * @param container a container + * @return a view or null + */ + @Nullable + View onAttach(@NonNull Context context, @NonNull ViewGroup container); +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/markers/MarkerLayout.java b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/MarkerLayout.java new file mode 100644 index 00000000..8c2d8c97 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/MarkerLayout.java @@ -0,0 +1,69 @@ +package com.otaliastudios.cameraview.markers; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.PointF; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import java.util.HashMap; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Manages markers and provides an hierarchy / Canvas for them. + * It is responsible for calling {@link Marker#onAttach(Context, ViewGroup)}. + */ +public final class MarkerLayout extends FrameLayout { + + public final static int TYPE_AUTOFOCUS = 1; + + @SuppressLint("UseSparseArrays") + private final HashMap mViews = new HashMap<>(); + + public MarkerLayout(@NonNull Context context) { + super(context); + } + + /** + * Notifies that a new marker was added, possibly replacing another one. + * @param type the marker type + * @param marker the marker + */ + public void onMarker(int type, @Nullable Marker marker) { + // First check if we have a view for a previous marker of this type. + View oldView = mViews.get(type); + if (oldView != null) removeView(oldView); + // If new marker is null, we're done. + if (marker == null) return; + // Now see if we have a new view. + View newView = marker.onAttach(getContext(), this); + if (newView != null) { + mViews.put(type, newView); + addView(newView); + } + } + + /** + * The event that should trigger the drawing is about to be dispatched to + * markers. If we have a valid View, cancel any animations on it and reposition + * it. + * @param type the event type + * @param points the position + */ + public void onEvent(int type, @NonNull PointF[] points) { + View view = mViews.get(type); + if (view == null) return; + view.clearAnimation(); + if (type == TYPE_AUTOFOCUS) { + // TODO can't be sure that getWidth and getHeight are available here. + PointF point = points[0]; + float x = (int) (point.x - view.getWidth() / 2); + float y = (int) (point.y - view.getHeight() / 2); + view.setTranslationX(x); + view.setTranslationY(y); + } + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/markers/MarkerParser.java b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/MarkerParser.java new file mode 100644 index 00000000..a478617f --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/markers/MarkerParser.java @@ -0,0 +1,41 @@ +package com.otaliastudios.cameraview.markers; + +import android.content.Context; +import android.content.res.TypedArray; + +import com.otaliastudios.cameraview.R; +import com.otaliastudios.cameraview.controls.Audio; +import com.otaliastudios.cameraview.controls.Facing; +import com.otaliastudios.cameraview.controls.Flash; +import com.otaliastudios.cameraview.controls.Grid; +import com.otaliastudios.cameraview.controls.Hdr; +import com.otaliastudios.cameraview.controls.Mode; +import com.otaliastudios.cameraview.controls.Preview; +import com.otaliastudios.cameraview.controls.VideoCodec; +import com.otaliastudios.cameraview.controls.WhiteBalance; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Parses markers from XML attributes. + */ +public class MarkerParser { + + private AutoFocusMarker autoFocusMarker = null; + + public MarkerParser(@NonNull TypedArray array) { + String autoFocusName = array.getString(R.styleable.CameraView_cameraAutoFocusMarker); + if (autoFocusName != null) { + try { + Class autoFocusClass = Class.forName(autoFocusName); + autoFocusMarker = (AutoFocusMarker) autoFocusClass.newInstance(); + } catch (Exception ignore) { } + } + } + + @Nullable + public AutoFocusMarker getAutoFocusMarker() { + return autoFocusMarker; + } +} diff --git a/cameraview/src/main/res/drawable/focus_marker_fill.xml b/cameraview/src/main/res/drawable/cameraview_focus_marker_fill.xml similarity index 100% rename from cameraview/src/main/res/drawable/focus_marker_fill.xml rename to cameraview/src/main/res/drawable/cameraview_focus_marker_fill.xml diff --git a/cameraview/src/main/res/drawable/focus_marker_outline.xml b/cameraview/src/main/res/drawable/cameraview_focus_marker_outline.xml similarity index 100% rename from cameraview/src/main/res/drawable/focus_marker_outline.xml rename to cameraview/src/main/res/drawable/cameraview_focus_marker_outline.xml diff --git a/cameraview/src/main/res/layout/cameraview_layout_focus_marker.xml b/cameraview/src/main/res/layout/cameraview_layout_focus_marker.xml index b16ee101..77f21428 100644 --- a/cameraview/src/main/res/layout/cameraview_layout_focus_marker.xml +++ b/cameraview/src/main/res/layout/cameraview_layout_focus_marker.xml @@ -1,25 +1,17 @@ - - - - - - - - - - - \ No newline at end of file + + + + diff --git a/cameraview/src/main/res/values/attrs.xml b/cameraview/src/main/res/values/attrs.xml index c687a7d0..eac133e8 100644 --- a/cameraview/src/main/res/values/attrs.xml +++ b/cameraview/src/main/res/values/attrs.xml @@ -30,34 +30,32 @@ - - - + + - - - + + - - + + - - + + - - + + @@ -126,5 +124,7 @@ + + \ No newline at end of file diff --git a/cameraview/src/main/res/values/strings.xml b/cameraview/src/main/res/values/strings.xml new file mode 100644 index 00000000..dd25e92b --- /dev/null +++ b/cameraview/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker + \ No newline at end of file diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/Control.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/Control.java index be88a3ee..6910362c 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/Control.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/Control.java @@ -100,9 +100,8 @@ public enum Control { case LONG_TAP: ArrayList list2 = new ArrayList<>(); addIfSupported(options, list2, GestureAction.NONE); - addIfSupported(options, list2, GestureAction.CAPTURE); - addIfSupported(options, list2, GestureAction.FOCUS); - addIfSupported(options, list2, GestureAction.FOCUS_WITH_MARKER); + addIfSupported(options, list2, GestureAction.TAKE_PICTURE); + addIfSupported(options, list2, GestureAction.AUTO_FOCUS); return list2; case GRID_COLOR: ArrayList list3 = new ArrayList<>(); diff --git a/demo/src/main/res/layout/activity_camera.xml b/demo/src/main/res/layout/activity_camera.xml index 91c43953..2c30c12d 100644 --- a/demo/src/main/res/layout/activity_camera.xml +++ b/demo/src/main/res/layout/activity_camera.xml @@ -21,12 +21,13 @@ app:cameraGrid="off" app:cameraFlash="off" app:cameraAudio="on" - app:cameraGestureTap="focusWithMarker" + app:cameraGestureTap="autoFocus" app:cameraGestureLongTap="none" app:cameraGesturePinch="zoom" app:cameraGestureScrollHorizontal="exposureCorrection" app:cameraGestureScrollVertical="none" - app:cameraMode="picture" /> + app:cameraMode="picture" + app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker"/> ``` diff --git a/docs/_posts/2018-12-20-more-features.md b/docs/_posts/2018-12-20-more-features.md index 0cbc969e..084676c9 100644 --- a/docs/_posts/2018-12-20-more-features.md +++ b/docs/_posts/2018-12-20-more-features.md @@ -57,6 +57,24 @@ cameraView.setGridColor(Color.WHITE); cameraView.setGridColor(Color.BLACK); ``` +##### cameraAutoFocusMarker + +Lets you set a marker for drawing on screen in response to auto focus events. +In XML, you should pass the qualified class name of your marker. + +```java +cameraView.setAutoFocusMarker(null); +cameraView.setAutoFocusMarker(marker); +``` + +We offer a default marker (similar to the old `focusWithMarker` attribute in v1), +which you can set in XML using the `@string/cameraview_default_autofocus_marker` resource, +or programmatically: + +```java +cameraView.setAutoFocusMarker(new DefaultAutoFocusMarker()); +``` + ##### cameraAutoFocusResetDelay Lets you control how an auto-focus operation is reset after completed. diff --git a/docs/_posts/2018-12-20-v1-migration-guide.md b/docs/_posts/2018-12-20-v1-migration-guide.md index 80aa6bdf..b2f98f7a 100644 --- a/docs/_posts/2018-12-20-v1-migration-guide.md +++ b/docs/_posts/2018-12-20-v1-migration-guide.md @@ -191,6 +191,19 @@ way for the future. These changes are listed below: |`SizeSelectors`|`com.otaliastudios.cameraview`|`com.otaliastudios.cameraview.size`| |`AspectRatio`|`com.otaliastudios.cameraview`|`com.otaliastudios.cameraview.size`| +### AutoFocus changes + +First, the listener methods `onFocusStart` and `onFocusEnd` are now called `onAutoFocusStart` and `onAutoFocusEnd`. + +Secondly, and most importantly, the gesture actions `focus` and `focusWithMarker` have been removed +and replaced by `autoFocus`, which shows no marker. A new API, called `setAutoFocusMarker()`, has been +added and can be used, if needed, to add back the old marker. + +|Old gesture action|New gesture action|Extra steps| +|------------------|------------------|-----------| +|`GestureAction.FOCUS`|`GestureAction.AUTO_FOCUS`|None| +|`GestureAction.FOCUS_WITH_MARKER`|`GestureAction.AUTO_FOCUS`|You can use `app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker"` in XML or `cameraView.setAutoFocusMarker(new DefaultAutoFocusMarker())` to use the default marker.| + ### Other improvements & changes - Added `@Nullable` and `@NonNull` annotations pretty much everywhere. This might **break** your Kotlin build. @@ -198,5 +211,3 @@ way for the future. These changes are listed below: - Default `Facing` value is not `BACK` anymore but rather a value that guarantees that you have cameras (if possible). If device has no `BACK` cameras, defaults to `FRONT`. - Removed `ExtraProperties` as it was useless. - -TODO: improve the focus marker drawing, move out of XML (accept a drawable?)