From b1d183a7145d082ab7088b0bb75e1bdfa83cb274 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Fri, 29 Sep 2017 20:49:30 +0200 Subject: [PATCH] Fix a hardware acceleration bug, disable SurfaceViewPreview features (#54) * Layout changes * SurfaceView onSurfaceCreated is too early. Fix * Add logs * Refactor some classes, fix hardware acceleration bug, disable cropping for SurfaceView * Update README with known limitations --- README.md | 9 ++ .../cameraview/CameraCallbacksTest.java | 83 +++++++------- .../cameraview/CameraViewTest.java | 12 +- .../cameraview/IntegrationTest.java | 4 +- .../cameraview/MockCameraController.java | 4 +- ...ockPreview.java => MockCameraPreview.java} | 14 ++- .../otaliastudios/cameraview/PreviewTest.java | 14 ++- ...est.java => SurfaceCameraPreviewTest.java} | 6 +- ...est.java => TextureCameraPreviewTest.java} | 6 +- .../com/otaliastudios/cameraview/Camera1.java | 9 +- .../com/otaliastudios/cameraview/Camera2.java | 4 +- .../cameraview/CameraController.java | 14 ++- .../otaliastudios/cameraview/CameraView.java | 68 ++++++----- .../src/main/res/layout/surface_view.xml | 15 ++- .../src/main/res/layout/texture_view.xml | 5 +- .../{Preview.java => CameraPreview.java} | 30 +++-- .../cameraview/SurfaceCameraPreview.java | 107 ++++++++++++++++++ .../cameraview/SurfaceViewPreview.java | 60 ---------- ...Preview.java => TextureCameraPreview.java} | 4 +- demo/src/main/AndroidManifest.xml | 1 + .../cameraview/demo/CameraActivity.java | 14 ++- .../cameraview/demo/ControlView.java | 8 +- demo/src/main/res/layout/activity_camera.xml | 4 +- 23 files changed, 305 insertions(+), 190 deletions(-) rename cameraview/src/androidTest/java/com/otaliastudios/cameraview/{MockPreview.java => MockCameraPreview.java} (70%) rename cameraview/src/androidTest/java/com/otaliastudios/cameraview/{SurfaceViewPreviewTest.java => SurfaceCameraPreviewTest.java} (54%) rename cameraview/src/androidTest/java/com/otaliastudios/cameraview/{TextureViewPreviewTest.java => TextureCameraPreviewTest.java} (78%) rename cameraview/src/main/views/com/otaliastudios/cameraview/{Preview.java => CameraPreview.java} (86%) create mode 100644 cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java delete mode 100644 cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java rename cameraview/src/main/views/com/otaliastudios/cameraview/{TextureViewPreview.java => TextureCameraPreview.java} (92%) diff --git a/README.md b/README.md index cb5af06d..1c3fa1ac 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ compile 'com.otaliastudios:cameraview:1.2.1' - [Permissions Behavior](#permissions-behavior) - [Manifest file](#manifest-file) - [Roadmap](#roadmap) +- [Device-specific issues](#device-specific-issues) ## Usage @@ -526,3 +527,11 @@ These are still things that need to be done, off the top of my head: - [ ] add onRequestPermissionResults for easy permission callback - [ ] better error handling, maybe with a onError(e) method in the public listener, or have each public method return a boolean - [ ] decent code coverage + +## Device-specific issues + +There are a couple of known issues if you are working with certain devices. The emulator is one of +the most tricky in this sense. + +- Devices, or activities, with hardware acceleration turned off: this can be the case with emulators. In this case we will use SurfaceView as our surface provider. That is intrinsically flawed and can't deal with all we want to do here (runtime layout changes, scaling, etc.). So, nothing to do in this case. +- Devices with no support for MediaRecorder: the emulator does not support it, officially. This means that video/audio recording is flawed. Again, not our fault. \ No newline at end of file diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java index 16b10405..f735fbe9 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java @@ -4,10 +4,7 @@ package com.otaliastudios.cameraview; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.graphics.Color; import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.YuvImage; import android.support.test.filters.MediumTest; import android.support.test.runner.AndroidJUnit4; import android.view.ViewGroup; @@ -18,9 +15,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; - -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; +import org.mockito.stubbing.Stubber; import static junit.framework.Assert.assertNotNull; import static org.junit.Assert.assertEquals; @@ -33,19 +28,18 @@ import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @RunWith(AndroidJUnit4.class) @MediumTest public class CameraCallbacksTest extends BaseTest { private CameraView camera; - private CameraView.CameraCallbacks callbacks; private CameraListener listener; private MockCameraController mockController; - private MockPreview mockPreview; + private MockCameraPreview mockPreview; private Task task; @@ -58,14 +52,14 @@ public class CameraCallbacksTest extends BaseTest { listener = mock(CameraListener.class); camera = new CameraView(context) { @Override - protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { - mockController = new MockCameraController(callbacks, preview); + protected CameraController instantiateCameraController(CameraCallbacks callbacks) { + mockController = new MockCameraController(callbacks); return mockController; } @Override - protected Preview instantiatePreview(Context context, ViewGroup container) { - mockPreview = new MockPreview(context, container); + protected CameraPreview instantiatePreview(Context context, ViewGroup container) { + mockPreview = new MockCameraPreview(context, container); return mockPreview; } @@ -74,8 +68,8 @@ public class CameraCallbacksTest extends BaseTest { return true; } }; + camera.instantiatePreview(); camera.addCameraListener(listener); - callbacks = camera.mCameraCallbacks; task = new Task<>(); task.listen(); } @@ -87,26 +81,25 @@ public class CameraCallbacksTest extends BaseTest { camera = null; mockController = null; mockPreview = null; - callbacks = null; listener = null; } // Completes our task. - private Answer completeTask() { - return new Answer() { + private Stubber completeTask() { + return doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { task.end(true); return null; } - }; + }); } @Test public void testDontDispatchIfRemoved() { camera.removeCameraListener(listener); - doAnswer(completeTask()).when(listener).onCameraOpened(null); - callbacks.dispatchOnCameraOpened(null); + completeTask().when(listener).onCameraOpened(null); + camera.mCameraCallbacks.dispatchOnCameraOpened(null); assertNull(task.await(200)); verify(listener, never()).onCameraOpened(null); @@ -115,8 +108,8 @@ public class CameraCallbacksTest extends BaseTest { @Test public void testDontDispatchIfCleared() { camera.clearCameraListeners(); - doAnswer(completeTask()).when(listener).onCameraOpened(null); - callbacks.dispatchOnCameraOpened(null); + completeTask().when(listener).onCameraOpened(null); + camera.mCameraCallbacks.dispatchOnCameraOpened(null); assertNull(task.await(200)); verify(listener, never()).onCameraOpened(null); @@ -124,8 +117,8 @@ public class CameraCallbacksTest extends BaseTest { @Test public void testDispatchOnCameraOpened() { - doAnswer(completeTask()).when(listener).onCameraOpened(null); - callbacks.dispatchOnCameraOpened(null); + completeTask().when(listener).onCameraOpened(null); + camera.mCameraCallbacks.dispatchOnCameraOpened(null); assertNotNull(task.await(200)); verify(listener, times(1)).onCameraOpened(null); @@ -133,8 +126,8 @@ public class CameraCallbacksTest extends BaseTest { @Test public void testDispatchOnCameraClosed() { - doAnswer(completeTask()).when(listener).onCameraClosed(); - callbacks.dispatchOnCameraClosed(); + completeTask().when(listener).onCameraClosed(); + camera.mCameraCallbacks.dispatchOnCameraClosed(); assertNotNull(task.await(200)); verify(listener, times(1)).onCameraClosed(); @@ -142,8 +135,8 @@ public class CameraCallbacksTest extends BaseTest { @Test public void testDispatchOnVideoTaken() { - doAnswer(completeTask()).when(listener).onVideoTaken(null); - callbacks.dispatchOnVideoTaken(null); + completeTask().when(listener).onVideoTaken(null); + camera.mCameraCallbacks.dispatchOnVideoTaken(null); assertNotNull(task.await(200)); verify(listener, times(1)).onVideoTaken(null); @@ -151,8 +144,8 @@ public class CameraCallbacksTest extends BaseTest { @Test public void testDispatchOnZoomChanged() { - doAnswer(completeTask()).when(listener).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class)); - callbacks.dispatchOnZoomChanged(0f, null); + completeTask().when(listener).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class)); + camera.mCameraCallbacks.dispatchOnZoomChanged(0f, null); assertNotNull(task.await(200)); verify(listener, times(1)).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class)); @@ -160,8 +153,8 @@ public class CameraCallbacksTest extends BaseTest { @Test public void testDispatchOnExposureCorrectionChanged() { - doAnswer(completeTask()).when(listener).onExposureCorrectionChanged(0f, null, null); - callbacks.dispatchOnExposureCorrectionChanged(0f, null, null); + completeTask().when(listener).onExposureCorrectionChanged(0f, null, null); + camera.mCameraCallbacks.dispatchOnExposureCorrectionChanged(0f, null, null); assertNotNull(task.await(200)); verify(listener, times(1)).onExposureCorrectionChanged(0f, null, null); @@ -174,8 +167,8 @@ public class CameraCallbacksTest extends BaseTest { camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); PointF point = new PointF(); - doAnswer(completeTask()).when(listener).onFocusStart(point); - callbacks.dispatchOnFocusStart(Gesture.TAP, point); + completeTask().when(listener).onFocusStart(point); + camera.mCameraCallbacks.dispatchOnFocusStart(Gesture.TAP, point); assertNotNull(task.await(200)); verify(listener, times(1)).onFocusStart(point); @@ -190,8 +183,8 @@ public class CameraCallbacksTest extends BaseTest { PointF point = new PointF(); boolean success = true; - doAnswer(completeTask()).when(listener).onFocusEnd(success, point); - callbacks.dispatchOnFocusEnd(Gesture.TAP, success, point); + completeTask().when(listener).onFocusEnd(success, point); + camera.mCameraCallbacks.dispatchOnFocusEnd(Gesture.TAP, success, point); assertNotNull(task.await(200)); verify(listener, times(1)).onFocusEnd(success, point); @@ -200,31 +193,31 @@ public class CameraCallbacksTest extends BaseTest { @Test public void testOrientationCallbacks_deviceOnly() { - doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt()); + completeTask().when(listener).onOrientationChanged(anyInt()); // Assert not called. Both methods must be called. - callbacks.onDeviceOrientationChanged(0); + camera.mCameraCallbacks.onDeviceOrientationChanged(0); assertNull(task.await(200)); verify(listener, never()).onOrientationChanged(anyInt()); } @Test public void testOrientationCallbacks_displayOnly() { - doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt()); + completeTask().when(listener).onOrientationChanged(anyInt()); // Assert not called. Both methods must be called. - callbacks.onDisplayOffsetChanged(0); + camera.mCameraCallbacks.onDisplayOffsetChanged(0); assertNull(task.await(200)); verify(listener, never()).onOrientationChanged(anyInt()); } @Test public void testOrientationCallbacks_both() { - doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt()); + completeTask().when(listener).onOrientationChanged(anyInt()); // Assert called. - callbacks.onDisplayOffsetChanged(0); - callbacks.onDeviceOrientationChanged(90); + camera.mCameraCallbacks.onDisplayOffsetChanged(0); + camera.mCameraCallbacks.onDeviceOrientationChanged(90); assertNotNull(task.await(200)); verify(listener, times(1)).onOrientationChanged(anyInt()); } @@ -290,9 +283,9 @@ public class CameraCallbacksTest extends BaseTest { // Create fake JPEG array and trigger the process. if (jpeg) { - callbacks.processImage(mockJpeg(imageDim[0], imageDim[1]), true, false); + camera.mCameraCallbacks.processImage(mockJpeg(imageDim[0], imageDim[1]), true, false); } else { - callbacks.processSnapshot(mockYuv(imageDim[0], imageDim[1]), true, false); + camera.mCameraCallbacks.processSnapshot(mockYuv(imageDim[0], imageDim[1]), true, false); } // Wait for result and get out dimensions. diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index f2b5618d..ed987fa6 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -26,7 +26,7 @@ public class CameraViewTest extends BaseTest { private CameraView cameraView; private MockCameraController mockController; - private Preview mockPreview; + private CameraPreview mockPreview; private boolean hasPermissions; @Before @@ -37,14 +37,14 @@ public class CameraViewTest extends BaseTest { Context context = context(); cameraView = new CameraView(context) { @Override - protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { - mockController = new MockCameraController(callbacks, preview); + protected CameraController instantiateCameraController(CameraCallbacks callbacks) { + mockController = new MockCameraController(callbacks); return mockController; } @Override - protected Preview instantiatePreview(Context context, ViewGroup container) { - mockPreview = new MockPreview(context, container); + protected CameraPreview instantiatePreview(Context context, ViewGroup container) { + mockPreview = new MockCameraPreview(context, container); return mockPreview; } @@ -53,6 +53,8 @@ public class CameraViewTest extends BaseTest { return hasPermissions; } }; + // Instantiate preview now. + cameraView.instantiatePreview(); } }); } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java index e6a928be..7f798576 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java @@ -56,8 +56,8 @@ public class IntegrationTest extends BaseTest { public void run() { camera = new CameraView(rule.getActivity()) { @Override - protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { - controller = new Camera1(callbacks, preview); + protected CameraController instantiateCameraController(CameraCallbacks callbacks) { + controller = new Camera1(callbacks); return controller; } }; diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java index 8aeda053..dee10dab 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java @@ -15,8 +15,8 @@ public class MockCameraController extends CameraController { boolean mZoomChanged; boolean mExposureCorrectionChanged; - MockCameraController(CameraView.CameraCallbacks callback, Preview preview) { - super(callback, preview); + MockCameraController(CameraView.CameraCallbacks callback) { + super(callback); } void setMockCameraOptions(CameraOptions options) { diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraPreview.java similarity index 70% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraPreview.java index 514fbd21..f3f8a4d7 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraPreview.java @@ -7,15 +7,21 @@ import android.view.Surface; import android.view.View; import android.view.ViewGroup; -public class MockPreview extends Preview { +public class MockCameraPreview extends CameraPreview { - MockPreview(Context context, ViewGroup parent) { + MockCameraPreview(Context context, ViewGroup parent) { super(context, parent, null); } + private boolean mCropping = false; + public void setIsCropping(boolean crop) { - getView().setScaleX(crop ? 2 : 1); - getView().setScaleY(crop ? 2 : 1); + mCropping = crop; + } + + @Override + boolean isCropping() { + return mCropping; } @NonNull diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java index 7a717082..e5fa7f9d 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java @@ -19,14 +19,14 @@ import static org.mockito.Mockito.*; public abstract class PreviewTest extends BaseTest { - protected abstract Preview createPreview(Context context, ViewGroup parent, Preview.SurfaceCallback callback); + protected abstract CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback); @Rule public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class); - protected Preview preview; + protected CameraPreview preview; protected Size surfaceSize; - private Preview.SurfaceCallback callback; + private CameraPreview.SurfaceCallback callback; private Task availability; @Before @@ -40,7 +40,7 @@ public abstract class PreviewTest extends BaseTest { TestActivity a = rule.getActivity(); surfaceSize = a.getContentSize(); - callback = mock(Preview.SurfaceCallback.class); + callback = mock(CameraPreview.SurfaceCallback.class); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -121,8 +121,10 @@ public abstract class PreviewTest extends BaseTest { // If we apply a different aspect ratio, there should be cropping. float desired = view * 1.2f; - setDesiredAspectRatio(desired); - assertTrue(preview.isCropping()); + if (preview.supportsCropping()) { + setDesiredAspectRatio(desired); + assertTrue(preview.isCropping()); + } // Since desired is 'desired', let's fake a new view size that is consistent with it. // Ensure crop is not happening anymore. diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceViewPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceCameraPreviewTest.java similarity index 54% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceViewPreviewTest.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceCameraPreviewTest.java index afa18eb2..d3d0cdb6 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceViewPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceCameraPreviewTest.java @@ -10,10 +10,10 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest -public class SurfaceViewPreviewTest extends PreviewTest { +public class SurfaceCameraPreviewTest extends PreviewTest { @Override - protected Preview createPreview(Context context, ViewGroup parent, Preview.SurfaceCallback callback) { - return new SurfaceViewPreview(context, parent, callback); + protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) { + return new SurfaceCameraPreview(context, parent, callback); } } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureViewPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java similarity index 78% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureViewPreviewTest.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java index 39391063..8a1f056e 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureViewPreviewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java @@ -10,11 +10,11 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) @SmallTest -public class TextureViewPreviewTest extends PreviewTest { +public class TextureCameraPreviewTest extends PreviewTest { @Override - protected Preview createPreview(Context context, ViewGroup parent, Preview.SurfaceCallback callback) { - return new TextureViewPreview(context, parent, callback); + protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) { + return new TextureCameraPreview(context, parent, callback); } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index 9cf02e59..a5f603bd 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -15,7 +15,6 @@ import android.support.annotation.WorkerThread; import android.view.SurfaceHolder; import java.io.File; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -57,8 +56,8 @@ class Camera1 extends CameraController { private final Object mLock = new Object(); - Camera1(CameraView.CameraCallbacks callback, final Preview preview) { - super(callback, preview); + Camera1(CameraView.CameraCallbacks callback) { + super(callback); } /** @@ -118,7 +117,7 @@ class Camera1 extends CameraController { } private boolean shouldSetup() { - return isCameraAvailable() && mPreview.isReady() && !mIsSetup; + return isCameraAvailable() && mPreview != null && mPreview.isReady() && !mIsSetup; } // The act of binding an "open" camera to a "ready" preview. @@ -557,7 +556,7 @@ class Camera1 extends CameraController { * This is called either on cameraView.start(), or when the underlying surface changes. * It is possible that in the first call the preview surface has not already computed its * dimensions. - * But when it does, the {@link Preview.SurfaceCallback} should be called, + * But when it does, the {@link CameraPreview.SurfaceCallback} should be called, * and this should be refreshed. */ private Size computeCaptureSize() { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java index 575e59a0..472b623c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java @@ -22,8 +22,8 @@ import java.util.List; @TargetApi(21) class Camera2 extends CameraController { - public Camera2(CameraView.CameraCallbacks callback, Preview preview) { - super(callback, preview); + public Camera2(CameraView.CameraCallbacks callback) { + super(callback); } @Override diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java index 7bde91da..c9c1038c 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java @@ -2,14 +2,13 @@ package com.otaliastudios.cameraview; import android.graphics.PointF; import android.location.Location; -import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import java.io.File; -abstract class CameraController implements Preview.SurfaceCallback { +abstract class CameraController implements CameraPreview.SurfaceCallback { private static final String TAG = CameraController.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); @@ -20,7 +19,7 @@ abstract class CameraController implements Preview.SurfaceCallback { static final int STATE_STARTED = 2; // Camera is available and we can set parameters. protected final CameraView.CameraCallbacks mCameraCallbacks; - protected final Preview mPreview; + protected CameraPreview mPreview; protected Facing mFacing; protected Flash mFlash; @@ -43,10 +42,8 @@ abstract class CameraController implements Preview.SurfaceCallback { protected WorkerHandler mHandler; - CameraController(CameraView.CameraCallbacks callback, Preview preview) { + CameraController(CameraView.CameraCallbacks callback) { mCameraCallbacks = callback; - mPreview = preview; - mPreview.setSurfaceCallback(this); mHandler = WorkerHandler.get("CameraViewController"); mHandler.getThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override @@ -67,6 +64,11 @@ abstract class CameraController implements Preview.SurfaceCallback { }); } + void setPreview(CameraPreview cameraPreview) { + mPreview = cameraPreview; + mPreview.setSurfaceCallback(this); + } + //region Start&Stop private String ss() { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 692a378b..5c97f25e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -58,9 +58,9 @@ public class CameraView extends FrameLayout { // Components /* for tests */ CameraCallbacks mCameraCallbacks; + private CameraPreview mCameraPreview; private OrientationHelper mOrientationHelper; private CameraController mCameraController; - private Preview mPreviewImpl; private ArrayList mListeners = new ArrayList<>(2); private MediaActionSound mSound; @@ -89,6 +89,7 @@ public class CameraView extends FrameLayout { @SuppressWarnings("WrongConstant") private void init(@NonNull Context context, @Nullable AttributeSet attrs) { + setWillNotDraw(false); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CameraView, 0, 0); // Self managed int jpegQuality = a.getInteger(R.styleable.CameraView_cameraJpegQuality, DEFAULT_JPEG_QUALITY); @@ -115,8 +116,7 @@ public class CameraView extends FrameLayout { // Components mCameraCallbacks = new Callbacks(); - mPreviewImpl = instantiatePreview(context, this); - mCameraController = instantiateCameraController(mCameraCallbacks, mPreviewImpl); + mCameraController = instantiateCameraController(mCameraCallbacks); mUiHandler = new Handler(Looper.getMainLooper()); mWorkerHandler = WorkerHandler.get("CameraViewWorker"); @@ -158,23 +158,31 @@ public class CameraView extends FrameLayout { } } + protected CameraController instantiateCameraController(CameraCallbacks callbacks) { + return new Camera1(callbacks); + } - protected Preview instantiatePreview(Context context, ViewGroup container) { + protected CameraPreview instantiatePreview(Context context, ViewGroup container) { // TextureView is not supported without hardware acceleration. + LOG.w("preview:", "isHardwareAccelerated:", isHardwareAccelerated()); return isHardwareAccelerated() ? - new TextureViewPreview(context, container, null) : - new SurfaceViewPreview(context, container, null); + new TextureCameraPreview(context, container, null) : + new SurfaceCameraPreview(context, container, null); } - - protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { - return new Camera1(callbacks, preview); + /* for tests */ void instantiatePreview() { + mCameraPreview = instantiatePreview(getContext(), this); + mCameraController.setPreview(mCameraPreview); } - @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + if (mCameraPreview == null) { + // isHardwareAccelerated will return the real value only after we are + // attached. That's why we instantiate the preview here. + instantiatePreview(); + } if (!isInEditMode()) { mOrientationHelper.enable(getContext()); } @@ -211,7 +219,7 @@ public class CameraView extends FrameLayout { * aspect ratio. * * When both dimensions are MATCH_PARENT, CameraView will fill its - * parent no matter the preview. Thanks to what happens in {@link Preview}, this acts like + * parent no matter the preview. Thanks to what happens in {@link CameraPreview}, this acts like * a CENTER CROP scale type. * * When both dimensions are WRAP_CONTENT, CameraView will take the biggest dimensions that @@ -235,18 +243,24 @@ public class CameraView extends FrameLayout { final float previewWidth = flip ? previewSize.getHeight() : previewSize.getWidth(); final float previewHeight = flip ? previewSize.getWidth() : previewSize.getHeight(); - // If MATCH_PARENT is interpreted as AT_MOST, transform to EXACTLY - // to be consistent with our semantics (and our docs). + // Pre-process specs final ViewGroup.LayoutParams lp = getLayoutParams(); - if (widthMode == AT_MOST && lp.width == MATCH_PARENT) widthMode = EXACTLY; - if (heightMode == AT_MOST && lp.height == MATCH_PARENT) heightMode = EXACTLY; - LOG.i("onMeasure:", "requested dimensions are", - "(" + widthValue + "[" + ms(widthMode) + "]x" + + if (!mCameraPreview.supportsCropping()) { + // We can't allow EXACTLY constraints in this case. + if (widthMode == EXACTLY) widthMode = AT_MOST; + if (heightMode == EXACTLY) heightMode = AT_MOST; + } else { + // If MATCH_PARENT is interpreted as AT_MOST, transform to EXACTLY + // to be consistent with our semantics (and our docs). + if (widthMode == AT_MOST && lp.width == MATCH_PARENT) widthMode = EXACTLY; + if (heightMode == AT_MOST && lp.height == MATCH_PARENT) heightMode = EXACTLY; + } + + LOG.i("onMeasure:", "requested dimensions are", "(" + widthValue + "[" + ms(widthMode) + "]x" + heightValue + "[" + ms(heightMode) + "])"); LOG.i("onMeasure:", "previewSize is", "(" + previewWidth + "x" + previewHeight + ")"); - - // If we have fixed dimensions (either 300dp or MATCH_PARENT), there's nothing we should do, + // (1) If we have fixed dimensions (either 300dp or MATCH_PARENT), there's nothing we should do, // other than respect it. The preview will eventually be cropped at the sides (by PreviewImpl scaling) // except the case in which these fixed dimensions manage to fit exactly the preview aspect ratio. if (widthMode == EXACTLY && heightMode == EXACTLY) { @@ -256,8 +270,8 @@ public class CameraView extends FrameLayout { return; } - // If both dimensions are free, with no limits, then our size will be exactly the - // preview size. This can happen rarely, for example in scrollable containers. + // (2) If both dimensions are free, with no limits, then our size will be exactly the + // preview size. This can happen rarely, for example in 2d scrollable containers. if (widthMode == UNSPECIFIED && heightMode == UNSPECIFIED) { LOG.i("onMeasure:", "both are completely free.", "We respect that and extend to the whole preview size.", @@ -271,7 +285,7 @@ public class CameraView extends FrameLayout { // It's sure now that at least one dimension can be determined (either because EXACTLY or AT_MOST). // This starts to seem a pleasant situation. - // If one of the dimension is completely free (e.g. in a scrollable container), + // (3) If one of the dimension is completely free (e.g. in a scrollable container), // take the other and fit the ratio. // One of the two might be AT_MOST, but we use the value anyway. float ratio = previewHeight / previewWidth; @@ -292,7 +306,7 @@ public class CameraView extends FrameLayout { return; } - // At this point both dimensions are either AT_MOST-AT_MOST, EXACTLY-AT_MOST or AT_MOST-EXACTLY. + // (4) At this point both dimensions are either AT_MOST-AT_MOST, EXACTLY-AT_MOST or AT_MOST-EXACTLY. // Let's manage this sanely. If only one is EXACTLY, we can TRY to fit the aspect ratio, // but it is not guaranteed to succeed. It depends on the AT_MOST value of the other dimensions. if (widthMode == EXACTLY || heightMode == EXACTLY) { @@ -313,8 +327,8 @@ public class CameraView extends FrameLayout { return; } - // Last case, AT_MOST and AT_MOST. Here we can SURELY fit the aspect ratio by filling one - // dimension and adapting the other. + // (5) Last case, AT_MOST and AT_MOST. Here we can SURELY fit the aspect ratio by + // filling one dimension and adapting the other. int height, width; float atMostRatio = (float) heightValue / (float) widthValue; if (atMostRatio >= ratio) { @@ -1373,7 +1387,7 @@ public class CameraView extends FrameLayout { @Override public void run() { byte[] jpeg2 = jpeg; - if (mCropOutput && mPreviewImpl.isCropping()) { + if (mCropOutput && mCameraPreview.isCropping()) { // If consistent, dimensions of the jpeg Bitmap and dimensions of getWidth(), getHeight() // Live in the same reference system. int w = consistentWithView ? getWidth() : getHeight(); @@ -1395,7 +1409,7 @@ public class CameraView extends FrameLayout { @Override public void run() { byte[] jpeg; - if (mCropOutput && mPreviewImpl.isCropping()) { + if (mCropOutput && mCameraPreview.isCropping()) { int w = consistentWithView ? getWidth() : getHeight(); int h = consistentWithView ? getHeight() : getWidth(); AspectRatio targetRatio = AspectRatio.of(w, h); diff --git a/cameraview/src/main/res/layout/surface_view.xml b/cameraview/src/main/res/layout/surface_view.xml index 3db4f4ea..5c0c9dda 100644 --- a/cameraview/src/main/res/layout/surface_view.xml +++ b/cameraview/src/main/res/layout/surface_view.xml @@ -1,10 +1,19 @@ - + + android:layout_gravity="center" + android:gravity="center"> + + + diff --git a/cameraview/src/main/res/layout/texture_view.xml b/cameraview/src/main/res/layout/texture_view.xml index 4714b43c..c4f6b0f1 100644 --- a/cameraview/src/main/res/layout/texture_view.xml +++ b/cameraview/src/main/res/layout/texture_view.xml @@ -3,8 +3,9 @@ + android:layout_height="match_parent" + android:gravity="center" + android:layout_gravity="center" /> diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java similarity index 86% rename from cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java rename to cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java index b8b0e58d..cefd105d 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java @@ -6,9 +6,9 @@ import android.view.Surface; import android.view.View; import android.view.ViewGroup; -abstract class Preview { +abstract class CameraPreview { - protected final static CameraLogger LOG = CameraLogger.create(Preview.class.getSimpleName()); + private final static CameraLogger LOG = CameraLogger.create(CameraPreview.class.getSimpleName()); // Used for testing. Task mCropTask = new Task<>(); @@ -22,6 +22,7 @@ abstract class Preview { private SurfaceCallback mSurfaceCallback; private T mView; + private boolean mCropping; // As far as I can see, these are the view/surface dimensions. // This live in the 'View' orientation. @@ -32,7 +33,7 @@ abstract class Preview { private int mDesiredWidth; private int mDesiredHeight; - Preview(Context context, ViewGroup parent, SurfaceCallback callback) { + CameraPreview(Context context, ViewGroup parent, SurfaceCallback callback) { mView = onCreateView(context, parent); mSurfaceCallback = callback; } @@ -118,6 +119,12 @@ abstract class Preview { */ private final void crop() { mCropTask.start(); + + if (!supportsCropping()) { + mCropTask.end(null); + return; + }; + getView().post(new Runnable() { @Override public void run() { @@ -137,8 +144,8 @@ abstract class Preview { // We must increase width. scaleX = target.toFloat() / current.toFloat(); } - getView().setScaleX(scaleX); - getView().setScaleY(scaleY); + applyCrop(scaleX, scaleY); + mCropping = scaleX > 1.02f || scaleY > 1.02f; LOG.i("crop:", "applied scaleX=", scaleX); LOG.i("crop:", "applied scaleY=", scaleY); mCropTask.end(null); @@ -146,14 +153,21 @@ abstract class Preview { }); } + protected void applyCrop(float scaleX, float scaleY) { + getView().setScaleX(scaleX); + getView().setScaleY(scaleY); + } + + boolean supportsCropping() { + return true; + } /** * Whether we are cropping the output. * If false, this means that the output image will match the visible bounds. * @return true if cropping */ - final boolean isCropping() { - // Account for some error - return getView().getScaleX() > 1.02f || getView().getScaleY() > 1.02f; + /* not final for tests */ boolean isCropping() { + return mCropping; } } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java new file mode 100644 index 00000000..ebbb05df --- /dev/null +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java @@ -0,0 +1,107 @@ +package com.otaliastudios.cameraview; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; + +// Fallback preview when hardware acceleration is off. +// Currently this does NOT support cropping (e. g. the crop inside behavior), +// so we return false in supportsCropping() in order to have proper measuring. +// This means that CameraView is forced to be wrap_content. +class SurfaceCameraPreview extends CameraPreview { + + private final static CameraLogger LOG = CameraLogger.create(SurfaceCameraPreview.class.getSimpleName()); + + SurfaceCameraPreview(Context context, ViewGroup parent, SurfaceCallback callback) { + super(context, parent, callback); + } + + private SurfaceView mSurfaceView; + + @NonNull + @Override + protected View onCreateView(Context context, ViewGroup parent) { + final View root = View.inflate(context, R.layout.surface_view, parent); // MATCH_PARENT + mSurfaceView = root.findViewById(R.id.surface_view); + final SurfaceHolder holder = mSurfaceView.getHolder(); + holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + holder.addCallback(new SurfaceHolder.Callback() { + + private boolean mFirstTime = true; + + @Override + public void surfaceCreated(SurfaceHolder holder) { + // Looks like this is too early to call anything. + // surfaceChanged is guaranteed to be called after, with exact dimensions. + LOG.i("callback:", "surfaceCreated"); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + LOG.i("callback:", "surfaceChanged", "w:", width, "h:", height, "firstTime:", mFirstTime); + if (mFirstTime) { + onSurfaceAvailable(width, height); + mFirstTime = false; + } else { + onSurfaceSizeChanged(width, height); + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + LOG.i("callback:", "surfaceDestroyed"); + onSurfaceDestroyed(); + mFirstTime = true; + } + }); + return root.findViewById(R.id.surface_view_root); + } + + @Override + Surface getSurface() { + return getOutput().getSurface(); + } + + @Override + SurfaceHolder getOutput() { + return mSurfaceView.getHolder(); + } + + @Override + Class getOutputClass() { + return SurfaceHolder.class; + } + + @Override + boolean supportsCropping() { + return false; + } + + @Override + protected void applyCrop(float scaleX, float scaleY) { + /* float currWidth = getView().getWidth(); + float currHeight = getView().getHeight(); + float cropX = currWidth * (scaleX - 1) / 2f; + float cropY = currHeight * (scaleX - 1) / 2f; + LOG.e("applyCrop:", "currWidth:", currWidth, "currHeight:", currHeight); + LOG.e("applyCrop:", "scaleX:", scaleX, "scaleY:", scaleY); + mSurfaceView.setTop((int) -cropY); + mSurfaceView.setLeft((int) -cropX); + mSurfaceView.setBottom((int) (currHeight + cropY)); + mSurfaceView.setRight((int) (currWidth + cropX)); + LOG.e("applyCrop:", "top:", -cropY, "left:", -cropX, "bottom:", currHeight+cropY, "right:", currWidth+cropX); + mSurfaceView.requestLayout(); */ + // mSurfaceView.getLayoutParams().width = (int) (currWidth * scaleX); + // mSurfaceView.getLayoutParams().height = (int) (currHeight * scaleY); + // mSurfaceView.setTranslationY(-cropY / 2f); + // mSurfaceView.setTranslationX(-cropX / 2f); + // getView().setScrollX((int) (cropX / 2f)); + // getView().setScrollY((int) (cropY / 2f)); + // mSurfaceView.requestLayout(); + // super.applyCrop(scaleX, scaleY); + } +} diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java deleted file mode 100644 index 4fe2b73a..00000000 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.otaliastudios.cameraview; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.view.Surface; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; - -// Fallback preview when hardware acceleration is off. -class SurfaceViewPreview extends Preview { - - - SurfaceViewPreview(Context context, ViewGroup parent, SurfaceCallback callback) { - super(context, parent, callback); - } - - @NonNull - @Override - protected SurfaceView onCreateView(Context context, ViewGroup parent) { - final View root = View.inflate(context, R.layout.surface_view, parent); // MATCH_PARENT - SurfaceView surface = root.findViewById(R.id.surface_view); - final SurfaceHolder holder = surface.getHolder(); - holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); - holder.addCallback(new SurfaceHolder.Callback() { - @Override - public void surfaceCreated(SurfaceHolder holder) { - onSurfaceAvailable(getView().getWidth(), getView().getHeight()); - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - onSurfaceSizeChanged(width, height); - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - onSurfaceDestroyed(); - } - }); - return surface; - } - - @Override - Surface getSurface() { - return getOutput().getSurface(); - } - - @Override - SurfaceHolder getOutput() { - return getView().getHolder(); - } - - @Override - Class getOutputClass() { - return SurfaceHolder.class; - } - -} diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/TextureViewPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java similarity index 92% rename from cameraview/src/main/views/com/otaliastudios/cameraview/TextureViewPreview.java rename to cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java index 9bff5d16..0e75b2c0 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/TextureViewPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java @@ -9,11 +9,11 @@ import android.view.TextureView; import android.view.View; import android.view.ViewGroup; -class TextureViewPreview extends Preview { +class TextureCameraPreview extends CameraPreview { private Surface mSurface; - TextureViewPreview(Context context, ViewGroup parent, SurfaceCallback callback) { + TextureCameraPreview(Context context, ViewGroup parent, SurfaceCallback callback) { super(context, parent, callback); } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 9447b52c..02c5b2b0 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ 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 7be67483..b1b678a2 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -11,6 +11,7 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.WindowManager; import android.widget.Toast; import com.otaliastudios.cameraview.Audio; @@ -43,6 +44,8 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); setContentView(R.layout.activity_camera); CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE); @@ -168,11 +171,20 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis } @Override - public void onValueChanged(Control control, Object value, String name) { + public boolean onValueChanged(Control control, Object value, String name) { + if (!camera.isHardwareAccelerated() && (control == Control.WIDTH || control == Control.HEIGHT)) { + if ((Integer) value > 0) { + message("This device does not support hardware acceleration. " + + "In this case you can not change width or height. " + + "The view will act as WRAP_CONTENT by default.", true); + return false; + } + } control.applyValue(camera, value); BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel); b.setState(BottomSheetBehavior.STATE_HIDDEN); message("Changed " + control.getName() + " to " + name, false); + return true; } //region Boilerplate diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/ControlView.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/ControlView.java index bab55baa..445c3d8b 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/ControlView.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/ControlView.java @@ -25,7 +25,7 @@ import java.util.Collection; public class ControlView extends LinearLayout implements Spinner.OnItemSelectedListener { interface Callback { - void onValueChanged(Control control, Object value, String name); + boolean onValueChanged(Control control, Object value, String name); } private Value value; @@ -86,7 +86,11 @@ public class ControlView extends LinearLayout implements Spinner.OnItemSe public void onItemSelected(AdapterView adapterView, View view, int i, long l) { if (values.get(i) != value) { Log.e("ControlView", "curr: " + value + " new: " + values.get(i)); - callback.onValueChanged(control, values.get(i), valuesStrings.get(i)); + if (!callback.onValueChanged(control, values.get(i), valuesStrings.get(i))) { + spinner.setSelection(values.indexOf(value)); // Go back. + } else { + value = values.get(i); + } } } diff --git a/demo/src/main/res/layout/activity_camera.xml b/demo/src/main/res/layout/activity_camera.xml index fdc520ff..75a8fc1a 100644 --- a/demo/src/main/res/layout/activity_camera.xml +++ b/demo/src/main/res/layout/activity_camera.xml @@ -11,8 +11,8 @@