diff --git a/README.md b/README.md index 26ef8f02..55801a9f 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,9 @@ motivation boost to push the library forward. app:cameraAudioBitRate="@integer/audio_bit_rate" app:cameraGestureTap="none|autoFocus|takePicture" app:cameraGestureLongTap="none|autoFocus|takePicture" - app:cameraGesturePinch="none|zoom|exposureCorrection" - app:cameraGestureScrollHorizontal="none|zoom|exposureCorrection" - app:cameraGestureScrollVertical="none|zoom|exposureCorrection" + app:cameraGesturePinch="none|zoom|exposureCorrection|filterControl1|filterControl2" + app:cameraGestureScrollHorizontal="none|zoom|exposureCorrection|filterControl1|filterControl2" + app:cameraGestureScrollVertical="none|zoom|exposureCorrection|filterControl1|filterControl2" app:cameraEngine="camera1|camera2" app:cameraPreview="glSurface|surface|texture" app:cameraFacing="back|front" diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java index 8787b889..2e72bb6a 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java @@ -221,6 +221,8 @@ public class CameraOptions1Test extends BaseTest { assertTrue(o.supports(GestureAction.TAKE_PICTURE)); assertTrue(o.supports(GestureAction.NONE)); assertTrue(o.supports(GestureAction.ZOOM)); + assertTrue(o.supports(GestureAction.FILTER_CONTROL_1)); + assertTrue(o.supports(GestureAction.FILTER_CONTROL_2)); assertFalse(o.supports(GestureAction.EXPOSURE_CORRECTION)); } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index 1c0d7ecd..8bf95be9 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -25,6 +25,7 @@ import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.filter.Filter; import com.otaliastudios.cameraview.filter.Filters; import com.otaliastudios.cameraview.filter.NoFilter; +import com.otaliastudios.cameraview.filters.DuotoneFilter; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.FrameProcessor; import com.otaliastudios.cameraview.gesture.Gesture; @@ -355,6 +356,90 @@ public class CameraViewTest extends BaseTest { assertTrue(mockController.mExposureCorrectionChanged); } + @Test + public void testGestureAction_filterControl1() { + mockController.setMockEngineState(true); + mockController.setMockCameraOptions(mock(CameraOptions.class)); + DuotoneFilter filter = new DuotoneFilter(); // supports two parameters + filter.setParameter1(0F); + filter = spy(filter); + cameraView.setExperimental(true); + cameraView.setFilter(filter); + mockController.mExposureCorrectionChanged = false; + MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0); + final FactorHolder factor = new FactorHolder(); + uiSync(new Runnable() { + @Override + public void run() { + cameraView.mScrollGestureFinder = new ScrollGestureFinder(cameraView.mCameraCallbacks) { + @Override + protected boolean handleTouchEvent(@NonNull MotionEvent event) { + setGesture(Gesture.SCROLL_HORIZONTAL); + return true; + } + + @Override + protected float getFactor() { + return factor.value; + } + }; + cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.FILTER_CONTROL_1); + } + }); + + // If factor is 0, we return the same value. The filter should not be notified. + factor.value = 0f; + cameraView.dispatchTouchEvent(event); + verify(filter, never()).setParameter1(anyFloat()); + + // For larger factors, the value is scaled. The filter should be notified. + factor.value = 1f; + cameraView.dispatchTouchEvent(event); + verify(filter, times(1)).setParameter1(anyFloat()); + } + + @Test + public void testGestureAction_filterControl2() { + mockController.setMockEngineState(true); + mockController.setMockCameraOptions(mock(CameraOptions.class)); + DuotoneFilter filter = new DuotoneFilter(); // supports two parameters + filter.setParameter2(0F); + filter = spy(filter); + cameraView.setExperimental(true); + cameraView.setFilter(filter); + mockController.mExposureCorrectionChanged = false; + MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0); + final FactorHolder factor = new FactorHolder(); + uiSync(new Runnable() { + @Override + public void run() { + cameraView.mScrollGestureFinder = new ScrollGestureFinder(cameraView.mCameraCallbacks) { + @Override + protected boolean handleTouchEvent(@NonNull MotionEvent event) { + setGesture(Gesture.SCROLL_HORIZONTAL); + return true; + } + + @Override + protected float getFactor() { + return factor.value; + } + }; + cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.FILTER_CONTROL_2); + } + }); + + // If factor is 0, we return the same value. The filter should not be notified. + factor.value = 0f; + cameraView.dispatchTouchEvent(event); + verify(filter, never()).setParameter2(anyFloat()); + + // For larger factors, the value is scaled. The filter should be notified. + factor.value = 1f; + cameraView.dispatchTouchEvent(event); + verify(filter, times(1)).setParameter2(anyFloat()); + } + //endregion //region testMeasure diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java index 25f90bff..559d24dc 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java @@ -273,6 +273,8 @@ public class CameraOptions { case AUTO_FOCUS: return isAutoFocusSupported(); case TAKE_PICTURE: + case FILTER_CONTROL_1: + case FILTER_CONTROL_2: case NONE: return true; case ZOOM: diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index bf7fb76e..9508da0d 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -51,6 +51,8 @@ import com.otaliastudios.cameraview.filter.Filter; import com.otaliastudios.cameraview.filter.FilterParser; import com.otaliastudios.cameraview.filter.Filters; import com.otaliastudios.cameraview.filter.NoFilter; +import com.otaliastudios.cameraview.filter.OneParameterFilter; +import com.otaliastudios.cameraview.filter.TwoParameterFilter; import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.FrameProcessor; import com.otaliastudios.cameraview.gesture.Gesture; @@ -627,6 +629,30 @@ public class CameraView extends FrameLayout implements LifecycleObserver { mCameraEngine.setExposureCorrection(newValue, bounds, points, true); } break; + + case FILTER_CONTROL_1: + if (!mExperimental) break; + if (getFilter() instanceof OneParameterFilter) { + OneParameterFilter filter = (OneParameterFilter) getFilter(); + oldValue = filter.getParameter1(); + newValue = source.computeValue(oldValue, 0, 1); + if (newValue != oldValue) { + filter.setParameter1(newValue); + } + } + break; + + case FILTER_CONTROL_2: + if (!mExperimental) break; + if (getFilter() instanceof TwoParameterFilter) { + TwoParameterFilter filter = (TwoParameterFilter) getFilter(); + oldValue = filter.getParameter2(); + newValue = source.computeValue(oldValue, 0, 1); + if (newValue != oldValue) { + filter.setParameter2(newValue); + } + } + break; } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java index e85d2a9d..9016a8ab 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java @@ -96,7 +96,8 @@ public abstract class BaseFilter implements Filter { private int vertexTranformMatrixLocation = -1; private int vertexPositionLocation = -1; private int vertexTextureCoordinateLocation = -1; - private Size outputSize; + private int programHandle = -1; + private Size size; @SuppressWarnings("WeakerAccess") protected String vertexPositionName = DEFAULT_VERTEX_POSITION_NAME; @@ -127,6 +128,7 @@ public abstract class BaseFilter implements Filter { @Override public void onCreate(int programHandle) { + this.programHandle = programHandle; vertexPositionLocation = GLES20.glGetAttribLocation(programHandle, vertexPositionName); GlUtils.checkLocation(vertexPositionLocation, vertexPositionName); vertexTextureCoordinateLocation = GLES20.glGetAttribLocation(programHandle, vertexTextureCoordinateName); @@ -139,6 +141,7 @@ public abstract class BaseFilter implements Filter { @Override public void onDestroy() { + programHandle = -1; vertexPositionLocation = -1; vertexTextureCoordinateLocation = -1; vertexModelViewProjectionMatrixLocation = -1; @@ -153,14 +156,18 @@ public abstract class BaseFilter implements Filter { @Override public void setSize(int width, int height) { - outputSize = new Size(width, height); + size = new Size(width, height); } @Override public void draw(float[] transformMatrix) { - onPreDraw(transformMatrix); - onDraw(); - onPostDraw(); + if (programHandle == -1) { + LOG.w("Filter.draw() called after destroying the filter. This can happen rarely because of threading."); + } else { + onPreDraw(transformMatrix); + onDraw(); + onPostDraw(); + } } protected void onPreDraw(float[] transformMatrix) { @@ -203,7 +210,7 @@ public abstract class BaseFilter implements Filter { @Override public final BaseFilter copy() { BaseFilter copy = onCopy(); - copy.setSize(outputSize.getWidth(), outputSize.getHeight()); + copy.setSize(size.getWidth(), size.getHeight()); if (this instanceof OneParameterFilter) { ((OneParameterFilter) copy).setParameter1(((OneParameterFilter) this).getParameter1()); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java index 5700c542..385ed99b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java @@ -55,7 +55,8 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter { */ @SuppressWarnings("WeakerAccess") public void setFirstColor(@ColorInt int color) { - mFirstColor = color; + // Remove any alpha. + mFirstColor = Color.rgb(Color.red(color), Color.green(color), Color.blue(color)); } /** @@ -66,7 +67,8 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter { */ @SuppressWarnings("WeakerAccess") public void setSecondColor(@ColorInt int color) { - mSecondColor = color; + // Remove any alpha. + mSecondColor = Color.rgb(Color.red(color), Color.green(color), Color.blue(color)); } /** @@ -96,23 +98,27 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter { @Override public void setParameter1(float value) { // no easy way to transform 0...1 into a color. - setFirstColor((int) (value * Integer.MAX_VALUE)); + setFirstColor((int) (value * 0xFFFFFF)); } @Override public float getParameter1() { - return (float) getFirstColor() / Integer.MAX_VALUE; + int color = getFirstColor(); + color = Color.argb(0, Color.red(color), Color.green(color), Color.blue(color)); + return (float) color / 0xFFFFFF; } @Override public void setParameter2(float value) { // no easy way to transform 0...1 into a color. - setSecondColor((int) (value * Integer.MAX_VALUE)); + setSecondColor((int) (value * 0xFFFFFF)); } @Override public float getParameter2() { - return (float) getSecondColor() / Integer.MAX_VALUE; + int color = getSecondColor(); + color = Color.argb(0, Color.red(color), Color.green(color), Color.blue(color)); + return (float) color / 0xFFFFFF; } @NonNull diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java index e5f31769..3958c20f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java @@ -42,7 +42,8 @@ public class TintFilter extends BaseFilter implements OneParameterFilter { */ @SuppressWarnings("WeakerAccess") public void setTint(@ColorInt int color) { - this.tint = color; + // Remove any alpha. + this.tint = Color.rgb(Color.red(color), Color.green(color), Color.blue(color)); } /** @@ -60,12 +61,14 @@ public class TintFilter extends BaseFilter implements OneParameterFilter { @Override public void setParameter1(float value) { // no easy way to transform 0...1 into a color. - setTint((int) (value * Integer.MAX_VALUE)); + setTint((int) (value * 0xFFFFFF)); } @Override public float getParameter1() { - return (float) getTint() / Integer.MAX_VALUE; + int color = getTint(); + color = Color.argb(0, Color.red(color), Color.green(color), Color.blue(color)); + return (float) color / 0xFFFFFF; } @NonNull diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java index 0599f60f..8f38f89f 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java @@ -22,6 +22,8 @@ public enum Gesture { * * - {@link GestureAction#ZOOM} * - {@link GestureAction#EXPOSURE_CORRECTION} + * - {@link GestureAction#FILTER_CONTROL_1} + * - {@link GestureAction#FILTER_CONTROL_2} * - {@link GestureAction#NONE} */ PINCH(GestureType.CONTINUOUS), @@ -52,6 +54,8 @@ public enum Gesture { * * - {@link GestureAction#ZOOM} * - {@link GestureAction#EXPOSURE_CORRECTION} + * - {@link GestureAction#FILTER_CONTROL_1} + * - {@link GestureAction#FILTER_CONTROL_2} * - {@link GestureAction#NONE} */ SCROLL_HORIZONTAL(GestureType.CONTINUOUS), @@ -62,6 +66,8 @@ public enum Gesture { * * - {@link GestureAction#ZOOM} * - {@link GestureAction#EXPOSURE_CORRECTION} + * - {@link GestureAction#FILTER_CONTROL_1} + * - {@link GestureAction#FILTER_CONTROL_2} * - {@link GestureAction#NONE} */ SCROLL_VERTICAL(GestureType.CONTINUOUS); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java index 61294b96..b4fa3659 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java @@ -60,8 +60,27 @@ public enum GestureAction { * - {@link Gesture#SCROLL_HORIZONTAL} * - {@link Gesture#SCROLL_VERTICAL} */ - EXPOSURE_CORRECTION(4, GestureType.CONTINUOUS); + EXPOSURE_CORRECTION(4, GestureType.CONTINUOUS), + /** + * Controls the first parameter of a real-time {@link com.otaliastudios.cameraview.filter.Filter}, + * if it accepts one. This action can be mapped to continuous gestures: + * + * - {@link Gesture#PINCH} + * - {@link Gesture#SCROLL_HORIZONTAL} + * - {@link Gesture#SCROLL_VERTICAL} + */ + FILTER_CONTROL_1(5, GestureType.CONTINUOUS), + + /** + * Controls the second parameter of a real-time {@link com.otaliastudios.cameraview.filter.Filter}, + * if it accepts one. This action can be mapped to continuous gestures: + * + * - {@link Gesture#PINCH} + * - {@link Gesture#SCROLL_HORIZONTAL} + * - {@link Gesture#SCROLL_VERTICAL} + */ + FILTER_CONTROL_2(6, GestureType.CONTINUOUS); final static GestureAction DEFAULT_PINCH = NONE; final static GestureAction DEFAULT_TAP = NONE; diff --git a/cameraview/src/main/res/values/attrs.xml b/cameraview/src/main/res/values/attrs.xml index 83eb4dfc..59905eb1 100644 --- a/cameraview/src/main/res/values/attrs.xml +++ b/cameraview/src/main/res/values/attrs.xml @@ -44,18 +44,24 @@ + + + + + + diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java index 672765a2..8b97aa4a 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -331,7 +331,9 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis } else { mCurrentFilter = 0; } - camera.setFilter(mAllFilters[mCurrentFilter].newInstance()); + Filters filter = mAllFilters[mCurrentFilter]; + camera.setFilter(filter.newInstance()); + message(filter.toString(), false); } @Override diff --git a/demo/src/main/res/layout/activity_camera.xml b/demo/src/main/res/layout/activity_camera.xml index f02ec291..87bea9cf 100644 --- a/demo/src/main/res/layout/activity_camera.xml +++ b/demo/src/main/res/layout/activity_camera.xml @@ -26,8 +26,8 @@ app:cameraGestureTap="autoFocus" app:cameraGestureLongTap="none" app:cameraGesturePinch="zoom" - app:cameraGestureScrollHorizontal="exposureCorrection" - app:cameraGestureScrollVertical="none" + app:cameraGestureScrollHorizontal="filterControl1" + app:cameraGestureScrollVertical="exposureCorrection" app:cameraMode="picture" app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker"> diff --git a/docs/_posts/2018-12-20-gestures.md b/docs/_posts/2018-12-20-gestures.md index 2f446903..6a510ba8 100644 --- a/docs/_posts/2018-12-20-gestures.md +++ b/docs/_posts/2018-12-20-gestures.md @@ -26,21 +26,35 @@ 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.|`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`| +|`PINCH`|Pinch gesture, typically assigned to the zoom control.|`ZOOM` `EXPOSURE_CORRECTION` `FILTER_CONTROL_1` `FILTER_CONTROL_2` `NONE`| +|`TAP`|Single tap gesture, typically assigned to the focus control.|`AUTO_FOCUS` `TAKE_PICTURE` `NONE`| +|`LONG_TAP`|Long tap gesture.|`AUTO_FOCUS` `TAKE_PICTURE` `NONE`| +|`SCROLL_HORIZONTAL`|Horizontal movement gesture.|`ZOOM` `EXPOSURE_CORRECTION` `FILTER_CONTROL_1` `FILTER_CONTROL_2` `NONE`| +|`SCROLL_VERTICAL`|Vertical movement gesture.|`ZOOM` `EXPOSURE_CORRECTION` `FILTER_CONTROL_1` `FILTER_CONTROL_2` `NONE`| + +### Gesture Actions + +Looking at this from the other side: + +|Gesture action|Description|Can be mapped to| +|--------------|-----------|----------------| +|`NONE`|Disables this gesture.|`TAP` `LONG_TAP` `PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`| +|`AUTO_FOCUS`|Launches an [auto-focus operation](controls.html#auto-focus) on the finger position.|`TAP` `LONG_TAP`| +|`TAKE_PICTURE`|Takes a picture using [takePicture](capturing-media.html).|`TAP` `LONG_TAP`| +|`ZOOM`|[Zooms](controls.html#zoom) in or out.|`PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`| +|`EXPOSURE_CORRECTION`|Controls the [exposure correction](controls.html#exposure-correction).|`PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`| +|`FILTER_CONTROL_1`|Controls the first parameter (if any) of a [real-time filter](filters.html).|`PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`| +|`FILTER_CONTROL_2`|Controls the second parameter (if any) of a [real-time filter](filters.html).|`PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`| ### XML Attributes ```xml + app:cameraGestureScrollHorizontal="zoom|exposureCorrection|filterControl1|filterControl2|none" + app:cameraGestureScrollVertical="zoom|exposureCorrection|filterControl1|filterControl2|none"/> ``` ### Related APIs diff --git a/docs/_posts/2019-08-06-filters.md b/docs/_posts/2019-08-06-filters.md index 1d8c6a0c..099ad559 100644 --- a/docs/_posts/2019-08-06-filters.md +++ b/docs/_posts/2019-08-06-filters.md @@ -70,7 +70,9 @@ filter that was previously set and return back to normal. |`TintFilter`|`Filters.TINT`|`@string/cameraview_filter_tint`| |`VignetteFilter`|`Filters.VIGNETTE`|`@string/cameraview_filter_vignette`| -Most of these filters accept input parameters to tune them. For example, `DuotoneFilter` will +### Filters controls + +Most of the provided filters accept input parameters to tune them. For example, `DuotoneFilter` will accept two colors to apply the duotone effect. ```java @@ -81,6 +83,14 @@ duotoneFilter.setSecondColor(Color.GREEN); You can change these values by acting on the filter object, before or after passing it to `CameraView`. Whenever something is changed, the updated values will be visible immediately in the next frame. +You can also map the first or second filter control to a gesture (like horizontal or vertical scrolling), +as explained in [the gesture documentation](gestures.html): + +```java +camera.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.FILTER_CONTROL_1); +camera.mapGesture(Gesture.SCROLL_VERTICAL, GestureAction.FILTER_CONTROL_2); +``` + ### Advanced usage Advanced users with OpenGL experience can create their own filters by implementing the `Filter` interface