Add SizeSelectors and tests

pull/99/head
Mattia Iavarone 8 years ago
parent 2e9715fa89
commit 1ac397c58f
  1. 16
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  2. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
  3. 55
      cameraview/src/main/java/com/otaliastudios/cameraview/AspectRatio.java
  4. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  5. 25
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
  6. 30
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  7. 22
      cameraview/src/main/java/com/otaliastudios/cameraview/SizeSelector.java
  8. 276
      cameraview/src/main/java/com/otaliastudios/cameraview/SizeSelectors.java
  9. 27
      cameraview/src/test/java/com/otaliastudios/cameraview/AspectRatioTest.java
  10. 167
      cameraview/src/test/java/com/otaliastudios/cameraview/SizeSelectorsTest.java
  11. 4
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.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,18 @@ 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();
// Ensure we wrapped the selector in a OrSelector to have fallbacks.
assertNotNull(result);
assertNotEquals(result, source);
assertTrue(result instanceof SizeSelectors.OrSelector);
}
//endregion
//region Lists of listeners and processors

@ -456,7 +456,7 @@ public class IntegrationTest extends BaseTest {
camera.start();
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);

@ -5,9 +5,11 @@ import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.util.SparseArray;
public class AspectRatio implements Comparable<AspectRatio>, Parcelable {
import java.util.HashMap;
final static SparseArray<SparseArray<AspectRatio>> sCache = new SparseArray<>(16);
public class AspectRatio implements Comparable<AspectRatio> {
final static HashMap<String, AspectRatio> sCache = new HashMap<>(16);
private final int mX;
private final int mY;
@ -71,6 +73,7 @@ public class AspectRatio implements Comparable<AspectRatio>, Parcelable {
return -1;
}
@SuppressWarnings("SuspiciousNameCombination")
public AspectRatio inverse() {
return AspectRatio.of(mY, mX);
}
@ -79,21 +82,13 @@ public class AspectRatio implements Comparable<AspectRatio>, Parcelable {
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;
String key = x + ":" + y;
AspectRatio cached = sCache.get(key);
if (cached == null) {
cached = new AspectRatio(x, y);
sCache.put(key, cached);
}
return cached;
}
private static int gcd(int a, int b) {
@ -104,32 +99,4 @@ public class AspectRatio implements Comparable<AspectRatio>, 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<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];
}
};
}

@ -944,6 +944,4 @@ class Camera1 extends CameraController implements Camera.PreviewCallback {
LOG.i("size", "matchSize:", "returning result", result);
return result;
}
}

@ -1,12 +1,16 @@
package com.otaliastudios.cameraview;
import android.graphics.PointF;
import android.hardware.Camera;
import android.location.Location;
import android.media.CamcorderProfile;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import java.io.File;
import java.util.Collections;
import java.util.List;
abstract class CameraController implements CameraPreview.SurfaceCallback, FrameManager.BufferCallback {
@ -37,6 +41,7 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
protected ExtraProperties mExtraProperties;
protected CameraOptions mOptions;
protected FrameManager mFrameManager;
protected SizeSelector mPictureSizeSelector;
protected int mDisplayOffset;
protected int mDeviceOrientation;
@ -207,10 +212,9 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
return mState;
}
//endregion
//region Rotation callbacks
//region Simple setters
void onDisplayOffset(int displayOrientation) {
// I doubt this will ever change.
@ -221,9 +225,13 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
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);
@ -318,7 +326,11 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
return mAudio;
}
final Size getCaptureSize() {
final SizeSelector getPictureSizeSelector() {
return mPictureSizeSelector;
}
final Size getPictureSize() {
return mCaptureSize;
}
@ -326,5 +338,10 @@ abstract class CameraController implements CameraPreview.SurfaceCallback, FrameM
return mPreviewSize;
}
//endregion
//region Size utils
//endregion
}

@ -148,6 +148,8 @@ public class CameraView extends FrameLayout {
setGrid(grid);
setHdr(hdr);
setAudio(audio);
// TODO: add from XML
setPictureSize(SizeSelectors.max());
// Apply gestures
mapGesture(Gesture.TAP, tapGesture);
@ -978,6 +980,21 @@ 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) {
// Add a max fallback.
mCameraController.setPictureSizeSelector(
SizeSelectors.or(selector, SizeSelectors.max())
);
}
/**
* 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.
@ -1266,8 +1283,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;
}
@ -1626,5 +1643,14 @@ public class CameraView extends FrameLayout {
//region Deprecated
/**
* @deprecated use {@link #getPictureSize()} instead.
*/
@Deprecated
@Nullable
public Size getCaptureSize() {
return getPictureSize();
}
//endregion
}

@ -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 max() {
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 min() {
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
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;
}
}
static class AndSelector implements SizeSelector {
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;
}
}
static class OrSelector implements SizeSelector {
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
}

@ -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);
@ -61,16 +50,4 @@ public class AspectRatioTest extends BaseTest {
assertEquals(inverse.getX(), 1f, 0);
assertEquals(inverse.getY(), 5f, 0);
}
@Test
public void testParcelable() {
AspectRatio ratio = AspectRatio.of(50, 10);
Parcel parcel = Parcel.obtain();
ratio.writeToParcel(parcel, ratio.describeContents());
parcel.setDataPosition(0);
AspectRatio other = AspectRatio.CREATOR.createFromParcel(parcel);
assertEquals(ratio, other);
}
}

@ -0,0 +1,167 @@
package com.otaliastudios.cameraview;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
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.max();
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.min();
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));
}
}

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

Loading…
Cancel
Save