AutoFocusMarker (#484)

* Create AutoFocusMarker and DefaultAutoFocusMarker

* Ensure onFocusEnd is called

* Add cameraAutoFocusMarker XML tag

* Update docs

* Fix changelog and migration guide

* Fix tests
pull/488/head
Mattia Iavarone 5 years ago committed by GitHub
parent cd5f0a12bf
commit ecd2cdba13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java
  2. 16
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
  3. 14
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  4. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/IntegrationTest.java
  5. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
  6. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  7. 46
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  8. 40
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  9. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java
  10. 19
      cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java
  11. 71
      cameraview/src/main/java/com/otaliastudios/cameraview/gesture/TapGestureLayout.java
  12. 38
      cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusMarker.java
  13. 20
      cameraview/src/main/java/com/otaliastudios/cameraview/markers/AutoFocusTrigger.java
  14. 79
      cameraview/src/main/java/com/otaliastudios/cameraview/markers/DefaultAutoFocusMarker.java
  15. 31
      cameraview/src/main/java/com/otaliastudios/cameraview/markers/Marker.java
  16. 69
      cameraview/src/main/java/com/otaliastudios/cameraview/markers/MarkerLayout.java
  17. 41
      cameraview/src/main/java/com/otaliastudios/cameraview/markers/MarkerParser.java
  18. 0
      cameraview/src/main/res/drawable/cameraview_focus_marker_fill.xml
  19. 0
      cameraview/src/main/res/drawable/cameraview_focus_marker_outline.xml
  20. 40
      cameraview/src/main/res/layout/cameraview_layout_focus_marker.xml
  21. 24
      cameraview/src/main/res/values/attrs.xml
  22. 4
      cameraview/src/main/res/values/strings.xml
  23. 5
      demo/src/main/java/com/otaliastudios/cameraview/demo/Control.java
  24. 5
      demo/src/main/res/layout/activity_camera.xml
  25. 4
      docs/_posts/2018-12-20-camera-events.md
  26. 11
      docs/_posts/2018-12-20-changelog.md
  27. 6
      docs/_posts/2018-12-20-controls.md
  28. 12
      docs/_posts/2018-12-20-gestures.md
  29. 18
      docs/_posts/2018-12-20-more-features.md
  30. 15
      docs/_posts/2018-12-20-v1-migration-guide.md

@ -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));

@ -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

@ -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);
}

@ -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<PointF> 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);

