Scroll gestures support, abstracting GestureLayout

pull/1/head
Mattia Iavarone 7 years ago
parent 455e3522e9
commit 9d9e93e67d
  1. 3
      README.md
  2. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  3. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
  4. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  5. 96
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  6. 21
      cameraview/src/main/options/com/otaliastudios/cameraview/Gesture.java
  7. 6
      cameraview/src/main/options/com/otaliastudios/cameraview/GestureAction.java
  8. 14
      cameraview/src/main/res/values/attrs.xml
  9. 13
      cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java
  10. 13
      cameraview/src/main/views/com/otaliastudios/cameraview/PinchGestureLayout.java
  11. 98
      cameraview/src/main/views/com/otaliastudios/cameraview/ScrollGestureLayout.java
  12. 20
      cameraview/src/main/views/com/otaliastudios/cameraview/TapGestureLayout.java
  13. 5
      demo/src/main/res/layout/activity_main.xml

@ -232,6 +232,8 @@ Simple as that. More gestures are coming. There are two things to be noted:
|`PINCH`|`cameraGesturePinch`|Pinch gesture, typically assigned to the zoom control.|`zoom` `exposureCorrection` `none`|
|`TAP`|`cameraGestureTap`|Single tap gesture, typically assigned to the focus control.|`focus` `focusWithMarker` `capture` `none`|
|`LONG_TAP`|`cameraGestureLongTap`|Long tap gesture.|`focus` `focusWithMarker` `capture` `none`|
|`SCROLL_HORIZONTAL`|`cameraGestureScrollHorizontal`|Horizontal movement gesture.|`zoom` `exposureCorrection` `none`|
|`SCROLL_VERTICAL`|`cameraGestureScrollVertical`|Vertical movement gesture.|`zoom` `exposureCorrection` `none`|
## Dynamic Sizing Behavior
@ -457,6 +459,7 @@ This is what was done since the library was forked. I have kept the original str
- *measure `CameraView` as center crop or center inside*
- *add multiple `CameraListener`s for events*
- *gesture framework support*
- *scroll gestures support*
These are still things that need to be done, off the top of my head:

@ -345,11 +345,11 @@ class Camera1 extends CameraController {
}
@Override
void capturePicture() {
if (mIsCapturingImage) return;
if (!isCameraOpened()) return;
boolean capturePicture() {
if (mIsCapturingImage) return false;
if (!isCameraOpened()) return false;
if (mSessionType == SessionType.VIDEO && mIsCapturingVideo) {
if (!mOptions.isVideoSnapshotSupported()) return;
if (!mOptions.isVideoSnapshotSupported()) return false;
}
// Set boolean to wait for image callback
@ -376,6 +376,7 @@ class Camera1 extends CameraController {
mCameraCallbacks.processImage(data, consistentWithView, exifFlip);
}
});
return true;
}

