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
pull/55/head
Mattia Iavarone 7 years ago committed by GitHub
parent 1be4a313d1
commit b1d183a714
  1. 9
      README.md
  2. 83
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java
  3. 12
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  4. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
  5. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
  6. 14
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraPreview.java
  7. 10
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java
  8. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceCameraPreviewTest.java
  9. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java
  10. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  11. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
  12. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  13. 62
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  14. 13
      cameraview/src/main/res/layout/surface_view.xml
  15. 5
      cameraview/src/main/res/layout/texture_view.xml
  16. 30
      cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java
  17. 107
      cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java
  18. 60
      cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java
  19. 4
      cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java
  20. 1
      demo/src/main/AndroidManifest.xml
  21. 14
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
  22. 8
      demo/src/main/java/com/otaliastudios/cameraview/demo/ControlView.java
  23. 4
      demo/src/main/res/layout/activity_camera.xml

@ -57,6 +57,7 @@ compile 'com.otaliastudios:cameraview:1.2.1'
- [Permissions Behavior](#permissions-behavior) - [Permissions Behavior](#permissions-behavior)
- [Manifest file](#manifest-file) - [Manifest file](#manifest-file)
- [Roadmap](#roadmap) - [Roadmap](#roadmap)
- [Device-specific issues](#device-specific-issues)
## Usage ## 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 - [ ] 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 - [ ] better error handling, maybe with a onError(e) method in the public listener, or have each public method return a boolean
- [ ] decent code coverage - [ ] 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.

@ -4,10 +4,7 @@ package com.otaliastudios.cameraview;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.support.test.filters.MediumTest; import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -18,9 +15,7 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.mockito.stubbing.Stubber;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNotNull;
import static org.junit.Assert.assertEquals; 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.doAnswer;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@MediumTest @MediumTest
public class CameraCallbacksTest extends BaseTest { public class CameraCallbacksTest extends BaseTest {
private CameraView camera; private CameraView camera;
private CameraView.CameraCallbacks callbacks;
private CameraListener listener; private CameraListener listener;
private MockCameraController mockController; private MockCameraController mockController;
private MockPreview mockPreview; private MockCameraPreview mockPreview;
private Task<Boolean> task; private Task<Boolean> task;
@ -58,14 +52,14 @@ public class CameraCallbacksTest extends BaseTest {
listener = mock(CameraListener.class); listener = mock(CameraListener.class);
camera = new CameraView(context) { camera = new CameraView(context) {
@Override @Override
protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { protected CameraController instantiateCameraController(CameraCallbacks callbacks) {
mockController = new MockCameraController(callbacks, preview); mockController = new MockCameraController(callbacks);
return mockController; return mockController;
} }
@Override @Override
protected Preview instantiatePreview(Context context, ViewGroup container) { protected CameraPreview instantiatePreview(Context context, ViewGroup container) {
mockPreview = new MockPreview(context, container); mockPreview = new MockCameraPreview(context, container);
return mockPreview; return mockPreview;
} }
@ -74,8 +68,8 @@ public class CameraCallbacksTest extends BaseTest {
return true; return true;
} }
}; };
camera.instantiatePreview();
camera.addCameraListener(listener); camera.addCameraListener(listener);
callbacks = camera.mCameraCallbacks;
task = new Task<>(); task = new Task<>();
task.listen(); task.listen();
} }
@ -87,26 +81,25 @@ public class CameraCallbacksTest extends BaseTest {
camera = null; camera = null;
mockController = null; mockController = null;
mockPreview = null; mockPreview = null;
callbacks = null;
listener = null; listener = null;
} }
// Completes our task. // Completes our task.
private Answer completeTask() { private Stubber completeTask() {
return new Answer() { return doAnswer(new Answer() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {
task.end(true); task.end(true);
return null; return null;
} }
}; });
} }
@Test @Test
public void testDontDispatchIfRemoved() { public void testDontDispatchIfRemoved() {
camera.removeCameraListener(listener); camera.removeCameraListener(listener);
doAnswer(completeTask()).when(listener).onCameraOpened(null); completeTask().when(listener).onCameraOpened(null);
callbacks.dispatchOnCameraOpened(null); camera.mCameraCallbacks.dispatchOnCameraOpened(null);
assertNull(task.await(200)); assertNull(task.await(200));
verify(listener, never()).onCameraOpened(null); verify(listener, never()).onCameraOpened(null);
@ -115,8 +108,8 @@ public class CameraCallbacksTest extends BaseTest {
@Test @Test
public void testDontDispatchIfCleared() { public void testDontDispatchIfCleared() {
camera.clearCameraListeners(); camera.clearCameraListeners();
doAnswer(completeTask()).when(listener).onCameraOpened(null); completeTask().when(listener).onCameraOpened(null);
callbacks.dispatchOnCameraOpened(null); camera.mCameraCallbacks.dispatchOnCameraOpened(null);
assertNull(task.await(200)); assertNull(task.await(200));
verify(listener, never()).onCameraOpened(null); verify(listener, never()).onCameraOpened(null);
@ -124,8 +117,8 @@ public class CameraCallbacksTest extends BaseTest {
@Test @Test
public void testDispatchOnCameraOpened() { public void testDispatchOnCameraOpened() {
doAnswer(completeTask()).when(listener).onCameraOpened(null); completeTask().when(listener).onCameraOpened(null);
callbacks.dispatchOnCameraOpened(null); camera.mCameraCallbacks.dispatchOnCameraOpened(null);
assertNotNull(task.await(200)); assertNotNull(task.await(200));
verify(listener, times(1)).onCameraOpened(null); verify(listener, times(1)).onCameraOpened(null);
@ -133,8 +126,8 @@ public class CameraCallbacksTest extends BaseTest {
@Test @Test
public void testDispatchOnCameraClosed() { public void testDispatchOnCameraClosed() {
doAnswer(completeTask()).when(listener).onCameraClosed(); completeTask().when(listener).onCameraClosed();
callbacks.dispatchOnCameraClosed(); camera.mCameraCallbacks.dispatchOnCameraClosed();
assertNotNull(task.await(200)); assertNotNull(task.await(200));
verify(listener, times(1)).onCameraClosed(); verify(listener, times(1)).onCameraClosed();
@ -142,8 +135,8 @@ public class CameraCallbacksTest extends BaseTest {
@Test @Test
public void testDispatchOnVideoTaken() { public void testDispatchOnVideoTaken() {
doAnswer(completeTask()).when(listener).onVideoTaken(null); completeTask().when(listener).onVideoTaken(null);
callbacks.dispatchOnVideoTaken(null); camera.mCameraCallbacks.dispatchOnVideoTaken(null);
assertNotNull(task.await(200)); assertNotNull(task.await(200));
verify(listener, times(1)).onVideoTaken(null); verify(listener, times(1)).onVideoTaken(null);
@ -151,8 +144,8 @@ public class CameraCallbacksTest extends BaseTest {
@Test @Test
public void testDispatchOnZoomChanged() { public void testDispatchOnZoomChanged() {
doAnswer(completeTask()).when(listener).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class)); completeTask().when(listener).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class));
callbacks.dispatchOnZoomChanged(0f, null); camera.mCameraCallbacks.dispatchOnZoomChanged(0f, null);
assertNotNull(task.await(200)); assertNotNull(task.await(200));
verify(listener, times(1)).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class)); verify(listener, times(1)).onZoomChanged(anyFloat(), any(float[].class), any(PointF[].class));
@ -160,8 +153,8 @@ public class CameraCallbacksTest extends BaseTest {
@Test @Test
public void testDispatchOnExposureCorrectionChanged() { public void testDispatchOnExposureCorrectionChanged() {
doAnswer(completeTask()).when(listener).onExposureCorrectionChanged(0f, null, null); completeTask().when(listener).onExposureCorrectionChanged(0f, null, null);
callbacks.dispatchOnExposureCorrectionChanged(0f, null, null); camera.mCameraCallbacks.dispatchOnExposureCorrectionChanged(0f, null, null);
assertNotNull(task.await(200)); assertNotNull(task.await(200));
verify(listener, times(1)).onExposureCorrectionChanged(0f, null, null); 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); camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER);
PointF point = new PointF(); PointF point = new PointF();
doAnswer(completeTask()).when(listener).onFocusStart(point); completeTask().when(listener).onFocusStart(point);
callbacks.dispatchOnFocusStart(Gesture.TAP, point); camera.mCameraCallbacks.dispatchOnFocusStart(Gesture.TAP, point);
assertNotNull(task.await(200)); assertNotNull(task.await(200));
verify(listener, times(1)).onFocusStart(point); verify(listener, times(1)).onFocusStart(point);
@ -190,8 +183,8 @@ public class CameraCallbacksTest extends BaseTest {
PointF point = new PointF(); PointF point = new PointF();
boolean success = true; boolean success = true;
doAnswer(completeTask()).when(listener).onFocusEnd(success, point); completeTask().when(listener).onFocusEnd(success, point);
callbacks.dispatchOnFocusEnd(Gesture.TAP, success, point); camera.mCameraCallbacks.dispatchOnFocusEnd(Gesture.TAP, success, point);
assertNotNull(task.await(200)); assertNotNull(task.await(200));
verify(listener, times(1)).onFocusEnd(success, point); verify(listener, times(1)).onFocusEnd(success, point);
@ -200,31 +193,31 @@ public class CameraCallbacksTest extends BaseTest {
@Test @Test
public void testOrientationCallbacks_deviceOnly() { public void testOrientationCallbacks_deviceOnly() {
doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt()); completeTask().when(listener).onOrientationChanged(anyInt());
// Assert not called. Both methods must be called. // Assert not called. Both methods must be called.
callbacks.onDeviceOrientationChanged(0); camera.mCameraCallbacks.onDeviceOrientationChanged(0);
assertNull(task.await(200)); assertNull(task.await(200));
verify(listener, never()).onOrientationChanged(anyInt()); verify(listener, never()).onOrientationChanged(anyInt());
} }
@Test @Test
public void testOrientationCallbacks_displayOnly() { public void testOrientationCallbacks_displayOnly() {
doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt()); completeTask().when(listener).onOrientationChanged(anyInt());
// Assert not called. Both methods must be called. // Assert not called. Both methods must be called.
callbacks.onDisplayOffsetChanged(0); camera.mCameraCallbacks.onDisplayOffsetChanged(0);
assertNull(task.await(200)); assertNull(task.await(200));
verify(listener, never()).onOrientationChanged(anyInt()); verify(listener, never()).onOrientationChanged(anyInt());
} }
@Test @Test
public void testOrientationCallbacks_both() { public void testOrientationCallbacks_both() {
doAnswer(completeTask()).when(listener).onOrientationChanged(anyInt()); completeTask().when(listener).onOrientationChanged(anyInt());
// Assert called. // Assert called.
callbacks.onDisplayOffsetChanged(0); camera.mCameraCallbacks.onDisplayOffsetChanged(0);
callbacks.onDeviceOrientationChanged(90); camera.mCameraCallbacks.onDeviceOrientationChanged(90);
assertNotNull(task.await(200)); assertNotNull(task.await(200));
verify(listener, times(1)).onOrientationChanged(anyInt()); verify(listener, times(1)).onOrientationChanged(anyInt());
} }
@ -290,9 +283,9 @@ public class CameraCallbacksTest extends BaseTest {
// Create fake JPEG array and trigger the process. // Create fake JPEG array and trigger the process.
if (jpeg) { if (jpeg) {
callbacks.processImage(mockJpeg(imageDim[0], imageDim[1]), true, false); camera.mCameraCallbacks.processImage(mockJpeg(imageDim[0], imageDim[1]), true, false);
} else { } 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. // Wait for result and get out dimensions.

@ -26,7 +26,7 @@ public class CameraViewTest extends BaseTest {
private CameraView cameraView; private CameraView cameraView;
private MockCameraController mockController; private MockCameraController mockController;
private Preview mockPreview; private CameraPreview mockPreview;
private boolean hasPermissions; private boolean hasPermissions;
@Before @Before
@ -37,14 +37,14 @@ public class CameraViewTest extends BaseTest {
Context context = context(); Context context = context();
cameraView = new CameraView(context) { cameraView = new CameraView(context) {
@Override @Override
protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { protected CameraController instantiateCameraController(CameraCallbacks callbacks) {
mockController = new MockCameraController(callbacks, preview); mockController = new MockCameraController(callbacks);
return mockController; return mockController;
} }
@Override @Override
protected Preview instantiatePreview(Context context, ViewGroup container) { protected CameraPreview instantiatePreview(Context context, ViewGroup container) {
mockPreview = new MockPreview(context, container); mockPreview = new MockCameraPreview(context, container);
return mockPreview; return mockPreview;
} }
@ -53,6 +53,8 @@ public class CameraViewTest extends BaseTest {
return hasPermissions; return hasPermissions;
} }
}; };
// Instantiate preview now.
cameraView.instantiatePreview();
} }
}); });
} }

@ -56,8 +56,8 @@ public class IntegrationTest extends BaseTest {
public void run() { public void run() {
camera = new CameraView(rule.getActivity()) { camera = new CameraView(rule.getActivity()) {
@Override @Override
protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { protected CameraController instantiateCameraController(CameraCallbacks callbacks) {
controller = new Camera1(callbacks, preview); controller = new Camera1(callbacks);
return controller; return controller;
} }
}; };

@ -15,8 +15,8 @@ public class MockCameraController extends CameraController {
boolean mZoomChanged; boolean mZoomChanged;
boolean mExposureCorrectionChanged; boolean mExposureCorrectionChanged;
MockCameraController(CameraView.CameraCallbacks callback, Preview preview) { MockCameraController(CameraView.CameraCallbacks callback) {
super(callback, preview); super(callback);
} }
void setMockCameraOptions(CameraOptions options) { void setMockCameraOptions(CameraOptions options) {

@ -7,15 +7,21 @@ import android.view.Surface;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
public class MockPreview extends Preview<View, Void> { public class MockCameraPreview extends CameraPreview<View, Void> {
MockPreview(Context context, ViewGroup parent) { MockCameraPreview(Context context, ViewGroup parent) {
super(context, parent, null); super(context, parent, null);
} }
private boolean mCropping = false;
public void setIsCropping(boolean crop) { public void setIsCropping(boolean crop) {
getView().setScaleX(crop ? 2 : 1); mCropping = crop;
getView().setScaleY(crop ? 2 : 1); }
@Override
boolean isCropping() {
return mCropping;
} }
@NonNull @NonNull

@ -19,14 +19,14 @@ import static org.mockito.Mockito.*;
public abstract class PreviewTest extends BaseTest { 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 @Rule
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class); public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
protected Preview preview; protected CameraPreview preview;
protected Size surfaceSize; protected Size surfaceSize;
private Preview.SurfaceCallback callback; private CameraPreview.SurfaceCallback callback;
private Task<Boolean> availability; private Task<Boolean> availability;
@Before @Before
@ -40,7 +40,7 @@ public abstract class PreviewTest extends BaseTest {
TestActivity a = rule.getActivity(); TestActivity a = rule.getActivity();
surfaceSize = a.getContentSize(); surfaceSize = a.getContentSize();
callback = mock(Preview.SurfaceCallback.class); callback = mock(CameraPreview.SurfaceCallback.class);
doAnswer(new Answer() { doAnswer(new Answer() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { 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. // If we apply a different aspect ratio, there should be cropping.
float desired = view * 1.2f; float desired = view * 1.2f;
if (preview.supportsCropping()) {
setDesiredAspectRatio(desired); setDesiredAspectRatio(desired);
assertTrue(preview.isCropping()); assertTrue(preview.isCropping());
}
// Since desired is 'desired', let's fake a new view size that is consistent with it. // Since desired is 'desired', let's fake a new view size that is consistent with it.
// Ensure crop is not happening anymore. // Ensure crop is not happening anymore.

@ -10,10 +10,10 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class SurfaceViewPreviewTest extends PreviewTest { public class SurfaceCameraPreviewTest extends PreviewTest {
@Override @Override
protected Preview createPreview(Context context, ViewGroup parent, Preview.SurfaceCallback callback) { protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) {
return new SurfaceViewPreview(context, parent, callback); return new SurfaceCameraPreview(context, parent, callback);
} }
} }

@ -10,11 +10,11 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class TextureViewPreviewTest extends PreviewTest { public class TextureCameraPreviewTest extends PreviewTest {
@Override @Override
protected Preview createPreview(Context context, ViewGroup parent, Preview.SurfaceCallback callback) { protected CameraPreview createPreview(Context context, ViewGroup parent, CameraPreview.SurfaceCallback callback) {
return new TextureViewPreview(context, parent, callback); return new TextureCameraPreview(context, parent, callback);
} }
@Override @Override

@ -15,7 +15,6 @@ import android.support.annotation.WorkerThread;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -57,8 +56,8 @@ class Camera1 extends CameraController {
private final Object mLock = new Object(); private final Object mLock = new Object();
Camera1(CameraView.CameraCallbacks callback, final Preview preview) { Camera1(CameraView.CameraCallbacks callback) {
super(callback, preview); super(callback);
} }
/** /**
@ -118,7 +117,7 @@ class Camera1 extends CameraController {
} }
private boolean shouldSetup() { 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. // 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. * 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 * It is possible that in the first call the preview surface has not already computed its
* dimensions. * 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. * and this should be refreshed.
*/ */
private Size computeCaptureSize() { private Size computeCaptureSize() {

@ -22,8 +22,8 @@ import java.util.List;
@TargetApi(21) @TargetApi(21)
class Camera2 extends CameraController { class Camera2 extends CameraController {
public Camera2(CameraView.CameraCallbacks callback, Preview preview) { public Camera2(CameraView.CameraCallbacks callback) {
super(callback, preview); super(callback);
} }
@Override @Override

@ -2,14 +2,13 @@ package com.otaliastudios.cameraview;
import android.graphics.PointF; import android.graphics.PointF;
import android.location.Location; import android.location.Location;
import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread; import android.support.annotation.WorkerThread;
import java.io.File; 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 String TAG = CameraController.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); 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. static final int STATE_STARTED = 2; // Camera is available and we can set parameters.
protected final CameraView.CameraCallbacks mCameraCallbacks; protected final CameraView.CameraCallbacks mCameraCallbacks;
protected final Preview mPreview; protected CameraPreview mPreview;
protected Facing mFacing; protected Facing mFacing;
protected Flash mFlash; protected Flash mFlash;
@ -43,10 +42,8 @@ abstract class CameraController implements Preview.SurfaceCallback {
protected WorkerHandler mHandler; protected WorkerHandler mHandler;
CameraController(CameraView.CameraCallbacks callback, Preview preview) { CameraController(CameraView.CameraCallbacks callback) {
mCameraCallbacks = callback; mCameraCallbacks = callback;
mPreview = preview;
mPreview.setSurfaceCallback(this);
mHandler = WorkerHandler.get("CameraViewController"); mHandler = WorkerHandler.get("CameraViewController");
mHandler.getThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { mHandler.getThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override @Override
@ -67,6 +64,11 @@ abstract class CameraController implements Preview.SurfaceCallback {
}); });
} }
void setPreview(CameraPreview cameraPreview) {
mPreview = cameraPreview;
mPreview.setSurfaceCallback(this);
}
//region Start&Stop //region Start&Stop
private String ss() { private String ss() {

@ -58,9 +58,9 @@ public class CameraView extends FrameLayout {
// Components // Components
/* for tests */ CameraCallbacks mCameraCallbacks; /* for tests */ CameraCallbacks mCameraCallbacks;
private CameraPreview mCameraPreview;
private OrientationHelper mOrientationHelper; private OrientationHelper mOrientationHelper;
private CameraController mCameraController; private CameraController mCameraController;
private Preview mPreviewImpl;
private ArrayList<CameraListener> mListeners = new ArrayList<>(2); private ArrayList<CameraListener> mListeners = new ArrayList<>(2);
private MediaActionSound mSound; private MediaActionSound mSound;
@ -89,6 +89,7 @@ public class CameraView extends FrameLayout {
@SuppressWarnings("WrongConstant") @SuppressWarnings("WrongConstant")
private void init(@NonNull Context context, @Nullable AttributeSet attrs) { private void init(@NonNull Context context, @Nullable AttributeSet attrs) {
setWillNotDraw(false);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CameraView, 0, 0); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CameraView, 0, 0);
// Self managed // Self managed
int jpegQuality = a.getInteger(R.styleable.CameraView_cameraJpegQuality, DEFAULT_JPEG_QUALITY); int jpegQuality = a.getInteger(R.styleable.CameraView_cameraJpegQuality, DEFAULT_JPEG_QUALITY);
@ -115,8 +116,7 @@ public class CameraView extends FrameLayout {
// Components // Components
mCameraCallbacks = new Callbacks(); mCameraCallbacks = new Callbacks();
mPreviewImpl = instantiatePreview(context, this); mCameraController = instantiateCameraController(mCameraCallbacks);
mCameraController = instantiateCameraController(mCameraCallbacks, mPreviewImpl);
mUiHandler = new Handler(Looper.getMainLooper()); mUiHandler = new Handler(Looper.getMainLooper());
mWorkerHandler = WorkerHandler.get("CameraViewWorker"); 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. // TextureView is not supported without hardware acceleration.
LOG.w("preview:", "isHardwareAccelerated:", isHardwareAccelerated());
return isHardwareAccelerated() ? return isHardwareAccelerated() ?
new TextureViewPreview(context, container, null) : new TextureCameraPreview(context, container, null) :
new SurfaceViewPreview(context, container, null); new SurfaceCameraPreview(context, container, null);
} }
/* for tests */ void instantiatePreview() {
protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { mCameraPreview = instantiatePreview(getContext(), this);
return new Camera1(callbacks, preview); mCameraController.setPreview(mCameraPreview);
} }
@Override @Override
protected void onAttachedToWindow() { protected void onAttachedToWindow() {
super.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()) { if (!isInEditMode()) {
mOrientationHelper.enable(getContext()); mOrientationHelper.enable(getContext());
} }
@ -211,7 +219,7 @@ public class CameraView extends FrameLayout {
* aspect ratio. * aspect ratio.
* *
* When both dimensions are MATCH_PARENT, CameraView will fill its * 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. * a CENTER CROP scale type.
* *
* When both dimensions are WRAP_CONTENT, CameraView will take the biggest dimensions that * 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 previewWidth = flip ? previewSize.getHeight() : previewSize.getWidth();
final float previewHeight = flip ? previewSize.getWidth() : previewSize.getHeight(); final float previewHeight = flip ? previewSize.getWidth() : previewSize.getHeight();
// Pre-process specs
final ViewGroup.LayoutParams lp = getLayoutParams();
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 // If MATCH_PARENT is interpreted as AT_MOST, transform to EXACTLY
// to be consistent with our semantics (and our docs). // to be consistent with our semantics (and our docs).
final ViewGroup.LayoutParams lp = getLayoutParams();
if (widthMode == AT_MOST && lp.width == MATCH_PARENT) widthMode = EXACTLY; if (widthMode == AT_MOST && lp.width == MATCH_PARENT) widthMode = EXACTLY;
if (heightMode == AT_MOST && lp.height == MATCH_PARENT) heightMode = EXACTLY; if (heightMode == AT_MOST && lp.height == MATCH_PARENT) heightMode = EXACTLY;
LOG.i("onMeasure:", "requested dimensions are", }
"(" + widthValue + "[" + ms(widthMode) + "]x" +
LOG.i("onMeasure:", "requested dimensions are", "(" + widthValue + "[" + ms(widthMode) + "]x" +
heightValue + "[" + ms(heightMode) + "])"); heightValue + "[" + ms(heightMode) + "])");
LOG.i("onMeasure:", "previewSize is", "(" + previewWidth + "x" + previewHeight + ")"); LOG.i("onMeasure:", "previewSize is", "(" + previewWidth + "x" + previewHeight + ")");
// (1) If we have fixed dimensions (either 300dp or MATCH_PARENT), there's nothing we should do,
// 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) // 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. // except the case in which these fixed dimensions manage to fit exactly the preview aspect ratio.
if (widthMode == EXACTLY && heightMode == EXACTLY) { if (widthMode == EXACTLY && heightMode == EXACTLY) {
@ -256,8 +270,8 @@ public class CameraView extends FrameLayout {
return; return;
} }
// If both dimensions are free, with no limits, then our size will be exactly the // (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 scrollable containers. // preview size. This can happen rarely, for example in 2d scrollable containers.
if (widthMode == UNSPECIFIED && heightMode == UNSPECIFIED) { if (widthMode == UNSPECIFIED && heightMode == UNSPECIFIED) {
LOG.i("onMeasure:", "both are completely free.", LOG.i("onMeasure:", "both are completely free.",
"We respect that and extend to the whole preview size.", "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). // 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. // 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. // take the other and fit the ratio.
// One of the two might be AT_MOST, but we use the value anyway. // One of the two might be AT_MOST, but we use the value anyway.
float ratio = previewHeight / previewWidth; float ratio = previewHeight / previewWidth;
@ -292,7 +306,7 @@ public class CameraView extends FrameLayout {
return; 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, // 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. // but it is not guaranteed to succeed. It depends on the AT_MOST value of the other dimensions.
if (widthMode == EXACTLY || heightMode == EXACTLY) { if (widthMode == EXACTLY || heightMode == EXACTLY) {
@ -313,8 +327,8 @@ public class CameraView extends FrameLayout {
return; return;
} }
// Last case, AT_MOST and AT_MOST. Here we can SURELY fit the aspect ratio by filling one // (5) Last case, AT_MOST and AT_MOST. Here we can SURELY fit the aspect ratio by
// dimension and adapting the other. // filling one dimension and adapting the other.
int height, width; int height, width;
float atMostRatio = (float) heightValue / (float) widthValue; float atMostRatio = (float) heightValue / (float) widthValue;
if (atMostRatio >= ratio) { if (atMostRatio >= ratio) {
@ -1373,7 +1387,7 @@ public class CameraView extends FrameLayout {
@Override @Override
public void run() { public void run() {
byte[] jpeg2 = jpeg; byte[] jpeg2 = jpeg;
if (mCropOutput && mPreviewImpl.isCropping()) { if (mCropOutput && mCameraPreview.isCropping()) {
// If consistent, dimensions of the jpeg Bitmap and dimensions of getWidth(), getHeight() // If consistent, dimensions of the jpeg Bitmap and dimensions of getWidth(), getHeight()
// Live in the same reference system. // Live in the same reference system.
int w = consistentWithView ? getWidth() : getHeight(); int w = consistentWithView ? getWidth() : getHeight();
@ -1395,7 +1409,7 @@ public class CameraView extends FrameLayout {
@Override @Override
public void run() { public void run() {
byte[] jpeg; byte[] jpeg;
if (mCropOutput && mPreviewImpl.isCropping()) { if (mCropOutput && mCameraPreview.isCropping()) {
int w = consistentWithView ? getWidth() : getHeight(); int w = consistentWithView ? getWidth() : getHeight();
int h = consistentWithView ? getHeight() : getWidth(); int h = consistentWithView ? getHeight() : getWidth();
AspectRatio targetRatio = AspectRatio.of(w, h); AspectRatio targetRatio = AspectRatio.of(w, h);

@ -1,10 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"> <merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- This FL could be removed, I added to test if we could implement
proper cropping with SurfaceView. I was not easily able to. -->
<FrameLayout
android:id="@+id/surface_view_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center">
<SurfaceView <SurfaceView
android:id="@+id/surface_view" android:id="@+id/surface_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent" />
android:gravity="center" />
</FrameLayout>
</merge> </merge>

@ -3,8 +3,9 @@
<TextureView <TextureView
android:id="@+id/texture_view" android:id="@+id/texture_view"
android:gravity="center"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
android:gravity="center"
android:layout_gravity="center" />
</merge> </merge>

@ -6,9 +6,9 @@ import android.view.Surface;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
abstract class Preview<T extends View, Output> { abstract class CameraPreview<T extends View, Output> {
protected final static CameraLogger LOG = CameraLogger.create(Preview.class.getSimpleName()); private final static CameraLogger LOG = CameraLogger.create(CameraPreview.class.getSimpleName());
// Used for testing. // Used for testing.
Task<Void> mCropTask = new Task<>(); Task<Void> mCropTask = new Task<>();
@ -22,6 +22,7 @@ abstract class Preview<T extends View, Output> {
private SurfaceCallback mSurfaceCallback; private SurfaceCallback mSurfaceCallback;
private T mView; private T mView;
private boolean mCropping;
// As far as I can see, these are the view/surface dimensions. // As far as I can see, these are the view/surface dimensions.
// This live in the 'View' orientation. // This live in the 'View' orientation.
@ -32,7 +33,7 @@ abstract class Preview<T extends View, Output> {
private int mDesiredWidth; private int mDesiredWidth;
private int mDesiredHeight; private int mDesiredHeight;
Preview(Context context, ViewGroup parent, SurfaceCallback callback) { CameraPreview(Context context, ViewGroup parent, SurfaceCallback callback) {
mView = onCreateView(context, parent); mView = onCreateView(context, parent);
mSurfaceCallback = callback; mSurfaceCallback = callback;
} }
@ -118,6 +119,12 @@ abstract class Preview<T extends View, Output> {
*/ */
private final void crop() { private final void crop() {
mCropTask.start(); mCropTask.start();
if (!supportsCropping()) {
mCropTask.end(null);
return;
};
getView().post(new Runnable() { getView().post(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -137,8 +144,8 @@ abstract class Preview<T extends View, Output> {
// We must increase width. // We must increase width.
scaleX = target.toFloat() / current.toFloat(); scaleX = target.toFloat() / current.toFloat();
} }
getView().setScaleX(scaleX); applyCrop(scaleX, scaleY);
getView().setScaleY(scaleY); mCropping = scaleX > 1.02f || scaleY > 1.02f;
LOG.i("crop:", "applied scaleX=", scaleX); LOG.i("crop:", "applied scaleX=", scaleX);
LOG.i("crop:", "applied scaleY=", scaleY); LOG.i("crop:", "applied scaleY=", scaleY);
mCropTask.end(null); mCropTask.end(null);
@ -146,14 +153,21 @@ abstract class Preview<T extends View, Output> {
}); });
} }
protected void applyCrop(float scaleX, float scaleY) {
getView().setScaleX(scaleX);
getView().setScaleY(scaleY);
}
boolean supportsCropping() {
return true;
}
/** /**
* Whether we are cropping the output. * Whether we are cropping the output.
* If false, this means that the output image will match the visible bounds. * If false, this means that the output image will match the visible bounds.
* @return true if cropping * @return true if cropping
*/ */
final boolean isCropping() { /* not final for tests */ boolean isCropping() {
// Account for some error return mCropping;
return getView().getScaleX() > 1.02f || getView().getScaleY() > 1.02f;
} }
} }

@ -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<View, SurfaceHolder> {
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<SurfaceHolder> 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);
}
}

@ -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<SurfaceView, SurfaceHolder> {
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<SurfaceHolder> getOutputClass() {
return SurfaceHolder.class;
}
}

@ -9,11 +9,11 @@ import android.view.TextureView;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
class TextureViewPreview extends Preview<TextureView, SurfaceTexture> { class TextureCameraPreview extends CameraPreview<TextureView, SurfaceTexture> {
private Surface mSurface; private Surface mSurface;
TextureViewPreview(Context context, ViewGroup parent, SurfaceCallback callback) { TextureCameraPreview(Context context, ViewGroup parent, SurfaceCallback callback) {
super(context, parent, callback); super(context, parent, callback);
} }

@ -15,6 +15,7 @@
<activity <activity
android:name=".CameraActivity" android:name=".CameraActivity"
android:theme="@style/Theme.MainActivity" android:theme="@style/Theme.MainActivity"
android:hardwareAccelerated="true"
android:configChanges="orientation|screenLayout|keyboardHidden"> android:configChanges="orientation|screenLayout|keyboardHidden">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

@ -11,6 +11,7 @@ import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.Toast; import android.widget.Toast;
import com.otaliastudios.cameraview.Audio; import com.otaliastudios.cameraview.Audio;
@ -43,6 +44,8 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
setContentView(R.layout.activity_camera); setContentView(R.layout.activity_camera);
CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE); CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE);
@ -168,11 +171,20 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
} }
@Override @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); control.applyValue(camera, value);
BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel); BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel);
b.setState(BottomSheetBehavior.STATE_HIDDEN); b.setState(BottomSheetBehavior.STATE_HIDDEN);
message("Changed " + control.getName() + " to " + name, false); message("Changed " + control.getName() + " to " + name, false);
return true;
} }
//region Boilerplate //region Boilerplate

@ -25,7 +25,7 @@ import java.util.Collection;
public class ControlView<Value> extends LinearLayout implements Spinner.OnItemSelectedListener { public class ControlView<Value> extends LinearLayout implements Spinner.OnItemSelectedListener {
interface Callback { interface Callback {
void onValueChanged(Control control, Object value, String name); boolean onValueChanged(Control control, Object value, String name);
} }
private Value value; private Value value;
@ -86,7 +86,11 @@ public class ControlView<Value> extends LinearLayout implements Spinner.OnItemSe
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
if (values.get(i) != value) { if (values.get(i) != value) {
Log.e("ControlView", "curr: " + value + " new: " + values.get(i)); 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);
}
} }
} }

@ -11,8 +11,8 @@
<com.otaliastudios.cameraview.CameraView <com.otaliastudios.cameraview.CameraView
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/camera" android:id="@+id/camera"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"
android:keepScreenOn="true" android:keepScreenOn="true"
app:cameraGrid="off" app:cameraGrid="off"

Loading…
Cancel
Save