summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPaul Rohde <codelogic@google.com>2014-12-05 12:17:15 -0800
committerGerrit Code Review <gerrit@cyanogenmod.org>2016-01-15 10:01:23 -0800
commitd8420e3702c5504e45a51fd29642167b4ab66312 (patch)
tree6aaebf6a1982a8897966821edc86efb004b079ae /src
parentdb2718622223b230618e9dae5b0c5d2eb4e68741 (diff)
downloadandroid_packages_apps_Snap-d8420e3702c5504e45a51fd29642167b4ab66312.tar.gz
android_packages_apps_Snap-d8420e3702c5504e45a51fd29642167b4ab66312.tar.bz2
android_packages_apps_Snap-d8420e3702c5504e45a51fd29642167b4ab66312.zip
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
Diffstat (limited to 'src')
-rw-r--r--src/com/android/camera/FocusOverlayManager.java151
-rw-r--r--src/com/android/camera/PhotoModule.java21
-rw-r--r--src/com/android/camera/PhotoUI.java61
-rw-r--r--src/com/android/camera/SoundPlayer.java95
-rw-r--r--src/com/android/camera/VideoModule.java2
-rw-r--r--src/com/android/camera/VideoUI.java49
-rw-r--r--src/com/android/camera/async/HandlerExecutor.java37
-rw-r--r--src/com/android/camera/async/MainThread.java47
-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.java141
-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
-rw-r--r--src/com/android/camera/ui/motion/AnimationClock.java39
-rw-r--r--src/com/android/camera/ui/motion/DampedSpring.java145
-rw-r--r--src/com/android/camera/ui/motion/DynamicAnimation.java41
-rw-r--r--src/com/android/camera/ui/motion/DynamicAnimator.java116
-rw-r--r--src/com/android/camera/ui/motion/InterpolateUtils.java66
-rw-r--r--src/com/android/camera/ui/motion/InterpolatorHelper.java38
-rw-r--r--src/com/android/camera/ui/motion/Invalidator.java28
-rw-r--r--src/com/android/camera/ui/motion/LinearScale.java85
-rw-r--r--src/com/android/camera/ui/motion/UnitBezier.java157
-rw-r--r--src/com/android/camera/ui/motion/UnitCurve.java41
-rw-r--r--src/com/android/camera/ui/motion/UnitCurves.java44
-rw-r--r--src/com/android/camera/util/CameraUtil.java19
29 files changed, 2121 insertions, 243 deletions
diff --git a/src/com/android/camera/FocusOverlayManager.java b/src/com/android/camera/FocusOverlayManager.java
index 97909c1b8..5e9606168 100644
--- a/src/com/android/camera/FocusOverlayManager.java
+++ b/src/com/android/camera/FocusOverlayManager.java
@@ -16,18 +16,17 @@
package com.android.camera;
-import android.annotation.TargetApi;
-import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.Camera.Area;
import android.hardware.Camera.Parameters;
-import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
+import com.android.camera.ui.focus.CameraCoordinateTransformer;
+import com.android.camera.ui.focus.FocusRing;
import com.android.camera.util.CameraUtil;
import com.android.camera.util.UsageStatistics;
@@ -61,6 +60,9 @@ public class FocusOverlayManager {
private static final int RESET_TOUCH_FOCUS = 0;
+ public static final float AF_REGION_BOX = 0.2f;
+ public static final float AE_REGION_BOX = 0.3f;
+
private int mState = STATE_IDLE;
public static final int STATE_IDLE = 0; // Focus is not active.
public static final int STATE_FOCUSING = 1; // Focus is in progress.
@@ -74,7 +76,7 @@ public class FocusOverlayManager {
private boolean mMeteringAreaSupported;
private boolean mLockAeAwbNeeded;
private boolean mAeAwbLock;
- private Matrix mMatrix;
+ private CameraCoordinateTransformer mCoordinateTransformer;
private boolean mMirror; // true if the camera is front-facing.
private int mDisplayOrientation;
@@ -92,20 +94,9 @@ public class FocusOverlayManager {
private boolean mTouchAFRunning = false;
private boolean mIsAFRunning = false;
- private FocusUI mUI;
+ private FocusRing mFocusRing;
private final Rect mPreviewRect = new Rect(0, 0, 0, 0);
- public interface FocusUI {
- public boolean hasFaces();
- public void clearFocus();
- public void setFocusPosition(int x, int y);
- public void onFocusStarted();
- public void onFocusSucceeded(boolean timeOut);
- public void onFocusFailed(boolean timeOut);
- public void pauseFaceDetection();
- public void resumeFaceDetection();
- }
-
private int mFocusTime; // time after touch-to-focus
public interface Listener {
@@ -136,19 +127,18 @@ public class FocusOverlayManager {
public FocusOverlayManager(ComboPreferences preferences, String[] defaultFocusModes,
Parameters parameters, Listener listener,
- boolean mirror, Looper looper, FocusUI ui) {
+ boolean mirror, Looper looper, FocusRing focusRing) {
mHandler = new MainHandler(looper);
- mMatrix = new Matrix();
mPreferences = preferences;
mDefaultFocusModes = defaultFocusModes;
setParameters(parameters);
mListener = listener;
setMirror(mirror);
- mUI = ui;
+ mFocusRing = focusRing;
}
- public void setPhotoUI(FocusUI ui) {
- mUI = ui;
+ public void setFocusRing(FocusRing focusRing) {
+ mFocusRing = focusRing;
}
public void setParameters(Parameters parameters) {
@@ -174,35 +164,28 @@ public class FocusOverlayManager {
public void setPreviewRect(Rect previewRect) {
if (!mPreviewRect.equals(previewRect)) {
mPreviewRect.set(previewRect);
- setMatrix();
+ resetCoordinateTransformer();
+ mInitialized = true;
}
}
- /** Returns a copy of mPreviewRect so that outside class cannot modify preview
- * rect except deliberately doing so through the setter. */
- public Rect getPreviewRect() {
- return new Rect(mPreviewRect);
- }
-
public void setMirror(boolean mirror) {
mMirror = mirror;
- setMatrix();
+ resetCoordinateTransformer();
}
public void setDisplayOrientation(int displayOrientation) {
mDisplayOrientation = displayOrientation;
- setMatrix();
+ resetCoordinateTransformer();
}
- private void setMatrix() {
- if (mPreviewRect.width() != 0 && mPreviewRect.height() != 0) {
- Matrix matrix = new Matrix();
- CameraUtil.prepareMatrix(matrix, mMirror, mDisplayOrientation, getPreviewRect());
- // In face detection, the matrix converts the driver coordinates to UI
- // coordinates. In tap focus, the inverted matrix converts the UI
- // coordinates to driver coordinates.
- matrix.invert(mMatrix);
- mInitialized = true;
+ private void resetCoordinateTransformer() {
+ if (mPreviewRect.width() > 0 && mPreviewRect.height() > 0) {
+ mCoordinateTransformer = new CameraCoordinateTransformer(mMirror, mDisplayOrientation,
+ CameraUtil.rectToRectF(mPreviewRect));
+ } else {
+ Log.w(TAG, "The coordinate transformer could not be built because the preview rect"
+ + "did not have a width and height");
}
}
@@ -288,7 +271,6 @@ public class FocusOverlayManager {
} else {
mState = STATE_FAIL;
}
- updateFocusUI();
capture();
} else if (mState == STATE_FOCUSING) {
// This happens when (1) user is half-pressing the focus key or
@@ -299,7 +281,6 @@ public class FocusOverlayManager {
} else {
mState = STATE_FAIL;
}
- updateFocusUI();
// If this is triggered by touch focus, cancel focus after a
// while.
if ((mFocusArea != null) && (mFocusTime != 0)) {
@@ -319,32 +300,31 @@ public class FocusOverlayManager {
if (!mInitialized) return;
- // Ignore if the camera has detected some faces.
- if (mUI.hasFaces()) {
- if (mIsAFRunning) {
- mUI.clearFocus();
- mUI.onFocusSucceeded(true);
- mIsAFRunning = false;
- }
- return;
- }
-
// Ignore if we have requested autofocus. This method only handles
// continuous autofocus.
if (mState != STATE_IDLE) return;
// animate on false->true trasition only b/8219520
if (moving && !mPreviousMoving) {
- mUI.onFocusStarted();
+ mFocusRing.startPassiveFocus();
mIsAFRunning = true;
} else if (!moving) {
- mUI.onFocusSucceeded(true);
+ mFocusRing.stopFocusAnimations();
mIsAFRunning = false;
}
mPreviousMoving = moving;
}
- @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ /** Returns width of auto focus region in pixels. */
+ private int getAFRegionSizePx() {
+ return (int) (Math.min(mPreviewRect.width(), mPreviewRect.height()) * AF_REGION_BOX);
+ }
+
+ /** Returns width of metering region in pixels. */
+ private int getAERegionSizePx() {
+ return (int) (Math.min(mPreviewRect.width(), mPreviewRect.height()) * AE_REGION_BOX);
+ }
+
private void initializeFocusAreas(int x, int y) {
if (mFocusArea == null) {
mFocusArea = new ArrayList<Object>();
@@ -352,10 +332,9 @@ public class FocusOverlayManager {
}
// Convert the coordinates to driver format.
- calculateTapArea(x, y, 1f, ((Area) mFocusArea.get(0)).rect);
+ ((Area) mFocusArea.get(0)).rect = computeCameraRectFromPreviewCoordinates(x, y, getAFRegionSizePx());
}
- @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
private void initializeMeteringAreas(int x, int y) {
if (mMeteringArea == null) {
mMeteringArea = new ArrayList<Object>();
@@ -365,7 +344,7 @@ public class FocusOverlayManager {
// Convert the coordinates to driver format.
// AE area is bigger because exposure is sensitive and
// easy to over- or underexposure if area is too small.
- calculateTapArea(x, y, 1.5f, ((Area) mMeteringArea.get(0)).rect);
+ ((Area) mMeteringArea.get(0)).rect = computeCameraRectFromPreviewCoordinates(x, y, getAERegionSizePx());
}
private void resetMeteringAreas() {
@@ -394,8 +373,8 @@ public class FocusOverlayManager {
initializeMeteringAreas(x, y);
}
- // Use margin to set the focus indicator to the touched area.
- mUI.setFocusPosition(x, y);
+ mFocusRing.startActiveFocus();
+ mFocusRing.setFocusLocation(x, y);
if (mZslEnabled) {
mTouchAFRunning = true;
@@ -409,7 +388,6 @@ public class FocusOverlayManager {
if (mFocusAreaSupported) {
autoFocus();
} else { // Just show the indicator in all other cases.
- updateFocusUI();
mHandler.removeMessages(RESET_TOUCH_FOCUS);
if (mFocusTime != 0) {
mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, mFocusTime);
@@ -425,7 +403,6 @@ public class FocusOverlayManager {
// If auto focus was in progress, it would have been stopped.
mState = STATE_IDLE;
resetTouchFocus();
- updateFocusUI();
}
public void onCameraReleased() {
@@ -439,8 +416,6 @@ public class FocusOverlayManager {
mState = STATE_FOCUSING;
// Pause the face view because the driver will keep sending face
// callbacks after the focus completes.
- mUI.pauseFaceDetection();
- updateFocusUI();
mHandler.removeMessages(RESET_TOUCH_FOCUS);
}
@@ -452,9 +427,7 @@ public class FocusOverlayManager {
// driver is not reset.
resetTouchFocus();
mListener.cancelAutoFocus();
- mUI.resumeFaceDetection();
mState = STATE_IDLE;
- updateFocusUI();
mHandler.removeMessages(RESET_TOUCH_FOCUS);
}
@@ -510,41 +483,9 @@ public class FocusOverlayManager {
return mMeteringArea;
}
- public void updateFocusUI() {
- if (!mInitialized) return;
- // Show only focus indicator or face indicator.
-
- if (mState == STATE_IDLE) {
- if (mFocusArea == null) {
- mUI.clearFocus();
- } else {
- // Users touch on the preview and the indicator represents the
- // metering area. Either focus area is not supported or
- // autoFocus call is not required.
- mUI.onFocusStarted();
- }
- } else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
- mUI.onFocusStarted();
- } else {
- if (CameraUtil.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) {
- // TODO: check HAL behavior and decide if this can be removed.
- mUI.onFocusSucceeded(false);
- } else if (mState == STATE_SUCCESS) {
- mUI.onFocusSucceeded(false);
- } else if (mState == STATE_FAIL) {
- mUI.onFocusFailed(false);
- }
- }
- }
-
public void resetTouchFocus() {
if (!mInitialized) return;
- // Put focus indicator to the center. clear reset position
- if (mUI != null) {
- mUI.clearFocus();
- }
- // Initialize mFocusArea.
mFocusArea = null;
// Initialize mMeteringArea.
mMeteringArea = null;
@@ -559,16 +500,14 @@ public class FocusOverlayManager {
}
}
- private void calculateTapArea(int x, int y, float areaMultiple, Rect rect) {
- int areaSize = (int) (getAreaSize() * areaMultiple);
- int left = CameraUtil.clamp(x - areaSize / 2, mPreviewRect.left,
- mPreviewRect.right - areaSize);
- int top = CameraUtil.clamp(y - areaSize / 2, mPreviewRect.top,
- mPreviewRect.bottom - areaSize);
+ 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 + areaSize, top + areaSize);
- mMatrix.mapRect(rectF);
- CameraUtil.rectFToRect(rectF, rect);
+ RectF rectF = new RectF(left, top, left + size, top + size);
+ return CameraUtil.rectFToRect(mCoordinateTransformer.toCameraSpace(rectF));
}
private int getAreaSize() {
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
index f92ef65ba..53468eb7f 100644
--- a/src/com/android/camera/PhotoModule.java
+++ b/src/com/android/camera/PhotoModule.java
@@ -624,7 +624,7 @@ public class PhotoModule
Log.v(TAG, "onCameraOpened");
openCameraCommon();
resizeForPreviewAspectRatio();
- updateFocusManager(mUI);
+ mFocusManager.setFocusRing(mUI.getFocusRing());
}
private void switchCamera() {
@@ -655,7 +655,6 @@ public class PhotoModule
}
closeCamera();
mUI.collapseCameraControls();
- mUI.clearFaces();
if (mFocusManager != null) mFocusManager.removeMessages();
// Restart the camera and initialize the UI. From onCreate.
@@ -927,7 +926,6 @@ public class PhotoModule
if (mParameters.getMaxNumDetectedFaces() > 0) {
mFaceDetectionStarted = false;
mCameraDevice.setFaceDetectionCallback(null, null);
- mUI.pauseFaceDetection();
mCameraDevice.stopFaceDetection();
mUI.onStopFaceDetection();
}
@@ -1133,8 +1131,6 @@ public class PhotoModule
return;
}
- mFocusManager.updateFocusUI(); // Ensure focus indicator is hidden.
-
String jpegFilePath = new String(jpegData);
mNamedImages.nameNewImage(mCaptureStartTime);
NamedEntity name = mNamedImages.getNextNameEntity();
@@ -1230,8 +1226,6 @@ public class PhotoModule
Log.v(TAG, "mPictureDisplayedToJpegCallbackTime = "
+ mPictureDisplayedToJpegCallbackTime + "ms");
- mFocusManager.updateFocusUI(); // Ensure focus indicator is hidden.
-
if (isLongshotDone()) {
mCameraDevice.setLongshot(false);
}
@@ -1278,7 +1272,6 @@ public class PhotoModule
|| CameraUtil.FOCUS_MODE_MW_CONTINUOUS_PICTURE.equals(mFocusManager.getFocusMode())) {
mCameraDevice.cancelAutoFocus();
}
- mUI.resumeFaceDetection();
if (!mIsImageCaptureIntent) {
setCameraState(IDLE);
}
@@ -2434,22 +2427,12 @@ public class PhotoModule
if (mFocusManager == null) {
mFocusManager = new FocusOverlayManager(mPreferences, defaultFocusModes,
mInitialParams, this, mMirror,
- mActivity.getMainLooper(), mUI);
+ mActivity.getMainLooper(), mUI != null ? mUI.getFocusRing() : null);
}
}
}
}
- private void updateFocusManager(PhotoUI mUI) {
- // Idea here is to let focus manager create in camera open thread
- // (in initializeFocusManager) even if photoUI is null by that time so
- // as to not block start preview process. Once UI creation is done,
- // we will update focus manager with proper UI.
- if (mFocusManager != null && mUI != null) {
- mFocusManager.setPhotoUI(mUI);
- }
- }
-
@Override
public void onConfigurationChanged(Configuration newConfig) {
Log.v(TAG, "onConfigurationChanged");
diff --git a/src/com/android/camera/PhotoUI.java b/src/com/android/camera/PhotoUI.java
index 936f52a99..693b94b9c 100644
--- a/src/com/android/camera/PhotoUI.java
+++ b/src/com/android/camera/PhotoUI.java
@@ -56,7 +56,6 @@ import android.widget.PopupWindow;
import android.widget.Toast;
import com.android.camera.CameraPreference.OnPreferenceChangedListener;
-import com.android.camera.FocusOverlayManager.FocusUI;
import com.android.camera.ui.AbstractSettingPopup;
import com.android.camera.ui.CameraControls;
import com.android.camera.ui.CameraRootView;
@@ -72,11 +71,11 @@ import com.android.camera.ui.RenderOverlay;
import com.android.camera.ui.RotateLayout;
import com.android.camera.ui.RotateTextToast;
import com.android.camera.ui.ZoomRenderer;
+import com.android.camera.ui.focus.FocusRing;
import com.android.camera.util.CameraUtil;
public class PhotoUI implements PieListener,
PreviewGestures.SingleTapListener,
- FocusUI,
SurfaceHolder.Callback,
LocationManager.Listener,
CameraRootView.MyDisplayListener,
@@ -85,6 +84,7 @@ public class PhotoUI implements PieListener,
private static final String TAG = "CAM_UI";
private int mDownSampleFactor = 4;
private final AnimationManager mAnimationManager;
+ private final FocusRing mFocusRing;
private CameraActivity mActivity;
private PhotoController mController;
private PreviewGestures mGestures;
@@ -292,6 +292,7 @@ public class PhotoUI implements PieListener,
mFaceView = (FaceView) mRootView.findViewById(R.id.face_view);
setSurfaceTextureSizeChangedListener(mFaceView);
}
+ mFocusRing = (FocusRing) mRootView.findViewById(R.id.focus_ring);
initIndicators();
mAnimationManager = new AnimationManager();
mOrientationResize = false;
@@ -992,7 +993,6 @@ public class PhotoUI implements PieListener,
CameraUtil.fadeIn(mReviewDoneButton);
mShutterButton.setVisibility(View.INVISIBLE);
CameraUtil.fadeIn(mReviewRetakeButton);
- pauseFaceDetection();
}
protected void hidePostCaptureAlert() {
@@ -1006,7 +1006,6 @@ public class PhotoUI implements PieListener,
CameraUtil.fadeOut(mReviewDoneButton);
mShutterButton.setVisibility(View.VISIBLE);
CameraUtil.fadeOut(mReviewRetakeButton);
- resumeFaceDetection();
}
public void setDisplayOrientation(int orientation) {
@@ -1163,58 +1162,8 @@ public class PhotoUI implements PieListener,
((CameraRootView) mRootView).removeDisplayChangeListener();
}
- // focus UI implementation
-
- private FocusIndicator getFocusIndicator() {
- return (mFaceView != null && mFaceView.faceExists()) ? mFaceView : mPieRenderer;
- }
-
- @Override
- public boolean hasFaces() {
- return (mFaceView != null && mFaceView.faceExists());
- }
-
- public void clearFaces() {
- if (mFaceView != null) mFaceView.clear();
- }
-
- @Override
- public void clearFocus() {
- FocusIndicator indicator = getFocusIndicator();
- if (indicator != null) indicator.clear();
- }
-
- @Override
- public void setFocusPosition(int x, int y) {
- mPieRenderer.setFocus(x, y);
- }
-
- @Override
- public void onFocusStarted() {
- FocusIndicator indicator = getFocusIndicator();
- if (indicator != null) indicator.showStart();
- }
-
- @Override
- public void onFocusSucceeded(boolean timeout) {
- FocusIndicator indicator = getFocusIndicator();
- if (indicator != null) indicator.showSuccess(timeout);
- }
-
- @Override
- public void onFocusFailed(boolean timeout) {
- FocusIndicator indicator = getFocusIndicator();
- if (indicator != null) indicator.showFail(timeout);
- }
-
- @Override
- public void pauseFaceDetection() {
- if (mFaceView != null) mFaceView.pause();
- }
-
- @Override
- public void resumeFaceDetection() {
- if (mFaceView != null) mFaceView.resume();
+ public FocusRing getFocusRing() {
+ return mFocusRing;
}
public void onStartFaceDetection(int orientation, boolean mirror) {
diff --git a/src/com/android/camera/SoundPlayer.java b/src/com/android/camera/SoundPlayer.java
new file mode 100644
index 000000000..ff3f37f7a
--- /dev/null
+++ b/src/com/android/camera/SoundPlayer.java
@@ -0,0 +1,95 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.SoundPool;
+import android.util.SparseIntArray;
+
+import com.android.camera.util.ApiHelper;
+
+/**
+ * Loads a plays custom sounds. For playing system-standard sounds for various
+ * camera actions, please refer to {@link SoundClips}.
+ */
+public class SoundPlayer {
+ private final Context mAppContext;
+ private final SoundPool mSoundPool;
+ /** Keeps a mapping from sound resource ID to sound ID */
+ private final SparseIntArray mResourceToSoundId = new SparseIntArray();
+ private boolean mIsReleased = false;
+
+ /**
+ * Construct a new sound player.
+ */
+ public SoundPlayer(Context appContext) {
+ mAppContext = appContext;
+ final int audioType = getAudioTypeForSoundPool();
+ mSoundPool = new SoundPool(1 /* max streams */, audioType, 0 /* quality */);
+ }
+
+ /**
+ * Load the sound from a resource.
+ */
+ public void loadSound(int resourceId) {
+ int soundId = mSoundPool.load(mAppContext, resourceId, 1/* priority */);
+ mResourceToSoundId.put(resourceId, soundId);
+ }
+
+ /**
+ * Play the sound with the given resource. The resource has to be loaded
+ * before it can be played, otherwise an exception will be thrown.
+ */
+ public void play(int resourceId, float volume) {
+ Integer soundId = mResourceToSoundId.get(resourceId);
+ if (soundId == null) {
+ throw new IllegalStateException("Sound not loaded. Must call #loadSound first.");
+ }
+ mSoundPool.play(soundId, volume, volume, 0 /* priority */, 0 /* loop */, 1 /* rate */);
+ }
+
+ /**
+ * Unload the given sound if it's not needed anymore to release memory.
+ */
+ public void unloadSound(int resourceId) {
+ Integer soundId = mResourceToSoundId.get(resourceId);
+ if (soundId == null) {
+ throw new IllegalStateException("Sound not loaded. Must call #loadSound first.");
+ }
+ mSoundPool.unload(soundId);
+ }
+
+ /**
+ * Call this if you don't need the SoundPlayer anymore. All memory will be
+ * released and the object cannot be re-used.
+ */
+ public void release() {
+ mIsReleased = true;
+ mSoundPool.release();
+ }
+
+ public boolean isReleased() {
+ return mIsReleased;
+ }
+
+ private static int getAudioTypeForSoundPool() {
+ // STREAM_SYSTEM_ENFORCED is hidden API.
+ return ApiHelper.getIntFieldIfExists(AudioManager.class,
+ "STREAM_SYSTEM_ENFORCED", null, AudioManager.STREAM_RING);
+ }
+}
diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java
index 0246d6b9c..996166f83 100644
--- a/src/com/android/camera/VideoModule.java
+++ b/src/com/android/camera/VideoModule.java
@@ -1335,7 +1335,7 @@ public class VideoModule implements CameraModule,
R.array.pref_video_focusmode_default_array);
mFocusManager = new FocusOverlayManager(mPreferences, defaultFocusModes,
mParameters, this, mirror,
- mActivity.getMainLooper(), mUI);
+ mActivity.getMainLooper(), mUI.getFocusRing());
}
}
diff --git a/src/com/android/camera/VideoUI.java b/src/com/android/camera/VideoUI.java
index 51a09bebf..42d40ab28 100644
--- a/src/com/android/camera/VideoUI.java
+++ b/src/com/android/camera/VideoUI.java
@@ -45,7 +45,6 @@ import android.widget.TextView;
import android.view.View.OnLayoutChangeListener;
import com.android.camera.CameraPreference.OnPreferenceChangedListener;
-import com.android.camera.FocusOverlayManager.FocusUI;
import com.android.camera.ui.AbstractSettingPopup;
import com.android.camera.ui.CameraControls;
import com.android.camera.ui.CameraRootView;
@@ -57,16 +56,17 @@ import com.android.camera.ui.RenderOverlay;
import com.android.camera.ui.RotateLayout;
import com.android.camera.ui.RotateTextToast;
import com.android.camera.ui.ZoomRenderer;
+import com.android.camera.ui.focus.FocusRing;
import com.android.camera.util.CameraUtil;
public class VideoUI implements PieRenderer.PieListener,
PreviewGestures.SingleTapListener,
CameraRootView.MyDisplayListener,
- FocusUI,
SurfaceHolder.Callback,
PauseButton.OnPauseButtonListener {
private static final String TAG = "CAM_VideoUI";
// module fields
+ private final FocusRing mFocusRing;
private CameraActivity mActivity;
private View mRootView;
private SurfaceHolder mSurfaceHolder;
@@ -222,6 +222,7 @@ public class VideoUI implements PieRenderer.PieListener,
}
});
+ mFocusRing = (FocusRing) mRootView.findViewById(R.id.focus_ring);
mFlashOverlay = mRootView.findViewById(R.id.flash_overlay);
mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button);
mSwitcher = (ModuleSwitcher) mRootView.findViewById(R.id.camera_switcher);
@@ -1148,47 +1149,7 @@ public class VideoUI implements PieRenderer.PieListener,
setOrientation(mOrientation, false);
}
- // implement focusUI interface
- private FocusIndicator getFocusIndicator() {
- return mPieRenderer;
- }
-
- @Override
- public boolean hasFaces() {
- return false;
- }
-
- @Override
- public void clearFocus() {
- FocusIndicator indicator = getFocusIndicator();
- if (indicator != null) indicator.clear();
- }
-
- @Override
- public void setFocusPosition(int x, int y) {
- mPieRenderer.setFocus(x, y);
- }
-
- @Override
- public void onFocusStarted(){
- getFocusIndicator().showStart();
- }
-
- @Override
- public void onFocusSucceeded(boolean timeOut) {
- getFocusIndicator().showSuccess(timeOut);
- }
-
- @Override
- public void onFocusFailed(boolean timeOut) {
- getFocusIndicator().showFail(timeOut);
- }
-
- @Override
- public void pauseFaceDetection() {
- }
-
- @Override
- public void resumeFaceDetection() {
+ public FocusRing getFocusRing() {
+ return mFocusRing;
}
}
diff --git a/src/com/android/camera/async/HandlerExecutor.java b/src/com/android/camera/async/HandlerExecutor.java
new file mode 100644
index 000000000..87c8c0ce0
--- /dev/null
+++ b/src/com/android/camera/async/HandlerExecutor.java
@@ -0,0 +1,37 @@
+/*
+ * 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.async;
+
+import java.util.concurrent.Executor;
+
+import android.os.Handler;
+
+/**
+ * An {@link Executor} which posts to a {@link Handler}.
+ */
+public class HandlerExecutor implements Executor {
+ private final Handler mHandler;
+
+ public HandlerExecutor(Handler handler) {
+ mHandler = handler;
+ }
+
+ @Override
+ public void execute(Runnable runnable) {
+ mHandler.post(runnable);
+ }
+}
diff --git a/src/com/android/camera/async/MainThread.java b/src/com/android/camera/async/MainThread.java
new file mode 100644
index 000000000..7fdb3ec9b
--- /dev/null
+++ b/src/com/android/camera/async/MainThread.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.async;
+
+import android.os.Handler;
+import android.os.Looper;
+
+public class MainThread extends HandlerExecutor {
+ private MainThread(Handler handler) {
+ super(handler);
+ }
+
+ public static MainThread create() {
+ return new MainThread(new Handler(Looper.getMainLooper()));
+ }
+
+ /**
+ * Caches whether or not the current thread is the main thread.
+ */
+ private static final ThreadLocal<Boolean> sIsMainThread = new ThreadLocal<Boolean>() {
+ @Override
+ protected Boolean initialValue() {
+ return Looper.getMainLooper().getThread() == Thread.currentThread();
+ }
+ };
+
+ /**
+ * Returns true if the method is run on the main android thread.
+ */
+ public static boolean isMainThread() {
+ return sIsMainThread.get();
+ }
+}
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;
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/motion/AnimationClock.java b/src/com/android/camera/ui/motion/AnimationClock.java
new file mode 100644
index 000000000..d2504de6b
--- /dev/null
+++ b/src/com/android/camera/ui/motion/AnimationClock.java
@@ -0,0 +1,39 @@
+/*
+ * 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.motion;
+
+import android.os.SystemClock;
+
+/**
+ * Wraps the SystemClock static time methods so they can be exercised in tests.
+ */
+public abstract class AnimationClock {
+
+ public abstract long getTimeMillis();
+
+ /**
+ * Forwards calls to SystemClock.uptimeMillis() since it is the most consistent clock for
+ * animations.
+ */
+ public static class SystemTimeClock extends AnimationClock {
+
+ @Override
+ public long getTimeMillis() {
+ return SystemClock.uptimeMillis();
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/motion/DampedSpring.java b/src/com/android/camera/ui/motion/DampedSpring.java
new file mode 100644
index 000000000..84cbfa6f8
--- /dev/null
+++ b/src/com/android/camera/ui/motion/DampedSpring.java
@@ -0,0 +1,145 @@
+/*
+ * 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.motion;
+
+/**
+ * This models a value after the behavior of a spring. The value tracks the current value, a target
+ * value, and the current velocity and applies both a directional force and a damping force to the
+ * value on each update call.
+ */
+public class DampedSpring {
+ public static final float DEFAULT_TIME_TO_90_PERCENT_MILLIS = 200.0f;
+ public static final float DEFAULT_SPRING_STIFFNESS = 3.75f;
+ public static final float EPSILON = 0.01f;
+
+ private final float mSpringStiffness;
+ private final float mTimeTo90PercentMs;
+
+ private float mTarget = 0f;
+ private float mVelocity = 0f;
+ private float mValue = 0f;
+
+ public DampedSpring() {
+ this(DEFAULT_TIME_TO_90_PERCENT_MILLIS, DEFAULT_SPRING_STIFFNESS);
+ }
+
+ public DampedSpring(float timeTo90PercentMs) {
+ this(timeTo90PercentMs, DEFAULT_SPRING_STIFFNESS);
+ }
+
+ public DampedSpring(float timeTo90PercentMs, float springStiffness) {
+ // TODO: Assert timeTo90PercentMs >= 1ms, it might behave badly at low values.
+ // TODO: Assert springStiffness > 2.0f
+
+ mTimeTo90PercentMs = timeTo90PercentMs;
+ mSpringStiffness = springStiffness;
+
+ if (springStiffness > timeTo90PercentMs) {
+ throw new IllegalArgumentException("Creating a spring value with "
+ + "excessive stiffness will oscillate endlessly.");
+ }
+ }
+
+ /**
+ * @return the current value.
+ */
+ public float getValue() {
+ return mValue;
+ }
+
+ /**
+ * @param value the value to set this instance's current state too.
+ */
+ public void setValue(float value) {
+ mValue = value;
+ }
+
+ /**
+ * @return the current target value.
+ */
+ public float getTarget() {
+ return mTarget;
+ }
+
+ /**
+ * Set a target value. The current value will maintain any existing velocity values and will
+ * move towards the new target value. To forcibly stopAt the value use the stopAt() method.
+ *
+ * @param value the new value to move the current value towards.
+ */
+ public void setTarget(float value) {
+ mTarget = value;
+ }
+
+ /**
+ * Update the current value, moving it towards the actual value over the given
+ * time delta (in milliseconds) since the last update. This works off of the
+ * principle of a critically damped spring such that any given current value
+ * will move elastically towards the target value. The current value maintains
+ * and applies velocity, acceleration, and a damping force to give a continuous,
+ * smooth transition towards the target value.
+ *
+ * @param dtMs the time since the last update, or zero.
+ * @return the current value after the update occurs.
+ */
+ public float update(float dtMs) {
+ float dt = dtMs / mTimeTo90PercentMs;
+ float dts = dt * mSpringStiffness;
+
+ // If the dts > 1, and the velocity is zero, the force will exceed the
+ // distance to the target value and it will overshoot the value, causing
+ // weird behavior and unintended oscillation. since a critically damped
+ // spring should never overshoot the value, simply the current value to the
+ // target value.
+ if (dts > 1.0f || dts < 0.0f) {
+ stop();
+ return mValue;
+ }
+
+ float delta = (mTarget - mValue);
+ float force = delta - 2.0f * mVelocity;
+
+ mVelocity += force * dts;
+ mValue += mVelocity * dts;
+
+ // If we get close enough to the actual value, simply set the current value
+ // to the current target value and stop.
+ if (!isActive()) {
+ stop();
+ }
+
+ return mValue;
+ }
+
+ /**
+ * @return true if this instance has velocity or it is not at the target value.
+ */
+ public boolean isActive() {
+ boolean hasVelocity = Math.abs(mVelocity) >= EPSILON;
+ boolean atTarget = Math.abs(mTarget - mValue) < EPSILON;
+ return hasVelocity || !atTarget;
+ }
+
+ /**
+ * Stop the spring motion wherever it is currently at. Sets target to the
+ * current value and sets the velocity to zero.
+ */
+ public void stop() {
+ mTarget = mValue;
+ mVelocity = 0.0f;
+ }
+}
diff --git a/src/com/android/camera/ui/motion/DynamicAnimation.java b/src/com/android/camera/ui/motion/DynamicAnimation.java
new file mode 100644
index 000000000..57d5a1021
--- /dev/null
+++ b/src/com/android/camera/ui/motion/DynamicAnimation.java
@@ -0,0 +1,41 @@
+/*
+ * 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.motion;
+
+import android.graphics.Canvas;
+
+/**
+ * Rendering object that can be driven by an animator instance.
+ */
+public interface DynamicAnimation {
+
+ /**
+ * Check to determine if this animation is currently in a stable state.
+ *
+ * @return true if the animation is stable, false if it should continue to be redrawn.
+ */
+ boolean isActive();
+
+ /**
+ * Update and draw the animation onto the given canvas.
+ *
+ * @param t current animation frame time.
+ * @param dt delta since the last update.
+ * @param canvas the canvas to draw the animation onto.
+ */
+ void draw(long t, long dt, Canvas canvas);
+}
diff --git a/src/com/android/camera/ui/motion/DynamicAnimator.java b/src/com/android/camera/ui/motion/DynamicAnimator.java
new file mode 100644
index 000000000..542ac1e37
--- /dev/null
+++ b/src/com/android/camera/ui/motion/DynamicAnimator.java
@@ -0,0 +1,116 @@
+/*
+ * 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.motion;
+
+import android.graphics.Canvas;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Designed to handle the lifecycle of a view that needs a continuous update /
+ * redraw cycle that does not have a defined start / end time.
+ *
+ * Fixed length animations should NOT use this class.
+ */
+public class DynamicAnimator implements Invalidator {
+
+ public final List<DynamicAnimation> animations = new ArrayList<>();
+
+ private final Invalidator mInvalidator;
+ private final AnimationClock mClock;
+
+ private boolean mUpdateRequested = false;
+ private boolean mIsDrawing = false;
+ private long mLastDrawTimeMillis = 0;
+ private long mDrawTimeMillis = 0;
+
+ public DynamicAnimator(Invalidator invalidator, AnimationClock clock) {
+ mInvalidator = invalidator;
+ mClock = clock;
+ }
+
+ public void draw(Canvas canvas) {
+ mIsDrawing = true;
+ mUpdateRequested = false;
+
+ mDrawTimeMillis = mClock.getTimeMillis();
+
+ if (mLastDrawTimeMillis <= 0) {
+ mLastDrawTimeMillis = mDrawTimeMillis; // On the initial draw, dt is zero.
+ }
+
+ long dt = mDrawTimeMillis - mLastDrawTimeMillis;
+ mLastDrawTimeMillis = mDrawTimeMillis;
+
+ // Run the animation
+ for (DynamicAnimation renderer : animations) {
+ if (renderer.isActive()) {
+ renderer.draw(mDrawTimeMillis, dt, canvas);
+ }
+ }
+
+ // If either the update or the draw methods requested new frames, then
+ // invalidate the view which should give us another frame to work with.
+ // Otherwise, stopAt the last update time.
+ if (mUpdateRequested) {
+ mInvalidator.invalidate();
+ } else {
+ mLastDrawTimeMillis = -1;
+ }
+
+ mIsDrawing = false;
+ }
+
+ /**
+ * If a scheduleNewFrame request comes in outside of the animation loop,
+ * and we didn't schedule a frame after the previous loop (or it's the
+ * first time we've used this instance), invalidate the view and set the
+ * last update time to the current time. Theoretically, a few milliseconds
+ * have elapsed before the view gets updated.
+ */
+ @Override
+ public void invalidate() {
+ if (!mIsDrawing && !mUpdateRequested) {
+ mInvalidator.invalidate();
+ mLastDrawTimeMillis = mClock.getTimeMillis();
+ }
+
+ mUpdateRequested = true;
+ }
+
+ /**
+ * This will return the "best guess" for the most current animation frame
+ * time. If the loop is currently drawing, then it will return the time the
+ * draw began, and if an update is currently requested it will return the
+ * time that the update was requested at, and if neither of these are true
+ * it will return the current system clock time.
+ *
+ * This method will not trigger a new update.
+ */
+ public long getTimeMillis() {
+ if (mIsDrawing) {
+ return mDrawTimeMillis;
+ }
+
+ if (mUpdateRequested) {
+ return mLastDrawTimeMillis;
+ }
+
+ return mClock.getTimeMillis();
+ }
+}
diff --git a/src/com/android/camera/ui/motion/InterpolateUtils.java b/src/com/android/camera/ui/motion/InterpolateUtils.java
new file mode 100644
index 000000000..3c3cd532f
--- /dev/null
+++ b/src/com/android/camera/ui/motion/InterpolateUtils.java
@@ -0,0 +1,66 @@
+/*
+ * 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.motion;
+
+/**
+ * Various static helper functions for interpolating between values.
+ */
+public class InterpolateUtils {
+
+ private InterpolateUtils() {
+ }
+
+ /**
+ * Linear interpolation from v0 to v1 as t goes from 0...1
+ *
+ * @param v0 the value at t=0
+ * @param v1 the value at t=1
+ * @param t value in the range of 0 to 1.
+ * @return the value between v0 and v1 as a ratio between 0 and 1 defined by t.
+ */
+ public static float lerp(float v0, float v1, float t) {
+ return v0 + t * (v1 - v0);
+ }
+
+ /**
+ * Project a value that is within the in(Min/Max) number space into the to(Min/Max) number
+ * space.
+ *
+ * @param v value to scale into the 'to' number space.
+ * @param vMin min value of the values number space.
+ * @param vMax max value of the values number space.
+ * @param pMin min value of the projection number space.
+ * @param pMax max value of the projection number space.
+ * @return the ratio of the value in the source number space as a value in the to(Min/Max)
+ * number space.
+ */
+ public static float scale(float v, float vMin, float vMax, float pMin, float pMax) {
+ return (pMax - pMin) * (v - vMin) / (vMax - vMin) + pMin;
+ }
+
+ /**
+ * Value between 0 and 1 as a ratio between tBegin over tDuration
+ * with no upper bound.
+ */
+ public static float unitRatio(long t, long tBegin, float tDuration) {
+ if (t <= tBegin) {
+ return 0.0f;
+ }
+
+ return (t - tBegin) / tDuration;
+ }
+}
diff --git a/src/com/android/camera/ui/motion/InterpolatorHelper.java b/src/com/android/camera/ui/motion/InterpolatorHelper.java
new file mode 100644
index 000000000..84114cb03
--- /dev/null
+++ b/src/com/android/camera/ui/motion/InterpolatorHelper.java
@@ -0,0 +1,38 @@
+/*
+ * 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.motion;
+
+import android.content.Context;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.camera.util.ApiHelper;
+
+public class InterpolatorHelper {
+ private static Interpolator LINEAR_OUT_SLOW_IN = null;
+
+ public static Interpolator getLinearOutSlowInInterpolator(final Context context) {
+ if (LINEAR_OUT_SLOW_IN != null) {
+ return LINEAR_OUT_SLOW_IN;
+ }
+
+ LINEAR_OUT_SLOW_IN = AnimationUtils.loadInterpolator(
+ context, android.R.interpolator.linear_out_slow_in);
+ return LINEAR_OUT_SLOW_IN;
+ }
+}
diff --git a/src/com/android/camera/ui/motion/Invalidator.java b/src/com/android/camera/ui/motion/Invalidator.java
new file mode 100644
index 000000000..fdb548748
--- /dev/null
+++ b/src/com/android/camera/ui/motion/Invalidator.java
@@ -0,0 +1,28 @@
+/*
+ * 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.motion;
+
+/**
+ * Basic interface for objects that can be invalidated.
+ */
+public interface Invalidator {
+ /**
+ * Request that the object should be redrawn whenever it gets
+ * the chance.
+ */
+ void invalidate();
+}
diff --git a/src/com/android/camera/ui/motion/LinearScale.java b/src/com/android/camera/ui/motion/LinearScale.java
new file mode 100644
index 000000000..5886f6882
--- /dev/null
+++ b/src/com/android/camera/ui/motion/LinearScale.java
@@ -0,0 +1,85 @@
+/*
+ * 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.motion;
+
+/**
+ * Represents a discrete linear scale function.
+ */
+public final class LinearScale {
+ private final float mDomainA;
+ private final float mDomainB;
+ private final float mRangeA;
+ private final float mRangeB;
+
+ private final float mScale;
+
+ public LinearScale(float domainA, float domainB, float rangeA, float rangeB) {
+ mDomainA = domainA;
+ mDomainB = domainB;
+ mRangeA = rangeA;
+ mRangeB = rangeB;
+
+ // Precomputed ratio between input domain and output range.
+ float scale = (mRangeB - mRangeA) / (mDomainB - mDomainA);
+ mScale = Float.isNaN(scale) ? 0.0f : scale;
+ }
+
+ /**
+ * Clamp a given domain value to the given domain.
+ */
+ public float clamp(float domainValue) {
+ if (mDomainA > mDomainB) {
+ return Math.max(mDomainB, Math.min(mDomainA, domainValue));
+ }
+
+ return Math.max(mDomainA, Math.min(mDomainB, domainValue));
+ }
+
+ /**
+ * Returns true if the value is within the domain.
+ */
+ public boolean isInDomain(float domainValue) {
+ if (mDomainA > mDomainB) {
+ return domainValue <= mDomainA && domainValue >= mDomainB;
+ }
+ return domainValue >= mDomainA && domainValue <= mDomainB;
+ }
+
+ /**
+ * Linearly scale a given domain value into the output range.
+ */
+ public float scale(float domainValue) {
+ return mRangeA + (domainValue - mDomainA) * mScale;
+ }
+
+ /**
+ * For the current domain and range parameters produce a new scale function
+ * that is the inverse of the current scale function.
+ */
+ public LinearScale inverse() {
+ return new LinearScale(mRangeA, mRangeB, mDomainA, mDomainB);
+ }
+
+ @Override
+ public String toString() {
+ return "LinearScale{" +
+ "mDomainA=" + mDomainA +
+ ", mDomainB=" + mDomainB +
+ ", mRangeA=" + mRangeA +
+ ", mRangeB=" + mRangeB + "}";
+ }
+} \ No newline at end of file
diff --git a/src/com/android/camera/ui/motion/UnitBezier.java b/src/com/android/camera/ui/motion/UnitBezier.java
new file mode 100644
index 000000000..242f54556
--- /dev/null
+++ b/src/com/android/camera/ui/motion/UnitBezier.java
@@ -0,0 +1,157 @@
+/*
+ * 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.motion;
+
+/**
+ * This represents is a precomputed cubic bezier curve starting at (0,0) and
+ * going to (1,1) with two configurable control points. Once the instance is
+ * created, the control points cannot be modified.
+ *
+ * Generally, this will be used for computing timing curves for with control
+ * points where an x value will be provide from 0.0 - 1.0, and the y value will
+ * be solved for where y is used as the timing value in some linear
+ * interpolation of a value.
+ */
+public class UnitBezier implements UnitCurve {
+
+ private static final float EPSILON = 1e-6f;
+
+ private final DerivableFloatFn mXFn;
+ private final DerivableFloatFn mYFn;
+
+ /**
+ * Build and pre-compute a unit bezier. This assumes a starting point of
+ * (0, 0) and end point of (1.0, 1.0).
+ *
+ * @param c0x control point x value for p0
+ * @param c0y control point y value for p0
+ * @param c1x control point x value for p1
+ * @param c1y control point y value for p1
+ */
+ public UnitBezier(float c0x, float c0y, float c1x, float c1y) {
+ mXFn = new CubicBezierFn(c0x, c1x);
+ mYFn = new CubicBezierFn(c0y, c1y);
+ }
+
+ /**
+ * Given a unit bezier curve find the height of the curve at t (which is
+ * internally represented as the xAxis).
+ *
+ * @param t the x position between 0 and 1 to solve for y.
+ * @return the closest approximate height of the curve at x.
+ */
+ @Override
+ public float valueAt(float t) {
+ return mYFn.value(solve(t, mXFn));
+ }
+
+ /**
+ * Given a unit bezier curve find a value along the x axis such that
+ * valueAt(result) produces the input value.
+ *
+ * @param value the y position between 0 and 1 to solve for x
+ * @return the closest approximate input that will produce value when provided
+ * to the valueAt function.
+ */
+ @Override
+ public float tAt(float value) {
+ return mXFn.value(solve(value, mYFn));
+ }
+
+ private float solve(float target, DerivableFloatFn fn) {
+ // For a linear fn, t = value. This makes value a good starting guess.
+ float input = target;
+
+ // Newton's method (Faster than bisection)
+ for (int i = 0; i < 8; i++) {
+ float value = fn.value(input) - target;
+ if (Math.abs(value) < EPSILON) {
+ return input;
+ }
+ float derivative = fn.derivative(input);
+ if (Math.abs(derivative) < EPSILON) {
+ break;
+ }
+ input = input - value / derivative;
+ }
+
+ // Fallback on bi-section
+ float min = 0.0f;
+ float max = 1.0f;
+ input = target;
+
+ if (input < min) {
+ return min;
+ }
+ if (input > max) {
+ return max;
+ }
+
+ while (min < max) {
+ float value = fn.value(input);
+ if (Math.abs(value - target) < EPSILON) {
+ return input;
+ }
+
+ if (target > value) {
+ min = input;
+ } else {
+ max = input;
+ }
+
+ input = (max - min) * .5f + min;
+ }
+
+ // Give up, return the closest match we got too.
+ return input;
+ }
+
+ private interface DerivableFloatFn {
+ float value(float x);
+ float derivative(float x);
+ }
+
+ /**
+ * Precomputed constants for a given set of control points along a given
+ * cubic bezier axis.
+ */
+ private static class CubicBezierFn implements DerivableFloatFn {
+ private final float c;
+ private final float a;
+ private final float b;
+
+ /**
+ * Build and pre-compute a single axis for a unit bezier. This assumes p0
+ * is 0 and p1 is 1.
+ *
+ * @param c0 start control point.
+ * @param c1 end control point.
+ */
+ public CubicBezierFn(float c0, float c1) {
+ c = 3.0f * c0;
+ b = 3.0f * (c1 - c0) - c;
+ a = 1.0f - c - b;
+ }
+
+ public float value(float x) {
+ return ((a * x + b) * x + c) * x;
+ }
+ public float derivative(float x) {
+ return (3.0f * a * x + 2.0f * b) * x + c;
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/motion/UnitCurve.java b/src/com/android/camera/ui/motion/UnitCurve.java
new file mode 100644
index 000000000..d89f1fa4d
--- /dev/null
+++ b/src/com/android/camera/ui/motion/UnitCurve.java
@@ -0,0 +1,41 @@
+/*
+ * 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.motion;
+
+/**
+ * Simple functions that produce values along a curve for any given input and can compute input
+ * times for a given output value.
+ */
+public interface UnitCurve {
+
+ /**
+ * Produce a unit value of this curve at time t. The function should always return a valid
+ * return value for any valid t input.
+ *
+ * @param t ratio of time passed from (0..1)
+ * @return the unit value at t.
+ */
+ float valueAt(float t);
+
+ /**
+ * If possible, find a value for t such that valueAt(t) == value or best guess.
+ *
+ * @param value to match to the output of valueAt(t)
+ * @return t where valueAt(t) == value or throw.
+ */
+ float tAt(float value);
+}
diff --git a/src/com/android/camera/ui/motion/UnitCurves.java b/src/com/android/camera/ui/motion/UnitCurves.java
new file mode 100644
index 000000000..a1117fa96
--- /dev/null
+++ b/src/com/android/camera/ui/motion/UnitCurves.java
@@ -0,0 +1,44 @@
+/*
+ * 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.motion;
+
+/**
+ * Predefined material curves and animations.
+ */
+public class UnitCurves {
+ public static final UnitCurve FAST_OUT_SLOW_IN = new UnitBezier(0.4f, 0.0f, 0.2f, 1.0f);
+ public static final UnitCurve LINEAR_OUT_SLOW_IN = new UnitBezier(0.0f, 0.0f, 0.2f, 1.0f);
+ public static final UnitCurve FAST_OUT_LINEAR_IN = new UnitBezier(0.4f, 0.0f, 1.0f, 1.0f);
+ public static final UnitCurve LINEAR = new UnitBezier(0.0f, 0.0f, 1.0f, 1.0f);
+
+ /**
+ * Given two curves (from and to) and a time along the from curve, compute
+ * the time at t, and find a t along the 'toCurve' that will produce the
+ * same output. This is useful when interpolating between two different curves
+ * when the animation is not at the beginning or end.
+ *
+ * @param enterCurve the curve to compute the value from
+ * @param exitCurve the curve to find a time t on that matches output of
+ * enterCurve at T.
+ * @param t the time at which to compute the value (0..1)
+ * @return the time along the exitCurve.
+ */
+ public static float mapEnterCurveToExitCurveAtT(UnitCurve enterCurve, UnitCurve exitCurve,
+ float t) {
+ return exitCurve.tAt(1 - enterCurve.valueAt(t));
+ }
+}
diff --git a/src/com/android/camera/util/CameraUtil.java b/src/com/android/camera/util/CameraUtil.java
index 6765d2027..d508c7b33 100644
--- a/src/com/android/camera/util/CameraUtil.java
+++ b/src/com/android/camera/util/CameraUtil.java
@@ -767,7 +767,7 @@ public class CameraUtil {
+ "," + rect.right + "," + rect.bottom + ")");
}
- public static void rectFToRect(RectF rectF, Rect rect) {
+ 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);
@@ -776,7 +776,7 @@ public class CameraUtil {
public static Rect rectFToRect(RectF rectF) {
Rect rect = new Rect();
- rectFToRect(rectF, rect);
+ inlineRectToRectF(rectF, rect);
return rect;
}
@@ -796,21 +796,6 @@ public class CameraUtil {
matrix.postTranslate(viewWidth / 2f, viewHeight / 2f);
}
- public static void prepareMatrix(Matrix matrix, boolean mirror, int displayOrientation,
- Rect previewRect) {
- // Need mirror for front camera.
- matrix.setScale(mirror ? -1 : 1, 1);
- // This is the value for android.hardware.Camera.setDisplayOrientation.
- matrix.postRotate(displayOrientation);
-
- // Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
- // We need to map camera driver coordinates to preview rect coordinates
- Matrix mapping = new Matrix();
- mapping.setRectToRect(new RectF(-1000, -1000, 1000, 1000), rectToRectF(previewRect),
- Matrix.ScaleToFit.FILL);
- matrix.setConcat(mapping, matrix);
- }
-
public static String createJpegName(long dateTaken, boolean refocus) {
synchronized (sImageFileNamer) {
return sImageFileNamer.generateName(dateTaken, refocus);