@ -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) { }
/**

@ -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:

@ -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<CameraListener> mListeners = new CopyOnWriteArrayList<>();
@VisibleForTesting List<FrameProcessor> 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);
}
}
});

@ -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).
}
}
});

@ -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),

@ -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;

@ -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();
}
}

@ -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);
}

@ -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
}

@ -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();
}
}

@ -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);
}

@ -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<Integer, View> 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);
}
}
}

@ -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;
}
}

@ -1,25 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/focusMarkerContainer"
android:alpha="0"
android:layout_width="55dp"
android:layout_height="55dp">
<ImageView
android:id="@+id/fill"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/focus_marker_fill" />
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/focus_marker_outline" />
</FrameLayout>
</FrameLayout>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/focusMarkerContainer"
android:alpha="0"
android:layout_width="55dp"
android:layout_height="55dp">
<ImageView
android:id="@+id/focusMarkerFill"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/cameraview_focus_marker_fill" />
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/cameraview_focus_marker_outline" />
</FrameLayout>

@ -30,34 +30,32 @@
<attr name="cameraGestureTap" format="enum">
<enum name="none" value="0" />
<enum name="focus" value="1" />
<enum name="focusWithMarker" value="2" />
<enum name="capture" value="3" />
<enum name="autoFocus" value="1" />
<enum name="takePicture" value="2" />
</attr>
<attr name="cameraGestureLongTap" format="enum">
<enum name="none" value="0" />
<enum name="focus" value="1" />
<enum name="focusWithMarker" value="2" />
<enum name="capture" value="3" />
<enum name="autoFocus" value="1" />
<enum name="takePicture" value="2" />
</attr>
<attr name="cameraGesturePinch" format="enum">
<enum name="none" value="0" />
<enum name="zoom" value="4" />
<enum name="exposureCorrection" value="5" />
<enum name="zoom" value="3" />
<enum name="exposureCorrection" value="4" />
</attr>
<attr name="cameraGestureScrollHorizontal" format="enum">
<enum name="none" value="0" />
<enum name="zoom" value="4" />
<enum name="exposureCorrection" value="5" />
<enum name="zoom" value="3" />
<enum name="exposureCorrection" value="4" />
</attr>
<attr name="cameraGestureScrollVertical" format="enum">
<enum name="none" value="0" />
<enum name="zoom" value="4" />
<enum name="exposureCorrection" value="5" />
<enum name="zoom" value="3" />
<enum name="exposureCorrection" value="4" />
</attr>
<attr name="cameraFacing" format="enum">
@ -126,5 +124,7 @@
<attr name="cameraAutoFocusResetDelay" format="integer|reference"/>
<attr name="cameraAutoFocusMarker" format="string|reference"/>
</declare-styleable>
</resources>

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cameraview_default_autofocus_marker">com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker</string>
</resources>

@ -100,9 +100,8 @@ public enum Control {
case LONG_TAP:
ArrayList<GestureAction> 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<GridColor> list3 = new ArrayList<>();

@ -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"/>
<ImageButton

@ -30,9 +30,9 @@ camera.addCameraListener(new CameraListener() {
public void onOrientationChanged(int orientation) {}
public void onFocusStart(PointF point) {}
public void onAutoFocusStart(PointF point) {}
public void onFocusEnd(boolean successful, PointF point) {}
public void onAutoFocusEnd(boolean successful, PointF point) {}
public void onZoomChanged(float newValue, float[] bounds, PointF[] fingers) {}

@ -12,6 +12,14 @@ New versions are released through GitHub, so the reference page is the [GitHub R
- Improvement: we now choose a video recording profile that is compatible with the chosen size. Should fix some video recording issues. ([#477][477])
- **Breaking change**: some public classes have been moved to different packages. See [table here](../extra/v1-migration-guide.html#repackaging). ([#482][482])
- **Breaking change**: the listener methods `onFocusStart` and `onFocusEnd` are now called `onAutoFocusStart` and `onAutoFocusEnd`. ([#484][484])
- **Breaking change**: the gesture actions `focus` and `focusWithMarker` have been removed and replaced by `autoFocus`, which shows no marker. ([#484][484])
- New: new API called `setAutoFocusMarker()` lets you choose your own marker. ([#484][484])
If you were using `focus`, just switch to `autoFocus`.
If you were using `focusWithMarker`, you can [add back the old marker](../docs/more-features.html#cameraautofocusmarker).
### v2.0.0-beta05
@ -52,4 +60,5 @@ This is the first beta release. For changes with respect to v1, please take a lo
[403]: https://github.com/natario1/CameraView/pull/403
[435]: https://github.com/natario1/CameraView/pull/435
[477]: https://github.com/natario1/CameraView/pull/477
[482]: https://github.com/natario1/CameraView/pull/482
[482]: https://github.com/natario1/CameraView/pull/482
[484]: https://github.com/natario1/CameraView/pull/484

@ -155,17 +155,17 @@ The last two actions will trigger the focus callbacks:
cameraView.addCameraListener(new CameraListener() {
@Override
public void onFocusStart(@NonNull PointF point) {
public void onAutoFocusStart(@NonNull PointF point) {
// Auto focus was started by a gesture or by startAutoFocus(float, float).
// The camera is currently trying to focus around that area.
// This can be used to draw things on screen.
}
@Override
public void onFocusEnd(boolean successful, @NonNull PointF point) {
public void onAutoFocusEnd(boolean successful, @NonNull PointF point) {
// Auto focus operation just ended. If successful, the camera will have converged
// to a new focus point, and possibly changed exposure and white balance as well.
// The point is the same that was passed to onFocusStart.
// The point is the same that was passed to onAutoFocusStart.
}
});
```

@ -15,8 +15,8 @@ This lets you emulate typical behaviors in a single line:
```java
cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM); // Pinch to zoom!
cameraView.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); // Tap to focus!
cameraView.mapGesture(Gesture.LONG_TAP, GestureAction.CAPTURE); // Long tap to shoot!
cameraView.mapGesture(Gesture.TAP, GestureAction.AUTO_FOCUS); // Tap to focus!
cameraView.mapGesture(Gesture.LONG_TAP, GestureAction.TAKE_PICTURE); // Long tap to shoot!
```
Simple as that. There are two things to be noted:
@ -27,8 +27,8 @@ Simple as that. There are two things to be noted:
|Gesture|Description|Can be mapped to|
|-------------|-----------|----------------|
|`PINCH`|Pinch gesture, typically assigned to the zoom control.|`zoom` `exposureCorrection` `none`|
|`TAP`|Single tap gesture, typically assigned to the focus control.|`focus` `focusWithMarker` `capture` `none`|
|`LONG_TAP`|Long tap gesture.|`focus` `focusWithMarker` `capture` `none`|
|`TAP`|Single tap gesture, typically assigned to the focus control.|`autoFocus` `takePicture` `none`|
|`LONG_TAP`|Long tap gesture.|`autoFocus` `takePicture` `none`|
|`SCROLL_HORIZONTAL`|Horizontal movement gesture.|`zoom` `exposureCorrection` `none`|
|`SCROLL_VERTICAL`|Vertical movement gesture.|`zoom` `exposureCorrection` `none`|
@ -37,8 +37,8 @@ Simple as that. There are two things to be noted:
```xml
<com.otaliastudios.cameraview.CameraView
app:cameraGesturePinch="zoom|exposureCorrection|none"
app:cameraGestureTap="focus|focusWithMarker|capture|none"
app:cameraGestureLongTap="focus|focusWithMarker|capture|none"
app:cameraGestureTap="autoFocus|takePicture|none"
app:cameraGestureLongTap="autoFocus|takePicture|none"
app:cameraGestureScrollHorizontal="zoom|exposureCorrection|none"
app:cameraGestureScrollVertical="zoom|exposureCorrection|none"/>
```

@ -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.

@ -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?)

Loading…
Cancel
Save