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. 14
      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. 68
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  14. 15
      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)
- [Manifest file](#manifest-file)
- [Roadmap](#roadmap)
- [Device-specific issues](#device-specific-issues)
## Usage
@ -526,3 +527,11 @@ These are still things that need to be done, off the top of my head:
- [ ] add onRequestPermissionResults for easy permission callback
- [ ] better error handling, maybe with a onError(e) method in the public listener, or have each public method return a boolean
- [ ] decent code coverage
## Device-specific issues
There are a couple of known issues if you are working with certain devices. The emulator is one of
the most tricky in this sense.
- Devices, or activities, with hardware acceleration turned off: this can be the case with emulators. In this case we will use SurfaceView as our surface provider. That is intrinsically flawed and can't deal with all we want to do here (runtime layout changes, scaling, etc.). So, nothing to do in this case.
- Devices with no support for MediaRecorder: the emulator does not support it, officially. This means that video/audio recording is flawed. Again, not our fault.

@ -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<Boolean> 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.

@ -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();
}
});
}

@ -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;
}
};

@ -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) {

@ -7,15 +7,21 @@ import android.view.Surface;
import android.view.View;
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);
}
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

@ -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<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
protected Preview preview;
protected CameraPreview preview;
protected Size surfaceSize;
private Preview.SurfaceCallback callback;
private CameraPreview.SurfaceCallback callback;
private Task<Boolean> 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.

@ -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);
}
}

@ -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

@ -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() {

@ -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

@ -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() {

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

@ -1,10 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<SurfaceView
android:id="@+id/surface_view"
<!-- 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:gravity="center" />
android:layout_gravity="center"
android:gravity="center">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</merge>

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

@ -6,9 +6,9 @@ import android.view.Surface;
import android.view.View;
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.
Task<Void> mCropTask = new Task<>();
@ -22,6 +22,7 @@ abstract class Preview<T extends View, Output> {
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<T extends View, Output> {
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<T extends View, Output> {
*/
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<T extends View, Output> {
// 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<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.
* 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;
}
}

@ -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.ViewGroup;
class TextureViewPreview extends Preview<TextureView, SurfaceTexture> {
class TextureCameraPreview extends CameraPreview<TextureView, SurfaceTexture> {
private Surface mSurface;
TextureViewPreview(Context context, ViewGroup parent, SurfaceCallback callback) {
TextureCameraPreview(Context context, ViewGroup parent, SurfaceCallback callback) {
super(context, parent, callback);
}

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

@ -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

@ -25,7 +25,7 @@ import java.util.Collection;
public class ControlView<Value> 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<Value> 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);
}
}
}

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

Loading…
Cancel
Save