Refactor some classes, fix hardware acceleration bug, disable cropping for SurfaceView

pull/54/head
Mattia Iavarone 8 years ago
parent 83ec114726
commit fa65c5247e
  1. 83
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraCallbacksTest.java
  2. 12
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  3. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
  4. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
  5. 14
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraPreview.java
  6. 14
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java
  7. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceCameraPreviewTest.java
  8. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureCameraPreviewTest.java
  9. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  10. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
  11. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  12. 69
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  13. 14
      cameraview/src/main/res/layout/surface_view.xml
  14. 30
      cameraview/src/main/views/com/otaliastudios/cameraview/CameraPreview.java
  15. 107
      cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceCameraPreview.java
  16. 74
      cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java
  17. 4
      cameraview/src/main/views/com/otaliastudios/cameraview/TextureCameraPreview.java
  18. 1
      demo/src/main/AndroidManifest.xml
  19. 14
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
  20. 8
      demo/src/main/java/com/otaliastudios/cameraview/demo/ControlView.java
  21. 4
      demo/src/main/res/layout/activity_camera.xml

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

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

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

@ -1,11 +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:layout_gravity="center"
android:gravity="center" />
android:gravity="center">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</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> {
private 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,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<SurfaceView, SurfaceHolder> {
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<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