From fa65c5247e70dd87dad421374a8bffc77f268d9d Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Fri, 29 Sep 2017 20:19:02 +0200 Subject: [PATCH] Refactor some classes, fix hardware acceleration bug, disable cropping for SurfaceView --- .../cameraview/CameraCallbacksTest.java | 83 +++++++------- .../cameraview/CameraViewTest.java | 12 +- .../cameraview/IntegrationTest.java | 6 +- .../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 | 69 ++++++----- .../src/main/res/layout/surface_view.xml | 14 ++- .../{Preview.java => CameraPreview.java} | 30 +++-- .../cameraview/SurfaceCameraPreview.java | 107 ++++++++++++++++++ .../cameraview/SurfaceViewPreview.java | 74 ------------ ...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 +- 21 files changed, 293 insertions(+), 204 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} (87%) 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/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..ea30e8d5 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java @@ -33,7 +33,7 @@ import static org.junit.Assert.*; */ @RunWith(AndroidJUnit4.class) @MediumTest -@Ignore +// @Ignore public class IntegrationTest extends BaseTest { @Rule @@ -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 97588dbf..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,24 +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.i("preview:", "isHardwareAccelerated:", isHardwareAccelerated()); + 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()); } @@ -212,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 @@ -236,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) { @@ -257,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.", @@ -272,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; @@ -293,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) { @@ -314,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) { @@ -1374,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(); @@ -1396,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 6d6012fa..5c0c9dda 100644 --- a/cameraview/src/main/res/layout/surface_view.xml +++ b/cameraview/src/main/res/layout/surface_view.xml @@ -1,11 +1,19 @@ - + + android: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 87% rename from cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java rename to cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java index 769ac1ce..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 { - private 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 f9bd5499..00000000 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java +++ /dev/null @@ -1,74 +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 { - - private final static CameraLogger LOG = CameraLogger.create(SurfaceViewPreview.class.getSimpleName()); - - 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() { - - private boolean mFirstTime = true; - - @Override - public void surfaceCreated(SurfaceHolder holder) { - LOG.i("callback:", "surfaceCreated"); - // Looks like this is too early to call anything. - // onSurfaceAvailable(getView().getWidth(), getView().getHeight()); - } - - @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 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 @@