diff --git a/README.md b/README.md index 8d6b5beb..88f324b2 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ # CameraView CameraView is a well documented, high-level library that makes capturing pictures and videos easy, -addressing most of the common issues and needs, and still leaving you with flexibility where needed. See [CHANGELOG](https://github.com/natario1/CameraView/blob/master/CHANGELOG.md). +addressing most of the common issues and needs, and still leaving you with flexibility where needed. +See [CHANGELOG](https://github.com/natario1/CameraView/blob/master/CHANGELOG.md). ```groovy compile 'com.otaliastudios:cameraview:1.3.2' @@ -30,10 +31,11 @@ See below for a [list of what was done](#roadmap) and [licensing info](#contribu - Seamless image and video capturing - **Gestures** support (tap to focus, pinch to zoom and much more) - System permission handling -- Dynamic sizing behavior - - Create a `CameraView` of **any size** - - Center inside or center crop behaviors - - Automatic output cropping to match your `CameraView` bounds +- Smart sizing behavior + - Preview: Create a `CameraView` of **any size** + - Preview: Center inside or center crop behaviors + - Output: Handy utilities to set the output size + - Output: Automatic cropping to match your `CameraView` preview bounds - Built-in **grid drawing** - Multiple capture methods - Take high-resolution pictures with `capturePicture` @@ -55,9 +57,9 @@ See below for a [list of what was done](#roadmap) and [licensing info](#contribu - [Capturing Video](#capturing-video) - [Other camera events](#other-camera-events) - [Gestures](#gestures) -- [Dynamic Sizing Behavior](#dynamic-sizing-behavior) - - [Center Inside](#center-inside) - - [Center Crop](#center-crop) +- [Sizing Behavior](#sizing-behavior) + - [Preview Size](#preview-size) + - [Picture Size](#picture-size) - [Camera Controls](#camera-controls) - [Frame Processing](#frame-processing) - [Other APIs](#other-apis) @@ -78,7 +80,9 @@ To use the CameraView engine, simply add a `CameraView` to your layout: android:layout_height="wrap_content" /> ``` -`CameraView` has lots of XML attributes, so keep reading. Make sure you override `onResume`, `onPause` and `onDestroy` in your activity or fragment, and call `CameraView.start()`, `stop()` and `destroy()`. +`CameraView` has lots of XML attributes, so keep reading. Make sure you override `onResume`, +`onPause` and `onDestroy` in your activity or fragment, and call `CameraView.start()`, `stop()` +and `destroy()`. ```java @Override @@ -102,7 +106,8 @@ protected void onDestroy() { ### Capturing Images -To capture an image just call `CameraView.capturePicture()`. Make sure you setup a `CameraListener` to handle the image callback. +To capture an image just call `CameraView.capturePicture()`. Make sure you setup a `CameraListener` +to handle the image callback. ```java camera.addCameraListener(new CameraListener() { @@ -117,11 +122,14 @@ camera.addCameraListener(new CameraListener() { camera.capturePicture(); ``` -You can also use `camera.captureSnapshot()` to capture a preview frame. This is faster, though will ensure lower quality output. +You can also use `camera.captureSnapshot()` to capture a preview frame. This is faster, though will +ensure lower quality output. ### Capturing Video -To capture video just call `CameraView.startRecordingVideo(file)` to start, and `CameraView.stopRecordingVideo()` to finish. Make sure you setup a `CameraListener` to handle the video callback. +To capture video just call `CameraView.startRecordingVideo(file)` to start, and +`CameraView.stopRecordingVideo()` to finish. Make sure you setup a `CameraListener` to handle +the video callback. ```java camera.addCameraListener(new CameraListener() { @@ -152,7 +160,8 @@ camera.postDelayed(new Runnable() { ### Other camera events -Make sure you can react to different camera events by setting up one or more `CameraListener` instances. All these are executed on the UI thread. +Make sure you can react to different camera events by setting up one or more `CameraListener` +instances. All these are executed on the UI thread. ```java camera.addCameraListener(new CameraListener() { @@ -239,7 +248,9 @@ camera.addCameraListener(new CameraListener() { ## Gestures -`CameraView` listen to lots of different gestures inside its bounds. You have the chance to map these gestures to particular actions or camera controls, using `mapGesture()`. This lets you emulate typical behaviors in a single line: +`CameraView` listen to lots of different gestures inside its bounds. You have the chance to map +these gestures to particular actions or camera controls, using `mapGesture()`. +This lets you emulate typical behaviors in a single line: ```java cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM); // Pinch to zoom! @@ -261,19 +272,25 @@ Simple as that. More gestures are coming. There are two things to be noted: |`SCROLL_VERTICAL` (`cameraGestureScrollVertical`)|Vertical movement gesture.|`zoom` `exposureCorrection` `none`| -## Dynamic Sizing Behavior +## Sizing Behavior -`CameraView` has a smart measuring behavior that will let you do what you want with a few flags. Measuring is controlled simply by `layout_width` and `layout_height` attributes, with this meaning: +### Preview Size + +`CameraView` has a smart measuring behavior that will let you do what you want with a few flags. +Measuring is controlled simply by `layout_width` and `layout_height` attributes, with this meaning: - `WRAP_CONTENT` : try to stretch this dimension to respect the preview aspect ratio. - `MATCH_PARENT` : fill this dimension, even if this means ignoring the aspect ratio. - Fixed values (e.g. `500dp`) : respect this dimension. -You can have previews of all sizes, not just the supported presets. Whaterever you do, the preview will never be distorted. +You can have previews of all sizes, not just the supported presets. Whaterever you do, +the preview will never be distorted. -### Center inside +#### Center Inside -You can emulate a **center inside** behavior (like the `ImageView` scaletype) by setting both dimensions to `wrap_content`. The camera will get the biggest possible size that fits into your bounds, just like what happens with image views. +You can emulate a **center inside** behavior (like the `ImageView` scaletype) by setting +both dimensions to `wrap_content`. The camera will get the biggest possible size that fits +into your bounds, just like what happens with image views. ```xml @@ -282,11 +299,15 @@ You can emulate a **center inside** behavior (like the `ImageView` scaletype) by android:layout_height="wrap_content" /> ``` -This means that the whole preview is visible, and the image output matches what was visible during the capture. +This means that the whole preview is visible, and the image output matches what was visible +during the capture. -### Center crop +#### Center Crop -You can emulate a **center crop** behavior by setting both dimensions to fixed values or to `MATCH_PARENT`. The camera view will fill the rect. If your dimensions don't match the aspect ratio of the internal preview surface, the surface will be cropped to fill the view, just like `android:scaleType="centerCrop"` on an `ImageView`. +You can emulate a **center crop** behavior by setting both dimensions to fixed values or to +`MATCH_PARENT`. The camera view will fill the rect. If your dimensions don't match the aspect ratio +of the internal preview surface, the surface will be cropped to fill the view, +just like `android:scaleType="centerCrop"` on an `ImageView`. ```xml ``` -This means that part of the preview is hidden, and the image output will contain parts of the scene that were not visible during the capture. If this is a problem, see [cameraCropOutput](#cameracropoutput). +This means that part of the preview is hidden, and the image output will contain parts of the scene +that were not visible during the capture. If this is a problem, see [cameraCropOutput](#cameracropoutput). + +### Picture Size + +On top of this, you can control the actual size of the output picture, among the list of available sizes. +It is the size of the final JPEG picture. This can be achieved directly through XML, or +using the `SizeSelector` class: + +```java +cameraView.setPictureSize(new SizeSelector() { + @Override + public List select(List source) { + // Receives a list of available sizes. + // Must return a list of acceptable sizes. + } +}); +``` + +In practice, this is way easier using XML attributes or leveraging the `SizeSelectors` utilities: + +|Constraint|XML attr|SizeSelector| +|----------|--------|------------| +|min. width|`app:cameraPictureSizeMinWidth="100"`|`SizeSelectors.minWidth(100)`| +|min. height|`app:cameraPictureSizeMinHeight="100"`|`SizeSelectors.minHeight(100)`| +|max. width|`app:cameraPictureSizeMaxWidth="3000"`|`SizeSelectors.maxWidth(3000)`| +|max. height|`app:cameraPictureSizeMaxHeight="3000"`|`SizeSelectors.maxHeight(3000)`| +|min. area|`app:cameraPictureSizeMinArea="1000000"`|`SizeSelectors.minArea(1000000)`| +|max. area|`app:cameraPictureSizeMaxArea="5000000"`|`SizeSelectors.maxArea(5000000)`| +|aspect ratio|`app:cameraPictureSizeAspectRatio="1:1"`|`SizeSelectors.aspectRatio(AspectRatio.of(1,1), 0)`| +|smallest|`app:cameraPictureSizeSmallest="true"`|`SizeSelectors.smallest()`| +|biggest (**default**)|`app:cameraPictureSizeBiggest="true"`|`SizeSelectors.biggest()`| + +If you declare more than one XML constraint, the resulting selector will try to match **all** the +constraints. Be careful - it is very likely that applying lots of constraints will give +empty results. + +#### SizeSelectors utilities + +For more versatility, or to address selection issues with multiple constraints, +we encourage you to use `SizeSelectors` utilities, that will let you merge different selectors. + +This selector will try to find square sizes bigger than 1000x2000. If none is found, it falls back +to just square sizes: + +```java +SizeSelector width = SizeSelectors.minWidth(1000); +SizeSelector height = SizeSelectors.minWidth(2000); +SizeSelector dimensions = SizeSelectors.and(width, height); // Matches sizes bigger than 1000x2000. +SizeSelector ratio = SizeSelectors.aspectRatio(AspectRatio.of(1, 1), 0); // Matches 1:1 sizes. + +SizeSelector result = SizeSelectors.or( + SizeSelectors.and(ratio, dimensions), // Try to match both constraints + ratio, // If none is found, at least try to match the aspect ratio + SizeSelectors.biggest() // If none is found, take the biggest +); +camera.setPictureSize(result); +``` ## Camera controls @@ -337,10 +415,16 @@ Most camera parameters can be controlled through XML attributes or linked method What to capture - either picture or video. This has a couple of consequences: -- Sizing: capture and preview size are chosen among the available picture or video sizes, depending on the flag. When `picture`, we choose the max possible picture size and adapt the preview. When `video`, we respect the `videoQuality` choice and adapt the picture and the preview size. -- Picture capturing: due to sizing behavior, capturing pictures in `video` mode might lead to inconsistent results. In this case it is encouraged to use `captureSnapshot` instead, which will capture preview frames. This is fast and thus works well with slower camera sensors. -- Picture capturing: while recording a video, image capturing might work, but it is not guaranteed (it's device dependent) -- Permission behavior: when requesting a `video` session, the record audio permission will be requested. If this is needed, the audio permission should be added to your manifest or the app will crash. +- Sizing: picture and preview size are chosen among the available picture or video sizes, + depending on the flag. The picture size is chosen according to the given [size selector](#picture-size). + When `video`, in addition, we try to match the `videoQuality` aspect ratio. +- Picture capturing: due to sizing behavior, capturing pictures in `video` mode might lead to + inconsistent results. In this case it is encouraged to use `captureSnapshot` instead, which will + capture preview frames. This is fast and thus works well with slower camera sensors. +- Picture capturing: while recording a video, image capturing might work, but it is not guaranteed + (it's device dependent) +- Permission behavior: when requesting a `video` session, the record audio permission will be requested. + If this is needed, the audio permission should be added to your manifest or the app will crash. ```java cameraView.setSessionType(SessionType.PICTURE); @@ -369,7 +453,8 @@ cameraView.setFlash(Flash.TORCH); #### cameraGrid -Lets you draw grids over the camera preview. Supported values are `off`, `draw3x3` and `draw4x4` for regular grids, and `drawPhi` for a grid based on the golden ratio constant, often used in photography. +Lets you draw grids over the camera preview. Supported values are `off`, `draw3x3` and `draw4x4` +for regular grids, and `drawPhi` for a grid based on the golden ratio constant, often used in photography. ```java cameraView.setGrid(Grid.OFF); @@ -381,7 +466,8 @@ cameraView.setGrid(Grid.DRAW_PHI); #### cameraCropOutput Whether the output picture should be cropped to fit the aspect ratio of the preview surface. -This can guarantee consistency between what the user sees and the final output, if you fixed the camera view dimensions. This does not support videos. +This can guarantee consistency between what the user sees and the final output, if you fixed +the camera view dimensions. This does not support videos. #### cameraJpegQuality @@ -438,8 +524,8 @@ cameraView.setAudio(Audio.ON); #### cameraPlaySounds -Controls whether we should play platform-provided sounds during certain events (shutter click, focus completed). -Please note that: +Controls whether we should play platform-provided sounds during certain events +(shutter click, focus completed). Please note that: - on API < 16, this flag is always set to `false` - the Camera1 engine will always play shutter sounds regardless of this flag @@ -501,7 +587,6 @@ Other APIs not mentioned above are provided, and are well documented and comment |`setZoom(float)`, `getZoom()`|Sets a zoom value, where 0 means camera zoomed out and 1 means zoomed in. No-op if zoom is not supported, or camera not started.| |`setExposureCorrection(float)`, `getExposureCorrection()`|Sets exposure compensation EV value, in camera stops. No-op if this is not supported. Should be between the bounds returned by CameraOptions.| |`toggleFacing()`|Toggles the facing value between `Facing.FRONT` and `Facing.BACK`.| -|`toggleFlash()`|Toggles the flash value between `Flash.OFF`, `Flash.ON`, and `Flash.AUTO`.| |`setLocation(Location)`|Sets location data to be appended to picture/video metadata.| |`setLocation(double, double)`|Sets latitude and longitude to be appended to picture/video metadata.| |`getLocation()`|Retrieves location data previously applied with setLocation().| @@ -617,11 +702,11 @@ all the code was changed. - *Frame processor support* - *inject external loggers* - *error handling* +- *capture size selectors* These are still things that need to be done, off the top of my head: - [ ] `Camera2` integration -- [ ] add a `setPreferredAspectRatio` API to choose the capture size. Preview size will adapt, and then, if let free, the CameraView will adapt as well - [ ] animate grid lines similar to stock camera app - [ ] add onRequestPermissionResults for easy permission callback - [ ] decent code coverage diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index 8eb5ddeb..8c96a170 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -14,6 +14,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.List; + import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -76,7 +78,7 @@ public class CameraViewTest extends BaseTest { assertNull(cameraView.getCameraOptions()); assertNull(cameraView.getExtraProperties()); assertNull(cameraView.getPreviewSize()); - assertNull(cameraView.getCaptureSize()); + assertNull(cameraView.getPictureSize()); assertNull(cameraView.getSnapshotSize()); } @@ -551,6 +553,15 @@ public class CameraViewTest extends BaseTest { assertEquals(cameraView.getVideoQuality(), VideoQuality.LOWEST); } + @Test + public void testPictureSizeSelector() { + SizeSelector source = SizeSelectors.minHeight(50); + cameraView.setPictureSize(source); + SizeSelector result = mockController.getPictureSizeSelector(); + assertNotNull(result); + assertEquals(result, source); + } + //endregion //region Lists of listeners and processors diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java index d3d24cd3..60b8fe72 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java @@ -490,7 +490,7 @@ public class IntegrationTest extends BaseTest { camera.setCropOutput(false); waitForOpen(true); - Size size = camera.getCaptureSize(); + Size size = camera.getPictureSize(); camera.capturePicture(); byte[] jpeg = waitForPicture(true); Bitmap b = CameraUtils.decodeBitmap(jpeg, Integer.MAX_VALUE, Integer.MAX_VALUE); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/AspectRatio.java b/cameraview/src/main/java/com/otaliastudios/cameraview/AspectRatio.java index 90279b00..cf054d04 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/AspectRatio.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/AspectRatio.java @@ -5,9 +5,51 @@ import android.os.Parcelable; import android.support.annotation.NonNull; import android.util.SparseArray; -public class AspectRatio implements Comparable, Parcelable { +import java.util.HashMap; - final static SparseArray> sCache = new SparseArray<>(16); + +/** + * A simple class representing an aspect ratio. + */ +public class AspectRatio implements Comparable { + + final static HashMap sCache = new HashMap<>(16); + + /** + * Creates an aspect ratio with the given values. + * @param x the width + * @param y the height + * @return a (possibly cached) aspect ratio + */ + public static AspectRatio of(int x, int y) { + int gcd = gcd(x, y); + x /= gcd; + y /= gcd; + String key = x + ":" + y; + AspectRatio cached = sCache.get(key); + if (cached == null) { + cached = new AspectRatio(x, y); + sCache.put(key, cached); + } + return cached; + } + + /** + * Parses an aspect ratio string, for example those previously obtained + * with {@link #toString()}. + * + * @param string a string of the format x:y where x and y are integers + * @return a (possibly cached) aspect ratio + */ + public static AspectRatio parse(@NonNull String string) { + String[] parts = string.split(":"); + if (parts.length != 2) { + throw new NumberFormatException("Illegal AspectRatio string. Must be x:y"); + } + int x = Integer.valueOf(parts[0]); + int y = Integer.valueOf(parts[1]); + return of(x, y); + } private final int mX; private final int mY; @@ -71,31 +113,11 @@ public class AspectRatio implements Comparable, Parcelable { return -1; } + @SuppressWarnings("SuspiciousNameCombination") public AspectRatio inverse() { return AspectRatio.of(mY, mX); } - public static AspectRatio of(int x, int y) { - int gcd = gcd(x, y); - x /= gcd; - y /= gcd; - SparseArray arrayX = sCache.get(x); - if (arrayX == null) { - AspectRatio ratio = new AspectRatio(x, y); - arrayX = new SparseArray<>(); - arrayX.put(y, ratio); - sCache.put(x, arrayX); - return ratio; - } else { - AspectRatio ratio = arrayX.get(y); - if (ratio == null) { - ratio = new AspectRatio(x, y); - arrayX.put(y, ratio); - } - return ratio; - } - } - private static int gcd(int a, int b) { while (b != 0) { int c = b; @@ -104,32 +126,4 @@ public class AspectRatio implements Comparable, Parcelable { } return a; } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mX); - dest.writeInt(mY); - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - - @Override - public AspectRatio createFromParcel(Parcel source) { - int x = source.readInt(); - int y = source.readInt(); - return AspectRatio.of(x, y); - } - - @Override - public AspectRatio[] newArray(int size) { - return new AspectRatio[size]; - } - - }; - } \ No newline at end of file diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index 695c910e..e3199eee 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -28,7 +28,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera private static final String TAG = Camera1.class.getSimpleName(); private static final CameraLogger LOG = CameraLogger.create(TAG); - private int mCameraId; private Camera mCamera; private MediaRecorder mMediaRecorder; private File mVideoFile; @@ -104,7 +103,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera if (!mIsBound) return; // Compute a new camera preview size. - Size newSize = computePreviewSize(); + Size newSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); if (newSize.equals(mPreviewSize)) return; // Apply. @@ -136,8 +135,8 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera throw new CameraException(e); } - mCaptureSize = computeCaptureSize(); - mPreviewSize = computePreviewSize(); + mPictureSize = computePictureSize(sizesFromList(mCamera.getParameters().getSupportedPictureSizes())); + mPreviewSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes())); applySizesAndStartPreview("bindToSurface:"); mIsBound = true; } @@ -156,7 +155,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera Camera.Parameters params = mCamera.getParameters(); mPreviewFormat = params.getPreviewFormat(); params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); // <- not allowed during preview - params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed + params.setPictureSize(mPictureSize.getWidth(), mPictureSize.getHeight()); // <- allowed mCamera.setParameters(params); mCamera.setPreviewCallbackWithBuffer(null); // Release anything left @@ -213,8 +212,8 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera try { LOG.i("onStop:", "Clean up.", "Stopping preview."); - mCamera.stopPreview(); mCamera.setPreviewCallbackWithBuffer(null); + mCamera.stopPreview(); LOG.i("onStop:", "Clean up.", "Stopped preview."); } catch (Exception e) { LOG.w("onStop:", "Clean up.", "Exception while stopping preview.", e); @@ -234,9 +233,9 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera mOptions = null; mCamera = null; mPreviewSize = null; - mCaptureSize = null; + mPictureSize = null; mIsBound = false; - + LOG.w("onStop:", "Clean up.", "Returning."); if (error != null) throw new CameraException(error); } @@ -456,17 +455,17 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera if (mSessionType == SessionType.VIDEO) { // Change capture size to a size that fits the video aspect ratio. - Size oldSize = mCaptureSize; - mCaptureSize = computeCaptureSize(); - if (!mCaptureSize.equals(oldSize)) { + Size oldSize = mPictureSize; + mPictureSize = computePictureSize(sizesFromList(mCamera.getParameters().getSupportedPictureSizes())); + if (!mPictureSize.equals(oldSize)) { // New video quality triggers a new aspect ratio. // Go on and see if preview size should change also. Camera.Parameters params = mCamera.getParameters(); - params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); + params.setPictureSize(mPictureSize.getWidth(), mPictureSize.getHeight()); mCamera.setParameters(params); onSurfaceChanged(); } - LOG.i("setVideoQuality:", "captureSize:", mCaptureSize); + LOG.i("setVideoQuality:", "captureSize:", mPictureSize); LOG.i("setVideoQuality:", "previewSize:", mPreviewSize); } } @@ -638,7 +637,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera } } - /** * Whether the exif tag should include a 'flip' operation. */ @@ -647,43 +645,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera } - /** - * 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 CameraPreview.SurfaceCallback} should be called, - * and this should be refreshed. - */ - private Size computeCaptureSize() { - Camera.Parameters params = mCamera.getParameters(); - if (mSessionType == SessionType.PICTURE) { - // Choose the max size. - List captureSizes = sizesFromList(params.getSupportedPictureSizes()); - Size maxSize = Collections.max(captureSizes); - LOG.i("size:", "computeCaptureSize:", "computed", maxSize); - return Collections.max(captureSizes); - } else { - // Choose according to developer choice in setVideoQuality. - // The Camcorder internally checks for cameraParameters.getSupportedVideoSizes() etc. - // We want the picture size to be the max picture consistent with the video aspect ratio. - List captureSizes = sizesFromList(params.getSupportedPictureSizes()); - CamcorderProfile profile = getCamcorderProfile(mCameraId, mVideoQuality); - AspectRatio targetRatio = AspectRatio.of(profile.videoFrameWidth, profile.videoFrameHeight); - LOG.i("size:", "computeCaptureSize:", "videoQuality:", mVideoQuality, "targetRatio:", targetRatio); - return matchSize(captureSizes, targetRatio, new Size(0, 0), true); - } - } - - private Size computePreviewSize() { - Camera.Parameters params = mCamera.getParameters(); - List previewSizes = sizesFromList(params.getSupportedPreviewSizes()); - AspectRatio targetRatio = AspectRatio.of(mCaptureSize.getWidth(), mCaptureSize.getHeight()); - Size biggerThan = mPreview.getSurfaceSize(); - LOG.i("size:", "computePreviewSize:", "targetRatio:", targetRatio, "surface size:", biggerThan); - return matchSize(previewSizes, targetRatio, biggerThan, false); - } - - // ----------------- // Video recording stuff. @@ -755,7 +716,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera // Must be called before setOutputFormat. mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); } - CamcorderProfile profile = getCamcorderProfile(mCameraId, mVideoQuality); + CamcorderProfile profile = getCamcorderProfile(); mMediaRecorder.setOutputFormat(profile.fileFormat); mMediaRecorder.setVideoFrameRate(profile.videoFrameRate); mMediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight); @@ -778,50 +739,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera // Not needed. mMediaRecorder.setPreviewDisplay(mPreview.getSurface()); } - @NonNull - private static CamcorderProfile getCamcorderProfile(int cameraId, VideoQuality videoQuality) { - switch (videoQuality) { - case HIGHEST: - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); - - case MAX_2160P: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && - CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_2160P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); - } - // Don't break. - - case MAX_1080P: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); - } - // Don't break. - - case MAX_720P: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); - } - // Don't break. - - case MAX_480P: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); - } - // Don't break. - - case MAX_QVGA: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); - } - // Don't break. - - case LOWEST: - default: - // Fallback to lowest. - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); - } - } - // ----------------- // Zoom and simpler stuff. @@ -962,64 +879,18 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera // ----------------- - // Size static stuff. + // Size stuff. - /** - * Returns a list of {@link Size} out of Camera.Sizes. - */ @Nullable - private static List sizesFromList(List sizes) { + private List sizesFromList(List sizes) { if (sizes == null) return null; List result = new ArrayList<>(sizes.size()); for (Camera.Size size : sizes) { - result.add(new Size(size.width, size.height)); + Size add = new Size(size.width, size.height); + if (!result.contains(add)) result.add(add); } LOG.i("size:", "sizesFromList:", result); return result; } - - - /** - * Policy here is to return a size that is big enough to fit the surface size, - * and possibly consistent with the target aspect ratio. - * @param sizes list of possible sizes - * @param targetRatio aspect ratio - * @param biggerThan size representing the current surface size - * @return chosen size - */ - private static Size matchSize(List sizes, AspectRatio targetRatio, Size biggerThan, boolean biggestPossible) { - if (sizes == null) return null; - - List consistent = new ArrayList<>(5); - List bigEnoughAndConsistent = new ArrayList<>(5); - - final Size targetSize = biggerThan; - for (Size size : sizes) { - AspectRatio ratio = AspectRatio.of(size.getWidth(), size.getHeight()); - if (ratio.equals(targetRatio)) { - consistent.add(size); - if (size.getHeight() >= targetSize.getHeight() && size.getWidth() >= targetSize.getWidth()) { - bigEnoughAndConsistent.add(size); - } - } - } - - LOG.i("size:", "matchSize:", "found consistent:", consistent.size()); - LOG.i("size:", "matchSize:", "found big enough and consistent:", bigEnoughAndConsistent.size()); - Size result; - if (bigEnoughAndConsistent.size() > 0) { - result = biggestPossible ? - Collections.max(bigEnoughAndConsistent) : - Collections.min(bigEnoughAndConsistent); - } else if (consistent.size() > 0) { - result = Collections.max(consistent); - } else { - result = Collections.max(sizes); - } - LOG.i("size", "matchSize:", "returning result", result); - return result; - } - - } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java index 4bc93579..4fc29f8b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java @@ -2,6 +2,10 @@ package com.otaliastudios.cameraview; import android.graphics.PointF; import android.location.Location; + + +import android.media.CamcorderProfile; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; @@ -9,6 +13,7 @@ import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import java.io.File; +import java.util.List; abstract class CameraController implements CameraPreview.SurfaceCallback, @@ -27,6 +32,7 @@ abstract class CameraController implements protected CameraPreview mPreview; protected WorkerHandler mHandler; /* for tests */ Handler mCrashHandler; + protected int mCameraId; protected Facing mFacing; protected Flash mFlash; @@ -40,13 +46,14 @@ abstract class CameraController implements protected float mZoomValue; protected float mExposureCorrectionValue; - protected Size mCaptureSize; + protected Size mPictureSize; protected Size mPreviewSize; protected int mPreviewFormat; protected ExtraProperties mExtraProperties; protected CameraOptions mOptions; protected FrameManager mFrameManager; + protected SizeSelector mPictureSizeSelector; protected int mDisplayOffset; protected int mDeviceOrientation; @@ -77,6 +84,13 @@ abstract class CameraController implements //region Error handling + private static class NoOpExceptionHandler implements Thread.UncaughtExceptionHandler { + @Override + public void uncaughtException(Thread t, Throwable e) { + // No-op. + } + } + @Override public void uncaughtException(final Thread thread, final Throwable throwable) { // Something went wrong. Thread is terminated (about to?). @@ -119,8 +133,9 @@ abstract class CameraController implements final void destroy() { LOG.i("destroy:", "state:", ss()); - // Prevent CameraController leaks. - mHandler.getThread().setUncaughtExceptionHandler(null); + // Prevent CameraController leaks. Don't set to null, or exceptions + // inside the standard stop() method might crash the main thread. + mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler()); // Stop if needed. stopImmediately(); } @@ -232,10 +247,9 @@ abstract class CameraController implements return mState; } - //endregion - //region Rotation callbacks + //region Simple setters void onDisplayOffset(int displayOrientation) { // I doubt this will ever change. @@ -246,9 +260,13 @@ abstract class CameraController implements mDeviceOrientation = deviceOrientation; } + void setPictureSizeSelector(SizeSelector selector) { + mPictureSizeSelector = selector; + } + //endregion - //region Abstract setParameters + //region Abstract setters // Should restart the session if active. abstract void setSessionType(SessionType sessionType); @@ -343,6 +361,14 @@ abstract class CameraController implements return mAudio; } + final SizeSelector getPictureSizeSelector() { + return mPictureSizeSelector; + } + + final Size getPictureSize() { + return mPictureSize; + } + final float getZoomValue() { return mZoomValue; } @@ -351,13 +377,128 @@ abstract class CameraController implements return mExposureCorrectionValue; } - final Size getCaptureSize() { - return mCaptureSize; - } - final Size getPreviewSize() { return mPreviewSize; } //endregion + + //region Size utils + + /** + * 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 CameraPreview.SurfaceCallback} should be called, + * and this should be refreshed. + */ + protected Size computePictureSize(List captureSizes) { + SizeSelector selector; + + // The external selector is expecting stuff in the view world, not in the sensor world. + // Flip before starting, and then flip again. + boolean flip = shouldFlipSizes(); + LOG.i("computePictureSize:", "flip:", flip); + if (flip) { + for (Size size : captureSizes) { + size.flip(); + } + } + + if (mSessionType == SessionType.PICTURE) { + selector = SizeSelectors.or( + mPictureSizeSelector, + SizeSelectors.biggest() // Fallback to biggest. + ); + } else { + // The Camcorder internally checks for cameraParameters.getSupportedVideoSizes() etc. + // And we want the picture size to be the biggest picture consistent with the video aspect ratio. + // -> Use the external picture selector, but enforce the ratio constraint. + CamcorderProfile profile = getCamcorderProfile(); + AspectRatio targetRatio = AspectRatio.of(profile.videoFrameWidth, profile.videoFrameHeight); + if (flip) targetRatio = targetRatio.inverse(); + LOG.i("size:", "computeCaptureSize:", "videoQuality:", mVideoQuality, "targetRatio:", targetRatio); + SizeSelector matchRatio = SizeSelectors.aspectRatio(targetRatio, 0); + selector = SizeSelectors.or( + SizeSelectors.and(matchRatio, mPictureSizeSelector), + SizeSelectors.and(matchRatio), + mPictureSizeSelector + ); + } + + Size result = selector.select(captureSizes).get(0); + LOG.i("computePictureSize:", "result:", result); + if (flip) result.flip(); + return result; + } + + protected Size computePreviewSize(List previewSizes) { + // instead of flipping everything to the view world, we can just flip the + // surface size to the sensor world + boolean flip = shouldFlipSizes(); + AspectRatio targetRatio = AspectRatio.of(mPictureSize.getWidth(), mPictureSize.getHeight()); + Size targetMinSize = mPreview.getSurfaceSize(); + if (flip) targetMinSize.flip(); + LOG.i("size:", "computePreviewSize:", "targetRatio:", targetRatio, "targetMinSize:", targetMinSize); + SizeSelector matchRatio = SizeSelectors.aspectRatio(targetRatio, 0); + SizeSelector matchSize = SizeSelectors.and( + SizeSelectors.minHeight(targetMinSize.getHeight()), + SizeSelectors.minWidth(targetMinSize.getWidth())); + SizeSelector matchAll = SizeSelectors.or( + SizeSelectors.and(matchRatio, matchSize), + matchRatio, // If couldn't match both, match ratio. + SizeSelectors.biggest() // If couldn't match any, take the biggest. + ); + Size result = matchAll.select(previewSizes).get(0); + LOG.i("computePreviewSize:", "result:", result); + // Flip back what we flipped. + if (flip) targetMinSize.flip(); + return result; + } + + @NonNull + protected CamcorderProfile getCamcorderProfile() { + switch (mVideoQuality) { + case HIGHEST: + return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_HIGH); + + case MAX_2160P: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && + CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_2160P); + } + // Don't break. + + case MAX_1080P: + if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_1080P); + } + // Don't break. + + case MAX_720P: + if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_720P)) { + return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_720P); + } + // Don't break. + + case MAX_480P: + if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_480P)) { + return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_480P); + } + // Don't break. + + case MAX_QVGA: + if (CamcorderProfile.hasProfile(mCameraId, CamcorderProfile.QUALITY_QVGA)) { + return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_QVGA); + } + // Don't break. + + case LOWEST: + default: + // Fallback to lowest. + return CamcorderProfile.get(mCameraId, CamcorderProfile.QUALITY_LOW); + } + } + + //endregion } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 71a95510..2ff7a8bf 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -92,6 +92,7 @@ public class CameraView extends FrameLayout { 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); boolean cropOutput = a.getBoolean(R.styleable.CameraView_cameraCropOutput, DEFAULT_CROP_OUTPUT); @@ -107,6 +108,36 @@ public class CameraView extends FrameLayout { Hdr hdr = Hdr.fromValue(a.getInteger(R.styleable.CameraView_cameraHdr, Hdr.DEFAULT.value())); Audio audio = Audio.fromValue(a.getInteger(R.styleable.CameraView_cameraAudio, Audio.DEFAULT.value())); + // Size selectors + List constraints = new ArrayList<>(3); + if (a.hasValue(R.styleable.CameraView_cameraPictureSizeMinWidth)) { + constraints.add(SizeSelectors.minWidth(a.getInteger(R.styleable.CameraView_cameraPictureSizeMinWidth, 0))); + } + if (a.hasValue(R.styleable.CameraView_cameraPictureSizeMaxWidth)) { + constraints.add(SizeSelectors.maxWidth(a.getInteger(R.styleable.CameraView_cameraPictureSizeMaxWidth, 0))); + } + if (a.hasValue(R.styleable.CameraView_cameraPictureSizeMinHeight)) { + constraints.add(SizeSelectors.minHeight(a.getInteger(R.styleable.CameraView_cameraPictureSizeMinHeight, 0))); + } + if (a.hasValue(R.styleable.CameraView_cameraPictureSizeMaxHeight)) { + constraints.add(SizeSelectors.maxHeight(a.getInteger(R.styleable.CameraView_cameraPictureSizeMaxHeight, 0))); + } + if (a.hasValue(R.styleable.CameraView_cameraPictureSizeMinArea)) { + constraints.add(SizeSelectors.minArea(a.getInteger(R.styleable.CameraView_cameraPictureSizeMinArea, 0))); + } + if (a.hasValue(R.styleable.CameraView_cameraPictureSizeMaxArea)) { + constraints.add(SizeSelectors.maxArea(a.getInteger(R.styleable.CameraView_cameraPictureSizeMaxArea, 0))); + } + if (a.hasValue(R.styleable.CameraView_cameraPictureSizeAspectRatio)) { + //noinspection ConstantConditions + constraints.add(SizeSelectors.aspectRatio(AspectRatio.parse(a.getString(R.styleable.CameraView_cameraPictureSizeAspectRatio)), 0)); + } + if (a.getBoolean(R.styleable.CameraView_cameraPictureSizeSmallest, false)) constraints.add(SizeSelectors.smallest()); + if (a.getBoolean(R.styleable.CameraView_cameraPictureSizeBiggest, false)) constraints.add(SizeSelectors.biggest()); + SizeSelector selector = !constraints.isEmpty() ? + SizeSelectors.and(constraints.toArray(new SizeSelector[constraints.size()])) : + SizeSelectors.biggest(); + // Gestures GestureAction tapGesture = GestureAction.fromValue(a.getInteger(R.styleable.CameraView_cameraGestureTap, GestureAction.DEFAULT_TAP.value())); GestureAction longTapGesture = GestureAction.fromValue(a.getInteger(R.styleable.CameraView_cameraGestureLongTap, GestureAction.DEFAULT_LONG_TAP.value())); @@ -146,10 +177,10 @@ public class CameraView extends FrameLayout { setGrid(grid); setHdr(hdr); setAudio(audio); + setPictureSize(selector); // Apply gestures mapGesture(Gesture.TAP, tapGesture); - // mapGesture(Gesture.DOUBLE_TAP, doubleTapGesture); mapGesture(Gesture.LONG_TAP, longTapGesture); mapGesture(Gesture.PINCH, pinchGesture); mapGesture(Gesture.SCROLL_HORIZONTAL, scrollHorizontalGesture); @@ -514,8 +545,8 @@ public class CameraView extends FrameLayout { /** * Checks that we have appropriate permissions for this session type. * Throws if session = audio and manifest did not add the microphone permissions. - * @param sessionType - * @param audio + * @param sessionType the sessionType to be checked + * @param audio the audio setting to be checked * @return true if we can go on, false otherwise. */ @SuppressLint("NewApi") @@ -847,33 +878,6 @@ public class CameraView extends FrameLayout { } - /** - * Toggles the flash mode between {@link Flash#OFF}, - * {@link Flash#ON} and {@link Flash#AUTO}, in this order. - * - * @return the new flash value - */ - public Flash toggleFlash() { - Flash flash = mCameraController.getFlash(); - switch (flash) { - case OFF: - setFlash(Flash.ON); - break; - - case ON: - setFlash(Flash.AUTO); - break; - - case AUTO: - case TORCH: - setFlash(Flash.OFF); - break; - } - - return mCameraController.getFlash(); - } - - /** * Controls the audio mode. * @@ -965,6 +969,18 @@ public class CameraView extends FrameLayout { } + /** + * Sets picture capture size. The {@link SizeSelector} will be invoked with the list of available + * size, and the first acceptable size will be accepted and passed to the internal engine. + * See the {@link SizeSelectors} class for handy utilities for creating selectors. + * + * @param selector a size selector + */ + public void setPictureSize(@NonNull SizeSelector selector) { + mCameraController.setPictureSizeSelector(selector); + } + + /** * Sets video recording quality. This is not guaranteed to be supported by current device. * If it's not, a lower quality will be chosen, until a supported one is found. @@ -1246,8 +1262,8 @@ public class CameraView extends FrameLayout { * @return a Size */ @Nullable - public Size getCaptureSize() { - return mCameraController != null ? mCameraController.getCaptureSize() : null; + public Size getPictureSize() { + return mCameraController != null ? mCameraController.getPictureSize() : null; } @@ -1625,4 +1641,37 @@ public class CameraView extends FrameLayout { } //endregion + + //region deprecated APIs + + /** + * @deprecated use {@link #getPictureSize()} instead. + */ + @Deprecated + @Nullable + public Size getCaptureSize() { + return getPictureSize(); + } + + /** + * Toggles the flash mode between {@link Flash#OFF}, + * {@link Flash#ON} and {@link Flash#AUTO}, in this order. + * + * @deprecated Don't use this. Flash values might not be supported, + * and the return value is unreliable. + * + * @return the new flash value + */ + @Deprecated + public Flash toggleFlash() { + Flash flash = mCameraController.getFlash(); + switch (flash) { + case OFF: setFlash(Flash.ON); break; + case ON: setFlash(Flash.AUTO); break; + case AUTO: case TORCH: setFlash(Flash.OFF); break; + } + return mCameraController.getFlash(); + } + + //endregion } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java index a3e2e217..79be98d9 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Size.java @@ -2,10 +2,13 @@ package com.otaliastudios.cameraview; import android.support.annotation.NonNull; +/** + * A simple class representing a size, with width and height values. + */ public class Size implements Comparable { - private final int mWidth; - private final int mHeight; + private int mWidth; + private int mHeight; Size(int width, int height) { mWidth = width; @@ -20,6 +23,16 @@ public class Size implements Comparable { return mHeight; } + /** + * Flips width and height altogether. + */ + @SuppressWarnings("SuspiciousNameCombination") + public void flip() { + int temp = mWidth; + mWidth = mHeight; + mHeight = temp; + } + @Override public boolean equals(Object o) { if (o == null) { diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/SizeSelector.java b/cameraview/src/main/java/com/otaliastudios/cameraview/SizeSelector.java new file mode 100644 index 00000000..70beb864 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/SizeSelector.java @@ -0,0 +1,22 @@ +package com.otaliastudios.cameraview; + +import android.support.annotation.NonNull; + +import java.util.List; + +/** + * A size selector receives a list of {@link Size}s and returns another list with + * sizes that are considered acceptable. + */ +public interface SizeSelector { + + /** + * Returns a list of acceptable sizes from the given input. + * The final size will be the first element in the output list. + * + * @param source input list + * @return output list + */ + @NonNull + List select(@NonNull List source); +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/SizeSelectors.java b/cameraview/src/main/java/com/otaliastudios/cameraview/SizeSelectors.java new file mode 100644 index 00000000..cd370024 --- /dev/null +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/SizeSelectors.java @@ -0,0 +1,276 @@ +package com.otaliastudios.cameraview; + +import android.support.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Static utilities to create, join and merge {@link SizeSelector}s instances. + */ +public class SizeSelectors { + + /** + * A size constraint to easily filter out + * sizes in a list. + */ + public interface Filter { + boolean accepts(Size size); + } + + /** + * Returns a new {@link SizeSelector} with the given {@link Filter}. + * This kind of selector will respect the order in the source array. + * + * @param filter a filter + * @return a new selector + */ + public static SizeSelector withFilter(@NonNull Filter filter) { + return new FilterSelector(filter); + } + + /** + * Returns a new {@link SizeSelector} that keeps only sizes + * whose width is at most equal to the given width. + * + * @param width the max width + * @return a new selector + */ + public static SizeSelector maxWidth(final int width) { + return withFilter(new Filter() { + @Override + public boolean accepts(Size size) { + return size.getWidth() <= width; + } + }); + } + + /** + * Returns a new {@link SizeSelector} that keeps only sizes + * whose width is at least equal to the given width. + * + * @param width the min width + * @return a new selector + */ + public static SizeSelector minWidth(final int width) { + return withFilter(new Filter() { + @Override + public boolean accepts(Size size) { + return size.getWidth() >= width; + } + }); + } + + /** + * Returns a new {@link SizeSelector} that keeps only sizes + * whose height is at most equal to the given height. + * + * @param height the max height + * @return a new selector + */ + public static SizeSelector maxHeight(final int height) { + return withFilter(new Filter() { + @Override + public boolean accepts(Size size) { + return size.getHeight() <= height; + } + }); + } + + /** + * Returns a new {@link SizeSelector} that keeps only sizes + * whose height is at least equal to the given height. + * + * @param height the min height + * @return a new selector + */ + public static SizeSelector minHeight(final int height) { + return withFilter(new Filter() { + @Override + public boolean accepts(Size size) { + return size.getHeight() >= height; + } + }); + } + + /** + * Returns a new {@link SizeSelector} that keeps only sizes + * which respect the given {@link AspectRatio}. You can pass a tolerance + * value to include aspect ratios that are slightly different. + * + * @param ratio the desired aspect ratio + * @param delta a small tolerance value + * @return a new selector + */ + public static SizeSelector aspectRatio(AspectRatio ratio, final float delta) { + final float desired = ratio.toFloat(); + return withFilter(new Filter() { + @Override + public boolean accepts(Size size) { + float candidate = AspectRatio.of(size.getWidth(), size.getHeight()).toFloat(); + return candidate >= desired - delta && candidate <= desired + delta; + } + }); + } + + /** + * Returns a {@link SizeSelector} that will order sizes from + * the biggest to the smallest. This means that the biggest size will be taken. + * + * @return a new selector + */ + public static SizeSelector biggest() { + return new SizeSelector() { + @NonNull + @Override + public List select(@NonNull List source) { + Collections.sort(source); + Collections.reverse(source); + return source; + } + }; + } + + /** + * Returns a {@link SizeSelector} that will order sizes from + * the smallest to the biggest. This means that the smallest size will be taken. + * + * @return a new selector + */ + public static SizeSelector smallest() { + return new SizeSelector() { + @NonNull + @Override + public List select(@NonNull List source) { + Collections.sort(source); + return source; + } + }; + } + + /** + * Returns a new {@link SizeSelector} that keeps only sizes + * whose area is at most equal to the given area in pixels. + * + * @param area the max area + * @return a new selector + */ + public static SizeSelector maxArea(final int area) { + return withFilter(new Filter() { + @Override + public boolean accepts(Size size) { + return size.getHeight() * size.getWidth() <= area; + } + }); + } + + /** + * Returns a new {@link SizeSelector} that keeps only sizes + * whose area is at least equal to the given area in pixels. + * + * @param area the min area + * @return a new selector + */ + public static SizeSelector minArea(final int area) { + return withFilter(new Filter() { + @Override + public boolean accepts(Size size) { + return size.getHeight() * size.getWidth() >= area; + } + }); + } + + /** + * Joins all the given selectors to create a new one that returns + * the intersection of all the inputs. Basically, all constraints are + * respected. + * + * Keep in mind there is good chance that the final list will be empty. + * + * @param selectors input selectors + * @return a new selector + */ + public static SizeSelector and(SizeSelector... selectors) { + return new AndSelector(selectors); + } + + /** + * Creates a new {@link SizeSelector} that 'or's the given filters. + * If the first selector returns an empty list, the next selector is queried, + * and so on until a non-empty list is found. + * + * @param selectors input selectors + * @return a new selector + */ + public static SizeSelector or(SizeSelector... selectors) { + return new OrSelector(selectors); + } + + + //region private utilities + + private static class FilterSelector implements SizeSelector { + + private Filter constraint; + + private FilterSelector(@NonNull Filter constraint) { + this.constraint = constraint; + } + + @Override + @NonNull + public List select(@NonNull List source) { + List sizes = new ArrayList<>(); + for (Size size : source) { + if (constraint.accepts(size)) { + sizes.add(size); + } + } + return sizes; + } + } + + private static class AndSelector implements SizeSelector { + + private SizeSelector[] values; + + private AndSelector(@NonNull SizeSelector... values) { + this.values = values; + } + + @Override + @NonNull + public List select(@NonNull List source) { + List temp = source; + for (SizeSelector selector : values) { + temp = selector.select(temp); + } + return temp; + } + } + + private static class OrSelector implements SizeSelector { + + private SizeSelector[] values; + + private OrSelector(@NonNull SizeSelector... values) { + this.values = values; + } + + @Override + @NonNull + public List select(@NonNull List source) { + List temp = null; + for (SizeSelector selector : values) { + temp = selector.select(source); + if (!temp.isEmpty()) { + break; + } + } + return temp == null ? new ArrayList() : temp; + } + + } + //endregion +} diff --git a/cameraview/src/main/options/com/otaliastudios/cameraview/SessionType.java b/cameraview/src/main/options/com/otaliastudios/cameraview/SessionType.java index d40d9a39..ed033aaa 100644 --- a/cameraview/src/main/options/com/otaliastudios/cameraview/SessionType.java +++ b/cameraview/src/main/options/com/otaliastudios/cameraview/SessionType.java @@ -16,7 +16,7 @@ public enum SessionType { * * - Trying to take videos in this session will throw an exception * - Only the camera permission is requested - * - Preview and capture size is chosen as the max available size + * - Capture size is chosen according to the current picture size selector */ PICTURE(0), @@ -26,7 +26,7 @@ public enum SessionType { * - Trying to take pictures in this session will work, though with lower quality * - Trying to take pictures while recording a video will work if supported * - Camera and audio record permissions are requested - * - Preview and capture size are chosen to respect the {@link VideoQuality} aspect ratio + * - Capture size is chosen trying to respect the {@link VideoQuality} aspect ratio * * @see CameraOptions#isVideoSnapshotSupported() */ diff --git a/cameraview/src/main/res/values/attrs.xml b/cameraview/src/main/res/values/attrs.xml index e75d0fa1..07471631 100644 --- a/cameraview/src/main/res/values/attrs.xml +++ b/cameraview/src/main/res/values/attrs.xml @@ -1,12 +1,24 @@ - + + + + + + + + + + + + + + + + + + diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/AspectRatioTest.java b/cameraview/src/test/java/com/otaliastudios/cameraview/AspectRatioTest.java similarity index 65% rename from cameraview/src/androidTest/java/com/otaliastudios/cameraview/AspectRatioTest.java rename to cameraview/src/test/java/com/otaliastudios/cameraview/AspectRatioTest.java index 427c936c..8c5ac9e1 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/AspectRatioTest.java +++ b/cameraview/src/test/java/com/otaliastudios/cameraview/AspectRatioTest.java @@ -1,22 +1,11 @@ package com.otaliastudios.cameraview; -import android.annotation.TargetApi; -import android.hardware.Camera; -import android.hardware.camera2.CameraCharacteristics; -import android.os.Parcel; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; -import android.util.SizeF; - import org.junit.Test; -import org.junit.runner.RunWith; import static org.junit.Assert.*; -@RunWith(AndroidJUnit4.class) -@SmallTest -public class AspectRatioTest extends BaseTest { +public class AspectRatioTest { @Test public void testConstructor() { @@ -29,7 +18,7 @@ public class AspectRatioTest extends BaseTest { @Test public void testEquals() { AspectRatio ratio = AspectRatio.of(50, 10); - assertFalse(ratio.equals(null)); + assertNotNull(ratio); assertTrue(ratio.equals(ratio)); AspectRatio ratio1 = AspectRatio.of(5, 1); @@ -62,15 +51,20 @@ public class AspectRatioTest extends BaseTest { assertEquals(inverse.getY(), 5f, 0); } - @Test - public void testParcelable() { - AspectRatio ratio = AspectRatio.of(50, 10); - Parcel parcel = Parcel.obtain(); - ratio.writeToParcel(parcel, ratio.describeContents()); + @Test(expected = NumberFormatException.class) + public void testParse_notNumbers() { + AspectRatio.parse("a:b"); + } - parcel.setDataPosition(0); - AspectRatio other = AspectRatio.CREATOR.createFromParcel(parcel); - assertEquals(ratio, other); + @Test(expected = NumberFormatException.class) + public void testParse_noColon() { + AspectRatio.parse("24"); } + @Test + public void testParse() { + AspectRatio ratio = AspectRatio.parse("16:9"); + assertEquals(ratio.getX(), 16); + assertEquals(ratio.getY(), 9); + } } diff --git a/cameraview/src/test/java/com/otaliastudios/cameraview/SizeSelectorsTest.java b/cameraview/src/test/java/com/otaliastudios/cameraview/SizeSelectorsTest.java new file mode 100644 index 00000000..34352455 --- /dev/null +++ b/cameraview/src/test/java/com/otaliastudios/cameraview/SizeSelectorsTest.java @@ -0,0 +1,166 @@ +package com.otaliastudios.cameraview; + + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +public class SizeSelectorsTest { + + private List input; + + @Before + public void setUp() { + input = Arrays.asList( + new Size(100, 200), + new Size(150, 300), + new Size(600, 900), + new Size(600, 600), + new Size(1600, 900), + new Size(30, 40), + new Size(40, 30), + new Size(2000, 4000) + ); + } + + @Test + public void testWithFilter() { + SizeSelector selector = SizeSelectors.withFilter(new SizeSelectors.Filter() { + @Override + public boolean accepts(Size size) { + return size.getWidth() == 600; + } + }); + List list = selector.select(input); + assertEquals(list.size(), 2); + assertEquals(list.get(0), new Size(600, 900)); + assertEquals(list.get(1), new Size(600, 600)); + } + + @Test + public void testMaxWidth() { + SizeSelector selector = SizeSelectors.maxWidth(50); + List list = selector.select(input); + assertEquals(list.size(), 2); + assertEquals(list.get(0), new Size(30, 40)); + assertEquals(list.get(1), new Size(40, 30)); + } + + @Test + public void testMinWidth() { + SizeSelector selector = SizeSelectors.minWidth(1000); + List list = selector.select(input); + assertEquals(list.size(), 2); + assertEquals(list.get(0), new Size(1600, 900)); + assertEquals(list.get(1), new Size(2000, 4000)); + } + + @Test + public void testMaxHeight() { + SizeSelector selector = SizeSelectors.maxHeight(50); + List list = selector.select(input); + assertEquals(list.size(), 2); + assertEquals(list.get(0), new Size(30, 40)); + assertEquals(list.get(1), new Size(40, 30)); + } + + @Test + public void testMinHeight() { + SizeSelector selector = SizeSelectors.minHeight(1000); + List list = selector.select(input); + assertEquals(list.size(), 1); + assertEquals(list.get(0), new Size(2000, 4000)); + } + + @Test + public void testAspectRatio() { + SizeSelector selector = SizeSelectors.aspectRatio(AspectRatio.of(16, 9), 0); + List list = selector.select(input); + assertEquals(list.size(), 1); + assertEquals(list.get(0), new Size(1600, 900)); + + selector = SizeSelectors.aspectRatio(AspectRatio.of(1, 2), 0); + list = selector.select(input); + assertEquals(list.size(), 3); + assertEquals(list.get(0), new Size(100, 200)); + assertEquals(list.get(1), new Size(150, 300)); + assertEquals(list.get(2), new Size(2000, 4000)); + } + + @Test + public void testMax() { + SizeSelector selector = SizeSelectors.biggest(); + List list = selector.select(input); + assertEquals(list.size(), input.size()); + assertEquals(list.get(0), new Size(2000, 4000)); + } + + @Test + public void testMin() { + SizeSelector selector = SizeSelectors.smallest(); + List list = selector.select(input); + assertEquals(list.size(), input.size()); + assertTrue(list.get(0).equals(new Size(30, 40)) || list.get(0).equals(new Size(40, 30))); + } + + @Test + public void testMaxArea() { + SizeSelector selector = SizeSelectors.maxArea(100 * 100); + List list = selector.select(input); + assertEquals(list.size(), 2); + assertEquals(list.get(0), new Size(30, 40)); + assertEquals(list.get(1), new Size(40, 30)); + } + + @Test + public void testMinArea() { + SizeSelector selector = SizeSelectors.minArea(1000 * 1000); // 1 MP + List list = selector.select(input); + assertEquals(list.size(), 2); + assertEquals(list.get(0), new Size(1600, 900)); + assertEquals(list.get(1), new Size(2000, 4000)); + } + + @Test + public void testAnd() { + SizeSelector selector = SizeSelectors.and( + SizeSelectors.aspectRatio(AspectRatio.of(1, 2), 0), + SizeSelectors.maxWidth(100) + ); + List list = selector.select(input); + assertEquals(list.size(), 1); + assertEquals(list.get(0), new Size(100, 200)); + } + + @Test + public void testOrNotPassed() { + SizeSelector mock = mock(SizeSelector.class); + SizeSelector selector = SizeSelectors.or( + SizeSelectors.aspectRatio(AspectRatio.of(1, 2), 0), + mock + ); + + // The first gives some result so the second is not queried. + selector.select(input); + verify(mock, never()).select(anyListOf(Size.class)); + } + + @Test + public void testOrPassed() { + SizeSelector mock = mock(SizeSelector.class); + SizeSelector selector = SizeSelectors.or( + SizeSelectors.minHeight(600000), + mock + ); + + // The first gives no result so the second is queried. + selector.select(input); + verify(mock, times(1)).select(anyListOf(Size.class)); + } +} diff --git a/cameraview/src/test/java/com/otaliastudios/cameraview/SizeTest.java b/cameraview/src/test/java/com/otaliastudios/cameraview/SizeTest.java index aff2dea3..fae207d9 100644 --- a/cameraview/src/test/java/com/otaliastudios/cameraview/SizeTest.java +++ b/cameraview/src/test/java/com/otaliastudios/cameraview/SizeTest.java @@ -11,11 +11,19 @@ public class SizeTest { @Test public void testDimensions() { Size size = new Size(10, 20); - assertEquals(size.getWidth(), 10f, 0f); - assertEquals(size.getHeight(), 20f, 0f); + assertEquals(size.getWidth(), 10); + assertEquals(size.getHeight(), 20); assertEquals("10x20", size.toString()); } + @Test + public void testFlip() { + Size size = new Size(10, 20); + size.flip(); + assertEquals(size.getWidth(), 20); + assertEquals(size.getHeight(), 10); + } + @Test public void testEquals() { Size s1 = new Size(10, 20); diff --git a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java index 57411606..705eef62 100644 --- a/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java +++ b/demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java @@ -102,7 +102,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis // This can happen if picture was taken with a gesture. if (mCaptureTime == 0) mCaptureTime = callbackTime - 300; - if (mCaptureNativeSize == null) mCaptureNativeSize = camera.getCaptureSize(); + if (mCaptureNativeSize == null) mCaptureNativeSize = camera.getPictureSize(); PicturePreviewActivity.setImage(jpeg); Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class); @@ -151,7 +151,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis if (mCapturingPicture) return; mCapturingPicture = true; mCaptureTime = System.currentTimeMillis(); - mCaptureNativeSize = camera.getCaptureSize(); + mCaptureNativeSize = camera.getPictureSize(); message("Capturing picture...", false); camera.capturePicture(); } diff --git a/demo/src/main/res/layout/activity_camera.xml b/demo/src/main/res/layout/activity_camera.xml index 75a8fc1a..fd820df3 100644 --- a/demo/src/main/res/layout/activity_camera.xml +++ b/demo/src/main/res/layout/activity_camera.xml @@ -15,6 +15,7 @@ android:layout_height="match_parent" android:layout_gravity="center" android:keepScreenOn="true" + app:cameraPlaySounds="true" app:cameraGrid="off" app:cameraCropOutput="false" app:cameraFacing="back"