diff --git a/library/src/main/api14/com/google/android/cameraview/Camera1.java b/library/src/main/api14/com/google/android/cameraview/Camera1.java index 1830c185..dbed6ef0 100644 --- a/library/src/main/api14/com/google/android/cameraview/Camera1.java +++ b/library/src/main/api14/com/google/android/cameraview/Camera1.java @@ -17,13 +17,17 @@ package com.google.android.cameraview; import android.annotation.SuppressLint; +import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.SurfaceTexture; import android.hardware.Camera; import android.os.Build; +import android.os.Handler; import android.support.v4.util.SparseArrayCompat; import android.view.SurfaceHolder; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.SortedSet; @@ -49,6 +53,8 @@ class Camera1 extends CameraViewImpl { private final AtomicBoolean isPictureCaptureInProgress = new AtomicBoolean(false); + private final AtomicBoolean isAutoFocusInProgress = new AtomicBoolean(false); + Camera mCamera; private Camera.Parameters mCameraParameters; @@ -71,11 +77,30 @@ class Camera1 extends CameraViewImpl { private int mDisplayOrientation; + private CameraCoordinateTransformer mCoordinateTransformer; + + private Rect mPreviewRect = new Rect(0, 0, 0, 0); + + private final Handler mCameraHandler; + + private final Runnable mReturnToContinuousAFRunnable = new Runnable() { + @Override + public void run() { + if (setAutoFocusInternal(mAutoFocus)) { + mCamera.setParameters(mCameraParameters); + mCamera.cancelAutoFocus(); + } + } + }; + Camera1(Callback callback, PreviewImpl preview) { super(callback, preview); + mCameraHandler = new Handler(); preview.setCallback(new PreviewImpl.Callback() { @Override public void onSurfaceChanged() { + mPreviewRect.set(0, 0, mPreview.getWidth(), mPreview.getHeight()); + resetCoordinateTransformer(); if (mCamera != null) { setUpPreview(); adjustCameraParameters(); @@ -103,6 +128,7 @@ void stop() { } mShowingPreview = false; releaseCamera(); + mCameraHandler.removeCallbacks(mReturnToContinuousAFRunnable); } // Suppresses Camera#setPreviewTexture @@ -141,6 +167,7 @@ void setFacing(int facing) { stop(); start(); } + resetCoordinateTransformer(); } @Override @@ -223,11 +250,12 @@ void takePicture() { throw new IllegalStateException( "Camera is not ready. Call start() before takePicture()."); } - if (getAutoFocus()) { + if (getAutoFocus() || isAutoFocusInProgress.get()) { mCamera.cancelAutoFocus(); mCamera.autoFocus(new Camera.AutoFocusCallback() { @Override public void onAutoFocus(boolean success, Camera camera) { + isAutoFocusInProgress.set(false); takePictureInternal(); } }); @@ -238,7 +266,14 @@ public void onAutoFocus(boolean success, Camera camera) { void takePictureInternal() { if (!isPictureCaptureInProgress.getAndSet(true)) { - mCamera.takePicture(null, null, null, new Camera.PictureCallback() { + mCamera.takePicture(new Camera.ShutterCallback() { + @Override + public void onShutter() { + if (setAutoFocusInternal(mAutoFocus)) { + mCamera.setParameters(mCameraParameters); + } + } + }, null, null, new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { isPictureCaptureInProgress.set(false); @@ -247,6 +282,7 @@ public void onPictureTaken(byte[] data, Camera camera) { camera.startPreview(); } }); + mCameraHandler.removeCallbacks(mReturnToContinuousAFRunnable); } } @@ -256,6 +292,7 @@ void setDisplayOrientation(int displayOrientation) { return; } mDisplayOrientation = displayOrientation; + resetCoordinateTransformer(); if (isCameraOpened()) { int cameraRotation = calcCameraRotation(displayOrientation); mCameraParameters.setRotation(cameraRotation); @@ -271,6 +308,49 @@ void setDisplayOrientation(int displayOrientation) { } } + @Override + boolean hasManualFocus() { + return isCameraOpened() && getFacing() == Constants.FACING_BACK + && (isFocusAreaSupported() || isMeteringAreaSupported()); + } + + @Override + void setFocusAt(int x, int y) { + if (isPictureCaptureInProgress.get()) { + return; + } + mCallback.onFocusAt(x, y); + if (isAutoFocusInProgress.getAndSet(false)) { + mCamera.cancelAutoFocus(); + } + if (!isAutoFocusInProgress.getAndSet(true) && setFocusAndMeterInternal(x, y)) { + mCamera.setParameters(mCameraParameters); + mCamera.autoFocus(new Camera.AutoFocusCallback() { + @Override + public void onAutoFocus(boolean success, Camera camera) { + isAutoFocusInProgress.set(false); + resumeContinuousAFAfterDelay(Constants.FOCUS_HOLD_MILLIS); + } + }); + } + } + + boolean isFocusAreaSupported() { + if (Build.VERSION.SDK_INT >= 14) { + List supportedFocusModes = mCameraParameters.getSupportedFocusModes(); + return (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO) + && mCameraParameters.getMaxNumFocusAreas() > 0); + } + return false; + } + + boolean isMeteringAreaSupported() { + if (Build.VERSION.SDK_INT >= 14) { + return mCameraParameters.getMaxNumMeteringAreas() > 0; + } + return false; + } + /** * This rewrites {@link #mCameraId} and {@link #mCameraInfo}. */ @@ -406,6 +486,10 @@ private boolean setAutoFocusInternal(boolean autoFocus) { } else { mCameraParameters.setFocusMode(modes.get(0)); } + if (Build.VERSION.SDK_INT >= 14) { + mCameraParameters.setFocusAreas(null); + mCameraParameters.setMeteringAreas(null); + } return true; } else { return false; @@ -437,4 +521,69 @@ private boolean setFlashInternal(int flash) { } } + /** + * @return {@code true} if {@link #mCameraParameters} was modified. + */ + private boolean setFocusAndMeterInternal(int x, int y) { + if (Build.VERSION.SDK_INT >= 14 && hasManualFocus() && mCoordinateTransformer != null) { + if (isFocusAreaSupported()) { + List focusArea = Collections.singletonList( + new Camera.Area(computeCameraRectFromPreviewCoordinates(x, y, + getAFRegionSizePx()), 1)); + mCameraParameters.setFocusAreas(focusArea); + } + if (isMeteringAreaSupported()) { + List meteringArea = Collections.singletonList( + new Camera.Area(computeCameraRectFromPreviewCoordinates(x, y, + getAERegionSizePx()), 1)); + mCameraParameters.setMeteringAreas(meteringArea); + } + mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); + return true; + } else { + return false; + } + } + + /** + * @return {@code width} of auto focus region in pixels. + */ + private int getAFRegionSizePx() { + return (int) (Math.min(mPreview.getWidth(), mPreview.getHeight()) + * Constants.AF_REGION_BOX); + } + + /** + * @return {@code width} of metering region in pixels. + */ + private int getAERegionSizePx() { + return (int) (Math.min(mPreview.getWidth(), mPreview.getHeight()) + * Constants.AE_REGION_BOX); + } + + private Rect computeCameraRectFromPreviewCoordinates(int x, int y, int size) { + int left = CameraUtil.clamp(x - size / 2, mPreviewRect.left, + mPreviewRect.right - size); + int top = CameraUtil.clamp(y - size / 2, mPreviewRect.top, + mPreviewRect.bottom - size); + RectF rectF = new RectF(left, top, left + size, top + size); + return CameraUtil.rectFToRect(mCoordinateTransformer.toCameraSpace(rectF)); + } + + private void resetCoordinateTransformer() { + if (mPreview.getWidth() > 0 && mPreview.getHeight() > 0) { + mCoordinateTransformer = new CameraCoordinateTransformer( + mFacing == Constants.FACING_FRONT, + calcCameraRotation(mDisplayOrientation), + CameraUtil.rectToRectF(mPreviewRect)); + } + } + + /** + * Resume AF_MODE_CONTINUOUS_PICTURE after FOCUS_HOLD_MILLIS. + */ + private void resumeContinuousAFAfterDelay(int millis) { + mCameraHandler.removeCallbacks(mReturnToContinuousAFRunnable); + mCameraHandler.postDelayed(mReturnToContinuousAFRunnable, millis); + } } diff --git a/library/src/main/api14/com/google/android/cameraview/CameraCoordinateTransformer.java b/library/src/main/api14/com/google/android/cameraview/CameraCoordinateTransformer.java new file mode 100644 index 00000000..e3aac596 --- /dev/null +++ b/library/src/main/api14/com/google/android/cameraview/CameraCoordinateTransformer.java @@ -0,0 +1,103 @@ +/* + * 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; + +import android.graphics.Matrix; +import android.graphics.RectF; + +/** + * Transform coordinates to and from preview coordinate space and camera driver + * coordinate space. + */ +public class CameraCoordinateTransformer { + + // http://developer.android.com/guide/topics/media/camera.html#metering-focus-areas + private static final RectF CAMERA_DRIVER_RECT = new RectF(-1000, -1000, 1000, 1000); + private final Matrix mCameraToPreviewTransform; + private final Matrix mPreviewToCameraTransform; + + /** + * Convert rectangles to / from camera coordinate and preview coordinate space. + * + * @param mirrorX if the preview is mirrored along the X axis. + * @param displayOrientation orientation in degrees. + * @param previewRect the preview rectangle size and position. + */ + public CameraCoordinateTransformer(boolean mirrorX, int displayOrientation, + RectF previewRect) { + if (!hasNonZeroArea(previewRect)) { + throw new IllegalArgumentException("previewRect"); + } + mCameraToPreviewTransform = cameraToPreviewTransform(mirrorX, displayOrientation, + previewRect); + mPreviewToCameraTransform = inverse(mCameraToPreviewTransform); + } + + /** + * Transform a rectangle in camera space into a new rectangle in preview + * view space. + * + * @param source the rectangle in camera space + * @return the rectangle in preview view space. + */ + public RectF toPreviewSpace(RectF source) { + RectF result = new RectF(); + mCameraToPreviewTransform.mapRect(result, source); + return result; + } + + /** + * Transform a rectangle in preview view space into a new rectangle in + * camera view space. + * + * @param source the rectangle in preview view space + * @return the rectangle in camera view space. + */ + public RectF toCameraSpace(RectF source) { + RectF result = new RectF(); + mPreviewToCameraTransform.mapRect(result, source); + return result; + } + + private Matrix cameraToPreviewTransform(boolean mirrorX, int displayOrientation, + RectF previewRect) { + Matrix transform = new Matrix(); + // Need mirror for front camera. + transform.setScale(mirrorX ? -1 : 1, 1); + // Apply a rotate transform. + // This is the value for android.hardware.Camera.setDisplayOrientation. + transform.postRotate(displayOrientation); + // Map camera driver coordinates to preview rect coordinates + Matrix fill = new Matrix(); + fill.setRectToRect(CAMERA_DRIVER_RECT, + previewRect, + Matrix.ScaleToFit.FILL); + // Concat the previous transform on top of the fill behavior. + transform.setConcat(fill, transform); + return transform; + } + + private Matrix inverse(Matrix source) { + Matrix newMatrix = new Matrix(); + source.invert(newMatrix); + return newMatrix; + } + + private boolean hasNonZeroArea(RectF rect) { + return rect.width() != 0 && rect.height() != 0; + } +} \ No newline at end of file diff --git a/library/src/main/api21/com/google/android/cameraview/AutoFocusHelper.java b/library/src/main/api21/com/google/android/cameraview/AutoFocusHelper.java new file mode 100644 index 00000000..31169d78 --- /dev/null +++ b/library/src/main/api21/com/google/android/cameraview/AutoFocusHelper.java @@ -0,0 +1,135 @@ +/* + * 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; + +import android.annotation.TargetApi; +import android.graphics.PointF; +import android.graphics.Rect; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.params.MeteringRectangle; + +@TargetApi(21) +public class AutoFocusHelper { + + /** camera2 API metering region weight. */ + private static final int CAMERA2_REGION_WEIGHT = (int) + (CameraUtil.lerp(MeteringRectangle.METERING_WEIGHT_MIN, + MeteringRectangle.METERING_WEIGHT_MAX, + Constants.METERING_REGION_FRACTION)); + + /** Zero weight 3A region, to reset regions per API. */ + private static final MeteringRectangle[] ZERO_WEIGHT_3A_REGION = new MeteringRectangle[]{ + new MeteringRectangle(0, 0, 0, 0, 0) + }; + + public static MeteringRectangle[] getZeroWeightRegion() { + return ZERO_WEIGHT_3A_REGION; + } + + /** + * Compute 3A regions for a sensor-referenced touch coordinate. + * Returns a MeteringRectangle[] with length 1. + * + * @param nx x coordinate of the touch point, in normalized portrait + * coordinates. + * @param ny y coordinate of the touch point, in normalized portrait + * coordinates. + * @param fraction Fraction in [0,1]. Multiplied by min(cropRegion.width(), + * cropRegion.height()) + * to determine the side length of the square MeteringRectangle. + * @param cropRegion Crop region of the image. + * @param sensorOrientation sensor orientation as defined by + * CameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION). + */ + private static MeteringRectangle[] regionsForNormalizedCoord(float nx, float ny, + float fraction, final Rect cropRegion, int sensorOrientation) { + // Compute half side length in pixels. + int minCropEdge = Math.min(cropRegion.width(), cropRegion.height()); + int halfSideLength = (int) (0.5f * fraction * minCropEdge); + // Compute the output MeteringRectangle in sensor space. + // nx, ny is normalized to the screen. + // Crop region itself is specified in sensor coordinates. + // Normalized coordinates, now rotated into sensor space. + PointF nsc = CameraUtil.normalizedSensorCoordsForNormalizedDisplayCoords( + nx, ny, sensorOrientation); + int xCenterSensor = (int) (cropRegion.left + nsc.x * cropRegion.width()); + int yCenterSensor = (int) (cropRegion.top + nsc.y * cropRegion.height()); + Rect meteringRegion = new Rect(xCenterSensor - halfSideLength, + yCenterSensor - halfSideLength, + xCenterSensor + halfSideLength, + yCenterSensor + halfSideLength); + // Clamp meteringRegion to cropRegion. + meteringRegion.left = CameraUtil.clamp(meteringRegion.left, cropRegion.left, + cropRegion.right); + meteringRegion.top = CameraUtil.clamp(meteringRegion.top, cropRegion.top, + cropRegion.bottom); + meteringRegion.right = CameraUtil.clamp(meteringRegion.right, cropRegion.left, + cropRegion.right); + meteringRegion.bottom = CameraUtil.clamp(meteringRegion.bottom, cropRegion.top, + cropRegion.bottom); + return new MeteringRectangle[]{new MeteringRectangle(meteringRegion, + CAMERA2_REGION_WEIGHT)}; + } + + /** + * Return AF region(s) for a sensor-referenced touch coordinate. + * + *

+ * Normalized coordinates are referenced to portrait preview window with + * (0, 0) top left and (1, 1) bottom right. Rotation has no effect. + *

+ * + * @return AF region(s). + */ + public static MeteringRectangle[] afRegionsForNormalizedCoord(float nx, + float ny, final Rect cropRegion, int sensorOrientation) { + return regionsForNormalizedCoord(nx, ny, Constants.METERING_REGION_FRACTION, + cropRegion, sensorOrientation); + } + + /** + * Return AE region(s) for a sensor-referenced touch coordinate. + * + *

+ * Normalized coordinates are referenced to portrait preview window with + * (0, 0) top left and (1, 1) bottom right. Rotation has no effect. + *

+ * + * @return AE region(s). + */ + public static MeteringRectangle[] aeRegionsForNormalizedCoord(float nx, + float ny, final Rect cropRegion, int sensorOrientation) { + return regionsForNormalizedCoord(nx, ny, Constants.METERING_REGION_FRACTION, + cropRegion, sensorOrientation); + } + + + /** + * Calculates sensor crop region for a zoom level (zoom >= 1.0). + * + * @return Crop region. + */ + public static Rect cropRegionForZoom(CameraCharacteristics characteristics, float zoom) { + Rect sensor = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + int xCenter = sensor.width() / 2; + int yCenter = sensor.height() / 2; + int xDelta = (int) (0.5f * sensor.width() / zoom); + int yDelta = (int) (0.5f * sensor.height() / zoom); + return new Rect(xCenter - xDelta, yCenter - yDelta, xCenter + xDelta, yCenter + yDelta); + } + +} diff --git a/library/src/main/api21/com/google/android/cameraview/Camera2.java b/library/src/main/api21/com/google/android/cameraview/Camera2.java index 86613338..adb0f749 100644 --- a/library/src/main/api21/com/google/android/cameraview/Camera2.java +++ b/library/src/main/api21/com/google/android/cameraview/Camera2.java @@ -19,6 +19,8 @@ import android.annotation.TargetApi; import android.content.Context; import android.graphics.ImageFormat; +import android.graphics.Matrix; +import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; @@ -27,9 +29,11 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.MeteringRectangle; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.Image; import android.media.ImageReader; +import android.os.Handler; import android.support.annotation.NonNull; import android.util.Log; import android.util.SparseIntArray; @@ -196,8 +200,35 @@ public void onImageAvailable(ImageReader reader) { private int mDisplayOrientation; + private Rect mCropRegion; + + private MeteringRectangle[] mAFRegions = AutoFocusHelper.getZeroWeightRegion(); + + private MeteringRectangle[] mAERegions = AutoFocusHelper.getZeroWeightRegion(); + + private final Handler mCameraHandler; + + /** Runnable that returns to CONTROL_AF_MODE = AF_CONTINUOUS_PICTURE. */ + private final Runnable mReturnToContinuousAFRunnable = new Runnable() { + @Override + public void run() { + if (mPreviewRequestBuilder != null) { + updateAutoFocus(); + if (mCaptureSession != null) { + try { + mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), + mCaptureCallback, null); + } catch (CameraAccessException e) { + mAutoFocus = !mAutoFocus; // Revert + } + } + } + } + }; + Camera2(Callback callback, PreviewImpl preview, Context context) { super(callback, preview); + mCameraHandler = new Handler(); mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); mPreview.setCallback(new PreviewImpl.Callback() { @Override @@ -212,6 +243,7 @@ boolean start() { if (!chooseCameraIdByFacing()) { return false; } + mCropRegion = AutoFocusHelper.cropRegionForZoom(mCameraCharacteristics, 1); collectCameraInfo(); prepareImageReader(); startOpeningCamera(); @@ -232,6 +264,7 @@ void stop() { mImageReader.close(); mImageReader = null; } + mCameraHandler.removeCallbacks(mReturnToContinuousAFRunnable); } @Override @@ -346,6 +379,62 @@ void setDisplayOrientation(int displayOrientation) { mPreview.setDisplayOrientation(mDisplayOrientation); } + @Override + boolean hasManualFocus() { + return isCameraOpened() && (isAutoFocusSupported() || isAutoExposureSupported()); + } + + @Override + void setFocusAt(int x, int y) { + mCallback.onFocusAt(x, y); + float points[] = new float[2]; + points[0] = (float) x / mPreview.getWidth(); + points[1] = (float) y / mPreview.getHeight(); + Matrix rotationMatrix = new Matrix(); + rotationMatrix.setRotate(mDisplayOrientation, 0.5f, 0.5f); + rotationMatrix.mapPoints(points); + if (mFacing == Constants.FACING_FRONT) { + points[0] = 1 - points[0]; + } + if (mPreviewRequestBuilder != null) { + updateManualFocus(points[0], points[1]); + if (mCaptureSession != null) { + try { + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, + CaptureRequest.CONTROL_AF_TRIGGER_START); + mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, null); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, + CaptureRequest.CONTROL_AF_TRIGGER_IDLE); + mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), + mCaptureCallback, null); + } catch (CameraAccessException e) { + Log.e(TAG, "Failed to set manual focus.", e); + } + } + resumeContinuousAFAfterDelay(Constants.FOCUS_HOLD_MILLIS); + } + } + + boolean isAutoFocusSupported() { + if (!isCameraOpened()) { + return false; + } + Integer maxAfRegions = mCameraCharacteristics.get( + CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + // Auto-Focus is supported if the device supports one or more AF regions + return maxAfRegions != null && maxAfRegions > 0; + } + + boolean isAutoExposureSupported() { + if (!isCameraOpened()) { + return false; + } + Integer maxAeRegions = mCameraCharacteristics.get( + CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + // Auto-Exposure is supported if the device supports one or more AE regions + return maxAeRegions != null && maxAeRegions > 0; + } + /** *

Chooses a camera ID by the specified camera facing ({@link #mFacing}).

*

This rewrites {@link #mCameraId}, {@link #mCameraCharacteristics}, and optionally @@ -532,6 +621,27 @@ void updateAutoFocus() { mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); } + mAFRegions = AutoFocusHelper.getZeroWeightRegion(); + mAERegions = AutoFocusHelper.getZeroWeightRegion(); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, mAFRegions); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, mAERegions); + } + + /** + * Updates the internal state of manual focus. + */ + void updateManualFocus(float x, float y) { + @SuppressWarnings("ConstantConditions") + int sensorOrientation = mCameraCharacteristics.get( + CameraCharacteristics.SENSOR_ORIENTATION); + mAFRegions = AutoFocusHelper.afRegionsForNormalizedCoord(x, y, mCropRegion, + sensorOrientation); + mAERegions = AutoFocusHelper.aeRegionsForNormalizedCoord(x, y, mCropRegion, + sensorOrientation); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, mAFRegions); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, mAERegions); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_AUTO); } /** @@ -641,6 +751,7 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session, unlockFocus(); } }, null); + mCameraHandler.removeCallbacks(mReturnToContinuousAFRunnable); } catch (CameraAccessException e) { Log.e(TAG, "Cannot capture a still picture.", e); } @@ -667,6 +778,14 @@ void unlockFocus() { } } + /** + * Resume AF_MODE_CONTINUOUS_PICTURE after FOCUS_HOLD_MILLIS. + */ + private void resumeContinuousAFAfterDelay(int millis) { + mCameraHandler.removeCallbacks(mReturnToContinuousAFRunnable); + mCameraHandler.postDelayed(mReturnToContinuousAFRunnable, millis); + } + /** * A {@link CameraCaptureSession.CaptureCallback} for capturing a still picture. */ diff --git a/library/src/main/base/com/google/android/cameraview/CameraUtil.java b/library/src/main/base/com/google/android/cameraview/CameraUtil.java new file mode 100644 index 00000000..43c44439 --- /dev/null +++ b/library/src/main/base/com/google/android/cameraview/CameraUtil.java @@ -0,0 +1,102 @@ +/* + * 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; + +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; + +public class CameraUtil { + + /** + * Clamps x to between min and max (inclusive on both ends, x = min --> min, + * x = max --> max). + */ + public static int clamp(int x, int min, int max) { + if (x > max) { + return max; + } + if (x < min) { + return min; + } + return x; + } + + /** + * Clamps x to between min and max (inclusive on both ends, x = min --> min, + * x = max --> max). + */ + public static float clamp(float x, float min, float max) { + if (x > max) { + return max; + } + if (x < min) { + return min; + } + return x; + } + + public static void inlineRectToRectF(RectF rectF, Rect rect) { + rect.left = Math.round(rectF.left); + rect.top = Math.round(rectF.top); + rect.right = Math.round(rectF.right); + rect.bottom = Math.round(rectF.bottom); + } + + public static Rect rectFToRect(RectF rectF) { + Rect rect = new Rect(); + inlineRectToRectF(rectF, rect); + return rect; + } + + public static RectF rectToRectF(Rect r) { + return new RectF(r.left, r.top, r.right, r.bottom); + } + + /** + * Linear interpolation between a and b by the fraction t. t = 0 --> a, t = + * 1 --> b. + */ + public static float lerp(float a, float b, float t) { + return a + t * (b - a); + } + + /** + * Given (nx, ny) \in [0, 1]^2, in the display's portrait coordinate system, + * returns normalized sensor coordinates \in [0, 1]^2 depending on how the + * sensor's orientation \in {0, 90, 180, 270}. + *

+ * Returns null if sensorOrientation is not one of the above. + *

+ */ + public static PointF normalizedSensorCoordsForNormalizedDisplayCoords( + float nx, float ny, int sensorOrientation) { + switch (sensorOrientation) { + case 0: + return new PointF(nx, ny); + case 90: + return new PointF(ny, 1.0f - nx); + case 180: + return new PointF(1.0f - nx, 1.0f - ny); + case 270: + return new PointF(1.0f - ny, nx); + default: + return null; + } + } + +} 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 31dab0c4..01bf4c93 100644 --- a/library/src/main/base/com/google/android/cameraview/CameraViewImpl.java +++ b/library/src/main/base/com/google/android/cameraview/CameraViewImpl.java @@ -69,6 +69,10 @@ View getView() { abstract void setDisplayOrientation(int displayOrientation); + abstract boolean hasManualFocus(); + + abstract void setFocusAt(int x, int y); + interface Callback { void onCameraOpened(); @@ -77,6 +81,8 @@ interface Callback { void onPictureTaken(byte[] data); + void onFocusAt(int x, int y); + } } diff --git a/library/src/main/base/com/google/android/cameraview/Constants.java b/library/src/main/base/com/google/android/cameraview/Constants.java index 223afeee..08b89484 100644 --- a/library/src/main/base/com/google/android/cameraview/Constants.java +++ b/library/src/main/base/com/google/android/cameraview/Constants.java @@ -30,4 +30,11 @@ interface Constants { int FLASH_AUTO = 3; int FLASH_RED_EYE = 4; + int FOCUS_HOLD_MILLIS = 3000; + + float AF_REGION_BOX = 0.2f; + float AE_REGION_BOX = 0.3f; + + float METERING_REGION_FRACTION = 0.1225f; + } 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 04826d84..d9a10428 100644 --- a/library/src/main/java/com/google/android/cameraview/CameraView.java +++ b/library/src/main/java/com/google/android/cameraview/CameraView.java @@ -29,6 +29,9 @@ import android.support.v4.os.ParcelableCompatCreatorCallbacks; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; import android.widget.FrameLayout; import java.lang.annotation.Retention; @@ -72,6 +75,8 @@ public class CameraView extends FrameLayout { CameraViewImpl mImpl; + PreviewOverlay mOverlay; + private final CallbackBridge mCallbacks; private boolean mAdjustViewBounds; @@ -89,7 +94,7 @@ public CameraView(Context context, AttributeSet attrs) { @SuppressWarnings("WrongConstant") public CameraView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - if (isInEditMode()){ + if (isInEditMode()) { mCallbacks = null; mDisplayOrientationDetector = null; return; @@ -104,6 +109,7 @@ public CameraView(Context context, AttributeSet attrs, int defStyleAttr) { } else { mImpl = new Camera2Api23(mCallbacks, preview, context); } + mOverlay = createPreviewOverlay(context); // Attributes TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView, defStyleAttr, R.style.Widget_CameraView); @@ -138,6 +144,11 @@ private PreviewImpl createPreviewImpl(Context context) { return preview; } + private PreviewOverlay createPreviewOverlay(Context context) { + final View view = View.inflate(context, R.layout.preview_overlay, this); + return (PreviewOverlay) view.findViewById(R.id.preview_overlay); + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -156,7 +167,7 @@ protected void onDetachedFromWindow() { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (isInEditMode()){ + if (isInEditMode()) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } @@ -245,7 +256,7 @@ protected void onRestoreInstanceState(Parcelable state) { public void start() { if (!mImpl.start()) { //store the state ,and restore this state after fall back o Camera1 - Parcelable state=onSaveInstanceState(); + Parcelable state = onSaveInstanceState(); // Camera2 uses legacy hardware layer; fall back to Camera1 mImpl = new Camera1(mCallbacks, createPreviewImpl(getContext())); onRestoreInstanceState(state); @@ -379,6 +390,32 @@ public boolean getAutoFocus() { return mImpl.getAutoFocus(); } + /** + * Enables or disables the manual focus mode. When the current camera doesn't support + * manual focus, calling this method will have no effect. + * + * @param manualFocus {@code true} to enable manual focus mode. {@code false} to + * disable it. + */ + public void setManualFocus(final boolean manualFocus) { + if (manualFocus) { + if (!mOverlay.hasGestureDetector()) { + mOverlay.setGestureListener(new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (!mImpl.hasManualFocus()) { + return false; + } + mImpl.setFocusAt((int) e.getX(), (int) e.getY()); + return true; + } + }); + } + } else { + mOverlay.setGestureListener(null); + } + } + /** * Sets the flash mode. * @@ -449,6 +486,13 @@ public void onPictureTaken(byte[] data) { } } + @Override + public void onFocusAt(int x, int y) { + for (Callback callback : mCallbacks) { + callback.onFocusAt(x, y); + } + } + public void reserveRequestLayoutOnOpen() { mRequestLayoutOnOpen = true; } @@ -535,6 +579,15 @@ public void onCameraClosed(CameraView cameraView) { */ public void onPictureTaken(CameraView cameraView, byte[] data) { } + + /** + * Called when focus happens. + * + * @param x View's x coordinate. + * @param y View's y coordinate. + */ + public void onFocusAt(int x, int y) { + } } } diff --git a/library/src/main/java/com/google/android/cameraview/PreviewOverlay.java b/library/src/main/java/com/google/android/cameraview/PreviewOverlay.java new file mode 100644 index 00000000..7c44fd96 --- /dev/null +++ b/library/src/main/java/com/google/android/cameraview/PreviewOverlay.java @@ -0,0 +1,52 @@ +/* + * 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; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; + +public class PreviewOverlay extends View { + + private GestureDetector mGestureDetector = null; + + public PreviewOverlay(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onTouchEvent(MotionEvent m) { + if (mGestureDetector != null) { + mGestureDetector.onTouchEvent(m); + } + return true; + } + + public void setGestureListener(GestureDetector.OnGestureListener gestureListener) { + if (gestureListener != null) { + mGestureDetector = new GestureDetector(getContext(), gestureListener); + } else { + mGestureDetector = null; + } + } + + public boolean hasGestureDetector() { + return mGestureDetector != null; + } +} diff --git a/library/src/main/res/layout/preview_overlay.xml b/library/src/main/res/layout/preview_overlay.xml new file mode 100644 index 00000000..860327db --- /dev/null +++ b/library/src/main/res/layout/preview_overlay.xml @@ -0,0 +1,23 @@ + + + + + + +