summaryrefslogtreecommitdiffstats
path: root/src/com/android/camera/ui/focus
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/camera/ui/focus')
-rw-r--r--src/com/android/camera/ui/focus/AutoFocusRing.java101
-rw-r--r--src/com/android/camera/ui/focus/CameraCoordinateTransformer.java109
-rw-r--r--src/com/android/camera/ui/focus/FocusController.java128
-rw-r--r--src/com/android/camera/ui/focus/FocusRing.java72
-rw-r--r--src/com/android/camera/ui/focus/FocusRingRenderer.java237
-rw-r--r--src/com/android/camera/ui/focus/FocusRingView.java211
-rw-r--r--src/com/android/camera/ui/focus/FocusSound.java47
-rw-r--r--src/com/android/camera/ui/focus/LensRangeCalculator.java71
-rw-r--r--src/com/android/camera/ui/focus/ManualFocusRing.java93
9 files changed, 1069 insertions, 0 deletions
diff --git a/src/com/android/camera/ui/focus/AutoFocusRing.java b/src/com/android/camera/ui/focus/AutoFocusRing.java
new file mode 100644
index 000000000..ff09ebc65
--- /dev/null
+++ b/src/com/android/camera/ui/focus/AutoFocusRing.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2014 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.android.camera.ui.focus;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import com.android.camera.ui.motion.InterpolateUtils;
+import com.android.camera.ui.motion.Invalidator;
+
+/**
+ * Passive focus ring animation renderer.
+ */
+class AutoFocusRing extends FocusRingRenderer {
+ private static final String TAG = "AutoFocusRing";
+
+ /**
+ * The auto focus ring encapsulates the animation logic for visualizing
+ * a focus event when triggered by the camera subsystem.
+ *
+ * @param invalidator the object to invalidate while running.
+ * @param ringPaint the paint to draw the ring with.
+ * @param enterDurationMillis the fade in time in milliseconds.
+ * @param exitDurationMillis the fade out time in milliseconds.
+ */
+ public AutoFocusRing(Invalidator invalidator, Paint ringPaint, float enterDurationMillis,
+ float exitDurationMillis) {
+ super(invalidator, ringPaint, enterDurationMillis, exitDurationMillis);
+ }
+
+ @Override
+ public void draw(long t, long dt, Canvas canvas) {
+ float ringRadius = mRingRadius.update(dt);
+ processStates(t);
+
+ if (!isActive()) {
+ return;
+ }
+
+ mInvalidator.invalidate();
+ int ringAlpha = 255;
+
+ if (mFocusState == FocusState.STATE_ENTER) {
+ float rFade = InterpolateUtils.unitRatio(t, mEnterStartMillis, mEnterDurationMillis);
+ ringAlpha = (int) InterpolateUtils
+ .lerp(0, 255, mEnterOpacityCurve.valueAt(rFade));
+ } else if (mFocusState == FocusState.STATE_FADE_OUT) {
+ float rFade = InterpolateUtils.unitRatio(t, mExitStartMillis, mExitDurationMillis);
+ ringAlpha = (int) InterpolateUtils
+ .lerp(255, 0, mExitOpacityCurve.valueAt(rFade));
+ } else if (mFocusState == FocusState.STATE_HARD_STOP) {
+ float rFade = InterpolateUtils
+ .unitRatio(t, mHardExitStartMillis, mHardExitDurationMillis);
+ ringAlpha = (int) InterpolateUtils
+ .lerp(255, 0, mExitOpacityCurve.valueAt(rFade));
+ } else if (mFocusState == FocusState.STATE_INACTIVE) {
+ ringAlpha = 0;
+ }
+
+ mRingPaint.setAlpha(ringAlpha);
+ canvas.drawCircle(getCenterX(), getCenterY(), ringRadius, mRingPaint);
+ }
+
+ private void processStates(long t) {
+ if (mFocusState == FocusState.STATE_INACTIVE) {
+ return;
+ }
+
+ if (mFocusState == FocusState.STATE_ENTER && t > mEnterStartMillis + mEnterDurationMillis) {
+ mFocusState = FocusState.STATE_ACTIVE;
+ }
+
+ if (mFocusState == FocusState.STATE_ACTIVE && !mRingRadius.isActive()) {
+ mFocusState = FocusState.STATE_FADE_OUT;
+ mExitStartMillis = t;
+ }
+
+ if (mFocusState == FocusState.STATE_FADE_OUT && t > mExitStartMillis + mExitDurationMillis) {
+ mFocusState = FocusState.STATE_INACTIVE;
+ }
+
+ if (mFocusState == FocusState.STATE_HARD_STOP
+ && t > mHardExitStartMillis + mHardExitDurationMillis) {
+ mFocusState = FocusState.STATE_INACTIVE;
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/focus/CameraCoordinateTransformer.java b/src/com/android/camera/ui/focus/CameraCoordinateTransformer.java
new file mode 100644
index 000000000..809503c9a
--- /dev/null
+++ b/src/com/android/camera/ui/focus/CameraCoordinateTransformer.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2014 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.android.camera.ui.focus;
+
+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;
+ }
+}
diff --git a/src/com/android/camera/ui/focus/FocusController.java b/src/com/android/camera/ui/focus/FocusController.java
new file mode 100644
index 000000000..a0c103443
--- /dev/null
+++ b/src/com/android/camera/ui/focus/FocusController.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2014 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.android.camera.ui.focus;
+
+import android.graphics.RectF;
+import android.util.Log;
+
+import com.android.camera.async.MainThread;
+import com.android.camera.ui.motion.LinearScale;
+
+/**
+ * The focus controller interacts with the focus ring UI element.
+ */
+public class FocusController {
+ private static final String TAG = "FocusController";
+
+ private final FocusRing mFocusRing;
+ private final FocusSound mFocusSound;
+ private final MainThread mMainThread;
+
+ public FocusController(FocusRing focusRing, FocusSound focusSound, MainThread mainThread) {
+ mFocusRing = focusRing;
+ mFocusSound = focusSound;
+ mMainThread = mainThread;
+ }
+
+ /**
+ * Show a passive focus animation at the center of the active area.
+ * This will likely be different than the view bounds due to varying image
+ * ratios and dimensions.
+ */
+ public void showPassiveFocusAtCenter() {
+ mMainThread.execute(new Runnable() {
+ @Override
+ public void run() {
+ Log.v(TAG, "Running showPassiveFocusAtCenter()");
+ mFocusRing.startPassiveFocus();
+ mFocusRing.centerFocusLocation();
+ }
+ });
+ }
+
+ /**
+ * Show a passive focus animation at the given viewX and viewY position.
+ * This is usually indicates the camera subsystem kicked off an auto-focus
+ * at the given screen position.
+ *
+ * @param viewX the view's x coordinate
+ * @param viewY the view's y coordinate
+ */
+ public void showPassiveFocusAt(final int viewX, final int viewY) {
+ mMainThread.execute(new Runnable() {
+ @Override
+ public void run() {
+ Log.v(TAG, "Running showPassiveFocusAt(" + viewX + ", " + viewY + ")");
+ mFocusRing.startPassiveFocus();
+ mFocusRing.setFocusLocation(viewX, viewY);
+ }
+ });
+ }
+
+ /**
+ * Show an active focus animation at the given viewX and viewY position.
+ * This is normally initiated by the user touching the screen at a given
+ * point.
+ *
+ * @param viewX the view's x coordinate
+ * @param viewY the view's y coordinate
+ */
+ public void showActiveFocusAt(final int viewX, final int viewY) {
+ mMainThread.execute(new Runnable() {
+ @Override
+ public void run() {
+ Log.v(TAG, "showActiveFocusAt(" + viewX + ", " + viewY + ")");
+ mFocusRing.startActiveFocus();
+ mFocusRing.setFocusLocation(viewX, viewY);
+
+ // TODO: Enable focus sound when better audio controls exist.
+ // mFocusSound.play();
+ }
+ });
+ }
+
+ /**
+ * Computing the correct location for the focus ring requires knowing
+ * the screen position and size of the preview area so the drawing
+ * operations can be clipped correctly.
+ */
+ public void configurePreviewDimensions(final RectF previewArea) {
+ mMainThread.execute(new Runnable() {
+ @Override
+ public void run() {
+ Log.v(TAG, "configurePreviewDimensions(" + previewArea + ")");
+ mFocusRing.configurePreviewDimensions(previewArea);
+ }
+ });
+ }
+
+ /**
+ * Set the radius of the focus ring as a radius between 0 and 1.
+ * This will map to the min and max values computed for the UI.
+ */
+ public void setFocusRatio(final float ratio) {
+ mMainThread.execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mFocusRing.isPassiveFocusRunning() ||
+ mFocusRing.isActiveFocusRunning()) {
+ mFocusRing.setRadiusRatio(ratio);
+ }
+ }
+ });
+ }
+}
diff --git a/src/com/android/camera/ui/focus/FocusRing.java b/src/com/android/camera/ui/focus/FocusRing.java
new file mode 100644
index 000000000..89de357ad
--- /dev/null
+++ b/src/com/android/camera/ui/focus/FocusRing.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2014 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.android.camera.ui.focus;
+
+import android.graphics.RectF;
+
+/**
+ * Primary interface for interacting with the focus ring UI.
+ */
+public interface FocusRing {
+ /**
+ * Check the state of the passive focus ring animation.
+ *
+ * @return whether the passive focus animation is running.
+ */
+ public boolean isPassiveFocusRunning();
+ /**
+ * Check the state of the active focus ring animation.
+ *
+ * @return whether the active focus animation is running.
+ */
+ public boolean isActiveFocusRunning();
+ /**
+ * Start a passive focus animation.
+ */
+ public void startPassiveFocus();
+ /**
+ * Start an active focus animation.
+ */
+ public void startActiveFocus();
+ /**
+ * Stop any currently running focus animations.
+ */
+ public void stopFocusAnimations();
+ /**
+ * Set the location of the focus ring animation center.
+ */
+ public void setFocusLocation(float viewX, float viewY);
+
+ /**
+ * Set the location of the focus ring animation center.
+ */
+ public void centerFocusLocation();
+
+ /**
+ * Set the target radius as a ratio of min to max visible radius
+ * which will internally convert and clamp the value to the
+ * correct pixel radius.
+ */
+ public void setRadiusRatio(float ratio);
+
+ /**
+ * The physical size of preview can vary and does not map directly
+ * to the size of the view. This allows for conversions between view
+ * and preview space for values that are provided in preview space.
+ */
+ void configurePreviewDimensions(RectF previewArea);
+} \ No newline at end of file
diff --git a/src/com/android/camera/ui/focus/FocusRingRenderer.java b/src/com/android/camera/ui/focus/FocusRingRenderer.java
new file mode 100644
index 000000000..264af2ace
--- /dev/null
+++ b/src/com/android/camera/ui/focus/FocusRingRenderer.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2014 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.android.camera.ui.focus;
+
+import android.graphics.Paint;
+import android.util.Log;
+
+import com.android.camera.ui.motion.DampedSpring;
+import com.android.camera.ui.motion.DynamicAnimation;
+import com.android.camera.ui.motion.Invalidator;
+import com.android.camera.ui.motion.UnitCurve;
+import com.android.camera.ui.motion.UnitCurves;
+
+/**
+ * Base class for defining the focus ring states, enter and exit durations, and
+ * positioning logic.
+ */
+abstract class FocusRingRenderer implements DynamicAnimation {
+ private static final String TAG = "FocusRingRenderer";
+
+ /**
+ * Primary focus states that a focus ring renderer can go through.
+ */
+ protected static enum FocusState {
+ STATE_INACTIVE,
+ STATE_ENTER,
+ STATE_ACTIVE,
+ STATE_FADE_OUT,
+ STATE_HARD_STOP,
+ }
+
+ protected final Invalidator mInvalidator;
+ protected final Paint mRingPaint;
+ protected final DampedSpring mRingRadius;
+ protected final UnitCurve mEnterOpacityCurve;
+ protected final UnitCurve mExitOpacityCurve;
+ protected final UnitCurve mHardExitOpacityCurve;
+ protected final float mEnterDurationMillis;
+ protected final float mExitDurationMillis;
+ protected final float mHardExitDurationMillis = 64;
+
+ private int mCenterX;
+ private int mCenterY;
+ protected long mEnterStartMillis = 0;
+ protected long mExitStartMillis = 0;
+ protected long mHardExitStartMillis = 0;
+
+ protected FocusState mFocusState = FocusState.STATE_INACTIVE;
+
+ /**
+ * A dynamic, configurable, self contained ring render that will inform
+ * via invalidation if it should continue to be receive updates
+ * and re-draws.
+ *
+ * @param invalidator the object to inform if it requires more draw calls.
+ * @param ringPaint the paint to use to draw the ring.
+ * @param enterDurationMillis the fade in duration in milliseconds
+ * @param exitDurationMillis the fade out duration in milliseconds.
+ */
+ FocusRingRenderer(Invalidator invalidator, Paint ringPaint, float enterDurationMillis,
+ float exitDurationMillis) {
+ mInvalidator = invalidator;
+ mRingPaint = ringPaint;
+ mEnterDurationMillis = enterDurationMillis;
+ mExitDurationMillis = exitDurationMillis;
+
+ mEnterOpacityCurve = UnitCurves.FAST_OUT_SLOW_IN;
+ mExitOpacityCurve = UnitCurves.FAST_OUT_LINEAR_IN;
+ mHardExitOpacityCurve = UnitCurves.FAST_OUT_LINEAR_IN;
+
+ mRingRadius = new DampedSpring();
+ }
+
+ /**
+ * Set the centerX position for this focus ring renderer.
+ *
+ * @param value the x position
+ */
+ public void setCenterX(int value) {
+ mCenterX = value;
+ }
+
+ protected int getCenterX() {
+ return mCenterX;
+ }
+
+ /**
+ * Set the centerY position for this focus ring renderer.
+ *
+ * @param value the y position
+ */
+ public void setCenterY(int value) {
+ mCenterY = value;
+ }
+
+ protected int getCenterY() {
+ return mCenterY;
+ }
+
+ /**
+ * Set the physical radius of this ring.
+ *
+ * @param value the radius of the ring.
+ */
+ public void setRadius(long tMs, float value) {
+ if (mFocusState == FocusState.STATE_FADE_OUT
+ && Math.abs(mRingRadius.getTarget() - value) > 0.1) {
+ Log.v(TAG, "FOCUS STATE ENTER VIA setRadius(" + tMs + ", " + value + ")");
+ mFocusState = FocusState.STATE_ENTER;
+ mEnterStartMillis = computeEnterStartTimeMillis(tMs, mEnterDurationMillis);
+ }
+
+ mRingRadius.setTarget(value);
+ }
+
+ /**
+ * returns true if the renderer is not in an inactive state.
+ */
+ @Override
+ public boolean isActive() {
+ return mFocusState != FocusState.STATE_INACTIVE;
+ }
+
+ /**
+ * returns true if the renderer is in an exit state.
+ */
+ public boolean isExiting() {
+ return mFocusState == FocusState.STATE_FADE_OUT
+ || mFocusState == FocusState.STATE_HARD_STOP;
+ }
+
+ /**
+ * returns true if the renderer is in an enter state.
+ */
+ public boolean isEntering() {
+ return mFocusState == FocusState.STATE_ENTER;
+ }
+
+ /**
+ * Initialize and start the animation with the given start and
+ * target radius.
+ */
+ public void start(long startMs, float initialRadius, float targetRadius) {
+ if (mFocusState != FocusState.STATE_INACTIVE) {
+ Log.w(TAG, "start() called while the ring was still focusing!");
+ }
+ mRingRadius.stop();
+ mRingRadius.setValue(initialRadius);
+ mRingRadius.setTarget(targetRadius);
+ mEnterStartMillis = startMs;
+
+ mFocusState = FocusState.STATE_ENTER;
+ mInvalidator.invalidate();
+ }
+
+ /**
+ * Put the animation in the exit state regardless of the current
+ * dynamic transition. If the animation is currently in an enter state
+ * this will compute an exit start time such that the exit time lines
+ * up with the enter time at the current transition value.
+ *
+ * @param t the current animation time.
+ */
+ public void exit(long t) {
+ if (mRingRadius.isActive()) {
+ mRingRadius.stop();
+ }
+
+ mFocusState = FocusState.STATE_FADE_OUT;
+ mExitStartMillis = computeExitStartTimeMs(t, mExitDurationMillis);
+ }
+
+ /**
+ * Put the animation in the hard stop state regardless of the current
+ * dynamic transition. If the animation is currently in an enter state
+ * this will compute an exit start time such that the exit time lines
+ * up with the enter time at the current transition value.
+ *
+ * @param tMillis the current animation time in milliseconds.
+ */
+ public void stop(long tMillis) {
+ if (mRingRadius.isActive()) {
+ mRingRadius.stop();
+ }
+
+ mFocusState = FocusState.STATE_HARD_STOP;
+ mHardExitStartMillis = computeExitStartTimeMs(tMillis, mHardExitDurationMillis);
+ }
+
+ private long computeExitStartTimeMs(long tMillis, float exitDuration) {
+ if (mEnterStartMillis + mEnterDurationMillis <= tMillis) {
+ return tMillis;
+ }
+
+ // Compute the current progress on the enter animation.
+ float enterT = (tMillis - mEnterStartMillis) / mEnterDurationMillis;
+
+ // Find a time on the exit curve such that it will produce the same value.
+ float exitT = UnitCurves.mapEnterCurveToExitCurveAtT(mEnterOpacityCurve, mExitOpacityCurve,
+ enterT);
+
+ // Compute the a start time before tMs such that the ratio of time completed
+ // equals the computed exit curve animation position.
+ return tMillis - (long) (exitT * exitDuration);
+ }
+
+ private long computeEnterStartTimeMillis(long tMillis, float enterDuration) {
+ if (mExitStartMillis + mExitDurationMillis <= tMillis) {
+ return tMillis;
+ }
+
+ // Compute the current progress on the enter animation.
+ float exitT = (tMillis - mExitStartMillis) / mExitDurationMillis;
+
+ // Find a time on the exit curve such that it will produce the same value.
+ float enterT = UnitCurves.mapEnterCurveToExitCurveAtT(mExitOpacityCurve, mEnterOpacityCurve,
+ exitT);
+
+ // Compute the a start time before tMs such that the ratio of time completed
+ // equals the computed exit curve animation position.
+ return tMillis - (long) (enterT * enterDuration);
+ }
+}
diff --git a/src/com/android/camera/ui/focus/FocusRingView.java b/src/com/android/camera/ui/focus/FocusRingView.java
new file mode 100644
index 000000000..14a7f6cc9
--- /dev/null
+++ b/src/com/android/camera/ui/focus/FocusRingView.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2014 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.android.camera.ui.focus;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import com.android.camera.ui.motion.AnimationClock.SystemTimeClock;
+import com.android.camera.ui.motion.DynamicAnimator;
+import com.android.camera.ui.motion.Invalidator;
+import com.android.camera.ui.motion.LinearScale;
+
+import org.codeaurora.snapcam.R;
+
+/**
+ * Custom view for running the focus ring animations.
+ */
+public class FocusRingView extends View implements Invalidator, FocusRing {
+ private static final String TAG = "FocusRingView";
+ private static final float FADE_IN_DURATION_MILLIS = 1000f;
+ private static final float FADE_OUT_DURATION_MILLIS = 250f;
+
+ private final AutoFocusRing mAutoFocusRing;
+ private final ManualFocusRing mManualFocusRing;
+ private final DynamicAnimator mAnimator;
+ private final LinearScale mRatioScale;
+ private final float mDefaultRadiusPx;
+
+ private FocusRingRenderer currentFocusAnimation;
+ private boolean isFirstDraw;
+ private float mLastRadiusPx;
+
+ private RectF mPreviewSize;
+
+ public FocusRingView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ Resources res = getResources();
+ Paint paint = makePaint(res, R.color.focus_color);
+
+ float focusCircleMinSize = res.getDimensionPixelSize(R.dimen.focus_circle_min_size);
+ float focusCircleMaxSize = res.getDimensionPixelSize(R.dimen.focus_circle_max_size);
+ mDefaultRadiusPx = res.getDimensionPixelSize(R.dimen.focus_circle_initial_size);
+
+ mRatioScale = new LinearScale(0, 1, focusCircleMinSize, focusCircleMaxSize);
+ mAnimator = new DynamicAnimator(this, new SystemTimeClock());
+
+ mAutoFocusRing = new AutoFocusRing(mAnimator, paint,
+ FADE_IN_DURATION_MILLIS,
+ FADE_OUT_DURATION_MILLIS);
+ mManualFocusRing = new ManualFocusRing(mAnimator, paint,
+ FADE_OUT_DURATION_MILLIS);
+
+ mAnimator.animations.add(mAutoFocusRing);
+ mAnimator.animations.add(mManualFocusRing);
+
+ isFirstDraw = true;
+ mLastRadiusPx = mDefaultRadiusPx;
+ }
+
+ @Override
+ public boolean isPassiveFocusRunning() {
+ return mAutoFocusRing.isActive();
+ }
+
+ @Override
+ public boolean isActiveFocusRunning() {
+ return mManualFocusRing.isActive();
+ }
+
+ @Override
+ public void startPassiveFocus() {
+ mAnimator.invalidate();
+ long tMs = mAnimator.getTimeMillis();
+
+ if (mManualFocusRing.isActive() && !mManualFocusRing.isExiting()) {
+ mManualFocusRing.stop(tMs);
+ }
+
+ mAutoFocusRing.start(tMs, mLastRadiusPx, mLastRadiusPx);
+ currentFocusAnimation = mAutoFocusRing;
+ }
+
+ @Override
+ public void startActiveFocus() {
+ mAnimator.invalidate();
+ long tMs = mAnimator.getTimeMillis();
+
+ if (mAutoFocusRing.isActive() && !mAutoFocusRing.isExiting()) {
+ mAutoFocusRing.stop(tMs);
+ }
+
+ mManualFocusRing.start(tMs, 0.0f, mLastRadiusPx);
+ currentFocusAnimation = mManualFocusRing;
+ }
+
+ @Override
+ public void stopFocusAnimations() {
+ long tMs = mAnimator.getTimeMillis();
+ if (mManualFocusRing.isActive() && !mManualFocusRing.isExiting()
+ && !mManualFocusRing.isEntering()) {
+ mManualFocusRing.exit(tMs);
+ }
+
+ if (mAutoFocusRing.isActive() && !mAutoFocusRing.isExiting()) {
+ mAutoFocusRing.exit(tMs);
+ }
+ }
+
+ @Override
+ public void setFocusLocation(float viewX, float viewY) {
+ mAutoFocusRing.setCenterX((int) viewX);
+ mAutoFocusRing.setCenterY((int) viewY);
+ mManualFocusRing.setCenterX((int) viewX);
+ mManualFocusRing.setCenterY((int) viewY);
+ }
+
+ @Override
+ public void centerFocusLocation() {
+ Point center = computeCenter();
+ mAutoFocusRing.setCenterX(center.x);
+ mAutoFocusRing.setCenterY(center.y);
+ mManualFocusRing.setCenterX(center.x);
+ mManualFocusRing.setCenterY(center.y);
+ }
+
+ @Override
+ public void setRadiusRatio(float ratio) {
+ setRadius(mRatioScale.scale(mRatioScale.clamp(ratio)));
+ }
+
+ @Override
+ public void configurePreviewDimensions(RectF previewArea) {
+ mPreviewSize = previewArea;
+ mLastRadiusPx = mDefaultRadiusPx;
+
+ if (!isFirstDraw) {
+ centerAutofocusRing();
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (isFirstDraw) {
+ isFirstDraw = false;
+ centerAutofocusRing();
+ }
+
+ if (mPreviewSize != null) {
+ canvas.clipRect(mPreviewSize, Region.Op.REPLACE);
+ }
+
+ mAnimator.draw(canvas);
+ }
+
+ private void setRadius(float radiusPx) {
+ long tMs = mAnimator.getTimeMillis();
+ // Some devices return zero for invalid or "unknown" diopter values.
+ if (currentFocusAnimation != null && radiusPx > 0.1f) {
+ currentFocusAnimation.setRadius(tMs, radiusPx);
+ mLastRadiusPx = radiusPx;
+ }
+ }
+
+ private void centerAutofocusRing() {
+ Point center = computeCenter();
+ mAutoFocusRing.setCenterX(center.x);
+ mAutoFocusRing.setCenterY(center.y);
+ }
+
+ private Point computeCenter() {
+ if (mPreviewSize != null && (mPreviewSize.width() * mPreviewSize.height() > 0.01f)) {
+ Log.i(TAG, "Computing center via preview size.");
+ return new Point((int) mPreviewSize.centerX(), (int) mPreviewSize.centerY());
+ }
+ Log.i(TAG, "Computing center via view bounds.");
+ return new Point(getWidth() / 2, getHeight() / 2);
+ }
+
+ private Paint makePaint(Resources res, int color) {
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setColor(res.getColor(color));
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeCap(Paint.Cap.ROUND);
+ paint.setStrokeWidth(res.getDimension(R.dimen.focus_circle_stroke));
+ return paint;
+ }
+}
diff --git a/src/com/android/camera/ui/focus/FocusSound.java b/src/com/android/camera/ui/focus/FocusSound.java
new file mode 100644
index 000000000..c3ff0107d
--- /dev/null
+++ b/src/com/android/camera/ui/focus/FocusSound.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 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.android.camera.ui.focus;
+
+import com.android.camera.SoundPlayer;
+
+/**
+ * Wraps the focus sound and the player into a single object that can
+ * be played on demand.
+ *
+ * TODO: This needs some way to better manage the sound lifetimes
+ */
+public class FocusSound {
+ private static final float DEFAULT_VOLUME = 0.6f;
+ private final SoundPlayer mPlayer;
+ private final int mSoundId;
+ public FocusSound(SoundPlayer player, int soundId) {
+ mPlayer = player;
+ mSoundId = soundId;
+
+ mPlayer.loadSound(mSoundId);
+ }
+
+ /**
+ * Play the focus sound with the sound player at the default
+ * volume.
+ */
+ public void play() {
+ if(!mPlayer.isReleased()) {
+ mPlayer.play(mSoundId, DEFAULT_VOLUME);
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/focus/LensRangeCalculator.java b/src/com/android/camera/ui/focus/LensRangeCalculator.java
new file mode 100644
index 000000000..ef9cbaec7
--- /dev/null
+++ b/src/com/android/camera/ui/focus/LensRangeCalculator.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2015 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.android.camera.ui.focus;
+
+import android.annotation.TargetApi;
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build.VERSION_CODES;
+
+import com.android.camera.ui.motion.LinearScale;
+
+/**
+ * Compute diopter range scale to convert lens focus distances into
+ * a ratio value.
+ */
+@TargetApi(VERSION_CODES.LOLLIPOP)
+public class LensRangeCalculator {
+
+ /**
+ * A NoOp linear scale for computing diopter values will always return 0
+ */
+ public static LinearScale getNoOp() {
+ return new LinearScale(0, 0, 0, 0);
+ }
+
+ /**
+ * Compute the focus range from the camera characteristics and build
+ * a linear scale model that maps a focus distance to a ratio between
+ * the min and max range.
+ */
+ public static LinearScale getDiopterToRatioCalculator(CameraCharacteristics characteristics) {
+ // From the android documentation:
+ //
+ // 0.0f represents farthest focus, and LENS_INFO_MINIMUM_FOCUS_DISTANCE
+ // represents the nearest focus the device can achieve.
+ //
+ // Example:
+ //
+ // Infinity Hyperfocal Minimum Camera
+ // <----------|-----------------------------| |
+ // [0.0] [0.31] [14.29]
+ Float nearest = characteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE);
+ Float hyperfocal = characteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE);
+
+ if (nearest == null && hyperfocal == null) {
+ return getNoOp();
+ }
+
+ nearest = (nearest == null) ? 0.0f : nearest;
+ hyperfocal = (hyperfocal == null) ? 0.0f : hyperfocal;
+
+ if (nearest > hyperfocal) {
+ return new LinearScale(hyperfocal, nearest, 0, 1);
+ }
+
+ return new LinearScale(nearest, hyperfocal, 0, 1);
+ }
+}
diff --git a/src/com/android/camera/ui/focus/ManualFocusRing.java b/src/com/android/camera/ui/focus/ManualFocusRing.java
new file mode 100644
index 000000000..0133d8e09
--- /dev/null
+++ b/src/com/android/camera/ui/focus/ManualFocusRing.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014 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.android.camera.ui.focus;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import com.android.camera.ui.motion.InterpolateUtils;
+import com.android.camera.ui.motion.Invalidator;
+
+/**
+ * Manual focus ring animation renderer.
+ */
+class ManualFocusRing extends FocusRingRenderer {
+ /**
+ * The manual focus ring encapsulates the animation logic for visualizing
+ * a focus event when triggered by a physical screen touch.
+ *
+ * @param invalidator the object to invalidate while running.
+ * @param ringPaint the paint to draw the ring with.
+ * @param exitDurationMillis the fade out time in milliseconds.
+ */
+ public ManualFocusRing(Invalidator invalidator, Paint ringPaint,
+ float exitDurationMillis) {
+ super(invalidator, ringPaint, 0.0f, exitDurationMillis);
+ }
+
+ @Override
+ public void draw(long t, long dt, Canvas canvas) {
+ float ringRadius = mRingRadius.update(dt);
+ processStates(t);
+
+ if (!isActive()) {
+ return;
+ }
+
+ mInvalidator.invalidate();
+ int ringAlpha = 255;
+
+ if (mFocusState == FocusState.STATE_FADE_OUT) {
+ float rFade = InterpolateUtils.unitRatio(t, mExitStartMillis, mExitDurationMillis);
+ ringAlpha = (int) InterpolateUtils.lerp(255, 0, mExitOpacityCurve.valueAt(rFade));
+ } else if (mFocusState == FocusState.STATE_HARD_STOP) {
+ float rFade = InterpolateUtils.unitRatio(t, mHardExitStartMillis,
+ mHardExitDurationMillis);
+ ringAlpha = (int) InterpolateUtils.lerp(255, 0, mExitOpacityCurve.valueAt(rFade));
+ } else if (mFocusState == FocusState.STATE_INACTIVE) {
+ ringAlpha = 0;
+ }
+
+ mRingPaint.setAlpha(ringAlpha);
+ canvas.drawCircle(getCenterX(), getCenterY(), ringRadius, mRingPaint);
+ }
+
+ private void processStates(long t) {
+ if (mFocusState == FocusState.STATE_INACTIVE) {
+ return;
+ }
+
+ if (mFocusState == FocusState.STATE_ENTER
+ && (t > mEnterStartMillis + mEnterDurationMillis)) {
+ mFocusState = FocusState.STATE_ACTIVE;
+ }
+
+ if (mFocusState == FocusState.STATE_ACTIVE && !mRingRadius.isActive()) {
+ mFocusState = FocusState.STATE_FADE_OUT;
+ mExitStartMillis = t;
+ }
+
+ if (mFocusState == FocusState.STATE_FADE_OUT && t > mExitStartMillis + mExitDurationMillis) {
+ mFocusState = FocusState.STATE_INACTIVE;
+ }
+
+ if (mFocusState == FocusState.STATE_HARD_STOP
+ && t > mHardExitStartMillis + mHardExitDurationMillis) {
+ mFocusState = FocusState.STATE_INACTIVE;
+ }
+ }
+}