Gesture support, map gestures to camera actions

pull/1/head
Mattia Iavarone 7 years ago
parent 59a001ab4d
commit 2207ae12d4
  1. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  2. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
  3. 55
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraConstants.java
  4. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  5. 30
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
  6. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  7. 207
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  8. 26
      cameraview/src/main/java/com/otaliastudios/cameraview/Gesture.java
  9. 1
      cameraview/src/main/java/com/otaliastudios/cameraview/Mapper.java
  10. 1
      cameraview/src/main/options/com/otaliastudios/cameraview/Focus.java
  11. 14
      cameraview/src/main/options/com/otaliastudios/cameraview/GestureAction.java
  12. 1
      cameraview/src/main/options/com/otaliastudios/cameraview/ZoomMode.java
  13. 1
      cameraview/src/main/res/layout/layout_focus_marker.xml
  14. 27
      cameraview/src/main/res/values/attrs.xml
  15. 2
      cameraview/src/main/views/com/otaliastudios/cameraview/FocusMarkerLayout.java
  16. 30
      cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java
  17. 99
      cameraview/src/main/views/com/otaliastudios/cameraview/PinchGestureLayout.java
  18. 1
      cameraview/src/main/views/com/otaliastudios/cameraview/PinchToZoomLayout.java
  19. 166
      cameraview/src/main/views/com/otaliastudios/cameraview/TapGestureLayout.java
  20. 73
      demo/src/main/java/com/otaliastudios/cameraview/demo/MainActivity.java
  21. 4
      demo/src/main/res/layout/activity_main.xml

@ -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<Camera.Area> 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<Camera.Area> meteringAreas2 = computeMeteringAreas(p.x, p.y);
List<Camera.Area> 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;
}

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

@ -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
* <uses-permission android:name="android.permission.RECORD_AUDIO" />
* 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;

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

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

@ -18,13 +18,14 @@ public class CameraOptions {
private Set<Integer> supportedWhiteBalance = new HashSet<>(5);
private Set<Integer> supportedFacing = new HashSet<>(2);
private Set<Integer> supportedFlash = new HashSet<>(4);
private Set<Integer> supportedFocus = new HashSet<>(4);
@Deprecated private Set<Integer> 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.

@ -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;
* }
* }
* </pre>
*
* 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<Gesture, Integer> 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:
* <code>
* cameraView.mapGesture(Gesture.PINCH, CameraConstants.GESTURE_ACTION_ZOOM);
* </code>
*
* 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);
}
}
});

@ -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<Integer> mControls;
boolean isAssignableTo(@GestureAction Integer control) {
return control == GESTURE_ACTION_NONE || mControls.contains(control);
}
}

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

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

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

@ -10,5 +10,6 @@ import static com.otaliastudios.cameraview.CameraConstants.ZOOM_PINCH;
@Retention(RetentionPolicy.SOURCE)
@IntDef({ZOOM_OFF, ZOOM_PINCH})
@Deprecated
public @interface ZoomMode {
}

@ -5,6 +5,7 @@
<FrameLayout
android:id="@+id/focusMarkerContainer"
android:alpha="0"
android:layout_width="55dp"
android:layout_height="55dp">

@ -3,6 +3,33 @@
<declare-styleable name="CameraView">
<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" />
</attr>
<attr name="cameraGestureDoubleTap" format="enum">
<enum name="none" value="0" />
<enum name="focus" value="1" />
<enum name="focusWithMarker" value="2" />
<enum name="capture" value="3" />
</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" />
</attr>
<attr name="cameraGesturePinch" format="enum">
<enum name="zoom" value="4" />
<enum name="aeCorrection" value="5" />
</attr>
<attr name="cameraFacing" format="enum">
<enum name="back" value="0" />
<enum name="front" value="1" />

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

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

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

@ -10,6 +10,7 @@ import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
@Deprecated
class PinchToZoomLayout extends View {

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

@ -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() {

@ -30,7 +30,9 @@
app:cameraCropOutput="false"
app:cameraFacing="back"
app:cameraFlash="off"
app:cameraFocus="tapWithMarker"
app:cameraGestureTap="focusWithMarker"
app:cameraGestureLongTap="none"
app:cameraGesturePinch="zoom"
app:cameraJpegQuality="100"
app:cameraSessionType="picture" />
</RelativeLayout>

Loading…
Cancel
Save