Option to choose the picture output size (#99)

* Add SizeSelectors and tests

* Implement SizeSelectors in CameraController

* XML attrs, improve AspectRatio cache

* AspectRatio tests

* Flip sizes before passing to selectors

* Add README info

* Fix preview sizing bug

* Nits

* Fix #98
pull/105/head
Mattia Iavarone 7 years ago committed by GitHub
parent 6b9affc435
commit dcf5ef4120
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 151
      README.md
  2. 13
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  3. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
  4. 96
      cameraview/src/main/java/com/otaliastudios/cameraview/AspectRatio.java
  5. 163
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  6. 161
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  7. 113
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  8. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/Size.java
  9. 22
      cameraview/src/main/java/com/otaliastudios/cameraview/SizeSelector.java
  10. 276
      cameraview/src/main/java/com/otaliastudios/cameraview/SizeSelectors.java
  11. 4
      cameraview/src/main/options/com/otaliastudios/cameraview/SessionType.java
  12. 24
      cameraview/src/main/res/values/attrs.xml
  13. 36
      cameraview/src/test/java/com/otaliastudios/cameraview/AspectRatioTest.java
  14. 166
      cameraview/src/test/java/com/otaliastudios/cameraview/SizeSelectorsTest.java
  15. 12
      cameraview/src/test/java/com/otaliastudios/cameraview/SizeTest.java
  16. 4
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
  17. 1
      demo/src/main/res/layout/activity_camera.xml

@ -8,7 +8,8 @@
# CameraView # CameraView
CameraView is a well documented, high-level library that makes capturing pictures and videos easy, 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 ```groovy
compile 'com.otaliastudios:cameraview:1.3.2' 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 - Seamless image and video capturing
- **Gestures** support (tap to focus, pinch to zoom and much more) - **Gestures** support (tap to focus, pinch to zoom and much more)
- System permission handling - System permission handling
- Dynamic sizing behavior - Smart sizing behavior
- Create a `CameraView` of **any size** - Preview: Create a `CameraView` of **any size**
- Center inside or center crop behaviors - Preview: Center inside or center crop behaviors
- Automatic output cropping to match your `CameraView` bounds - Output: Handy utilities to set the output size
- Output: Automatic cropping to match your `CameraView` preview bounds
- Built-in **grid drawing** - Built-in **grid drawing**
- Multiple capture methods - Multiple capture methods
- Take high-resolution pictures with `capturePicture` - 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) - [Capturing Video](#capturing-video)
- [Other camera events](#other-camera-events) - [Other camera events](#other-camera-events)
- [Gestures](#gestures) - [Gestures](#gestures)
- [Dynamic Sizing Behavior](#dynamic-sizing-behavior) - [Sizing Behavior](#sizing-behavior)
- [Center Inside](#center-inside) - [Preview Size](#preview-size)
- [Center Crop](#center-crop) - [Picture Size](#picture-size)
- [Camera Controls](#camera-controls) - [Camera Controls](#camera-controls)
- [Frame Processing](#frame-processing) - [Frame Processing](#frame-processing)
- [Other APIs](#other-apis) - [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" /> 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 ```java
@Override @Override
@ -102,7 +106,8 @@ protected void onDestroy() {
### Capturing Images ### 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 ```java
camera.addCameraListener(new CameraListener() { camera.addCameraListener(new CameraListener() {
@ -117,11 +122,14 @@ camera.addCameraListener(new CameraListener() {
camera.capturePicture(); 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 ### 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 ```java
camera.addCameraListener(new CameraListener() { camera.addCameraListener(new CameraListener() {
@ -152,7 +160,8 @@ camera.postDelayed(new Runnable() {
### Other camera events ### 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 ```java
camera.addCameraListener(new CameraListener() { camera.addCameraListener(new CameraListener() {
@ -239,7 +248,9 @@ camera.addCameraListener(new CameraListener() {
## Gestures ## 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 ```java
cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM); // Pinch to zoom! 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`| |`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. - `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. - `MATCH_PARENT` : fill this dimension, even if this means ignoring the aspect ratio.
- Fixed values (e.g. `500dp`) : respect this dimension. - 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 ```xml
@ -282,11 +299,15 @@ You can emulate a **center inside** behavior (like the `ImageView` scaletype) by
android:layout_height="wrap_content" /> 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 ```xml
<com.otaliastudios.cameraview.CameraView <com.otaliastudios.cameraview.CameraView
@ -294,7 +315,64 @@ You can emulate a **center crop** behavior by setting both dimensions to fixed v
android:layout_height="match_parent" /> android:layout_height="match_parent" />
``` ```
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<Size> select(List<Size> 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 ## 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: 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. - Sizing: picture and preview size are chosen among the available picture or video sizes,
- 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. depending on the flag. The picture size is chosen according to the given [size selector](#picture-size).
- Picture capturing: while recording a video, image capturing might work, but it is not guaranteed (it's device dependent) When `video`, in addition, we try to match the `videoQuality` aspect ratio.
- 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. - 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 ```java
cameraView.setSessionType(SessionType.PICTURE); cameraView.setSessionType(SessionType.PICTURE);
@ -369,7 +453,8 @@ cameraView.setFlash(Flash.TORCH);
#### cameraGrid #### 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 ```java
cameraView.setGrid(Grid.OFF); cameraView.setGrid(Grid.OFF);
@ -381,7 +466,8 @@ cameraView.setGrid(Grid.DRAW_PHI);
#### cameraCropOutput #### cameraCropOutput
Whether the output picture should be cropped to fit the aspect ratio of the preview surface. 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 #### cameraJpegQuality
@ -438,8 +524,8 @@ cameraView.setAudio(Audio.ON);
#### cameraPlaySounds #### cameraPlaySounds
Controls whether we should play platform-provided sounds during certain events (shutter click, focus completed). Controls whether we should play platform-provided sounds during certain events
Please note that: (shutter click, focus completed). Please note that:
- on API < 16, this flag is always set to `false` - on API < 16, this flag is always set to `false`
- the Camera1 engine will always play shutter sounds regardless of this flag - 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.| |`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.| |`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`.| |`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(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.| |`setLocation(double, double)`|Sets latitude and longitude to be appended to picture/video metadata.|
|`getLocation()`|Retrieves location data previously applied with setLocation().| |`getLocation()`|Retrieves location data previously applied with setLocation().|
@ -617,11 +702,11 @@ all the code was changed.
- *Frame processor support* - *Frame processor support*
- *inject external loggers* - *inject external loggers*
- *error handling* - *error handling*
- *capture size selectors*
These are still things that need to be done, off the top of my head: These are still things that need to be done, off the top of my head:
- [ ] `Camera2` integration - [ ] `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 - [ ] animate grid lines similar to stock camera app
- [ ] add onRequestPermissionResults for easy permission callback - [ ] add onRequestPermissionResults for easy permission callback
- [ ] decent code coverage - [ ] decent code coverage

@ -14,6 +14,8 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.util.List;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -76,7 +78,7 @@ public class CameraViewTest extends BaseTest {
assertNull(cameraView.getCameraOptions()); assertNull(cameraView.getCameraOptions());
assertNull(cameraView.getExtraProperties()); assertNull(cameraView.getExtraProperties());
assertNull(cameraView.getPreviewSize()); assertNull(cameraView.getPreviewSize());
assertNull(cameraView.getCaptureSize()); assertNull(cameraView.getPictureSize());
assertNull(cameraView.getSnapshotSize()); assertNull(cameraView.getSnapshotSize());
} }
@ -551,6 +553,15 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getVideoQuality(), VideoQuality.LOWEST); 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 //endregion
//region Lists of listeners and processors //region Lists of listeners and processors

@ -490,7 +490,7 @@ public class IntegrationTest extends BaseTest {
camera.setCropOutput(false); camera.setCropOutput(false);
waitForOpen(true); waitForOpen(true);
Size size = camera.getCaptureSize(); Size size = camera.getPictureSize();
camera.capturePicture(); camera.capturePicture();
byte[] jpeg = waitForPicture(true); byte[] jpeg = waitForPicture(true);
Bitmap b = CameraUtils.decodeBitmap(jpeg, Integer.MAX_VALUE, Integer.MAX_VALUE); Bitmap b = CameraUtils.decodeBitmap(jpeg, Integer.MAX_VALUE, Integer.MAX_VALUE);

@ -5,9 +5,51 @@ import android.os.Parcelable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.SparseArray; import android.util.SparseArray;
public class AspectRatio implements Comparable<AspectRatio>, Parcelable { import java.util.HashMap;
final static SparseArray<SparseArray<AspectRatio>> sCache = new SparseArray<>(16);
/**
* A simple class representing an aspect ratio.
*/
public class AspectRatio implements Comparable<AspectRatio> {
final static HashMap<String, AspectRatio> 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 mX;
private final int mY; private final int mY;
@ -71,31 +113,11 @@ public class AspectRatio implements Comparable<AspectRatio>, Parcelable {
return -1; return -1;
} }
@SuppressWarnings("SuspiciousNameCombination")
public AspectRatio inverse() { public AspectRatio inverse() {
return AspectRatio.of(mY, mX); return AspectRatio.of(mY, mX);
} }
public static AspectRatio of(int x, int y) {
int gcd = gcd(x, y);
x /= gcd;
y /= gcd;
SparseArray<AspectRatio> 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) { private static int gcd(int a, int b) {
while (b != 0) { while (b != 0) {
int c = b; int c = b;
@ -104,32 +126,4 @@ public class AspectRatio implements Comparable<AspectRatio>, Parcelable {
} }
return a; 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<AspectRatio> CREATOR = new Parcelable.Creator<AspectRatio>() {
@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];
}
};
} }

@ -28,7 +28,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
private static final String TAG = Camera1.class.getSimpleName(); private static final String TAG = Camera1.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG); private static final CameraLogger LOG = CameraLogger.create(TAG);
private int mCameraId;
private Camera mCamera; private Camera mCamera;
private MediaRecorder mMediaRecorder; private MediaRecorder mMediaRecorder;
private File mVideoFile; private File mVideoFile;
@ -104,7 +103,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
if (!mIsBound) return; if (!mIsBound) return;
// Compute a new camera preview size. // Compute a new camera preview size.
Size newSize = computePreviewSize(); Size newSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes()));
if (newSize.equals(mPreviewSize)) return; if (newSize.equals(mPreviewSize)) return;
// Apply. // Apply.
@ -136,8 +135,8 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
throw new CameraException(e); throw new CameraException(e);
} }
mCaptureSize = computeCaptureSize(); mPictureSize = computePictureSize(sizesFromList(mCamera.getParameters().getSupportedPictureSizes()));
mPreviewSize = computePreviewSize(); mPreviewSize = computePreviewSize(sizesFromList(mCamera.getParameters().getSupportedPreviewSizes()));
applySizesAndStartPreview("bindToSurface:"); applySizesAndStartPreview("bindToSurface:");
mIsBound = true; mIsBound = true;
} }
@ -156,7 +155,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
mPreviewFormat = params.getPreviewFormat(); mPreviewFormat = params.getPreviewFormat();
params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); // <- not allowed during preview 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.setParameters(params);
mCamera.setPreviewCallbackWithBuffer(null); // Release anything left mCamera.setPreviewCallbackWithBuffer(null); // Release anything left
@ -213,8 +212,8 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
try { try {
LOG.i("onStop:", "Clean up.", "Stopping preview."); LOG.i("onStop:", "Clean up.", "Stopping preview.");
mCamera.stopPreview();
mCamera.setPreviewCallbackWithBuffer(null); mCamera.setPreviewCallbackWithBuffer(null);
mCamera.stopPreview();
LOG.i("onStop:", "Clean up.", "Stopped preview."); LOG.i("onStop:", "Clean up.", "Stopped preview.");
} catch (Exception e) { } catch (Exception e) {
LOG.w("onStop:", "Clean up.", "Exception while stopping preview.", 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; mOptions = null;
mCamera = null; mCamera = null;
mPreviewSize = null; mPreviewSize = null;
mCaptureSize = null; mPictureSize = null;
mIsBound = false; mIsBound = false;
LOG.w("onStop:", "Clean up.", "Returning.");
if (error != null) throw new CameraException(error); if (error != null) throw new CameraException(error);
} }
@ -456,17 +455,17 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
if (mSessionType == SessionType.VIDEO) { if (mSessionType == SessionType.VIDEO) {
// Change capture size to a size that fits the video aspect ratio. // Change capture size to a size that fits the video aspect ratio.
Size oldSize = mCaptureSize; Size oldSize = mPictureSize;
mCaptureSize = computeCaptureSize(); mPictureSize = computePictureSize(sizesFromList(mCamera.getParameters().getSupportedPictureSizes()));
if (!mCaptureSize.equals(oldSize)) { if (!mPictureSize.equals(oldSize)) {
// New video quality triggers a new aspect ratio. // New video quality triggers a new aspect ratio.
// Go on and see if preview size should change also. // Go on and see if preview size should change also.
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params = mCamera.getParameters();
params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); params.setPictureSize(mPictureSize.getWidth(), mPictureSize.getHeight());
mCamera.setParameters(params); mCamera.setParameters(params);
onSurfaceChanged(); onSurfaceChanged();
} }
LOG.i("setVideoQuality:", "captureSize:", mCaptureSize); LOG.i("setVideoQuality:", "captureSize:", mPictureSize);
LOG.i("setVideoQuality:", "previewSize:", mPreviewSize); 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. * 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<Size> 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<Size> 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<Size> 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. // Video recording stuff.
@ -755,7 +716,7 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
// Must be called before setOutputFormat. // Must be called before setOutputFormat.
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT); mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
} }
CamcorderProfile profile = getCamcorderProfile(mCameraId, mVideoQuality); CamcorderProfile profile = getCamcorderProfile();
mMediaRecorder.setOutputFormat(profile.fileFormat); mMediaRecorder.setOutputFormat(profile.fileFormat);
mMediaRecorder.setVideoFrameRate(profile.videoFrameRate); mMediaRecorder.setVideoFrameRate(profile.videoFrameRate);
mMediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight); mMediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight);
@ -778,50 +739,6 @@ class Camera1 extends CameraController implements Camera.PreviewCallback, Camera
// Not needed. mMediaRecorder.setPreviewDisplay(mPreview.getSurface()); // 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. // 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 @Nullable
private static List<Size> sizesFromList(List<Camera.Size> sizes) { private List<Size> sizesFromList(List<Camera.Size> sizes) {
if (sizes == null) return null; if (sizes == null) return null;
List<Size> result = new ArrayList<>(sizes.size()); List<Size> result = new ArrayList<>(sizes.size());
for (Camera.Size size : sizes) { 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); LOG.i("size:", "sizesFromList:", result);
return 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<Size> sizes, AspectRatio targetRatio, Size biggerThan, boolean biggestPossible) {
if (sizes == null) return null;
List<Size> consistent = new ArrayList<>(5);
List<Size> 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;
}
} }

@ -2,6 +2,10 @@ package com.otaliastudios.cameraview;
import android.graphics.PointF; import android.graphics.PointF;
import android.location.Location; import android.location.Location;
import android.media.CamcorderProfile;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@ -9,6 +13,7 @@ import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread; import android.support.annotation.WorkerThread;
import java.io.File; import java.io.File;
import java.util.List;
abstract class CameraController implements abstract class CameraController implements
CameraPreview.SurfaceCallback, CameraPreview.SurfaceCallback,
@ -27,6 +32,7 @@ abstract class CameraController implements
protected CameraPreview mPreview; protected CameraPreview mPreview;
protected WorkerHandler mHandler; protected WorkerHandler mHandler;
/* for tests */ Handler mCrashHandler; /* for tests */ Handler mCrashHandler;
protected int mCameraId;
protected Facing mFacing; protected Facing mFacing;
protected Flash mFlash; protected Flash mFlash;
@ -40,13 +46,14 @@ abstract class CameraController implements
protected float mZoomValue; protected float mZoomValue;
protected float mExposureCorrectionValue; protected float mExposureCorrectionValue;
protected Size mCaptureSize; protected Size mPictureSize;
protected Size mPreviewSize; protected Size mPreviewSize;
protected int mPreviewFormat; protected int mPreviewFormat;
protected ExtraProperties mExtraProperties; protected ExtraProperties mExtraProperties;
protected CameraOptions mOptions; protected CameraOptions mOptions;
protected FrameManager mFrameManager; protected FrameManager mFrameManager;
protected SizeSelector mPictureSizeSelector;
protected int mDisplayOffset; protected int mDisplayOffset;
protected int mDeviceOrientation; protected int mDeviceOrientation;
@ -77,6 +84,13 @@ abstract class CameraController implements
//region Error handling //region Error handling
private static class NoOpExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
// No-op.
}
}
@Override @Override
public void uncaughtException(final Thread thread, final Throwable throwable) { public void uncaughtException(final Thread thread, final Throwable throwable) {
// Something went wrong. Thread is terminated (about to?). // Something went wrong. Thread is terminated (about to?).
@ -119,8 +133,9 @@ abstract class CameraController implements
final void destroy() { final void destroy() {
LOG.i("destroy:", "state:", ss()); LOG.i("destroy:", "state:", ss());
// Prevent CameraController leaks. // Prevent CameraController leaks. Don't set to null, or exceptions
mHandler.getThread().setUncaughtExceptionHandler(null); // inside the standard stop() method might crash the main thread.
mHandler.getThread().setUncaughtExceptionHandler(new NoOpExceptionHandler());
// Stop if needed. // Stop if needed.
stopImmediately(); stopImmediately();
} }
@ -232,10 +247,9 @@ abstract class CameraController implements
return mState; return mState;
} }
//endregion //endregion
//region Rotation callbacks //region Simple setters
void onDisplayOffset(int displayOrientation) { void onDisplayOffset(int displayOrientation) {
// I doubt this will ever change. // I doubt this will ever change.
@ -246,9 +260,13 @@ abstract class CameraController implements
mDeviceOrientation = deviceOrientation; mDeviceOrientation = deviceOrientation;
} }
void setPictureSizeSelector(SizeSelector selector) {
mPictureSizeSelector = selector;
}
//endregion //endregion
//region Abstract setParameters //region Abstract setters
// Should restart the session if active. // Should restart the session if active.
abstract void setSessionType(SessionType sessionType); abstract void setSessionType(SessionType sessionType);
@ -343,6 +361,14 @@ abstract class CameraController implements
return mAudio; return mAudio;
} }
final SizeSelector getPictureSizeSelector() {
return mPictureSizeSelector;
}
final Size getPictureSize() {
return mPictureSize;
}
final float getZoomValue() { final float getZoomValue() {
return mZoomValue; return mZoomValue;
} }
@ -351,13 +377,128 @@ abstract class CameraController implements
return mExposureCorrectionValue; return mExposureCorrectionValue;
} }
final Size getCaptureSize() {
return mCaptureSize;
}
final Size getPreviewSize() { final Size getPreviewSize() {
return mPreviewSize; return mPreviewSize;
} }
//endregion //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<Size> 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<Size> 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
} }

@ -92,6 +92,7 @@ public class CameraView extends FrameLayout {
private void init(@NonNull Context context, @Nullable AttributeSet attrs) { private void init(@NonNull Context context, @Nullable AttributeSet attrs) {
setWillNotDraw(false); setWillNotDraw(false);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CameraView, 0, 0); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CameraView, 0, 0);
// Self managed // Self managed
int jpegQuality = a.getInteger(R.styleable.CameraView_cameraJpegQuality, DEFAULT_JPEG_QUALITY); int jpegQuality = a.getInteger(R.styleable.CameraView_cameraJpegQuality, DEFAULT_JPEG_QUALITY);
boolean cropOutput = a.getBoolean(R.styleable.CameraView_cameraCropOutput, DEFAULT_CROP_OUTPUT); 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())); 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())); Audio audio = Audio.fromValue(a.getInteger(R.styleable.CameraView_cameraAudio, Audio.DEFAULT.value()));
// Size selectors
List<SizeSelector> 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 // Gestures
GestureAction tapGesture = GestureAction.fromValue(a.getInteger(R.styleable.CameraView_cameraGestureTap, GestureAction.DEFAULT_TAP.value())); 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())); 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); setGrid(grid);
setHdr(hdr); setHdr(hdr);
setAudio(audio); setAudio(audio);
setPictureSize(selector);
// Apply gestures // Apply gestures
mapGesture(Gesture.TAP, tapGesture); mapGesture(Gesture.TAP, tapGesture);
// mapGesture(Gesture.DOUBLE_TAP, doubleTapGesture);
mapGesture(Gesture.LONG_TAP, longTapGesture); mapGesture(Gesture.LONG_TAP, longTapGesture);
mapGesture(Gesture.PINCH, pinchGesture); mapGesture(Gesture.PINCH, pinchGesture);
mapGesture(Gesture.SCROLL_HORIZONTAL, scrollHorizontalGesture); mapGesture(Gesture.SCROLL_HORIZONTAL, scrollHorizontalGesture);
@ -514,8 +545,8 @@ public class CameraView extends FrameLayout {
/** /**
* Checks that we have appropriate permissions for this session type. * Checks that we have appropriate permissions for this session type.
* Throws if session = audio and manifest did not add the microphone permissions. * Throws if session = audio and manifest did not add the microphone permissions.
* @param sessionType * @param sessionType the sessionType to be checked
* @param audio * @param audio the audio setting to be checked
* @return true if we can go on, false otherwise. * @return true if we can go on, false otherwise.
*/ */
@SuppressLint("NewApi") @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. * 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. * 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. * 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 * @return a Size
*/ */
@Nullable @Nullable
public Size getCaptureSize() { public Size getPictureSize() {
return mCameraController != null ? mCameraController.getCaptureSize() : null; return mCameraController != null ? mCameraController.getPictureSize() : null;
} }
@ -1625,4 +1641,37 @@ public class CameraView extends FrameLayout {
} }
//endregion //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
} }

@ -2,10 +2,13 @@ package com.otaliastudios.cameraview;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
/**
* A simple class representing a size, with width and height values.
*/
public class Size implements Comparable<Size> { public class Size implements Comparable<Size> {
private final int mWidth; private int mWidth;
private final int mHeight; private int mHeight;
Size(int width, int height) { Size(int width, int height) {
mWidth = width; mWidth = width;
@ -20,6 +23,16 @@ public class Size implements Comparable<Size> {
return mHeight; return mHeight;
} }
/**
* Flips width and height altogether.
*/
@SuppressWarnings("SuspiciousNameCombination")
public void flip() {
int temp = mWidth;
mWidth = mHeight;
mHeight = temp;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o == null) { if (o == null) {

@ -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<Size> select(@NonNull List<Size> source);
}

@ -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<Size> select(@NonNull List<Size> 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<Size> select(@NonNull List<Size> 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<Size> select(@NonNull List<Size> source) {
List<Size> 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<Size> select(@NonNull List<Size> source) {
List<Size> 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<Size> select(@NonNull List<Size> source) {
List<Size> temp = null;
for (SizeSelector selector : values) {
temp = selector.select(source);
if (!temp.isEmpty()) {
break;
}
}
return temp == null ? new ArrayList<Size>() : temp;
}
}
//endregion
}

@ -16,7 +16,7 @@ public enum SessionType {
* *
* - Trying to take videos in this session will throw an exception * - Trying to take videos in this session will throw an exception
* - Only the camera permission is requested * - 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), 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 in this session will work, though with lower quality
* - Trying to take pictures while recording a video will work if supported * - Trying to take pictures while recording a video will work if supported
* - Camera and audio record permissions are requested * - 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() * @see CameraOptions#isVideoSnapshotSupported()
*/ */

@ -1,12 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<declare-styleable name="CameraView"> <declare-styleable name="CameraView">
<!-- WIP attr name="cameraGestureDoubleTap" format="enum">
<enum name="none" value="0" /> <attr name="cameraPictureSizeMinWidth" format="integer|reference"/>
<enum name="focus" value="1" />
<enum name="focusWithMarker" value="2" /> <attr name="cameraPictureSizeMaxWidth" format="integer|reference"/>
<enum name="capture" value="3" />
</attr--> <attr name="cameraPictureSizeMinHeight" format="integer|reference"/>
<attr name="cameraPictureSizeMaxHeight" format="integer|reference"/>
<attr name="cameraPictureSizeMinArea" format="integer|reference" />
<attr name="cameraPictureSizeMaxArea" format="integer|reference" />
<attr name="cameraPictureSizeSmallest" format="boolean"/>
<attr name="cameraPictureSizeBiggest" format="boolean"/>
<attr name="cameraPictureSizeAspectRatio" format="string|reference"/>
<attr name="cameraGestureTap" format="enum"> <attr name="cameraGestureTap" format="enum">
<enum name="none" value="0" /> <enum name="none" value="0" />

@ -1,22 +1,11 @@
package com.otaliastudios.cameraview; 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.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@RunWith(AndroidJUnit4.class) public class AspectRatioTest {
@SmallTest
public class AspectRatioTest extends BaseTest {
@Test @Test
public void testConstructor() { public void testConstructor() {
@ -29,7 +18,7 @@ public class AspectRatioTest extends BaseTest {
@Test @Test
public void testEquals() { public void testEquals() {
AspectRatio ratio = AspectRatio.of(50, 10); AspectRatio ratio = AspectRatio.of(50, 10);
assertFalse(ratio.equals(null)); assertNotNull(ratio);
assertTrue(ratio.equals(ratio)); assertTrue(ratio.equals(ratio));
AspectRatio ratio1 = AspectRatio.of(5, 1); AspectRatio ratio1 = AspectRatio.of(5, 1);
@ -62,15 +51,20 @@ public class AspectRatioTest extends BaseTest {
assertEquals(inverse.getY(), 5f, 0); assertEquals(inverse.getY(), 5f, 0);
} }
@Test @Test(expected = NumberFormatException.class)
public void testParcelable() { public void testParse_notNumbers() {
AspectRatio ratio = AspectRatio.of(50, 10); AspectRatio.parse("a:b");
Parcel parcel = Parcel.obtain(); }
ratio.writeToParcel(parcel, ratio.describeContents());
parcel.setDataPosition(0); @Test(expected = NumberFormatException.class)
AspectRatio other = AspectRatio.CREATOR.createFromParcel(parcel); public void testParse_noColon() {
assertEquals(ratio, other); AspectRatio.parse("24");
} }
@Test
public void testParse() {
AspectRatio ratio = AspectRatio.parse("16:9");
assertEquals(ratio.getX(), 16);
assertEquals(ratio.getY(), 9);
}
} }

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

@ -11,11 +11,19 @@ public class SizeTest {
@Test @Test
public void testDimensions() { public void testDimensions() {
Size size = new Size(10, 20); Size size = new Size(10, 20);
assertEquals(size.getWidth(), 10f, 0f); assertEquals(size.getWidth(), 10);
assertEquals(size.getHeight(), 20f, 0f); assertEquals(size.getHeight(), 20);
assertEquals("10x20", size.toString()); 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 @Test
public void testEquals() { public void testEquals() {
Size s1 = new Size(10, 20); Size s1 = new Size(10, 20);

@ -102,7 +102,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
// This can happen if picture was taken with a gesture. // This can happen if picture was taken with a gesture.
if (mCaptureTime == 0) mCaptureTime = callbackTime - 300; if (mCaptureTime == 0) mCaptureTime = callbackTime - 300;
if (mCaptureNativeSize == null) mCaptureNativeSize = camera.getCaptureSize(); if (mCaptureNativeSize == null) mCaptureNativeSize = camera.getPictureSize();
PicturePreviewActivity.setImage(jpeg); PicturePreviewActivity.setImage(jpeg);
Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class); Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class);
@ -151,7 +151,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
if (mCapturingPicture) return; if (mCapturingPicture) return;
mCapturingPicture = true; mCapturingPicture = true;
mCaptureTime = System.currentTimeMillis(); mCaptureTime = System.currentTimeMillis();
mCaptureNativeSize = camera.getCaptureSize(); mCaptureNativeSize = camera.getPictureSize();
message("Capturing picture...", false); message("Capturing picture...", false);
camera.capturePicture(); camera.capturePicture();
} }

@ -15,6 +15,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"
android:keepScreenOn="true" android:keepScreenOn="true"
app:cameraPlaySounds="true"
app:cameraGrid="off" app:cameraGrid="off"
app:cameraCropOutput="false" app:cameraCropOutput="false"
app:cameraFacing="back" app:cameraFacing="back"

Loading…
Cancel
Save