summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Rohde <codelogic@google.com>2014-12-05 12:17:15 -0800
committerMichael Bestas <mkbestas@lineageos.org>2019-11-09 22:15:24 +0200
commitae159942ae27f5798051851160b9bae652f5a118 (patch)
treef2209f9284653a277eaff160c7a60e72c50998a6
parent70e618bdd825eb8b088d66feeefc0a7f0b06c7ed (diff)
downloadandroid_packages_apps_Snap-ae159942ae27f5798051851160b9bae652f5a118.zip
android_packages_apps_Snap-ae159942ae27f5798051851160b9bae652f5a118.tar.gz
android_packages_apps_Snap-ae159942ae27f5798051851160b9bae652f5a118.tar.bz2
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
-rwxr-xr-xres/layout/capture_module.xml4
-rwxr-xr-xres/layout/photo_module.xml4
-rwxr-xr-xres/layout/video_module.xml5
-rw-r--r--res/raw/material_camera_focus.oggbin0 -> 19122 bytes
-rw-r--r--res/values/colors.xml1
-rwxr-xr-xres/values/dimens.xml4
-rwxr-xr-xsrc/com/android/camera/CaptureModule.java15
-rwxr-xr-xsrc/com/android/camera/CaptureUI.java93
-rw-r--r--src/com/android/camera/FocusOverlayManager.java152
-rw-r--r--src/com/android/camera/FocusStateListener.java14
-rwxr-xr-xsrc/com/android/camera/PhotoModule.java30
-rwxr-xr-xsrc/com/android/camera/PhotoUI.java67
-rw-r--r--src/com/android/camera/SoundPlayer.java95
-rw-r--r--src/com/android/camera/VideoModule.java3
-rwxr-xr-xsrc/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.java128
-rw-r--r--src/com/android/camera/ui/focus/FocusRing.java72
-rw-r--r--src/com/android/camera/ui/focus/FocusRingRenderer.java237
-rw-r--r--src/com/android/camera/ui/focus/FocusRingView.java211
-rw-r--r--src/com/android/camera/ui/focus/FocusSound.java47
-rw-r--r--src/com/android/camera/ui/focus/LensRangeCalculator.java71
-rw-r--r--src/com/android/camera/ui/focus/ManualFocusRing.java93
-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
-rwxr-xr-xsrc/com/android/camera/util/CameraUtil.java19
38 files changed, 2156 insertions, 352 deletions
diff --git a/res/layout/capture_module.xml b/res/layout/capture_module.xml
index b7be0b3..8600ddd 100755
--- a/res/layout/capture_module.xml
+++ b/res/layout/capture_module.xml
@@ -158,6 +158,10 @@
android:layout_height="200dip"
android:layout_marginTop="50dip"
android:layout_marginLeft="15dip" />
+ <com.android.camera.ui.focus.FocusRingView
+ android:id="@+id/focus_ring"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
</FrameLayout>
<com.android.camera.ui.RenderOverlay
diff --git a/res/layout/photo_module.xml b/res/layout/photo_module.xml
index 904b154..ba57587 100755
--- a/res/layout/photo_module.xml
+++ b/res/layout/photo_module.xml
@@ -58,6 +58,10 @@
android:layout_marginTop="50dip"
android:layout_marginLeft="15dip" />
</RelativeLayout>
+ <com.android.camera.ui.focus.FocusRingView
+ android:id="@+id/focus_ring"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
<com.android.camera.ui.RotateImageView
android:id="@+id/review_image"
android:layout_width="match_parent"
diff --git a/res/layout/video_module.xml b/res/layout/video_module.xml
index 56f5857..0d8c4cb 100755
--- a/res/layout/video_module.xml
+++ b/res/layout/video_module.xml
@@ -97,6 +97,11 @@
android:onClick="onReviewPlayClicked"/>
</FrameLayout>
+ <com.android.camera.ui.focus.FocusRingView
+ android:id="@+id/focus_ring"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
<include layout="@layout/camera_controls"
android:layout_gravity="center"
style="@style/CameraControls"/>
diff --git a/res/raw/material_camera_focus.ogg b/res/raw/material_camera_focus.ogg
new file mode 100644
index 0000000..555d7f4
--- /dev/null
+++ b/res/raw/material_camera_focus.ogg
Binary files differ
diff --git a/res/values/colors.xml b/res/values/colors.xml
index ee79867..719ef77 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -68,6 +68,7 @@
<color name="face_detect_start">@color/white</color>
<color name="face_detect_success">@color/green</color>
<color name="face_detect_fail">@color/red</color>
+ <color name="focus_color">#ffffffff</color>
<color name="gray">@color/grey</color>
<color name="setting_list_selected">#40fafafa</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 210b473..29fbbe5 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -93,6 +93,10 @@
<dimen name="toggle_size">30dp</dimen>
<dimen name="toggle_outer_size">60dp</dimen>
<dimen name="face_circle_stroke">2dip</dimen>
+ <dimen name="focus_circle_stroke">1.25dp</dimen>
+ <dimen name="focus_circle_min_size">24dp</dimen>
+ <dimen name="focus_circle_max_size">96dp</dimen>
+ <dimen name="focus_circle_initial_size">48dp</dimen>
<dimen name="zoom_font_size">14pt</dimen>
<dimen name="shutter_offset">11dp</dimen>
<dimen name="shutter_offset_top">2dp</dimen>
diff --git a/src/com/android/camera/CaptureModule.java b/src/com/android/camera/CaptureModule.java
index 591d82d..11017f7 100755
--- a/src/com/android/camera/CaptureModule.java
+++ b/src/com/android/camera/CaptureModule.java
@@ -120,6 +120,7 @@ import com.android.camera.imageprocessor.filter.SharpshooterFilter;
import com.android.camera.imageprocessor.filter.StillmoreFilter;
import com.android.camera.imageprocessor.filter.UbifocusFilter;
import com.android.camera.ui.CountDownView;
+import com.android.camera.ui.focus.FocusRing;
import com.android.camera.ui.ModuleSwitcher;
import com.android.camera.ui.ProMode;
import com.android.camera.ui.RotateTextToast;
@@ -2930,7 +2931,7 @@ public class CaptureModule implements CameraModule, PhotoController,
@Override
public void run() {
if (mUI.getCurrentProMode() != ProMode.MANUAL_MODE)
- mUI.clearFocus();
+ mUI.getFocusRing().stopFocusAnimations();
}
});
}
@@ -3919,11 +3920,11 @@ public class CaptureModule implements CameraModule, PhotoController,
int[] newXY = {x, y};
if (mUI.isOverControlRegion(newXY)) return;
if (!mUI.isOverSurfaceView(newXY)) return;
- mUI.setFocusPosition(x, y);
+ mUI.getFocusRing().startActiveFocus();
+ mUI.getFocusRing().setFocusLocation(x, y);
x = newXY[0];
y = newXY[1];
mInTAF = true;
- mUI.onFocusStarted();
if (isBackCamera()) {
switch (getCameraMode()) {
case DUAL_MODE:
@@ -4426,7 +4427,7 @@ public class CaptureModule implements CameraModule, PhotoController,
return;
}
- mUI.clearFocus();
+ mUI.getFocusRing().stopFocusAnimations();
mUI.resetPauseButton();
mRecordingTotalTime = 0L;
mRecordingStartTime = SystemClock.uptimeMillis();
@@ -4495,7 +4496,7 @@ public class CaptureModule implements CameraModule, PhotoController,
mHandler.post(new Runnable() {
@Override
public void run() {
- mUI.clearFocus();
+ mUI.getFocusRing().stopFocusAnimations();
mUI.resetPauseButton();
mRecordingTotalTime = 0L;
mRecordingStartTime = SystemClock.uptimeMillis();
@@ -4672,7 +4673,7 @@ public class CaptureModule implements CameraModule, PhotoController,
try {
setUpMediaRecorder(cameraId);
- mUI.clearFocus();
+ mUI.getFocusRing().stopFocusAnimations();
mUI.hideUIwhileRecording();
mCameraHandler.removeMessages(CANCEL_TOUCH_FOCUS, mCameraId[cameraId]);
if (isAbortCapturesEnable() && mCaptureSession[cameraId] != null) {
@@ -4772,7 +4773,7 @@ public class CaptureModule implements CameraModule, PhotoController,
startRecordingFailed();
return;
}
- mUI.clearFocus();
+ mUI.getFocusRing().stopFocusAnimations();
mUI.resetPauseButton();
mRecordingTotalTime = 0L;
mRecordingStartTime = SystemClock.uptimeMillis();
diff --git a/src/com/android/camera/CaptureUI.java b/src/com/android/camera/CaptureUI.java
index 9032069..ef0e5fd 100755
--- a/src/com/android/camera/CaptureUI.java
+++ b/src/com/android/camera/CaptureUI.java
@@ -75,7 +75,7 @@ import com.android.camera.ui.CameraControls;
import com.android.camera.ui.OneUICameraControls;
import com.android.camera.ui.CountDownView;
import com.android.camera.ui.FlashToggleButton;
-import com.android.camera.ui.FocusIndicator;
+import com.android.camera.ui.focus.FocusRing;
import com.android.camera.ui.PieRenderer;
import com.android.camera.ui.ProMode;
import com.android.camera.ui.RenderOverlay;
@@ -94,8 +94,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
-public class CaptureUI implements FocusOverlayManager.FocusUI,
- PreviewGestures.SingleTapListener,
+public class CaptureUI implements PreviewGestures.SingleTapListener,
CameraManager.CameraFaceDetectionCallback,
SettingsManager.Listener,
PauseButton.OnPauseButtonListener {
@@ -108,6 +107,7 @@ public class CaptureUI implements FocusOverlayManager.FocusUI,
private static final int CLICK_THRESHOLD = 200;
private static final int AUTOMATIC_MODE = 0;
private static final String[] AWB_INFO_TITLE = {" R gain "," G gain "," B gain "," CCT "};
+ private final FocusRing mFocusRing;
private CameraActivity mActivity;
private View mRootView;
private View mPreviewCover;
@@ -306,6 +306,8 @@ public class CaptureUI implements FocusOverlayManager.FocusUI,
mSurfaceHolderMono = mSurfaceViewMono.getHolder();
mSurfaceHolderMono.addCallback(callbackMono);
+ mFocusRing = (FocusRing) mRootView.findViewById(R.id.focus_ring);
+
mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay);
mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button);
mVideoButton = (ImageView) mRootView.findViewById(R.id.video_button);
@@ -761,7 +763,6 @@ public class CaptureUI implements FocusOverlayManager.FocusUI,
mIsVideoUI) {
return;
}
- clearFocus();
removeFilterMenu(false);
Intent intent = new Intent(mActivity, SettingsActivity.class);
mActivity.startActivity(intent);
@@ -807,7 +808,6 @@ public class CaptureUI implements FocusOverlayManager.FocusUI,
mSceneModeSwitcher.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- clearFocus();
removeFilterMenu(false);
Intent intent = new Intent(mActivity, SceneModeActivity.class);
intent.putExtra(CameraUtil.KEY_IS_SECURE_CAMERA, mActivity.isSecureCamera());
@@ -1002,7 +1002,6 @@ public class CaptureUI implements FocusOverlayManager.FocusUI,
}
}
-
public void resetTrackingFocus() {
if(mModule.isTrackingFocusSettingOn()) {
mTrackingFocusRenderer.setVisible(false);
@@ -1112,10 +1111,8 @@ public class CaptureUI implements FocusOverlayManager.FocusUI,
mSettingsManager.setValueIndex(SettingsManager
.KEY_COLOR_EFFECT, j);
for (View v1 : views) {
- v1.setBackground(null);
+ v1.setActivated(v1 == v);
}
- ImageView image = (ImageView) v.findViewById(R.id.image);
- image.setBackgroundColor(HIGHLIGHT_COLOR);
}
}
return true;
@@ -1123,10 +1120,8 @@ public class CaptureUI implements FocusOverlayManager.FocusUI,
});
views[j] = imageView;
- if (i == init)
- imageView.setBackgroundColor(HIGHLIGHT_COLOR);
+ imageView.setActivated(i == init);
TextView label = (TextView) filterBox.findViewById(R.id.label);
-
imageView.setImageResource(thumbnails[i]);
label.setText(entries[i]);
gridLayout.addView(filterBox);
@@ -1141,7 +1136,7 @@ public class CaptureUI implements FocusOverlayManager.FocusUI,
public void animateFadeIn(View v) {
ViewPropertyAnimator vp = v.animate();
- vp.alpha(0.85f).setDuration(ANIMATION_DURATION);
+ vp.alpha(1f).setDuration(ANIMATION_DURATION);
vp.start();
}
@@ -1282,7 +1277,6 @@ public class CaptureUI implements FocusOverlayManager.FocusUI,
if (!mUIhidden)
return;
mUIhidden = false;
- mPieRenderer.setBlockFocus(false);
mCameraControls.showUI();
}
@@ -1290,7 +1284,6 @@ public class CaptureUI implements FocusOverlayManager.FocusUI,
if (mUIhidden)
return;
mUIhidden = true;
- mPieRenderer.setBlockFocus(true);
mCameraControls.hideUI();
}
@@ -1550,74 +1543,8 @@ public class CaptureUI implements FocusOverlayManager.FocusUI,
mCameraControls.showRefocusToast(show);
}
- private FocusIndicator getFocusIndicator() {
- if (mModule.isTrackingFocusSettingOn()) {
- if (mPieRenderer != null) {
- mPieRenderer.clear();
- }
- return mTrackingFocusRenderer;
- }
- FocusIndicator focusIndicator;
- if (mFaceView != null && mFaceView.faceExists() && !mIsTouchAF) {
- if (mPieRenderer != null) {
- mPieRenderer.clear();
- }
- focusIndicator = mFaceView;
- } else {
- focusIndicator = mPieRenderer;
- }
-
- return focusIndicator;
- }
-
- @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();
- mIsTouchAF = false;
- }
-
- @Override
- public void setFocusPosition(int x, int y) {
- mPieRenderer.setFocus(x, y);
- mIsTouchAF = true;
- }
-
- @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() {
-
- }
-
- @Override
- public void resumeFaceDetection() {
+ public FocusRing getFocusRing() {
+ return mFocusRing;
}
public void onStartFaceDetection(int orientation, boolean mirror, Rect cameraBound,
diff --git a/src/com/android/camera/FocusOverlayManager.java b/src/com/android/camera/FocusOverlayManager.java
index 34a8521..1b4bed1 100644
--- a/src/com/android/camera/FocusOverlayManager.java
+++ b/src/com/android/camera/FocusOverlayManager.java
@@ -16,15 +16,12 @@
package com.android.camera;
-import android.annotation.TargetApi;
import android.content.Context;
-import android.graphics.Matrix;
import android.graphics.Point;
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;
@@ -32,6 +29,8 @@ import android.util.Log;
import org.codeaurora.snapcam.R;
import com.android.camera.app.CameraApp;
+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;
@@ -65,6 +64,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.
@@ -78,7 +80,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;
@@ -96,20 +98,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
private Point mDispSize;
private int mBottomMargin;
@@ -143,15 +134,13 @@ public class FocusOverlayManager {
public FocusOverlayManager(ComboPreferences preferences, String[] defaultFocusModes,
Parameters parameters, Listener listener,
- boolean mirror, Looper looper, FocusUI ui, CameraActivity activity) {
+ boolean mirror, Looper looper, FocusRing focusRing, CameraActivity activity) {
mHandler = new MainHandler(looper);
- mMatrix = new Matrix();
mPreferences = preferences;
mDefaultFocusModes = defaultFocusModes;
setParameters(parameters);
mListener = listener;
setMirror(mirror);
- mUI = ui;
mDispSize = new Point();
activity.getWindowManager().getDefaultDisplay().getRealSize(mDispSize);
Context context = CameraApp.getContext();
@@ -159,10 +148,11 @@ public class FocusOverlayManager {
context.getResources().getDimensionPixelSize(R.dimen.preview_bottom_margin);
mTopMargin =
context.getResources().getDimensionPixelSize(R.dimen.preview_top_margin);
+ mFocusRing = focusRing;
}
- public void setPhotoUI(FocusUI ui) {
- mUI = ui;
+ public void setFocusRing(FocusRing focusRing) {
+ mFocusRing = focusRing;
}
public void setParameters(Parameters parameters) {
@@ -188,35 +178,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");
}
}
@@ -307,7 +290,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
@@ -323,7 +305,6 @@ public class FocusOverlayManager {
} else {
mState = STATE_FAIL;
}
- updateFocusUI();
// If this is triggered by touch focus, cancel focus after a
// while.
if (mFocusArea != null) {
@@ -343,32 +324,31 @@ public class FocusOverlayManager {
if (!mInitialized) return;
- // Ignore if the camera has detected some faces.
- if (mUI.hasFaces()) {
- mUI.clearFocus();
- if (mIsAFRunning) {
- 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>();
@@ -376,10 +356,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>();
@@ -389,7 +368,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() {
@@ -421,8 +400,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;
@@ -441,7 +420,6 @@ public class FocusOverlayManager {
if (mFocusAreaSupported) {
autoFocus();
} else { // Just show the indicator in all other cases.
- updateFocusUI();
mHandler.removeMessages(RESET_TOUCH_FOCUS);
mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, mFocusTime);
}
@@ -455,7 +433,6 @@ public class FocusOverlayManager {
// If auto focus was in progress, it would have been stopped.
mState = STATE_IDLE;
resetTouchFocus();
- updateFocusUI();
}
public void onCameraReleased() {
@@ -467,7 +444,8 @@ public class FocusOverlayManager {
Log.v(TAG, "Start autofocus.");
mListener.autoFocus();
mState = STATE_FOCUSING;
- updateFocusUI();
+ // Pause the face view because the driver will keep sending face
+ // callbacks after the focus completes.
mHandler.removeMessages(RESET_TOUCH_FOCUS);
}
@@ -480,9 +458,7 @@ public class FocusOverlayManager {
resetTouchFocus();
setAeAwbLock(false);
mListener.cancelAutoFocus();
- mUI.resumeFaceDetection();
mState = STATE_IDLE;
- updateFocusUI();
mHandler.removeMessages(RESET_TOUCH_FOCUS);
}
@@ -552,41 +528,9 @@ public class FocusOverlayManager {
}
}
- 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;
@@ -601,16 +545,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/FocusStateListener.java b/src/com/android/camera/FocusStateListener.java
index 6c536c5..459e4ec 100644
--- a/src/com/android/camera/FocusStateListener.java
+++ b/src/com/android/camera/FocusStateListener.java
@@ -44,31 +44,31 @@ public class FocusStateListener {
switch (focusState) {
case CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN:
Log.d(TAG, "CONTROL_AF_STATE_ACTIVE_SCAN onFocusStarted");
- mUI.onFocusStarted();
+ mUI.getFocusRing().startActiveFocus();
break;
case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED:
Log.d(TAG, "CONTROL_AF_STATE_FOCUSED_LOCKED onFocusSucceeded");
- mUI.onFocusSucceeded(false);
+ mUI.getFocusRing().stopFocusAnimations();
break;
case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED:
Log.d(TAG, "CONTROL_AF_STATE_NOT_FOCUSED_LOCKED onFocusFailed");
- mUI.onFocusFailed(false);
+ mUI.getFocusRing().stopFocusAnimations();
break;
case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED:
Log.d(TAG, "CONTROL_AF_STATE_PASSIVE_FOCUSED onFocusSucceeded");
- mUI.onFocusSucceeded(true);
+ mUI.getFocusRing().stopFocusAnimations();
break;
case CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN:
Log.d(TAG, "CONTROL_AF_STATE_PASSIVE_SCAN onFocusStarted");
- mUI.onFocusStarted();
+ mUI.getFocusRing().startPassiveFocus();
break;
case CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED:
Log.d(TAG, "CONTROL_AF_STATE_PASSIVE_UNFOCUSED onFocusFailed");
- mUI.onFocusFailed(true);
+ mUI.getFocusRing().stopFocusAnimations();
break;
case CaptureResult.CONTROL_AF_STATE_INACTIVE:
Log.d(TAG, "CONTROL_AF_STATE_INACTIVE clearFocus");
- mUI.clearFocus();
+ mUI.getFocusRing().stopFocusAnimations();
break;
}
}
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
index 35bca15..08b0058 100755
--- a/src/com/android/camera/PhotoModule.java
+++ b/src/com/android/camera/PhotoModule.java
@@ -741,7 +741,7 @@ public class PhotoModule
Log.v(TAG, "onCameraOpened");
openCameraCommon();
resizeForPreviewAspectRatio();
- updateFocusManager(mUI);
+ mFocusManager.setFocusRing(mUI.getFocusRing());
}
private void switchCamera() {
@@ -765,7 +765,6 @@ public class PhotoModule
}
closeCamera();
mUI.collapseCameraControls();
- mUI.clearFaces();
if (mFocusManager != null) mFocusManager.removeMessages();
// Restart the camera and initialize the UI. From onCreate.
@@ -1031,7 +1030,6 @@ public class PhotoModule
if (mParameters.getMaxNumDetectedFaces() > 0) {
mFaceDetectionStarted = false;
mCameraDevice.setFaceDetectionCallback(null, null);
- mUI.pauseFaceDetection();
mCameraDevice.stopFaceDetection();
mUI.onStopFaceDetection();
}
@@ -1264,8 +1262,6 @@ public class PhotoModule
return;
}
- mFocusManager.updateFocusUI(); // Ensure focus indicator is hidden.
-
String jpegFilePath = new String(jpegData);
mNamedImages.nameNewImage(mCaptureStartTime);
NamedEntity name = mNamedImages.getNextNameEntity();
@@ -1424,8 +1420,6 @@ public class PhotoModule
mCameraDevice.setLongshot(false);
}
- mFocusManager.updateFocusUI(); // Ensure focus indicator is hidden.
-
boolean needRestartPreview = !mIsImageCaptureIntent
&& !mPreviewRestartSupport
&& (mCameraState != LONGSHOT)
@@ -1475,7 +1469,6 @@ public class PhotoModule
CameraUtil.FOCUS_MODE_MW_CONTINUOUS_PICTURE.equals(focusMode)) {
mCameraDevice.cancelAutoFocus();
}
- mUI.resumeFaceDetection();
if (!mIsImageCaptureIntent) {
setCameraState(IDLE);
}
@@ -2817,28 +2810,12 @@ public class PhotoModule
if (mFocusManager == null) {
mFocusManager = new FocusOverlayManager(mPreferences, defaultFocusModes,
mInitialParams, this, mMirror,
- mActivity.getMainLooper(), mUI, mActivity);
+ mActivity.getMainLooper(), mUI != null ? mUI.getFocusRing() : null, mActivity);
}
}
}
}
- 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);
-
- View root = mUI.getRootView();
- // These depend on camera parameters.
- int width = root.getWidth();
- int height = root.getHeight();
- mFocusManager.setPreviewSize(width, height);
- }
- }
-
@Override
public void onConfigurationChanged(Configuration newConfig) {
Log.v(TAG, "onConfigurationChanged");
@@ -2912,7 +2889,6 @@ public class PhotoModule
}
// Check if metering area or focus area is supported.
if (!mFocusAreaSupported && !mMeteringAreaSupported) return;
- if (! mFocusManager.getPreviewRect().contains(x, y)) return;
mFocusManager.onSingleTapUp(x, y);
}
@@ -3806,7 +3782,7 @@ public class PhotoModule
if(!mFocusManager.getFocusMode(false).equals(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) &&
!mFocusManager.isFocusCompleted()) {
- mUI.clearFocus();
+ mUI.getFocusRing().stopFocusAnimations();
}
String bokehMode = mPreferences.getString(
diff --git a/src/com/android/camera/PhotoUI.java b/src/com/android/camera/PhotoUI.java
index b63e652..57c11aa 100755
--- a/src/com/android/camera/PhotoUI.java
+++ b/src/com/android/camera/PhotoUI.java
@@ -58,7 +58,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.TsMakeupManager.MakeupLevelListener;
import com.android.camera.ui.AbstractSettingPopup;
import com.android.camera.ui.CameraControls;
@@ -77,11 +76,11 @@ import com.android.camera.ui.RotateLayout;
import com.android.camera.ui.RotateTextToast;
import com.android.camera.ui.SelfieFlashView;
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,
CameraRootView.MyDisplayListener,
CameraManager.CameraFaceDetectionCallback {
@@ -89,6 +88,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;
@@ -291,6 +291,7 @@ public class PhotoUI implements PieListener,
mFaceView = (FaceView) mRootView.findViewById(R.id.face_view);
setSurfaceTextureSizeChangedListener(mFaceView);
}
+ mFocusRing = (FocusRing) mRootView.findViewById(R.id.focus_ring);
mAnimationManager = new AnimationManager();
mOrientationResize = false;
mPrevOrientationResize = false;
@@ -543,8 +544,9 @@ public class PhotoUI implements PieListener,
@Override
public void onClick(View v) {
if (!CameraControls.isAnimating()
- && mController.getCameraState() != PhotoController.SNAPSHOT_IN_PROGRESS)
+ && mController.getCameraState() != PhotoController.SNAPSHOT_IN_PROGRESS) {
mActivity.gotoGallery();
+ }
}
});
}
@@ -948,7 +950,6 @@ public class PhotoUI implements PieListener,
CameraUtil.fadeIn(mReviewRetakeButton);
setOrientation(mOrientation, true);
mMenu.hideTopMenu(true);
- pauseFaceDetection();
}
protected void hidePostCaptureAlert() {
@@ -964,7 +965,6 @@ public class PhotoUI implements PieListener,
CameraUtil.fadeOut(mReviewDoneButton);
mShutterButton.setVisibility(View.VISIBLE);
CameraUtil.fadeOut(mReviewRetakeButton);
- resumeFaceDetection();
}
public void setDisplayOrientation(int orientation) {
@@ -1171,61 +1171,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 = mPieRenderer;
- if (hasFaces()) {
- mFaceView.showStart();
- }
- 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 0000000..ff3f37f
--- /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 12e0454..71ded05 100644
--- a/src/com/android/camera/VideoModule.java
+++ b/src/com/android/camera/VideoModule.java
@@ -1481,7 +1481,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);
+ mActivity.getMainLooper(), mUI.getFocusRing(), mActivity);
}
}
@@ -3405,7 +3405,6 @@ public class VideoModule implements CameraModule,
if (mParameters.getMaxNumDetectedFaces() > 0) {
mFaceDetectionStarted = false;
mCameraDevice.setFaceDetectionCallback(null, null);
- mUI.pauseFaceDetection();
mCameraDevice.stopFaceDetection();
mUI.onStopFaceDetection();
}
diff --git a/src/com/android/camera/VideoUI.java b/src/com/android/camera/VideoUI.java
index 3e24f85..66e73c6 100755
--- a/src/com/android/camera/VideoUI.java
+++ b/src/com/android/camera/VideoUI.java
@@ -48,7 +48,6 @@ import android.view.View.OnLayoutChangeListener;
import com.android.camera.CameraManager.CameraProxy;
import com.android.camera.CameraPreference.OnPreferenceChangedListener;
-import com.android.camera.FocusOverlayManager.FocusUI;
import com.android.camera.PhotoUI.SurfaceTextureSizeChangedListener;
import com.android.camera.ui.AbstractSettingPopup;
import com.android.camera.ui.CameraControls;
@@ -63,17 +62,18 @@ import com.android.camera.ui.RotateImageView;
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,
CameraManager.CameraFaceDetectionCallback{
private static final String TAG = "CAM_VideoUI";
// module fields
+ private final FocusRing mFocusRing;
private CameraActivity mActivity;
private View mRootView;
private SurfaceHolder mSurfaceHolder;
@@ -246,6 +246,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);
@@ -1241,14 +1242,6 @@ public class VideoUI implements PieRenderer.PieListener,
mFaceView.setFaces(faces);
}
- public void pauseFaceDetection() {
- if (mFaceView != null) mFaceView.pause();
- }
-
- public void resumeFaceDetection() {
- if (mFaceView != null) mFaceView.resume();
- }
-
public void onStartFaceDetection(int orientation, boolean mirror) {
mFaceView.setBlockDraw(false);
mFaceView.clear();
@@ -1265,39 +1258,7 @@ public class VideoUI implements PieRenderer.PieListener,
}
}
- // 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);
+ 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 0000000..87c8c0c
--- /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 0000000..7fdb3ec
--- /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 0000000..ff09ebc
--- /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 0000000..809503c
--- /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 0000000..a0c1034
--- /dev/null
+++ b/src/com/android/camera/ui/focus/FocusController.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera.ui.focus;
+
+import android.graphics.RectF;
+import android.util.Log;
+
+import com.android.camera.async.MainThread;
+import com.android.camera.ui.motion.LinearScale;
+
+/**
+ * The focus controller interacts with the focus ring UI element.
+ */
+public class FocusController {
+ private static final String TAG = "FocusController";
+
+ private final FocusRing mFocusRing;
+ private final FocusSound mFocusSound;
+ private final MainThread mMainThread;
+
+ public FocusController(FocusRing focusRing, FocusSound focusSound, MainThread mainThread) {
+ mFocusRing = focusRing;
+ mFocusSound = focusSound;
+ mMainThread = mainThread;
+ }
+
+ /**
+ * Show a passive focus animation at the center of the active area.
+ * This will likely be different than the view bounds due to varying image
+ * ratios and dimensions.
+ */
+ public void showPassiveFocusAtCenter() {
+ mMainThread.execute(new Runnable() {
+ @Override
+ public void run() {
+ Log.v(TAG, "Running showPassiveFocusAtCenter()");
+ mFocusRing.startPassiveFocus();
+ mFocusRing.centerFocusLocation();
+ }
+ });
+ }
+
+ /**
+ * Show a passive focus animation at the given viewX and viewY position.
+ * This is usually indicates the camera subsystem kicked off an auto-focus
+ * at the given screen position.
+ *
+ * @param viewX the view's x coordinate
+ * @param viewY the view's y coordinate
+ */
+ public void showPassiveFocusAt(final int viewX, final int viewY) {
+ mMainThread.execute(new Runnable() {
+ @Override
+ public void run() {
+ Log.v(TAG, "Running showPassiveFocusAt(" + viewX + ", " + viewY + ")");
+ mFocusRing.startPassiveFocus();
+ mFocusRing.setFocusLocation(viewX, viewY);
+ }
+ });
+ }
+
+ /**
+ * Show an active focus animation at the given viewX and viewY position.
+ * This is normally initiated by the user touching the screen at a given
+ * point.
+ *
+ * @param viewX the view's x coordinate
+ * @param viewY the view's y coordinate
+ */
+ public void showActiveFocusAt(final int viewX, final int viewY) {
+ mMainThread.execute(new Runnable() {
+ @Override
+ public void run() {
+ Log.v(TAG, "showActiveFocusAt(" + viewX + ", " + viewY + ")");
+ mFocusRing.startActiveFocus();
+ mFocusRing.setFocusLocation(viewX, viewY);
+
+ // TODO: Enable focus sound when better audio controls exist.
+ // mFocusSound.play();
+ }
+ });
+ }
+
+ /**
+ * Computing the correct location for the focus ring requires knowing
+ * the screen position and size of the preview area so the drawing
+ * operations can be clipped correctly.
+ */
+ public void configurePreviewDimensions(final RectF previewArea) {
+ mMainThread.execute(new Runnable() {
+ @Override
+ public void run() {
+ Log.v(TAG, "configurePreviewDimensions(" + previewArea + ")");
+ mFocusRing.configurePreviewDimensions(previewArea);
+ }
+ });
+ }
+
+ /**
+ * Set the radius of the focus ring as a radius between 0 and 1.
+ * This will map to the min and max values computed for the UI.
+ */
+ public void setFocusRatio(final float ratio) {
+ mMainThread.execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mFocusRing.isPassiveFocusRunning() ||
+ mFocusRing.isActiveFocusRunning()) {
+ mFocusRing.setRadiusRatio(ratio);
+ }
+ }
+ });
+ }
+}
diff --git a/src/com/android/camera/ui/focus/FocusRing.java b/src/com/android/camera/ui/focus/FocusRing.java
new file mode 100644
index 0000000..89de357
--- /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 0000000..264af2a
--- /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 0000000..14a7f6c
--- /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 0000000..c3ff010
--- /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 0000000..ef9cbae
--- /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 0000000..0133d8e
--- /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 0000000..d2504de
--- /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 0000000..84cbfa6
--- /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 0000000..57d5a10
--- /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 0000000..542ac1e
--- /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 0000000..3c3cd53
--- /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 0000000..84114cb
--- /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 0000000..fdb5487
--- /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 0000000..5886f68
--- /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 0000000..242f545
--- /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 0000000..d89f1fa
--- /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 0000000..a1117fa
--- /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 18ec724..70d5910 100755
--- a/src/com/android/camera/util/CameraUtil.java
+++ b/src/com/android/camera/util/CameraUtil.java
@@ -963,7 +963,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);
@@ -972,7 +972,7 @@ public class CameraUtil {
public static Rect rectFToRect(RectF rectF) {
Rect rect = new Rect();
- rectFToRect(rectF, rect);
+ inlineRectToRectF(rectF, rect);
return rect;
}
@@ -992,21 +992,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);