@ -162,8 +162,8 @@ class Camera2 extends CameraController {
}
@Override
void capturePicture() {
boolean capturePicture() {
return true;
}
@Override

@ -38,7 +38,7 @@ abstract class CameraController implements Preview.SurfaceCallback {
abstract void setSessionType(SessionType sessionType);
abstract void setLocation(double latitude, double longitude);
abstract void capturePicture();
abstract boolean capturePicture();
abstract void captureSnapshot();
abstract boolean startVideo(@NonNull File file);
abstract void endVideo();

@ -73,6 +73,7 @@ public class CameraView extends FrameLayout {
private GridLinesLayout mGridLinesLayout;
private PinchGestureLayout mPinchGestureLayout;
private TapGestureLayout mTapGestureLayout;
private ScrollGestureLayout mScrollGestureLayout;
private boolean mIsStarted;
private boolean mKeepScreenOn;
@ -96,15 +97,19 @@ public class CameraView extends FrameLayout {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CameraView, 0, 0);
mJpegQuality = a.getInteger(R.styleable.CameraView_cameraJpegQuality, DEFAULT_JPEG_QUALITY);
mCropOutput = a.getBoolean(R.styleable.CameraView_cameraCropOutput, DEFAULT_CROP_OUTPUT);
Facing facing = Facing.fromValue(a.getInteger(R.styleable.CameraView_cameraFacing, Facing.DEFAULT.value()));
Flash flash = Flash.fromValue(a.getInteger(R.styleable.CameraView_cameraFlash, Flash.DEFAULT.value()));
Grid grid = Grid.fromValue(a.getInteger(R.styleable.CameraView_cameraGrid, Grid.DEFAULT.value()));
WhiteBalance whiteBalance = WhiteBalance.fromValue(a.getInteger(R.styleable.CameraView_cameraWhiteBalance, WhiteBalance.DEFAULT.value()));
VideoQuality videoQuality = VideoQuality.fromValue(a.getInteger(R.styleable.CameraView_cameraVideoQuality, VideoQuality.DEFAULT.value()));
SessionType sessionType = SessionType.fromValue(a.getInteger(R.styleable.CameraView_cameraSessionType, SessionType.DEFAULT.value()));
GestureAction tapGesture = GestureAction.fromValue(a.getInteger(R.styleable.CameraView_cameraGestureTap, GestureAction.DEFAULT_TAP.value()));
GestureAction longTapGesture = GestureAction.fromValue(a.getInteger(R.styleable.CameraView_cameraGestureLongTap, GestureAction.DEFAULT_LONG_TAP.value()));
GestureAction pinchGesture = GestureAction.fromValue(a.getInteger(R.styleable.CameraView_cameraGesturePinch, GestureAction.DEFAULT_PINCH.value()));
GestureAction scrollHorizontalGesture = GestureAction.fromValue(a.getInteger(R.styleable.CameraView_cameraGestureScrollHorizontal, GestureAction.DEFAULT_SCROLL_HORIZONTAL.value()));
GestureAction scrollVerticalGesture = GestureAction.fromValue(a.getInteger(R.styleable.CameraView_cameraGestureScrollVertical, GestureAction.DEFAULT_SCROLL_VERTICAL.value()));
a.recycle();
mCameraCallbacks = new CameraCallbacks();
@ -113,9 +118,11 @@ public class CameraView extends FrameLayout {
mGridLinesLayout = new GridLinesLayout(context);
mPinchGestureLayout = new PinchGestureLayout(context);
mTapGestureLayout = new TapGestureLayout(context);
mScrollGestureLayout = new ScrollGestureLayout(context);
addView(mGridLinesLayout);
addView(mPinchGestureLayout);
addView(mTapGestureLayout);
addView(mScrollGestureLayout);
mIsStarted = false;
setFacing(facing);
@ -128,6 +135,8 @@ public class CameraView extends FrameLayout {
// mapGesture(Gesture.DOUBLE_TAP, doubleTapGesture);
mapGesture(Gesture.LONG_TAP, longTapGesture);
mapGesture(Gesture.PINCH, pinchGesture);
mapGesture(Gesture.SCROLL_HORIZONTAL, scrollHorizontalGesture);
mapGesture(Gesture.SCROLL_VERTICAL, scrollVerticalGesture);
if (!isInEditMode()) {
mOrientationHelper = new OrientationHelper(context) {
@ -324,23 +333,31 @@ public class CameraView extends FrameLayout {
* @return true if this action could be assigned to this gesture
*/
public boolean mapGesture(@NonNull Gesture gesture, GestureAction action) {
GestureAction none = GestureAction.NONE;
if (gesture.isAssignableTo(action)) {
mGestureMap.put(gesture, action);
switch (gesture) {
case PINCH:
mPinchGestureLayout.enable(mGestureMap.get(Gesture.PINCH) != GestureAction.NONE);
mPinchGestureLayout.enable(mGestureMap.get(Gesture.PINCH) != none);
break;
case TAP:
// case DOUBLE_TAP:
case LONG_TAP:
mTapGestureLayout.enable(mGestureMap.get(Gesture.TAP) != GestureAction.NONE ||
// mGestureMap.get(Gesture.DOUBLE_TAP) != GestureAction.NONE ||
mGestureMap.get(Gesture.LONG_TAP) != GestureAction.NONE);
mTapGestureLayout.enable(
mGestureMap.get(Gesture.TAP) != none ||
// mGestureMap.get(Gesture.DOUBLE_TAP) != none ||
mGestureMap.get(Gesture.LONG_TAP) != none);
break;
case SCROLL_HORIZONTAL:
case SCROLL_VERTICAL:
mScrollGestureLayout.enable(
mGestureMap.get(Gesture.SCROLL_HORIZONTAL) != none ||
mGestureMap.get(Gesture.SCROLL_VERTICAL) != none);
break;
}
return true;
}
mapGesture(gesture, GestureAction.NONE);
mapGesture(gesture, none);
return false;
}
@ -364,48 +381,63 @@ public class CameraView extends FrameLayout {
public boolean onTouchEvent(MotionEvent event) {
if (!mCameraController.isCameraOpened()) return true;
// Pass to our own GestureLayouts
CameraOptions options = mCameraController.getCameraOptions(); // Non null
if (mPinchGestureLayout.onTouchEvent(event)) {
GestureAction action = mGestureMap.get(Gesture.PINCH);
// This currently can be zoom or AE.
// Camera can either support these or not.
if (action == GestureAction.ZOOM) {
float oldValue = mZoomValue;
float newValue = mPinchGestureLayout.scaleValue(oldValue, 0, 1);
PointF[] points = mPinchGestureLayout.getPoints();
Log.e(TAG, "pinch!");
onGesture(mPinchGestureLayout, options);
} else if (mScrollGestureLayout.onTouchEvent(event)) {
Log.e(TAG, "scroll!");
onGesture(mScrollGestureLayout, options);
} else if (mTapGestureLayout.onTouchEvent(event)) {
Log.e(TAG, "tap!");
onGesture(mTapGestureLayout, options);
}
return true;
}
// Some gesture layout detected a gesture. It's not known at this moment:
// (1) if it was mapped to some action (we check here)
// (2) if it's supported by the camera (CameraController checks)
private boolean onGesture(GestureLayout source, @NonNull CameraOptions options) {
Gesture gesture = source.getGestureType();
GestureAction action = mGestureMap.get(gesture);
PointF[] points = source.getPoints();
float oldValue, newValue;
switch (action) {
case CAPTURE:
return mCameraController.capturePicture();
case FOCUS:
case FOCUS_WITH_MARKER:
return mCameraController.startAutoFocus(gesture, points[0]);
case ZOOM:
oldValue = mZoomValue;
newValue = source.scaleValue(oldValue, 0, 1);
if (mCameraController.setZoom(newValue)) {
mZoomValue = newValue;
mCameraCallbacks.dispatchOnZoomChanged(newValue, points);
return true;
}
break;
} else if (action == GestureAction.EXPOSURE_CORRECTION) {
float oldValue = mExposureCorrectionValue;
case EXPOSURE_CORRECTION:
oldValue = mExposureCorrectionValue;
float minValue = options.getExposureCorrectionMinValue();
float maxValue = options.getExposureCorrectionMaxValue();
float newValue = mPinchGestureLayout.scaleValue(oldValue, minValue, maxValue);
PointF[] points = mPinchGestureLayout.getPoints();
newValue = source.scaleValue(oldValue, minValue, maxValue);
float[] bounds = new float[]{minValue, maxValue};
if (mCameraController.setExposureCorrection(newValue)) {
mExposureCorrectionValue = newValue;
mCameraCallbacks.dispatchOnExposureCorrectionChanged(newValue, bounds, points);
return true;
}
break;
}
} else if (mTapGestureLayout.onTouchEvent(event)) {
Gesture gesture = mTapGestureLayout.getGestureType();
GestureAction action = mGestureMap.get(gesture);
// This currently can be capture, focus or focusWithMaker.
// Camera can either support these or not.
if (action == GestureAction.CAPTURE) {
capturePicture();
} else if (action == GestureAction.FOCUS ||
action == GestureAction.FOCUS_WITH_MARKER) {
PointF point = mTapGestureLayout.getPoint();
mCameraController.startAutoFocus(gesture, point); // This will call onFocusStart and onFocusEnd
}
}
return true;
return false;
}

@ -46,8 +46,27 @@ public enum Gesture {
* - {@link GestureAction#CAPTURE}
* - {@link GestureAction#NONE}
*/
LONG_TAP(GestureAction.FOCUS, GestureAction.FOCUS_WITH_MARKER, GestureAction.CAPTURE);
LONG_TAP(GestureAction.FOCUS, GestureAction.FOCUS_WITH_MARKER, GestureAction.CAPTURE),
/**
* Horizontal scroll gesture.
* This gesture can be mapped to:
*
* - {@link GestureAction#ZOOM}
* - {@link GestureAction#EXPOSURE_CORRECTION}
* - {@link GestureAction#NONE}
*/
SCROLL_HORIZONTAL(GestureAction.ZOOM, GestureAction.EXPOSURE_CORRECTION),
/**
* Vertical scroll gesture.
* This gesture can be mapped to:
*
* - {@link GestureAction#ZOOM}
* - {@link GestureAction#EXPOSURE_CORRECTION}
* - {@link GestureAction#NONE}
*/
SCROLL_VERTICAL(GestureAction.ZOOM, GestureAction.EXPOSURE_CORRECTION);
Gesture(GestureAction... controls) {
mControls = Arrays.asList(controls);

@ -49,6 +49,8 @@ public enum GestureAction {
* This action can be mapped to:
*
* - {@link Gesture#PINCH}
* - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL}
*/
ZOOM(4),
@ -57,6 +59,8 @@ public enum GestureAction {
* This action can be mapped to:
*
* - {@link Gesture#PINCH}
* - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL}
*/
EXPOSURE_CORRECTION(5);
@ -64,6 +68,8 @@ public enum GestureAction {
final static GestureAction DEFAULT_PINCH = NONE;
final static GestureAction DEFAULT_TAP = NONE;
final static GestureAction DEFAULT_LONG_TAP = NONE;
final static GestureAction DEFAULT_SCROLL_HORIZONTAL = NONE;
final static GestureAction DEFAULT_SCROLL_VERTICAL = NONE;
private int value;

@ -23,10 +23,24 @@
</attr>
<attr name="cameraGesturePinch" format="enum">
<enum name="none" value="0" />
<enum name="zoom" value="4" />
<enum name="exposureCorrection" value="5" />
</attr>
<attr name="cameraGestureScrollHorizontal" format="enum">
<enum name="none" value="0" />
<enum name="zoom" value="4" />
<enum name="exposureCorrection" value="5" />
</attr>
<attr name="cameraGestureScrollVertical" format="enum">
<enum name="none" value="0" />
<enum name="zoom" value="4" />
<enum name="exposureCorrection" value="5" />
</attr>
<attr name="cameraFacing" format="enum">
<enum name="back" value="0" />
<enum name="front" value="1" />

@ -1,6 +1,7 @@
package com.otaliastudios.cameraview;
import android.content.Context;
import android.graphics.PointF;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@ -13,6 +14,8 @@ import android.widget.FrameLayout;
abstract class GestureLayout extends FrameLayout {
protected boolean mEnabled;
protected Gesture mType;
protected PointF[] mPoints;
public GestureLayout(Context context) {
super(context);
@ -27,4 +30,14 @@ abstract class GestureLayout extends FrameLayout {
}
public abstract boolean onTouchEvent(MotionEvent event);
public final Gesture getGestureType() {
return mType;
}
public final PointF[] getPoints() {
return mPoints;
}
public abstract float scaleValue(float currValue, float minValue, float maxValue);
}

@ -17,10 +17,6 @@ class PinchGestureLayout extends GestureLayout {
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) {
@ -31,6 +27,7 @@ class PinchGestureLayout extends GestureLayout {
@Override
protected void onInitialize(Context context) {
super.onInitialize(context);
mPoints = new PointF[]{ new PointF(0, 0), new PointF(0, 0) };
mDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
@ -43,6 +40,9 @@ class PinchGestureLayout extends GestureLayout {
if (Build.VERSION.SDK_INT >= 19) {
mDetector.setQuickScaleEnabled(false);
}
// We listen only to the pinch type.
mType = Gesture.PINCH;
}
@ -73,6 +73,7 @@ class PinchGestureLayout extends GestureLayout {
return false;
}
@Override
public float scaleValue(float currValue, float minValue, float maxValue) {
float add = mAdditionFactor;
// ^ This works well if minValue = 0, maxValue = 1.
@ -92,8 +93,4 @@ class PinchGestureLayout extends GestureLayout {
if (newValue > maxValue) newValue = maxValue;
return newValue;
}
public PointF[] getPoints() {
return mPoints;
}
}

@ -0,0 +1,98 @@
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 ScrollGestureLayout extends GestureLayout {
private GestureDetector mDetector;
private boolean mNotify;
private float mDistance;
public ScrollGestureLayout(Context context) {
super(context);
}
@Override
protected void onInitialize(Context context) {
super.onInitialize(context);
mPoints = new PointF[]{ new PointF(0, 0), new PointF(0, 0) };
mDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
boolean horizontal;
Log.e("ScrollGestureLayout", "onScroll, distanceX="+distanceX+", distanceY="+distanceY);
if (e1.getX() != mPoints[0].x || e1.getY() != mPoints[0].y) {
// First step. We choose now if it's a vertical or horizontal scroll, and
// stick to it for the whole gesture.
horizontal = Math.abs(distanceX) >= Math.abs(distanceY);
mType = horizontal ? Gesture.SCROLL_HORIZONTAL : Gesture.SCROLL_VERTICAL;
mPoints[0].set(e1.getX(), e1.getY());
} else {
// Not the first step. We already defined the type.
horizontal = mType == Gesture.SCROLL_HORIZONTAL;
}
mPoints[1].set(e2.getX(), e2.getY());
mDistance = horizontal ? (distanceX / getWidth()) : (distanceY / getHeight());
mDistance = -mDistance; // they are provided inverted.
mNotify = true;
return true;
}
});
mDetector.setIsLongpressEnabled(false); // Looks important.
}
@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) Log.e("ScrollGestureLayout", "notifying a gesture "+mType.name());
return mNotify;
}
@Override
public float scaleValue(float currValue, float minValue, float maxValue) {
float delta = mDistance; // -1 ... 1
// ^ This works well if minValue = 0, maxValue = 1.
// Account for the different range:
delta *= (maxValue - minValue); // -(max-min) ... (max-min)
// Add some sensitivity.
delta *= 2;
// Cap
float newValue = currValue + delta;
if (newValue < minValue) newValue = minValue;
if (newValue > maxValue) newValue = maxValue;
Log.e("ScrollGestureLayout", "curr="+currValue+", min="+minValue+", max="+maxValue+", out="+newValue);
return newValue;
}
}

@ -4,7 +4,6 @@ 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;
@ -17,8 +16,6 @@ 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;
@ -31,6 +28,7 @@ class TapGestureLayout extends GestureLayout {
@Override
protected void onInitialize(Context context) {
super.onInitialize(context);
mPoints = new PointF[]{ new PointF(0, 0) };
mDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
@ -81,24 +79,18 @@ class TapGestureLayout extends GestureLayout {
// Keep notifying CameraView as long as the gesture goes.
if (mNotify) {
mPoint.x = event.getX();
mPoint.y = event.getY();
mPoints[0].x = event.getX();
mPoints[0].y = event.getY();
return true;
}
return false;
}
public Gesture getGestureType() {
return mType;
}
public PointF getPoint() {
return mPoint;
@Override
public float scaleValue(float currValue, float minValue, float maxValue) {
return 0;
}
// Draw
private final Runnable mFocusMarkerHideRunnable = new Runnable() {

@ -19,7 +19,8 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.otaliastudios.cameraview.CameraView xmlns:app="http://schemas.android.com/apk/res-auto"
<com.otaliastudios.cameraview.CameraView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/camera"
android:layout_width="match_parent"
android:layout_height="400dp"
@ -32,6 +33,8 @@
app:cameraGestureTap="focusWithMarker"
app:cameraGestureLongTap="none"
app:cameraGesturePinch="zoom"
app:cameraGestureScrollHorizontal="exposureCorrection"
app:cameraGestureScrollVertical="none"
app:cameraJpegQuality="100"
app:cameraSessionType="picture" />
</RelativeLayout>

Loading…
Cancel
Save