From ae159942ae27f5798051851160b9bae652f5a118 Mon Sep 17 00:00:00 2001 From: Paul Rohde Date: Fri, 5 Dec 2014 12:17:15 -0800 Subject: Drop new focus indicator into Camera2. * Create a new custom focus view that interacts with physical lens diopter changes. * Replace all occurances of the old focus indicator with the new one. Change-Id: Ia02646ce4d1eb059ecb8a1dfccc15dfc9c167e1b --- res/layout/capture_module.xml | 4 + res/layout/photo_module.xml | 4 + res/layout/video_module.xml | 5 + res/raw/material_camera_focus.ogg | Bin 0 -> 19122 bytes res/values/colors.xml | 1 + res/values/dimens.xml | 4 + src/com/android/camera/CaptureModule.java | 15 +- src/com/android/camera/CaptureUI.java | 93 +------- src/com/android/camera/FocusOverlayManager.java | 152 ++++--------- src/com/android/camera/FocusStateListener.java | 14 +- src/com/android/camera/PhotoModule.java | 30 +-- src/com/android/camera/PhotoUI.java | 67 +----- src/com/android/camera/SoundPlayer.java | 95 +++++++++ src/com/android/camera/VideoModule.java | 3 +- src/com/android/camera/VideoUI.java | 49 +---- src/com/android/camera/async/HandlerExecutor.java | 37 ++++ src/com/android/camera/async/MainThread.java | 47 ++++ src/com/android/camera/ui/focus/AutoFocusRing.java | 101 +++++++++ .../ui/focus/CameraCoordinateTransformer.java | 109 ++++++++++ .../android/camera/ui/focus/FocusController.java | 128 +++++++++++ src/com/android/camera/ui/focus/FocusRing.java | 72 +++++++ .../android/camera/ui/focus/FocusRingRenderer.java | 237 +++++++++++++++++++++ src/com/android/camera/ui/focus/FocusRingView.java | 211 ++++++++++++++++++ src/com/android/camera/ui/focus/FocusSound.java | 47 ++++ .../camera/ui/focus/LensRangeCalculator.java | 71 ++++++ .../android/camera/ui/focus/ManualFocusRing.java | 93 ++++++++ .../android/camera/ui/motion/AnimationClock.java | 39 ++++ src/com/android/camera/ui/motion/DampedSpring.java | 145 +++++++++++++ .../android/camera/ui/motion/DynamicAnimation.java | 41 ++++ .../android/camera/ui/motion/DynamicAnimator.java | 116 ++++++++++ .../android/camera/ui/motion/InterpolateUtils.java | 66 ++++++ .../camera/ui/motion/InterpolatorHelper.java | 38 ++++ src/com/android/camera/ui/motion/Invalidator.java | 28 +++ src/com/android/camera/ui/motion/LinearScale.java | 85 ++++++++ src/com/android/camera/ui/motion/UnitBezier.java | 157 ++++++++++++++ src/com/android/camera/ui/motion/UnitCurve.java | 41 ++++ src/com/android/camera/ui/motion/UnitCurves.java | 44 ++++ src/com/android/camera/util/CameraUtil.java | 19 +- 38 files changed, 2156 insertions(+), 352 deletions(-) create mode 100644 res/raw/material_camera_focus.ogg create mode 100644 src/com/android/camera/SoundPlayer.java create mode 100644 src/com/android/camera/async/HandlerExecutor.java create mode 100644 src/com/android/camera/async/MainThread.java create mode 100644 src/com/android/camera/ui/focus/AutoFocusRing.java create mode 100644 src/com/android/camera/ui/focus/CameraCoordinateTransformer.java create mode 100644 src/com/android/camera/ui/focus/FocusController.java create mode 100644 src/com/android/camera/ui/focus/FocusRing.java create mode 100644 src/com/android/camera/ui/focus/FocusRingRenderer.java create mode 100644 src/com/android/camera/ui/focus/FocusRingView.java create mode 100644 src/com/android/camera/ui/focus/FocusSound.java create mode 100644 src/com/android/camera/ui/focus/LensRangeCalculator.java create mode 100644 src/com/android/camera/ui/focus/ManualFocusRing.java create mode 100644 src/com/android/camera/ui/motion/AnimationClock.java create mode 100644 src/com/android/camera/ui/motion/DampedSpring.java create mode 100644 src/com/android/camera/ui/motion/DynamicAnimation.java create mode 100644 src/com/android/camera/ui/motion/DynamicAnimator.java create mode 100644 src/com/android/camera/ui/motion/InterpolateUtils.java create mode 100644 src/com/android/camera/ui/motion/InterpolatorHelper.java create mode 100644 src/com/android/camera/ui/motion/Invalidator.java create mode 100644 src/com/android/camera/ui/motion/LinearScale.java create mode 100644 src/com/android/camera/ui/motion/UnitBezier.java create mode 100644 src/com/android/camera/ui/motion/UnitCurve.java create mode 100644 src/com/android/camera/ui/motion/UnitCurves.java diff --git a/res/layout/capture_module.xml b/res/layout/capture_module.xml index b7be0b330..8600ddd73 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" /> + + + + diff --git a/res/raw/material_camera_focus.ogg b/res/raw/material_camera_focus.ogg new file mode 100644 index 000000000..555d7f444 Binary files /dev/null and b/res/raw/material_camera_focus.ogg differ diff --git a/res/values/colors.xml b/res/values/colors.xml index ee79867cb..719ef7723 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -68,6 +68,7 @@ @color/white @color/green @color/red + #ffffffff @color/grey #40fafafa diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 210b4739c..29fbbe561 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -93,6 +93,10 @@ 30dp 60dp 2dip + 1.25dp + 24dp + 96dp + 48dp 14pt 11dp 2dp diff --git a/src/com/android/camera/CaptureModule.java b/src/com/android/camera/CaptureModule.java index 591d82df7..11017f717 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 9032069ea..ef0e5fdbe 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 34a8521e2..1b4bed133 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(); @@ -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(); @@ -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 6c536c53b..459e4ec23 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 35bca15cc..08b0058e4 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 b63e6525f..57c11aa1b 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 000000000..ff3f37f7a --- /dev/null +++ b/src/com/android/camera/SoundPlayer.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera; + +import android.content.Context; +import android.media.AudioManager; +import android.media.SoundPool; +import android.util.SparseIntArray; + +import com.android.camera.util.ApiHelper; + +/** + * Loads a plays custom sounds. For playing system-standard sounds for various + * camera actions, please refer to {@link SoundClips}. + */ +public class SoundPlayer { + private final Context mAppContext; + private final SoundPool mSoundPool; + /** Keeps a mapping from sound resource ID to sound ID */ + private final SparseIntArray mResourceToSoundId = new SparseIntArray(); + private boolean mIsReleased = false; + + /** + * Construct a new sound player. + */ + public SoundPlayer(Context appContext) { + mAppContext = appContext; + final int audioType = getAudioTypeForSoundPool(); + mSoundPool = new SoundPool(1 /* max streams */, audioType, 0 /* quality */); + } + + /** + * Load the sound from a resource. + */ + public void loadSound(int resourceId) { + int soundId = mSoundPool.load(mAppContext, resourceId, 1/* priority */); + mResourceToSoundId.put(resourceId, soundId); + } + + /** + * Play the sound with the given resource. The resource has to be loaded + * before it can be played, otherwise an exception will be thrown. + */ + public void play(int resourceId, float volume) { + Integer soundId = mResourceToSoundId.get(resourceId); + if (soundId == null) { + throw new IllegalStateException("Sound not loaded. Must call #loadSound first."); + } + mSoundPool.play(soundId, volume, volume, 0 /* priority */, 0 /* loop */, 1 /* rate */); + } + + /** + * Unload the given sound if it's not needed anymore to release memory. + */ + public void unloadSound(int resourceId) { + Integer soundId = mResourceToSoundId.get(resourceId); + if (soundId == null) { + throw new IllegalStateException("Sound not loaded. Must call #loadSound first."); + } + mSoundPool.unload(soundId); + } + + /** + * Call this if you don't need the SoundPlayer anymore. All memory will be + * released and the object cannot be re-used. + */ + public void release() { + mIsReleased = true; + mSoundPool.release(); + } + + public boolean isReleased() { + return mIsReleased; + } + + private static int getAudioTypeForSoundPool() { + // STREAM_SYSTEM_ENFORCED is hidden API. + return ApiHelper.getIntFieldIfExists(AudioManager.class, + "STREAM_SYSTEM_ENFORCED", null, AudioManager.STREAM_RING); + } +} diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java index 12e045440..71ded0596 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 3e24f8510..66e73c60b 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 000000000..87c8c0ce0 --- /dev/null +++ b/src/com/android/camera/async/HandlerExecutor.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.async; + +import java.util.concurrent.Executor; + +import android.os.Handler; + +/** + * An {@link Executor} which posts to a {@link Handler}. + */ +public class HandlerExecutor implements Executor { + private final Handler mHandler; + + public HandlerExecutor(Handler handler) { + mHandler = handler; + } + + @Override + public void execute(Runnable runnable) { + mHandler.post(runnable); + } +} diff --git a/src/com/android/camera/async/MainThread.java b/src/com/android/camera/async/MainThread.java new file mode 100644 index 000000000..7fdb3ec9b --- /dev/null +++ b/src/com/android/camera/async/MainThread.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.async; + +import android.os.Handler; +import android.os.Looper; + +public class MainThread extends HandlerExecutor { + private MainThread(Handler handler) { + super(handler); + } + + public static MainThread create() { + return new MainThread(new Handler(Looper.getMainLooper())); + } + + /** + * Caches whether or not the current thread is the main thread. + */ + private static final ThreadLocal sIsMainThread = new ThreadLocal() { + @Override + protected Boolean initialValue() { + return Looper.getMainLooper().getThread() == Thread.currentThread(); + } + }; + + /** + * Returns true if the method is run on the main android thread. + */ + public static boolean isMainThread() { + return sIsMainThread.get(); + } +} diff --git a/src/com/android/camera/ui/focus/AutoFocusRing.java b/src/com/android/camera/ui/focus/AutoFocusRing.java new file mode 100644 index 000000000..ff09ebc65 --- /dev/null +++ b/src/com/android/camera/ui/focus/AutoFocusRing.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.focus; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.android.camera.ui.motion.InterpolateUtils; +import com.android.camera.ui.motion.Invalidator; + +/** + * Passive focus ring animation renderer. + */ +class AutoFocusRing extends FocusRingRenderer { + private static final String TAG = "AutoFocusRing"; + + /** + * The auto focus ring encapsulates the animation logic for visualizing + * a focus event when triggered by the camera subsystem. + * + * @param invalidator the object to invalidate while running. + * @param ringPaint the paint to draw the ring with. + * @param enterDurationMillis the fade in time in milliseconds. + * @param exitDurationMillis the fade out time in milliseconds. + */ + public AutoFocusRing(Invalidator invalidator, Paint ringPaint, float enterDurationMillis, + float exitDurationMillis) { + super(invalidator, ringPaint, enterDurationMillis, exitDurationMillis); + } + + @Override + public void draw(long t, long dt, Canvas canvas) { + float ringRadius = mRingRadius.update(dt); + processStates(t); + + if (!isActive()) { + return; + } + + mInvalidator.invalidate(); + int ringAlpha = 255; + + if (mFocusState == FocusState.STATE_ENTER) { + float rFade = InterpolateUtils.unitRatio(t, mEnterStartMillis, mEnterDurationMillis); + ringAlpha = (int) InterpolateUtils + .lerp(0, 255, mEnterOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_FADE_OUT) { + float rFade = InterpolateUtils.unitRatio(t, mExitStartMillis, mExitDurationMillis); + ringAlpha = (int) InterpolateUtils + .lerp(255, 0, mExitOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_HARD_STOP) { + float rFade = InterpolateUtils + .unitRatio(t, mHardExitStartMillis, mHardExitDurationMillis); + ringAlpha = (int) InterpolateUtils + .lerp(255, 0, mExitOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_INACTIVE) { + ringAlpha = 0; + } + + mRingPaint.setAlpha(ringAlpha); + canvas.drawCircle(getCenterX(), getCenterY(), ringRadius, mRingPaint); + } + + private void processStates(long t) { + if (mFocusState == FocusState.STATE_INACTIVE) { + return; + } + + if (mFocusState == FocusState.STATE_ENTER && t > mEnterStartMillis + mEnterDurationMillis) { + mFocusState = FocusState.STATE_ACTIVE; + } + + if (mFocusState == FocusState.STATE_ACTIVE && !mRingRadius.isActive()) { + mFocusState = FocusState.STATE_FADE_OUT; + mExitStartMillis = t; + } + + if (mFocusState == FocusState.STATE_FADE_OUT && t > mExitStartMillis + mExitDurationMillis) { + mFocusState = FocusState.STATE_INACTIVE; + } + + if (mFocusState == FocusState.STATE_HARD_STOP + && t > mHardExitStartMillis + mHardExitDurationMillis) { + mFocusState = FocusState.STATE_INACTIVE; + } + } +} diff --git a/src/com/android/camera/ui/focus/CameraCoordinateTransformer.java b/src/com/android/camera/ui/focus/CameraCoordinateTransformer.java new file mode 100644 index 000000000..809503c9a --- /dev/null +++ b/src/com/android/camera/ui/focus/CameraCoordinateTransformer.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.focus; + +import android.graphics.Matrix; +import android.graphics.RectF; + +/** + * Transform coordinates to and from preview coordinate space and camera driver + * coordinate space. + */ +public class CameraCoordinateTransformer { + // http://developer.android.com/guide/topics/media/camera.html#metering-focus-areas + private static final RectF CAMERA_DRIVER_RECT = new RectF(-1000, -1000, 1000, 1000); + + private final Matrix mCameraToPreviewTransform; + private final Matrix mPreviewToCameraTransform; + + /** + * Convert rectangles to / from camera coordinate and preview coordinate space. + * + * @param mirrorX if the preview is mirrored along the X axis. + * @param displayOrientation orientation in degrees. + * @param previewRect the preview rectangle size and position. + */ + public CameraCoordinateTransformer(boolean mirrorX, int displayOrientation, + RectF previewRect) { + if (!hasNonZeroArea(previewRect)) { + throw new IllegalArgumentException("previewRect"); + } + + mCameraToPreviewTransform = cameraToPreviewTransform(mirrorX, displayOrientation, + previewRect); + mPreviewToCameraTransform = inverse(mCameraToPreviewTransform); + } + + /** + * Transform a rectangle in camera space into a new rectangle in preview + * view space. + * + * @param source the rectangle in camera space + * @return the rectangle in preview view space. + */ + public RectF toPreviewSpace(RectF source) { + RectF result = new RectF(); + mCameraToPreviewTransform.mapRect(result, source); + return result; + } + + /** + * Transform a rectangle in preview view space into a new rectangle in + * camera view space. + * + * @param source the rectangle in preview view space + * @return the rectangle in camera view space. + */ + public RectF toCameraSpace(RectF source) { + RectF result = new RectF(); + mPreviewToCameraTransform.mapRect(result, source); + return result; + } + + private Matrix cameraToPreviewTransform(boolean mirrorX, int displayOrientation, + RectF previewRect) { + Matrix transform = new Matrix(); + + // Need mirror for front camera. + transform.setScale(mirrorX ? -1 : 1, 1); + + // Apply a rotate transform. + // This is the value for android.hardware.Camera.setDisplayOrientation. + transform.postRotate(displayOrientation); + + // Map camera driver coordinates to preview rect coordinates + Matrix fill = new Matrix(); + fill.setRectToRect(CAMERA_DRIVER_RECT, + previewRect, + Matrix.ScaleToFit.FILL); + + // Concat the previous transform on top of the fill behavior. + transform.setConcat(fill, transform); + + return transform; + } + + private Matrix inverse(Matrix source) { + Matrix newMatrix = new Matrix(); + source.invert(newMatrix); + return newMatrix; + } + + private boolean hasNonZeroArea(RectF rect) { + return rect.width() != 0 && rect.height() != 0; + } +} diff --git a/src/com/android/camera/ui/focus/FocusController.java b/src/com/android/camera/ui/focus/FocusController.java new file mode 100644 index 000000000..a0c103443 --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusController.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.focus; + +import android.graphics.RectF; +import android.util.Log; + +import com.android.camera.async.MainThread; +import com.android.camera.ui.motion.LinearScale; + +/** + * The focus controller interacts with the focus ring UI element. + */ +public class FocusController { + private static final String TAG = "FocusController"; + + private final FocusRing mFocusRing; + private final FocusSound mFocusSound; + private final MainThread mMainThread; + + public FocusController(FocusRing focusRing, FocusSound focusSound, MainThread mainThread) { + mFocusRing = focusRing; + mFocusSound = focusSound; + mMainThread = mainThread; + } + + /** + * Show a passive focus animation at the center of the active area. + * This will likely be different than the view bounds due to varying image + * ratios and dimensions. + */ + public void showPassiveFocusAtCenter() { + mMainThread.execute(new Runnable() { + @Override + public void run() { + Log.v(TAG, "Running showPassiveFocusAtCenter()"); + mFocusRing.startPassiveFocus(); + mFocusRing.centerFocusLocation(); + } + }); + } + + /** + * Show a passive focus animation at the given viewX and viewY position. + * This is usually indicates the camera subsystem kicked off an auto-focus + * at the given screen position. + * + * @param viewX the view's x coordinate + * @param viewY the view's y coordinate + */ + public void showPassiveFocusAt(final int viewX, final int viewY) { + mMainThread.execute(new Runnable() { + @Override + public void run() { + Log.v(TAG, "Running showPassiveFocusAt(" + viewX + ", " + viewY + ")"); + mFocusRing.startPassiveFocus(); + mFocusRing.setFocusLocation(viewX, viewY); + } + }); + } + + /** + * Show an active focus animation at the given viewX and viewY position. + * This is normally initiated by the user touching the screen at a given + * point. + * + * @param viewX the view's x coordinate + * @param viewY the view's y coordinate + */ + public void showActiveFocusAt(final int viewX, final int viewY) { + mMainThread.execute(new Runnable() { + @Override + public void run() { + Log.v(TAG, "showActiveFocusAt(" + viewX + ", " + viewY + ")"); + mFocusRing.startActiveFocus(); + mFocusRing.setFocusLocation(viewX, viewY); + + // TODO: Enable focus sound when better audio controls exist. + // mFocusSound.play(); + } + }); + } + + /** + * Computing the correct location for the focus ring requires knowing + * the screen position and size of the preview area so the drawing + * operations can be clipped correctly. + */ + public void configurePreviewDimensions(final RectF previewArea) { + mMainThread.execute(new Runnable() { + @Override + public void run() { + Log.v(TAG, "configurePreviewDimensions(" + previewArea + ")"); + mFocusRing.configurePreviewDimensions(previewArea); + } + }); + } + + /** + * Set the radius of the focus ring as a radius between 0 and 1. + * This will map to the min and max values computed for the UI. + */ + public void setFocusRatio(final float ratio) { + mMainThread.execute(new Runnable() { + @Override + public void run() { + if (mFocusRing.isPassiveFocusRunning() || + mFocusRing.isActiveFocusRunning()) { + mFocusRing.setRadiusRatio(ratio); + } + } + }); + } +} diff --git a/src/com/android/camera/ui/focus/FocusRing.java b/src/com/android/camera/ui/focus/FocusRing.java new file mode 100644 index 000000000..89de357ad --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusRing.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.focus; + +import android.graphics.RectF; + +/** + * Primary interface for interacting with the focus ring UI. + */ +public interface FocusRing { + /** + * Check the state of the passive focus ring animation. + * + * @return whether the passive focus animation is running. + */ + public boolean isPassiveFocusRunning(); + /** + * Check the state of the active focus ring animation. + * + * @return whether the active focus animation is running. + */ + public boolean isActiveFocusRunning(); + /** + * Start a passive focus animation. + */ + public void startPassiveFocus(); + /** + * Start an active focus animation. + */ + public void startActiveFocus(); + /** + * Stop any currently running focus animations. + */ + public void stopFocusAnimations(); + /** + * Set the location of the focus ring animation center. + */ + public void setFocusLocation(float viewX, float viewY); + + /** + * Set the location of the focus ring animation center. + */ + public void centerFocusLocation(); + + /** + * Set the target radius as a ratio of min to max visible radius + * which will internally convert and clamp the value to the + * correct pixel radius. + */ + public void setRadiusRatio(float ratio); + + /** + * The physical size of preview can vary and does not map directly + * to the size of the view. This allows for conversions between view + * and preview space for values that are provided in preview space. + */ + void configurePreviewDimensions(RectF previewArea); +} \ No newline at end of file diff --git a/src/com/android/camera/ui/focus/FocusRingRenderer.java b/src/com/android/camera/ui/focus/FocusRingRenderer.java new file mode 100644 index 000000000..264af2ace --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusRingRenderer.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.focus; + +import android.graphics.Paint; +import android.util.Log; + +import com.android.camera.ui.motion.DampedSpring; +import com.android.camera.ui.motion.DynamicAnimation; +import com.android.camera.ui.motion.Invalidator; +import com.android.camera.ui.motion.UnitCurve; +import com.android.camera.ui.motion.UnitCurves; + +/** + * Base class for defining the focus ring states, enter and exit durations, and + * positioning logic. + */ +abstract class FocusRingRenderer implements DynamicAnimation { + private static final String TAG = "FocusRingRenderer"; + + /** + * Primary focus states that a focus ring renderer can go through. + */ + protected static enum FocusState { + STATE_INACTIVE, + STATE_ENTER, + STATE_ACTIVE, + STATE_FADE_OUT, + STATE_HARD_STOP, + } + + protected final Invalidator mInvalidator; + protected final Paint mRingPaint; + protected final DampedSpring mRingRadius; + protected final UnitCurve mEnterOpacityCurve; + protected final UnitCurve mExitOpacityCurve; + protected final UnitCurve mHardExitOpacityCurve; + protected final float mEnterDurationMillis; + protected final float mExitDurationMillis; + protected final float mHardExitDurationMillis = 64; + + private int mCenterX; + private int mCenterY; + protected long mEnterStartMillis = 0; + protected long mExitStartMillis = 0; + protected long mHardExitStartMillis = 0; + + protected FocusState mFocusState = FocusState.STATE_INACTIVE; + + /** + * A dynamic, configurable, self contained ring render that will inform + * via invalidation if it should continue to be receive updates + * and re-draws. + * + * @param invalidator the object to inform if it requires more draw calls. + * @param ringPaint the paint to use to draw the ring. + * @param enterDurationMillis the fade in duration in milliseconds + * @param exitDurationMillis the fade out duration in milliseconds. + */ + FocusRingRenderer(Invalidator invalidator, Paint ringPaint, float enterDurationMillis, + float exitDurationMillis) { + mInvalidator = invalidator; + mRingPaint = ringPaint; + mEnterDurationMillis = enterDurationMillis; + mExitDurationMillis = exitDurationMillis; + + mEnterOpacityCurve = UnitCurves.FAST_OUT_SLOW_IN; + mExitOpacityCurve = UnitCurves.FAST_OUT_LINEAR_IN; + mHardExitOpacityCurve = UnitCurves.FAST_OUT_LINEAR_IN; + + mRingRadius = new DampedSpring(); + } + + /** + * Set the centerX position for this focus ring renderer. + * + * @param value the x position + */ + public void setCenterX(int value) { + mCenterX = value; + } + + protected int getCenterX() { + return mCenterX; + } + + /** + * Set the centerY position for this focus ring renderer. + * + * @param value the y position + */ + public void setCenterY(int value) { + mCenterY = value; + } + + protected int getCenterY() { + return mCenterY; + } + + /** + * Set the physical radius of this ring. + * + * @param value the radius of the ring. + */ + public void setRadius(long tMs, float value) { + if (mFocusState == FocusState.STATE_FADE_OUT + && Math.abs(mRingRadius.getTarget() - value) > 0.1) { + Log.v(TAG, "FOCUS STATE ENTER VIA setRadius(" + tMs + ", " + value + ")"); + mFocusState = FocusState.STATE_ENTER; + mEnterStartMillis = computeEnterStartTimeMillis(tMs, mEnterDurationMillis); + } + + mRingRadius.setTarget(value); + } + + /** + * returns true if the renderer is not in an inactive state. + */ + @Override + public boolean isActive() { + return mFocusState != FocusState.STATE_INACTIVE; + } + + /** + * returns true if the renderer is in an exit state. + */ + public boolean isExiting() { + return mFocusState == FocusState.STATE_FADE_OUT + || mFocusState == FocusState.STATE_HARD_STOP; + } + + /** + * returns true if the renderer is in an enter state. + */ + public boolean isEntering() { + return mFocusState == FocusState.STATE_ENTER; + } + + /** + * Initialize and start the animation with the given start and + * target radius. + */ + public void start(long startMs, float initialRadius, float targetRadius) { + if (mFocusState != FocusState.STATE_INACTIVE) { + Log.w(TAG, "start() called while the ring was still focusing!"); + } + mRingRadius.stop(); + mRingRadius.setValue(initialRadius); + mRingRadius.setTarget(targetRadius); + mEnterStartMillis = startMs; + + mFocusState = FocusState.STATE_ENTER; + mInvalidator.invalidate(); + } + + /** + * Put the animation in the exit state regardless of the current + * dynamic transition. If the animation is currently in an enter state + * this will compute an exit start time such that the exit time lines + * up with the enter time at the current transition value. + * + * @param t the current animation time. + */ + public void exit(long t) { + if (mRingRadius.isActive()) { + mRingRadius.stop(); + } + + mFocusState = FocusState.STATE_FADE_OUT; + mExitStartMillis = computeExitStartTimeMs(t, mExitDurationMillis); + } + + /** + * Put the animation in the hard stop state regardless of the current + * dynamic transition. If the animation is currently in an enter state + * this will compute an exit start time such that the exit time lines + * up with the enter time at the current transition value. + * + * @param tMillis the current animation time in milliseconds. + */ + public void stop(long tMillis) { + if (mRingRadius.isActive()) { + mRingRadius.stop(); + } + + mFocusState = FocusState.STATE_HARD_STOP; + mHardExitStartMillis = computeExitStartTimeMs(tMillis, mHardExitDurationMillis); + } + + private long computeExitStartTimeMs(long tMillis, float exitDuration) { + if (mEnterStartMillis + mEnterDurationMillis <= tMillis) { + return tMillis; + } + + // Compute the current progress on the enter animation. + float enterT = (tMillis - mEnterStartMillis) / mEnterDurationMillis; + + // Find a time on the exit curve such that it will produce the same value. + float exitT = UnitCurves.mapEnterCurveToExitCurveAtT(mEnterOpacityCurve, mExitOpacityCurve, + enterT); + + // Compute the a start time before tMs such that the ratio of time completed + // equals the computed exit curve animation position. + return tMillis - (long) (exitT * exitDuration); + } + + private long computeEnterStartTimeMillis(long tMillis, float enterDuration) { + if (mExitStartMillis + mExitDurationMillis <= tMillis) { + return tMillis; + } + + // Compute the current progress on the enter animation. + float exitT = (tMillis - mExitStartMillis) / mExitDurationMillis; + + // Find a time on the exit curve such that it will produce the same value. + float enterT = UnitCurves.mapEnterCurveToExitCurveAtT(mExitOpacityCurve, mEnterOpacityCurve, + exitT); + + // Compute the a start time before tMs such that the ratio of time completed + // equals the computed exit curve animation position. + return tMillis - (long) (enterT * enterDuration); + } +} diff --git a/src/com/android/camera/ui/focus/FocusRingView.java b/src/com/android/camera/ui/focus/FocusRingView.java new file mode 100644 index 000000000..14a7f6cc9 --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusRingView.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.focus; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.RectF; +import android.graphics.Region; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.android.camera.ui.motion.AnimationClock.SystemTimeClock; +import com.android.camera.ui.motion.DynamicAnimator; +import com.android.camera.ui.motion.Invalidator; +import com.android.camera.ui.motion.LinearScale; + +import org.codeaurora.snapcam.R; + +/** + * Custom view for running the focus ring animations. + */ +public class FocusRingView extends View implements Invalidator, FocusRing { + private static final String TAG = "FocusRingView"; + private static final float FADE_IN_DURATION_MILLIS = 1000f; + private static final float FADE_OUT_DURATION_MILLIS = 250f; + + private final AutoFocusRing mAutoFocusRing; + private final ManualFocusRing mManualFocusRing; + private final DynamicAnimator mAnimator; + private final LinearScale mRatioScale; + private final float mDefaultRadiusPx; + + private FocusRingRenderer currentFocusAnimation; + private boolean isFirstDraw; + private float mLastRadiusPx; + + private RectF mPreviewSize; + + public FocusRingView(Context context, AttributeSet attrs) { + super(context, attrs); + + Resources res = getResources(); + Paint paint = makePaint(res, R.color.focus_color); + + float focusCircleMinSize = res.getDimensionPixelSize(R.dimen.focus_circle_min_size); + float focusCircleMaxSize = res.getDimensionPixelSize(R.dimen.focus_circle_max_size); + mDefaultRadiusPx = res.getDimensionPixelSize(R.dimen.focus_circle_initial_size); + + mRatioScale = new LinearScale(0, 1, focusCircleMinSize, focusCircleMaxSize); + mAnimator = new DynamicAnimator(this, new SystemTimeClock()); + + mAutoFocusRing = new AutoFocusRing(mAnimator, paint, + FADE_IN_DURATION_MILLIS, + FADE_OUT_DURATION_MILLIS); + mManualFocusRing = new ManualFocusRing(mAnimator, paint, + FADE_OUT_DURATION_MILLIS); + + mAnimator.animations.add(mAutoFocusRing); + mAnimator.animations.add(mManualFocusRing); + + isFirstDraw = true; + mLastRadiusPx = mDefaultRadiusPx; + } + + @Override + public boolean isPassiveFocusRunning() { + return mAutoFocusRing.isActive(); + } + + @Override + public boolean isActiveFocusRunning() { + return mManualFocusRing.isActive(); + } + + @Override + public void startPassiveFocus() { + mAnimator.invalidate(); + long tMs = mAnimator.getTimeMillis(); + + if (mManualFocusRing.isActive() && !mManualFocusRing.isExiting()) { + mManualFocusRing.stop(tMs); + } + + mAutoFocusRing.start(tMs, mLastRadiusPx, mLastRadiusPx); + currentFocusAnimation = mAutoFocusRing; + } + + @Override + public void startActiveFocus() { + mAnimator.invalidate(); + long tMs = mAnimator.getTimeMillis(); + + if (mAutoFocusRing.isActive() && !mAutoFocusRing.isExiting()) { + mAutoFocusRing.stop(tMs); + } + + mManualFocusRing.start(tMs, 0.0f, mLastRadiusPx); + currentFocusAnimation = mManualFocusRing; + } + + @Override + public void stopFocusAnimations() { + long tMs = mAnimator.getTimeMillis(); + if (mManualFocusRing.isActive() && !mManualFocusRing.isExiting() + && !mManualFocusRing.isEntering()) { + mManualFocusRing.exit(tMs); + } + + if (mAutoFocusRing.isActive() && !mAutoFocusRing.isExiting()) { + mAutoFocusRing.exit(tMs); + } + } + + @Override + public void setFocusLocation(float viewX, float viewY) { + mAutoFocusRing.setCenterX((int) viewX); + mAutoFocusRing.setCenterY((int) viewY); + mManualFocusRing.setCenterX((int) viewX); + mManualFocusRing.setCenterY((int) viewY); + } + + @Override + public void centerFocusLocation() { + Point center = computeCenter(); + mAutoFocusRing.setCenterX(center.x); + mAutoFocusRing.setCenterY(center.y); + mManualFocusRing.setCenterX(center.x); + mManualFocusRing.setCenterY(center.y); + } + + @Override + public void setRadiusRatio(float ratio) { + setRadius(mRatioScale.scale(mRatioScale.clamp(ratio))); + } + + @Override + public void configurePreviewDimensions(RectF previewArea) { + mPreviewSize = previewArea; + mLastRadiusPx = mDefaultRadiusPx; + + if (!isFirstDraw) { + centerAutofocusRing(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (isFirstDraw) { + isFirstDraw = false; + centerAutofocusRing(); + } + + if (mPreviewSize != null) { + canvas.clipRect(mPreviewSize, Region.Op.REPLACE); + } + + mAnimator.draw(canvas); + } + + private void setRadius(float radiusPx) { + long tMs = mAnimator.getTimeMillis(); + // Some devices return zero for invalid or "unknown" diopter values. + if (currentFocusAnimation != null && radiusPx > 0.1f) { + currentFocusAnimation.setRadius(tMs, radiusPx); + mLastRadiusPx = radiusPx; + } + } + + private void centerAutofocusRing() { + Point center = computeCenter(); + mAutoFocusRing.setCenterX(center.x); + mAutoFocusRing.setCenterY(center.y); + } + + private Point computeCenter() { + if (mPreviewSize != null && (mPreviewSize.width() * mPreviewSize.height() > 0.01f)) { + Log.i(TAG, "Computing center via preview size."); + return new Point((int) mPreviewSize.centerX(), (int) mPreviewSize.centerY()); + } + Log.i(TAG, "Computing center via view bounds."); + return new Point(getWidth() / 2, getHeight() / 2); + } + + private Paint makePaint(Resources res, int color) { + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setColor(res.getColor(color)); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeWidth(res.getDimension(R.dimen.focus_circle_stroke)); + return paint; + } +} diff --git a/src/com/android/camera/ui/focus/FocusSound.java b/src/com/android/camera/ui/focus/FocusSound.java new file mode 100644 index 000000000..c3ff0107d --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusSound.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.focus; + +import com.android.camera.SoundPlayer; + +/** + * Wraps the focus sound and the player into a single object that can + * be played on demand. + * + * TODO: This needs some way to better manage the sound lifetimes + */ +public class FocusSound { + private static final float DEFAULT_VOLUME = 0.6f; + private final SoundPlayer mPlayer; + private final int mSoundId; + public FocusSound(SoundPlayer player, int soundId) { + mPlayer = player; + mSoundId = soundId; + + mPlayer.loadSound(mSoundId); + } + + /** + * Play the focus sound with the sound player at the default + * volume. + */ + public void play() { + if(!mPlayer.isReleased()) { + mPlayer.play(mSoundId, DEFAULT_VOLUME); + } + } +} diff --git a/src/com/android/camera/ui/focus/LensRangeCalculator.java b/src/com/android/camera/ui/focus/LensRangeCalculator.java new file mode 100644 index 000000000..ef9cbaec7 --- /dev/null +++ b/src/com/android/camera/ui/focus/LensRangeCalculator.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.focus; + +import android.annotation.TargetApi; +import android.hardware.camera2.CameraCharacteristics; +import android.os.Build.VERSION_CODES; + +import com.android.camera.ui.motion.LinearScale; + +/** + * Compute diopter range scale to convert lens focus distances into + * a ratio value. + */ +@TargetApi(VERSION_CODES.LOLLIPOP) +public class LensRangeCalculator { + + /** + * A NoOp linear scale for computing diopter values will always return 0 + */ + public static LinearScale getNoOp() { + return new LinearScale(0, 0, 0, 0); + } + + /** + * Compute the focus range from the camera characteristics and build + * a linear scale model that maps a focus distance to a ratio between + * the min and max range. + */ + public static LinearScale getDiopterToRatioCalculator(CameraCharacteristics characteristics) { + // From the android documentation: + // + // 0.0f represents farthest focus, and LENS_INFO_MINIMUM_FOCUS_DISTANCE + // represents the nearest focus the device can achieve. + // + // Example: + // + // Infinity Hyperfocal Minimum Camera + // <----------|-----------------------------| | + // [0.0] [0.31] [14.29] + Float nearest = characteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + Float hyperfocal = characteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); + + if (nearest == null && hyperfocal == null) { + return getNoOp(); + } + + nearest = (nearest == null) ? 0.0f : nearest; + hyperfocal = (hyperfocal == null) ? 0.0f : hyperfocal; + + if (nearest > hyperfocal) { + return new LinearScale(hyperfocal, nearest, 0, 1); + } + + return new LinearScale(nearest, hyperfocal, 0, 1); + } +} diff --git a/src/com/android/camera/ui/focus/ManualFocusRing.java b/src/com/android/camera/ui/focus/ManualFocusRing.java new file mode 100644 index 000000000..0133d8e09 --- /dev/null +++ b/src/com/android/camera/ui/focus/ManualFocusRing.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.focus; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.android.camera.ui.motion.InterpolateUtils; +import com.android.camera.ui.motion.Invalidator; + +/** + * Manual focus ring animation renderer. + */ +class ManualFocusRing extends FocusRingRenderer { + /** + * The manual focus ring encapsulates the animation logic for visualizing + * a focus event when triggered by a physical screen touch. + * + * @param invalidator the object to invalidate while running. + * @param ringPaint the paint to draw the ring with. + * @param exitDurationMillis the fade out time in milliseconds. + */ + public ManualFocusRing(Invalidator invalidator, Paint ringPaint, + float exitDurationMillis) { + super(invalidator, ringPaint, 0.0f, exitDurationMillis); + } + + @Override + public void draw(long t, long dt, Canvas canvas) { + float ringRadius = mRingRadius.update(dt); + processStates(t); + + if (!isActive()) { + return; + } + + mInvalidator.invalidate(); + int ringAlpha = 255; + + if (mFocusState == FocusState.STATE_FADE_OUT) { + float rFade = InterpolateUtils.unitRatio(t, mExitStartMillis, mExitDurationMillis); + ringAlpha = (int) InterpolateUtils.lerp(255, 0, mExitOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_HARD_STOP) { + float rFade = InterpolateUtils.unitRatio(t, mHardExitStartMillis, + mHardExitDurationMillis); + ringAlpha = (int) InterpolateUtils.lerp(255, 0, mExitOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_INACTIVE) { + ringAlpha = 0; + } + + mRingPaint.setAlpha(ringAlpha); + canvas.drawCircle(getCenterX(), getCenterY(), ringRadius, mRingPaint); + } + + private void processStates(long t) { + if (mFocusState == FocusState.STATE_INACTIVE) { + return; + } + + if (mFocusState == FocusState.STATE_ENTER + && (t > mEnterStartMillis + mEnterDurationMillis)) { + mFocusState = FocusState.STATE_ACTIVE; + } + + if (mFocusState == FocusState.STATE_ACTIVE && !mRingRadius.isActive()) { + mFocusState = FocusState.STATE_FADE_OUT; + mExitStartMillis = t; + } + + if (mFocusState == FocusState.STATE_FADE_OUT && t > mExitStartMillis + mExitDurationMillis) { + mFocusState = FocusState.STATE_INACTIVE; + } + + if (mFocusState == FocusState.STATE_HARD_STOP + && t > mHardExitStartMillis + mHardExitDurationMillis) { + mFocusState = FocusState.STATE_INACTIVE; + } + } +} diff --git a/src/com/android/camera/ui/motion/AnimationClock.java b/src/com/android/camera/ui/motion/AnimationClock.java new file mode 100644 index 000000000..d2504de6b --- /dev/null +++ b/src/com/android/camera/ui/motion/AnimationClock.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +import android.os.SystemClock; + +/** + * Wraps the SystemClock static time methods so they can be exercised in tests. + */ +public abstract class AnimationClock { + + public abstract long getTimeMillis(); + + /** + * Forwards calls to SystemClock.uptimeMillis() since it is the most consistent clock for + * animations. + */ + public static class SystemTimeClock extends AnimationClock { + + @Override + public long getTimeMillis() { + return SystemClock.uptimeMillis(); + } + } +} diff --git a/src/com/android/camera/ui/motion/DampedSpring.java b/src/com/android/camera/ui/motion/DampedSpring.java new file mode 100644 index 000000000..84cbfa6f8 --- /dev/null +++ b/src/com/android/camera/ui/motion/DampedSpring.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +/** + * This models a value after the behavior of a spring. The value tracks the current value, a target + * value, and the current velocity and applies both a directional force and a damping force to the + * value on each update call. + */ +public class DampedSpring { + public static final float DEFAULT_TIME_TO_90_PERCENT_MILLIS = 200.0f; + public static final float DEFAULT_SPRING_STIFFNESS = 3.75f; + public static final float EPSILON = 0.01f; + + private final float mSpringStiffness; + private final float mTimeTo90PercentMs; + + private float mTarget = 0f; + private float mVelocity = 0f; + private float mValue = 0f; + + public DampedSpring() { + this(DEFAULT_TIME_TO_90_PERCENT_MILLIS, DEFAULT_SPRING_STIFFNESS); + } + + public DampedSpring(float timeTo90PercentMs) { + this(timeTo90PercentMs, DEFAULT_SPRING_STIFFNESS); + } + + public DampedSpring(float timeTo90PercentMs, float springStiffness) { + // TODO: Assert timeTo90PercentMs >= 1ms, it might behave badly at low values. + // TODO: Assert springStiffness > 2.0f + + mTimeTo90PercentMs = timeTo90PercentMs; + mSpringStiffness = springStiffness; + + if (springStiffness > timeTo90PercentMs) { + throw new IllegalArgumentException("Creating a spring value with " + + "excessive stiffness will oscillate endlessly."); + } + } + + /** + * @return the current value. + */ + public float getValue() { + return mValue; + } + + /** + * @param value the value to set this instance's current state too. + */ + public void setValue(float value) { + mValue = value; + } + + /** + * @return the current target value. + */ + public float getTarget() { + return mTarget; + } + + /** + * Set a target value. The current value will maintain any existing velocity values and will + * move towards the new target value. To forcibly stopAt the value use the stopAt() method. + * + * @param value the new value to move the current value towards. + */ + public void setTarget(float value) { + mTarget = value; + } + + /** + * Update the current value, moving it towards the actual value over the given + * time delta (in milliseconds) since the last update. This works off of the + * principle of a critically damped spring such that any given current value + * will move elastically towards the target value. The current value maintains + * and applies velocity, acceleration, and a damping force to give a continuous, + * smooth transition towards the target value. + * + * @param dtMs the time since the last update, or zero. + * @return the current value after the update occurs. + */ + public float update(float dtMs) { + float dt = dtMs / mTimeTo90PercentMs; + float dts = dt * mSpringStiffness; + + // If the dts > 1, and the velocity is zero, the force will exceed the + // distance to the target value and it will overshoot the value, causing + // weird behavior and unintended oscillation. since a critically damped + // spring should never overshoot the value, simply the current value to the + // target value. + if (dts > 1.0f || dts < 0.0f) { + stop(); + return mValue; + } + + float delta = (mTarget - mValue); + float force = delta - 2.0f * mVelocity; + + mVelocity += force * dts; + mValue += mVelocity * dts; + + // If we get close enough to the actual value, simply set the current value + // to the current target value and stop. + if (!isActive()) { + stop(); + } + + return mValue; + } + + /** + * @return true if this instance has velocity or it is not at the target value. + */ + public boolean isActive() { + boolean hasVelocity = Math.abs(mVelocity) >= EPSILON; + boolean atTarget = Math.abs(mTarget - mValue) < EPSILON; + return hasVelocity || !atTarget; + } + + /** + * Stop the spring motion wherever it is currently at. Sets target to the + * current value and sets the velocity to zero. + */ + public void stop() { + mTarget = mValue; + mVelocity = 0.0f; + } +} diff --git a/src/com/android/camera/ui/motion/DynamicAnimation.java b/src/com/android/camera/ui/motion/DynamicAnimation.java new file mode 100644 index 000000000..57d5a1021 --- /dev/null +++ b/src/com/android/camera/ui/motion/DynamicAnimation.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +import android.graphics.Canvas; + +/** + * Rendering object that can be driven by an animator instance. + */ +public interface DynamicAnimation { + + /** + * Check to determine if this animation is currently in a stable state. + * + * @return true if the animation is stable, false if it should continue to be redrawn. + */ + boolean isActive(); + + /** + * Update and draw the animation onto the given canvas. + * + * @param t current animation frame time. + * @param dt delta since the last update. + * @param canvas the canvas to draw the animation onto. + */ + void draw(long t, long dt, Canvas canvas); +} diff --git a/src/com/android/camera/ui/motion/DynamicAnimator.java b/src/com/android/camera/ui/motion/DynamicAnimator.java new file mode 100644 index 000000000..542ac1e37 --- /dev/null +++ b/src/com/android/camera/ui/motion/DynamicAnimator.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +import android.graphics.Canvas; + +import java.util.ArrayList; +import java.util.List; + +/** + * Designed to handle the lifecycle of a view that needs a continuous update / + * redraw cycle that does not have a defined start / end time. + * + * Fixed length animations should NOT use this class. + */ +public class DynamicAnimator implements Invalidator { + + public final List animations = new ArrayList<>(); + + private final Invalidator mInvalidator; + private final AnimationClock mClock; + + private boolean mUpdateRequested = false; + private boolean mIsDrawing = false; + private long mLastDrawTimeMillis = 0; + private long mDrawTimeMillis = 0; + + public DynamicAnimator(Invalidator invalidator, AnimationClock clock) { + mInvalidator = invalidator; + mClock = clock; + } + + public void draw(Canvas canvas) { + mIsDrawing = true; + mUpdateRequested = false; + + mDrawTimeMillis = mClock.getTimeMillis(); + + if (mLastDrawTimeMillis <= 0) { + mLastDrawTimeMillis = mDrawTimeMillis; // On the initial draw, dt is zero. + } + + long dt = mDrawTimeMillis - mLastDrawTimeMillis; + mLastDrawTimeMillis = mDrawTimeMillis; + + // Run the animation + for (DynamicAnimation renderer : animations) { + if (renderer.isActive()) { + renderer.draw(mDrawTimeMillis, dt, canvas); + } + } + + // If either the update or the draw methods requested new frames, then + // invalidate the view which should give us another frame to work with. + // Otherwise, stopAt the last update time. + if (mUpdateRequested) { + mInvalidator.invalidate(); + } else { + mLastDrawTimeMillis = -1; + } + + mIsDrawing = false; + } + + /** + * If a scheduleNewFrame request comes in outside of the animation loop, + * and we didn't schedule a frame after the previous loop (or it's the + * first time we've used this instance), invalidate the view and set the + * last update time to the current time. Theoretically, a few milliseconds + * have elapsed before the view gets updated. + */ + @Override + public void invalidate() { + if (!mIsDrawing && !mUpdateRequested) { + mInvalidator.invalidate(); + mLastDrawTimeMillis = mClock.getTimeMillis(); + } + + mUpdateRequested = true; + } + + /** + * This will return the "best guess" for the most current animation frame + * time. If the loop is currently drawing, then it will return the time the + * draw began, and if an update is currently requested it will return the + * time that the update was requested at, and if neither of these are true + * it will return the current system clock time. + * + * This method will not trigger a new update. + */ + public long getTimeMillis() { + if (mIsDrawing) { + return mDrawTimeMillis; + } + + if (mUpdateRequested) { + return mLastDrawTimeMillis; + } + + return mClock.getTimeMillis(); + } +} diff --git a/src/com/android/camera/ui/motion/InterpolateUtils.java b/src/com/android/camera/ui/motion/InterpolateUtils.java new file mode 100644 index 000000000..3c3cd532f --- /dev/null +++ b/src/com/android/camera/ui/motion/InterpolateUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +/** + * Various static helper functions for interpolating between values. + */ +public class InterpolateUtils { + + private InterpolateUtils() { + } + + /** + * Linear interpolation from v0 to v1 as t goes from 0...1 + * + * @param v0 the value at t=0 + * @param v1 the value at t=1 + * @param t value in the range of 0 to 1. + * @return the value between v0 and v1 as a ratio between 0 and 1 defined by t. + */ + public static float lerp(float v0, float v1, float t) { + return v0 + t * (v1 - v0); + } + + /** + * Project a value that is within the in(Min/Max) number space into the to(Min/Max) number + * space. + * + * @param v value to scale into the 'to' number space. + * @param vMin min value of the values number space. + * @param vMax max value of the values number space. + * @param pMin min value of the projection number space. + * @param pMax max value of the projection number space. + * @return the ratio of the value in the source number space as a value in the to(Min/Max) + * number space. + */ + public static float scale(float v, float vMin, float vMax, float pMin, float pMax) { + return (pMax - pMin) * (v - vMin) / (vMax - vMin) + pMin; + } + + /** + * Value between 0 and 1 as a ratio between tBegin over tDuration + * with no upper bound. + */ + public static float unitRatio(long t, long tBegin, float tDuration) { + if (t <= tBegin) { + return 0.0f; + } + + return (t - tBegin) / tDuration; + } +} diff --git a/src/com/android/camera/ui/motion/InterpolatorHelper.java b/src/com/android/camera/ui/motion/InterpolatorHelper.java new file mode 100644 index 000000000..84114cb03 --- /dev/null +++ b/src/com/android/camera/ui/motion/InterpolatorHelper.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +import android.content.Context; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.camera.util.ApiHelper; + +public class InterpolatorHelper { + private static Interpolator LINEAR_OUT_SLOW_IN = null; + + public static Interpolator getLinearOutSlowInInterpolator(final Context context) { + if (LINEAR_OUT_SLOW_IN != null) { + return LINEAR_OUT_SLOW_IN; + } + + LINEAR_OUT_SLOW_IN = AnimationUtils.loadInterpolator( + context, android.R.interpolator.linear_out_slow_in); + return LINEAR_OUT_SLOW_IN; + } +} diff --git a/src/com/android/camera/ui/motion/Invalidator.java b/src/com/android/camera/ui/motion/Invalidator.java new file mode 100644 index 000000000..fdb548748 --- /dev/null +++ b/src/com/android/camera/ui/motion/Invalidator.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +/** + * Basic interface for objects that can be invalidated. + */ +public interface Invalidator { + /** + * Request that the object should be redrawn whenever it gets + * the chance. + */ + void invalidate(); +} diff --git a/src/com/android/camera/ui/motion/LinearScale.java b/src/com/android/camera/ui/motion/LinearScale.java new file mode 100644 index 000000000..5886f6882 --- /dev/null +++ b/src/com/android/camera/ui/motion/LinearScale.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +/** + * Represents a discrete linear scale function. + */ +public final class LinearScale { + private final float mDomainA; + private final float mDomainB; + private final float mRangeA; + private final float mRangeB; + + private final float mScale; + + public LinearScale(float domainA, float domainB, float rangeA, float rangeB) { + mDomainA = domainA; + mDomainB = domainB; + mRangeA = rangeA; + mRangeB = rangeB; + + // Precomputed ratio between input domain and output range. + float scale = (mRangeB - mRangeA) / (mDomainB - mDomainA); + mScale = Float.isNaN(scale) ? 0.0f : scale; + } + + /** + * Clamp a given domain value to the given domain. + */ + public float clamp(float domainValue) { + if (mDomainA > mDomainB) { + return Math.max(mDomainB, Math.min(mDomainA, domainValue)); + } + + return Math.max(mDomainA, Math.min(mDomainB, domainValue)); + } + + /** + * Returns true if the value is within the domain. + */ + public boolean isInDomain(float domainValue) { + if (mDomainA > mDomainB) { + return domainValue <= mDomainA && domainValue >= mDomainB; + } + return domainValue >= mDomainA && domainValue <= mDomainB; + } + + /** + * Linearly scale a given domain value into the output range. + */ + public float scale(float domainValue) { + return mRangeA + (domainValue - mDomainA) * mScale; + } + + /** + * For the current domain and range parameters produce a new scale function + * that is the inverse of the current scale function. + */ + public LinearScale inverse() { + return new LinearScale(mRangeA, mRangeB, mDomainA, mDomainB); + } + + @Override + public String toString() { + return "LinearScale{" + + "mDomainA=" + mDomainA + + ", mDomainB=" + mDomainB + + ", mRangeA=" + mRangeA + + ", mRangeB=" + mRangeB + "}"; + } +} \ No newline at end of file diff --git a/src/com/android/camera/ui/motion/UnitBezier.java b/src/com/android/camera/ui/motion/UnitBezier.java new file mode 100644 index 000000000..242f54556 --- /dev/null +++ b/src/com/android/camera/ui/motion/UnitBezier.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +/** + * This represents is a precomputed cubic bezier curve starting at (0,0) and + * going to (1,1) with two configurable control points. Once the instance is + * created, the control points cannot be modified. + * + * Generally, this will be used for computing timing curves for with control + * points where an x value will be provide from 0.0 - 1.0, and the y value will + * be solved for where y is used as the timing value in some linear + * interpolation of a value. + */ +public class UnitBezier implements UnitCurve { + + private static final float EPSILON = 1e-6f; + + private final DerivableFloatFn mXFn; + private final DerivableFloatFn mYFn; + + /** + * Build and pre-compute a unit bezier. This assumes a starting point of + * (0, 0) and end point of (1.0, 1.0). + * + * @param c0x control point x value for p0 + * @param c0y control point y value for p0 + * @param c1x control point x value for p1 + * @param c1y control point y value for p1 + */ + public UnitBezier(float c0x, float c0y, float c1x, float c1y) { + mXFn = new CubicBezierFn(c0x, c1x); + mYFn = new CubicBezierFn(c0y, c1y); + } + + /** + * Given a unit bezier curve find the height of the curve at t (which is + * internally represented as the xAxis). + * + * @param t the x position between 0 and 1 to solve for y. + * @return the closest approximate height of the curve at x. + */ + @Override + public float valueAt(float t) { + return mYFn.value(solve(t, mXFn)); + } + + /** + * Given a unit bezier curve find a value along the x axis such that + * valueAt(result) produces the input value. + * + * @param value the y position between 0 and 1 to solve for x + * @return the closest approximate input that will produce value when provided + * to the valueAt function. + */ + @Override + public float tAt(float value) { + return mXFn.value(solve(value, mYFn)); + } + + private float solve(float target, DerivableFloatFn fn) { + // For a linear fn, t = value. This makes value a good starting guess. + float input = target; + + // Newton's method (Faster than bisection) + for (int i = 0; i < 8; i++) { + float value = fn.value(input) - target; + if (Math.abs(value) < EPSILON) { + return input; + } + float derivative = fn.derivative(input); + if (Math.abs(derivative) < EPSILON) { + break; + } + input = input - value / derivative; + } + + // Fallback on bi-section + float min = 0.0f; + float max = 1.0f; + input = target; + + if (input < min) { + return min; + } + if (input > max) { + return max; + } + + while (min < max) { + float value = fn.value(input); + if (Math.abs(value - target) < EPSILON) { + return input; + } + + if (target > value) { + min = input; + } else { + max = input; + } + + input = (max - min) * .5f + min; + } + + // Give up, return the closest match we got too. + return input; + } + + private interface DerivableFloatFn { + float value(float x); + float derivative(float x); + } + + /** + * Precomputed constants for a given set of control points along a given + * cubic bezier axis. + */ + private static class CubicBezierFn implements DerivableFloatFn { + private final float c; + private final float a; + private final float b; + + /** + * Build and pre-compute a single axis for a unit bezier. This assumes p0 + * is 0 and p1 is 1. + * + * @param c0 start control point. + * @param c1 end control point. + */ + public CubicBezierFn(float c0, float c1) { + c = 3.0f * c0; + b = 3.0f * (c1 - c0) - c; + a = 1.0f - c - b; + } + + public float value(float x) { + return ((a * x + b) * x + c) * x; + } + public float derivative(float x) { + return (3.0f * a * x + 2.0f * b) * x + c; + } + } +} diff --git a/src/com/android/camera/ui/motion/UnitCurve.java b/src/com/android/camera/ui/motion/UnitCurve.java new file mode 100644 index 000000000..d89f1fa4d --- /dev/null +++ b/src/com/android/camera/ui/motion/UnitCurve.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +/** + * Simple functions that produce values along a curve for any given input and can compute input + * times for a given output value. + */ +public interface UnitCurve { + + /** + * Produce a unit value of this curve at time t. The function should always return a valid + * return value for any valid t input. + * + * @param t ratio of time passed from (0..1) + * @return the unit value at t. + */ + float valueAt(float t); + + /** + * If possible, find a value for t such that valueAt(t) == value or best guess. + * + * @param value to match to the output of valueAt(t) + * @return t where valueAt(t) == value or throw. + */ + float tAt(float value); +} diff --git a/src/com/android/camera/ui/motion/UnitCurves.java b/src/com/android/camera/ui/motion/UnitCurves.java new file mode 100644 index 000000000..a1117fa96 --- /dev/null +++ b/src/com/android/camera/ui/motion/UnitCurves.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +/** + * Predefined material curves and animations. + */ +public class UnitCurves { + public static final UnitCurve FAST_OUT_SLOW_IN = new UnitBezier(0.4f, 0.0f, 0.2f, 1.0f); + public static final UnitCurve LINEAR_OUT_SLOW_IN = new UnitBezier(0.0f, 0.0f, 0.2f, 1.0f); + public static final UnitCurve FAST_OUT_LINEAR_IN = new UnitBezier(0.4f, 0.0f, 1.0f, 1.0f); + public static final UnitCurve LINEAR = new UnitBezier(0.0f, 0.0f, 1.0f, 1.0f); + + /** + * Given two curves (from and to) and a time along the from curve, compute + * the time at t, and find a t along the 'toCurve' that will produce the + * same output. This is useful when interpolating between two different curves + * when the animation is not at the beginning or end. + * + * @param enterCurve the curve to compute the value from + * @param exitCurve the curve to find a time t on that matches output of + * enterCurve at T. + * @param t the time at which to compute the value (0..1) + * @return the time along the exitCurve. + */ + public static float mapEnterCurveToExitCurveAtT(UnitCurve enterCurve, UnitCurve exitCurve, + float t) { + return exitCurve.tAt(1 - enterCurve.valueAt(t)); + } +} diff --git a/src/com/android/camera/util/CameraUtil.java b/src/com/android/camera/util/CameraUtil.java index 18ec724f7..70d59101a 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); -- cgit v1.2.3