diff --git a/build.gradle b/build.gradle
index db868459..887ddacd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.1.0-alpha4'
+ classpath 'com.android.tools.build:gradle:2.1.0-alpha5'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/demo/src/main/java/com/google/android/cameraview/demo/MainActivity.java b/demo/src/main/java/com/google/android/cameraview/demo/MainActivity.java
index 60de98e4..12c720f8 100644
--- a/demo/src/main/java/com/google/android/cameraview/demo/MainActivity.java
+++ b/demo/src/main/java/com/google/android/cameraview/demo/MainActivity.java
@@ -18,9 +18,11 @@
import com.google.android.cameraview.CameraView;
+import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
+import android.view.View;
public class MainActivity extends AppCompatActivity {
@@ -32,6 +34,10 @@ public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
+ if (Build.VERSION.SDK_INT >= 16) {
+ // Hide the status bar
+ getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
+ }
mCameraView = (CameraView) findViewById(R.id.camera);
if (mCameraView != null) {
mCameraView.addCallback(mCallback);
diff --git a/demo/src/main/res/layout-land/activity_main.xml b/demo/src/main/res/layout-land/activity_main.xml
new file mode 100644
index 00000000..1b02cd1f
--- /dev/null
+++ b/demo/src/main/res/layout-land/activity_main.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/demo/src/main/res/layout/activity_main.xml b/demo/src/main/res/layout/activity_main.xml
index d2225eed..e2e75894 100644
--- a/demo/src/main/res/layout/activity_main.xml
+++ b/demo/src/main/res/layout/activity_main.xml
@@ -16,15 +16,12 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:paddingBottom="@dimen/activity_vertical_margin"
- android:paddingLeft="@dimen/activity_horizontal_margin"
- android:paddingRight="@dimen/activity_horizontal_margin"
- android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
+ android:layout_height="wrap_content"
+ android:adjustViewBounds="true"/>
diff --git a/library/src/androidTest/java/com/google/android/cameraview/CameraViewTest.java b/library/src/androidTest/java/com/google/android/cameraview/CameraViewTest.java
index 2fffd4b6..0e6da8f9 100644
--- a/library/src/androidTest/java/com/google/android/cameraview/CameraViewTest.java
+++ b/library/src/androidTest/java/com/google/android/cameraview/CameraViewTest.java
@@ -36,6 +36,7 @@
import android.support.test.runner.AndroidJUnit4;
import android.view.TextureView;
import android.view.View;
+import android.view.ViewGroup;
import java.io.Closeable;
import java.io.IOException;
@@ -49,7 +50,11 @@
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static junit.framework.Assert.assertFalse;
+import static org.hamcrest.CoreMatchers.either;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
@RunWith(AndroidJUnit4.class)
@@ -122,6 +127,64 @@ public void check(View view, NoMatchingViewException noViewFoundException) {
});
}
+ @Test
+ public void testAspectRatio() {
+ onView(withId(R.id.camera))
+ .check(new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewFoundException) {
+ CameraView cameraView = (CameraView) view;
+ AspectRatio ratio = cameraView.getAspectRatio();
+ assertThat(ratio, is(notNullValue()));
+ SizeMap map = cameraView.getSupportedPreviewSizes();
+ assertThat(map.ratios(), hasItem(ratio));
+ AspectRatio otherRatio = null;
+ for (AspectRatio r : map.ratios()) {
+ if (!r.equals(ratio)) {
+ otherRatio = r;
+ break;
+ }
+ }
+ if (otherRatio != null) {
+ cameraView.setAspectRatio(otherRatio);
+ assertThat(cameraView.getAspectRatio(), is(equalTo(otherRatio)));
+ }
+ }
+ });
+ }
+
+ @Test
+ public void testAdjustViewBounds() {
+ onView(withId(R.id.camera))
+ .check(new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewFoundException) {
+ CameraView cameraView = (CameraView) view;
+ assertThat(cameraView.getAdjustViewBounds(), is(false));
+ cameraView.setAdjustViewBounds(true);
+ assertThat(cameraView.getAdjustViewBounds(), is(true));
+ }
+ })
+ .perform(new AnythingAction("layout") {
+ @Override
+ public void perform(UiController uiController, View view) {
+ ViewGroup.LayoutParams params = view.getLayoutParams();
+ params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ view.setLayoutParams(params);
+ }
+ })
+ .check(new ViewAssertion() {
+ @Override
+ public void check(View view, NoMatchingViewException noViewFoundException) {
+ CameraView cameraView = (CameraView) view;
+ AspectRatio cameraRatio = cameraView.getAspectRatio();
+ AspectRatio viewRatio = new AspectRatio(view.getWidth(), view.getHeight());
+ assertThat(cameraRatio, is(either(equalTo(viewRatio))
+ .or(equalTo(viewRatio.inverse()))));
+ }
+ });
+ }
+
/**
* Wait for a camera to open.
*/
@@ -180,29 +243,39 @@ public void registerIdleTransitionCallback(ResourceCallback callback) {
}
- private static class WaitAction implements ViewAction {
+ private static class WaitAction extends AnythingAction {
private final long mMs;
public WaitAction(long ms) {
+ super("wait");
mMs = ms;
}
@Override
- public Matcher getConstraints() {
- return new IsAnything<>();
+ public void perform(UiController uiController, View view) {
+ SystemClock.sleep(mMs);
}
- @Override
- public String getDescription() {
- return "wait";
+ }
+
+ private static abstract class AnythingAction implements ViewAction {
+
+ private final String mDescription;
+
+ public AnythingAction(String description) {
+ mDescription = description;
}
@Override
- public void perform(UiController uiController, View view) {
- SystemClock.sleep(mMs);
+ public Matcher getConstraints() {
+ return new IsAnything<>();
}
+ @Override
+ public String getDescription() {
+ return mDescription;
+ }
}
}
diff --git a/library/src/main/base/com/google/android/cameraview/AspectRatio.java b/library/src/main/base/com/google/android/cameraview/AspectRatio.java
index f7c1bf2c..fd1f3859 100644
--- a/library/src/main/base/com/google/android/cameraview/AspectRatio.java
+++ b/library/src/main/base/com/google/android/cameraview/AspectRatio.java
@@ -94,6 +94,14 @@ public int compareTo(@NonNull AspectRatio another) {
return -1;
}
+ /**
+ * @return The inverse of this {@link AspectRatio}.
+ */
+ public AspectRatio inverse() {
+ //noinspection SuspiciousNameCombination
+ return new AspectRatio(mY, mX);
+ }
+
private static int gcd(int a, int b) {
while (b != 0) {
int c = b;
diff --git a/library/src/main/base/com/google/android/cameraview/CameraViewImpl.java b/library/src/main/base/com/google/android/cameraview/CameraViewImpl.java
index b15a0cab..89442879 100644
--- a/library/src/main/base/com/google/android/cameraview/CameraViewImpl.java
+++ b/library/src/main/base/com/google/android/cameraview/CameraViewImpl.java
@@ -20,9 +20,9 @@
abstract class CameraViewImpl {
- protected final InternalCameraViewCallback mCallback;
+ protected final Callback mCallback;
- public CameraViewImpl(InternalCameraViewCallback callback) {
+ public CameraViewImpl(Callback callback) {
mCallback = callback;
}
@@ -39,4 +39,16 @@ public CameraViewImpl(InternalCameraViewCallback callback) {
abstract SizeMap getSupportedPreviewSizes();
abstract boolean isCameraOpened();
+
+ abstract void setAspectRatio(AspectRatio ratio);
+
+ abstract AspectRatio getAspectRatio();
+
+ interface Callback {
+
+ void onCameraOpened();
+
+ void onCameraClosed();
+
+ }
}
diff --git a/library/src/main/base/com/google/android/cameraview/InternalCameraViewCallback.java b/library/src/main/base/com/google/android/cameraview/InternalCameraViewCallback.java
deleted file mode 100644
index a5e529e2..00000000
--- a/library/src/main/base/com/google/android/cameraview/InternalCameraViewCallback.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.android.cameraview;
-
-/**
- * Callbacks for {@link CameraViewImpl}
- */
-interface InternalCameraViewCallback {
-
- void onCameraOpened();
-
- void onCameraClosed();
-
-}
diff --git a/library/src/main/camera1/com/google/android/cameraview/Camera1.java b/library/src/main/camera1/com/google/android/cameraview/Camera1.java
index 30b40694..42668fab 100644
--- a/library/src/main/camera1/com/google/android/cameraview/Camera1.java
+++ b/library/src/main/camera1/com/google/android/cameraview/Camera1.java
@@ -24,24 +24,31 @@
import android.view.WindowManager;
import java.io.IOException;
+import java.util.List;
@SuppressWarnings("deprecation")
class Camera1 extends CameraViewImpl {
private static final int INVALID_CAMERA_ID = -1;
+ private static final AspectRatio DEFAULT_ASPECT_RATIO = new AspectRatio(4, 3);
+
private final Context mContext;
private int mCameraId;
private Camera mCamera;
+ private Camera.Parameters mCameraParameters;
+
private final Camera.CameraInfo mCameraInfo = new Camera.CameraInfo();
private final PreviewInfo mPreviewInfo = new PreviewInfo();
private final SizeMap mPreviewSizes = new SizeMap();
+ private AspectRatio mAspectRatio;
+
private static class PreviewInfo {
SurfaceTexture surface;
int width;
@@ -60,13 +67,17 @@ void configure(SurfaceTexture s, int w, int h) {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
mPreviewInfo.configure(surface, width, height);
- setUpPreview();
+ if (mCamera != null) {
+ setUpPreview();
+ }
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
mPreviewInfo.configure(surface, width, height);
- setUpPreview();
+ if (mCamera != null) {
+ setUpPreview();
+ }
}
@Override
@@ -80,7 +91,7 @@ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
};
- public Camera1(Context context, InternalCameraViewCallback callback) {
+ public Camera1(Context context, Callback callback) {
super(callback);
mContext = context;
}
@@ -131,6 +142,29 @@ boolean isCameraOpened() {
return mCamera != null;
}
+ @Override
+ void setAspectRatio(AspectRatio ratio) {
+ if (mAspectRatio == null || !isCameraOpened()) {
+ // Handle this later when camera is opened
+ mAspectRatio = ratio;
+ } else if (!mAspectRatio.equals(ratio)) {
+ final List sizes = mPreviewSizes.sizes(ratio);
+ if (sizes == null) {
+ throw new UnsupportedOperationException(ratio + " is not supported");
+ } else {
+ mAspectRatio = ratio;
+ Size size = chooseOptimalSize(sizes);
+ mCameraParameters.setPreviewSize(size.getWidth(), size.getHeight());
+ mCamera.setParameters(mCameraParameters);
+ }
+ }
+ }
+
+ @Override
+ AspectRatio getAspectRatio() {
+ return mAspectRatio;
+ }
+
/**
* This rewrites {@link #mCameraId} and {@link #mCameraInfo}.
*/
@@ -151,14 +185,43 @@ private void openCamera() {
releaseCamera();
}
mCamera = Camera.open(mCameraId);
- Camera.Parameters parameters = mCamera.getParameters();
+ mCameraParameters = mCamera.getParameters();
+ // Supported preview sizes
mPreviewSizes.clear();
- for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
+ for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) {
mPreviewSizes.add(new Size(size.width, size.height));
}
+ // AspectRatio
+ if (mAspectRatio == null) {
+ mAspectRatio = chooseAspectRatio();
+ } else {
+ final List sizes = mPreviewSizes.sizes(mAspectRatio);
+ if (sizes == null) { // Not supported
+ mAspectRatio = chooseAspectRatio();
+ } else { // The specified AspectRatio is supported
+ Size size = chooseOptimalSize(sizes);
+ mCameraParameters.setPreviewSize(size.getWidth(), size.getHeight());
+ mCamera.setParameters(mCameraParameters);
+ }
+ }
mCallback.onCameraOpened();
}
+ private AspectRatio chooseAspectRatio() {
+ AspectRatio r = null;
+ for (AspectRatio ratio : mPreviewSizes.ratios()) {
+ r = ratio;
+ if (ratio.equals(DEFAULT_ASPECT_RATIO)) {
+ return ratio;
+ }
+ }
+ return r;
+ }
+
+ private Size chooseOptimalSize(List sizes) {
+ return sizes.get(0); // TODO: Pick optimally
+ }
+
private void releaseCamera() {
if (mCamera != null) {
mCamera.release();
diff --git a/library/src/main/java/com/google/android/cameraview/CameraView.java b/library/src/main/java/com/google/android/cameraview/CameraView.java
index 439a9af8..d4a5718b 100644
--- a/library/src/main/java/com/google/android/cameraview/CameraView.java
+++ b/library/src/main/java/com/google/android/cameraview/CameraView.java
@@ -18,8 +18,10 @@
import android.app.Activity;
import android.content.Context;
+import android.content.res.TypedArray;
import android.os.Build;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.TextureView;
import android.widget.FrameLayout;
@@ -28,9 +30,13 @@
public class CameraView extends FrameLayout {
+ private static final String TAG = "CameraView";
+
private final CameraViewImpl mImpl;
- private final InternalCallbacks mCallbacks;
+ private final CallbackBridge mCallbacks;
+
+ private boolean mAdjustViewBounds;
public CameraView(Context context) {
this(context, null);
@@ -42,15 +48,58 @@ public CameraView(Context context, AttributeSet attrs) {
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- mCallbacks = new InternalCallbacks();
+ // Internal setup
+ mCallbacks = new CallbackBridge();
if (Build.VERSION.SDK_INT < 21) {
mImpl = new Camera1(context, mCallbacks);
} else {
mImpl = new Camera1(context, mCallbacks); // TODO: Implement Camera2 and replace this
}
+ // View content
inflate(context, R.layout.camera_view, this);
TextureView textureView = (TextureView) findViewById(R.id.texture_view);
textureView.setSurfaceTextureListener(mImpl.getSurfaceTextureListener());
+ // Attributes
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView, defStyleAttr,
+ R.style.Widget_CameraView);
+ mAdjustViewBounds = a.getBoolean(R.styleable.CameraView_android_adjustViewBounds, false);
+ a.recycle();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mAdjustViewBounds) {
+ if (!isCameraOpened()) {
+ mCallbacks.reserveRequestLayoutOnOpen();
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ return;
+ }
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
+ final AspectRatio ratio = getAspectRatio();
+ assert ratio != null;
+ int height = (int) (MeasureSpec.getSize(widthMeasureSpec) * ratio.toFloat());
+ if (heightMode == MeasureSpec.AT_MOST) {
+ height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
+ }
+ super.onMeasure(widthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ } else if (widthMode != MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
+ final AspectRatio ratio = getAspectRatio();
+ assert ratio != null;
+ int width = (int) (MeasureSpec.getSize(heightMeasureSpec) * ratio.toFloat());
+ if (widthMode == MeasureSpec.AT_MOST) {
+ width = Math.min(width, MeasureSpec.getSize(widthMeasureSpec));
+ }
+ super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ heightMeasureSpec);
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
}
/**
@@ -79,22 +128,79 @@ public SizeMap getSupportedPreviewSizes() {
return mImpl.getSupportedPreviewSizes();
}
+ /**
+ * @return {@code true} if the camera is opened.
+ */
public boolean isCameraOpened() {
return mImpl.isCameraOpened();
}
+ /**
+ * Add a new callback.
+ *
+ * @param callback The {@link Callback} to add.
+ * @see #removeCallback(Callback)
+ */
public void addCallback(@NonNull Callback callback) {
mCallbacks.add(callback);
}
+ /**
+ * Remove a callback.
+ *
+ * @param callback The {@link Callback} to remove.
+ * @see #addCallback(Callback)
+ */
public void removeCallback(@NonNull Callback callback) {
mCallbacks.remove(callback);
}
- private class InternalCallbacks implements InternalCameraViewCallback {
+ /**
+ * @param adjustViewBounds {@code true} if you want the CameraView to adjust its bounds to
+ * preserve the aspect ratio of camera.
+ * @see #getAdjustViewBounds()
+ */
+ public void setAdjustViewBounds(boolean adjustViewBounds) {
+ if (mAdjustViewBounds != adjustViewBounds) {
+ mAdjustViewBounds = adjustViewBounds;
+ requestLayout();
+ }
+ }
+
+ /**
+ * @return True when this CameraView is adjusting its bounds to preserve the aspect ratio of
+ * camera.
+ * @see #setAdjustViewBounds(boolean)
+ */
+ public boolean getAdjustViewBounds() {
+ return mAdjustViewBounds;
+ }
+
+ /**
+ * Sets the aspect ratio of camera.
+ *
+ * @param ratio The {@AspectRatio} to be set.
+ */
+ public void setAspectRatio(@NonNull AspectRatio ratio) {
+ mImpl.setAspectRatio(ratio);
+ }
+
+ /**
+ * Gets the current aspect ratio of camera.
+ *
+ * @return The current {@link AspectRatio}. Can be {@code null} if no camera is opened yet.
+ */
+ @Nullable
+ public AspectRatio getAspectRatio() {
+ return mImpl.getAspectRatio();
+ }
+
+ private class CallbackBridge implements CameraViewImpl.Callback {
private final ArrayList mCallbacks = new ArrayList<>();
+ private boolean mRequestLayoutOnOpen;
+
public void add(Callback callback) {
mCallbacks.add(callback);
}
@@ -105,6 +211,10 @@ public void remove(Callback callback) {
@Override
public void onCameraOpened() {
+ if (mRequestLayoutOnOpen) {
+ mRequestLayoutOnOpen = false;
+ requestLayout();
+ }
for (Callback callback : mCallbacks) {
callback.onCameraOpened(CameraView.this);
}
@@ -116,14 +226,31 @@ public void onCameraClosed() {
callback.onCameraClosed(CameraView.this);
}
}
+
+ public void reserveRequestLayoutOnOpen() {
+ mRequestLayoutOnOpen = true;
+ }
}
+ /**
+ * Callback for monitoring events about {@link CameraView}.
+ */
@SuppressWarnings("UnusedParameters")
public abstract static class Callback {
+ /**
+ * Called when camera is opened.
+ *
+ * @param cameraView The associated {@link CameraView}.
+ */
public void onCameraOpened(CameraView cameraView) {
}
+ /**
+ * Called when camera is closed.
+ *
+ * @param cameraView The associated {@link CameraView}.
+ */
public void onCameraClosed(CameraView cameraView) {
}
}
diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..e2ad2a02
--- /dev/null
+++ b/library/src/main/res/values/attrs.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/library/src/main/res/values/styles.xml b/library/src/main/res/values/styles.xml
new file mode 100644
index 00000000..7ffe735b
--- /dev/null
+++ b/library/src/main/res/values/styles.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+