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 @@ + + + + + + +