From 4f6daf129a7faf2ee3512361b4b82a54b38c6fbb Mon Sep 17 00:00:00 2001 From: Paul Rohde Date: Fri, 5 Dec 2014 12:17:15 -0800 Subject: Drop new focus indicator into Camera2. * Create a new custom focus view that interacts with physical lens diopter changes. * Replace all occurances of the old focus indicator with the new one. Change-Id: Ia02646ce4d1eb059ecb8a1dfccc15dfc9c167e1b --- src/com/android/camera/ui/focus/AutoFocusRing.java | 101 +++++++++ .../ui/focus/CameraCoordinateTransformer.java | 109 ++++++++++ .../android/camera/ui/focus/FocusController.java | 141 ++++++++++++ src/com/android/camera/ui/focus/FocusRing.java | 72 +++++++ .../android/camera/ui/focus/FocusRingRenderer.java | 237 +++++++++++++++++++++ src/com/android/camera/ui/focus/FocusRingView.java | 211 ++++++++++++++++++ src/com/android/camera/ui/focus/FocusSound.java | 47 ++++ .../camera/ui/focus/LensRangeCalculator.java | 71 ++++++ .../android/camera/ui/focus/ManualFocusRing.java | 93 ++++++++ 9 files changed, 1082 insertions(+) create mode 100644 src/com/android/camera/ui/focus/AutoFocusRing.java create mode 100644 src/com/android/camera/ui/focus/CameraCoordinateTransformer.java create mode 100644 src/com/android/camera/ui/focus/FocusController.java create mode 100644 src/com/android/camera/ui/focus/FocusRing.java create mode 100644 src/com/android/camera/ui/focus/FocusRingRenderer.java create mode 100644 src/com/android/camera/ui/focus/FocusRingView.java create mode 100644 src/com/android/camera/ui/focus/FocusSound.java create mode 100644 src/com/android/camera/ui/focus/LensRangeCalculator.java create mode 100644 src/com/android/camera/ui/focus/ManualFocusRing.java (limited to 'src/com/android/camera/ui/focus') 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..f303f1ea1 --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusController.java @@ -0,0 +1,141 @@ +/* + * 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(); + } + }); + } + + /** + * Stop any currently executing focus animation. + */ + public void clearFocusIndicator() { + mMainThread.execute(new Runnable() { + @Override + public void run() { + Log.v(TAG, "clearFocusIndicator()"); + mFocusRing.stopFocusAnimations(); + } + }); + } + + /** + * 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; + } + } +} -- cgit v1.2.3