diff options
author | Paul Rohde <codelogic@google.com> | 2014-12-05 12:17:15 -0800 |
---|---|---|
committer | Paul Rohde <codelogic@google.com> | 2014-12-17 10:46:54 -0800 |
commit | 987ee64612e2510004fdf08536746c87234d01c1 (patch) | |
tree | 453d22c3c2548eecaa9e23c2a6e0bcbe1482b949 | |
parent | b79279f49e365cc6da75c1bc44b4c786036d0aa7 (diff) | |
download | android_packages_apps_Camera2-987ee64612e2510004fdf08536746c87234d01c1.tar.gz android_packages_apps_Camera2-987ee64612e2510004fdf08536746c87234d01c1.tar.bz2 android_packages_apps_Camera2-987ee64612e2510004fdf08536746c87234d01c1.zip |
Drop new focus indicator into Camera2.
* Create a new custom focus view that interacts with physical lens diopter changes.
* Replace all occurances of the old focus indicator with the new one.
Change-Id: Ia02646ce4d1eb059ecb8a1dfccc15dfc9c167e1b
40 files changed, 1905 insertions, 594 deletions
diff --git a/res/layout/camera.xml b/res/layout/camera.xml index 121dc0fbc..860376ea0 100644 --- a/res/layout/camera.xml +++ b/res/layout/camera.xml @@ -26,8 +26,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> - <com.android.camera.ui.FocusOverlay - android:id="@+id/focus_overlay" + <com.android.camera.ui.focus.FocusRingView + android:id="@+id/focus_ring" android:layout_width="match_parent" android:layout_height="match_parent" /> diff --git a/res/raw/material_camera_focus.ogg b/res/raw/material_camera_focus.ogg Binary files differnew file mode 100644 index 000000000..555d7f444 --- /dev/null +++ b/res/raw/material_camera_focus.ogg diff --git a/res/values/colors.xml b/res/values/colors.xml index 2eee2ac82..3fc3131c9 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -66,6 +66,7 @@ <color name="bright_foreground_disabled_holo_dark">#ff4c4c4c</color> <color name="bright_foreground_holo_dark">#fff3f3f3</color> <color name="face_detect_start">#ffffff00</color> + <color name="focus_color">#ffffffff</color> <color name="focus_debug">#90ffffff</color> <color name="focus_debug_text">#b0ffffff</color> <color name="focus_debug_success">#9000ff00</color> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index c09463390..18257a24c 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -93,6 +93,9 @@ <dimen name="switcher_size">72dp</dimen> <dimen name="face_circle_stroke">1dip</dimen> <dimen name="focus_debug_stroke">1dip</dimen> + <dimen name="focus_circle_stroke">1.25dp</dimen> + <dimen name="focus_circle_min_size">24dp</dimen> + <dimen name="focus_circle_max_size">96dp</dimen> <dimen name="shutter_offset">-22dp</dimen> <dimen name="size_thumbnail">200dip</dimen> <dimen name="size_preview">400dip</dimen> diff --git a/src/com/android/camera/CaptureModule.java b/src/com/android/camera/CaptureModule.java index 6fd69b2ef..7c00015bc 100644 --- a/src/com/android/camera/CaptureModule.java +++ b/src/com/android/camera/CaptureModule.java @@ -64,7 +64,6 @@ import com.android.camera.one.OneCamera.OpenCallback; import com.android.camera.one.OneCamera.PhotoCaptureParameters; import com.android.camera.one.OneCamera.PhotoCaptureParameters.Flash; import com.android.camera.one.OneCameraManager; -import com.android.camera.one.Settings3A; import com.android.camera.one.v2.OneCameraManagerImpl; import com.android.camera.remote.RemoteCameraModule; import com.android.camera.session.CaptureSession; @@ -73,6 +72,8 @@ import com.android.camera.settings.SettingsManager; import com.android.camera.ui.CountDownView; import com.android.camera.ui.PreviewStatusListener; import com.android.camera.ui.TouchCoordinate; +import com.android.camera.ui.focus.FocusController; +import com.android.camera.ui.focus.FocusSound; import com.android.camera.util.CameraUtil; import com.android.camera.util.GcamHelper; import com.android.camera.util.Size; @@ -122,21 +123,6 @@ public class CaptureModule extends CameraModule } }; - /** - * Hide AF target UI element. - */ - Runnable mHideAutoFocusTargetRunnable = new Runnable() { - @Override - public void run() { - // For debug UI off, showAutoFocusSuccess() just hides the AF UI. - if (mFocusedAtEnd) { - mUI.showAutoFocusSuccess(); - } else { - mUI.showAutoFocusFailure(); - } - } - }; - private static final Tag TAG = new Tag("CaptureModule"); private static final String PHOTO_MODULE_STRING_ID = "PhotoModule"; /** Enable additional debug output. */ @@ -175,7 +161,10 @@ public class CaptureModule extends CameraModule /** Whether HDR is currently enabled. */ private boolean mHdrEnabled = false; - /** State by the module state machine. */ + private FocusController mFocusController; + + + /** State by the module state machine. */ private static enum ModuleState { IDLE, WATCH_FOR_NEXT_FRAME_AFTER_PREVIEW_STARTED, @@ -223,7 +212,7 @@ public class CaptureModule extends CameraModule /** Used to fetch and embed the location into captured images. */ private final LocationManager mLocationManager; /** Plays sounds for countdown timer. */ - private SoundPlayer mCountdownSoundPlayer; + private SoundPlayer mSoundPlayer; /** Whether the module is paused right now. */ private boolean mPaused; @@ -306,6 +295,10 @@ public class CaptureModule extends CameraModule mLayoutListener); mAppController.setPreviewStatusListener(mUI); + mSoundPlayer = new SoundPlayer(mContext); + FocusSound focusSound = new FocusSound(mSoundPlayer, R.raw.material_camera_focus); + mFocusController = new FocusController(mUI.getFocusRing(), focusSound, mMainHandler); + // Set the preview texture from UI for the SurfaceTextureConsumer. mPreviewConsumer.setSurfaceTexture( mAppController.getCameraAppUI().getSurfaceTexture(), @@ -315,7 +308,7 @@ public class CaptureModule extends CameraModule mSensorManager = (SensorManager) (mContext.getSystemService(Context.SENSOR_SERVICE)); mAccelerometerSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mMagneticSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); - mCountdownSoundPlayer = new SoundPlayer(mContext); + String action = activity.getIntent().getAction(); mIsImageCaptureIntent = (MediaStore.ACTION_IMAGE_CAPTURE.equals(action) @@ -417,9 +410,9 @@ public class CaptureModule extends CameraModule @Override public void onRemainingSecondsChanged(int remainingSeconds) { if (remainingSeconds == 1) { - mCountdownSoundPlayer.play(R.raw.timer_final_second, 0.6f); + mSoundPlayer.play(R.raw.timer_final_second, 0.6f); } else if (remainingSeconds == 2 || remainingSeconds == 3) { - mCountdownSoundPlayer.play(R.raw.timer_increment, 0.6f); + mSoundPlayer.play(R.raw.timer_increment, 0.6f); } } @@ -590,8 +583,8 @@ public class CaptureModule extends CameraModule mAppController.getCameraAppUI().getSurfaceHeight()); } - mCountdownSoundPlayer.loadSound(R.raw.timer_final_second); - mCountdownSoundPlayer.loadSound(R.raw.timer_increment); + mSoundPlayer.loadSound(R.raw.timer_final_second); + mSoundPlayer.loadSound(R.raw.timer_increment); } @Override @@ -603,8 +596,8 @@ public class CaptureModule extends CameraModule closeCamera(); resetTextureBufferSize(); mFrameDistributor.close(); - mCountdownSoundPlayer.unloadSound(R.raw.timer_final_second); - mCountdownSoundPlayer.unloadSound(R.raw.timer_increment); + mSoundPlayer.unloadSound(R.raw.timer_final_second); + mSoundPlayer.unloadSound(R.raw.timer_increment); // Remove delayed resume trigger, if it hasn't been executed yet. mMainHandler.removeCallbacksAndMessages(null); @@ -619,7 +612,7 @@ public class CaptureModule extends CameraModule @Override public void destroy() { - mCountdownSoundPlayer.release(); + mSoundPlayer.release(); mCameraHandler.getLooper().quitSafely(); } @@ -730,47 +723,36 @@ public class CaptureModule extends CameraModule * Focus sequence starts for zone around tap location for single tap. */ @Override - public void onSingleTapUp(View view, int x, int y) { - Log.v(TAG, "onSingleTapUp x=" + x + " y=" + y); + public void onSingleTapUp(View view, int viewX, int viewY) { + Log.v(TAG, "onSingleTapUp x=" + viewX + " y=" + viewY); // TODO: This should query actual capability. if (mCameraFacing == Facing.FRONT) { return; } - triggerFocusAtScreenCoord(x, y); + startActiveFocusAt(viewX, viewY); } // TODO: Consider refactoring FocusOverlayManager. // Currently AF state transitions are controlled in OneCameraImpl. // PhotoModule uses FocusOverlayManager which uses API1/portability // logic and coordinates. - private void triggerFocusAtScreenCoord(int x, int y) { + private void startActiveFocusAt(int viewX, int viewY) { if (mCamera == null) { // If we receive this after the camera is closed, do nothing. return; } - mTapToFocusWaitForActiveScan = true; - // Show UI immediately even though scan has not started yet. - float minEdge = Math.min(mPreviewArea.width(), mPreviewArea.height()); - mUI.setAutoFocusTarget(x, y, false, - (int) (Settings3A.getAutoFocusRegionWidth() * mZoomValue * minEdge), - (int) (Settings3A.getMeteringRegionWidth() * mZoomValue * minEdge)); - mUI.showAutoFocusInProgress(); - - // Cancel any scheduled auto focus target UI actions. - mMainHandler.removeCallbacks(mHideAutoFocusTargetRunnable); - // Timeout in case camera fails to stop (unlikely). - mMainHandler.postDelayed(new Runnable() { - @Override - public void run() { - mMainHandler.post(mHideAutoFocusTargetRunnable); - } - }, FOCUS_UI_TIMEOUT_MILLIS); + // TODO: make mFocusController final and remove null check. + if (mFocusController == null) { + Log.v(TAG, "CaptureModule mFocusController is null!"); + return; + } + mFocusController.showActiveFocusAt(viewX, viewY); // Normalize coordinates to [0,1] per CameraOne API. float points[] = new float[2]; - points[0] = (x - mPreviewArea.left) / mPreviewArea.width(); - points[1] = (y - mPreviewArea.top) / mPreviewArea.height(); + points[0] = (viewX - mPreviewArea.left) / mPreviewArea.width(); + points[1] = (viewY - mPreviewArea.top) / mPreviewArea.height(); // Rotate coordinates to portrait orientation per CameraOne API. Matrix rotationMatrix = new Matrix(); @@ -780,8 +762,11 @@ public class CaptureModule extends CameraModule // Log touch (screen coordinates). if (mZoomValue == 1f) { - TouchCoordinate touchCoordinate = new TouchCoordinate(x - mPreviewArea.left, - y - mPreviewArea.top, mPreviewArea.width(), mPreviewArea.height()); + TouchCoordinate touchCoordinate = new TouchCoordinate( + viewX - mPreviewArea.left, + viewY - mPreviewArea.top, + mPreviewArea.width(), + mPreviewArea.height()); // TODO: Add to logging: duration, rotation. UsageStatistics.instance().tapToFocus(touchCoordinate, null); } @@ -790,13 +775,17 @@ public class CaptureModule extends CameraModule /** * Show AF target in center of preview. */ - private void setAutoFocusTargetPassive() { - float minEdge = Math.min(mPreviewArea.width(), mPreviewArea.height()); - mUI.setAutoFocusTarget((int) mPreviewArea.centerX(), (int) mPreviewArea.centerY(), - true, - (int) (Settings3A.getAutoFocusRegionWidth() * mZoomValue * minEdge), - (int) (Settings3A.getMeteringRegionWidth() * mZoomValue * minEdge)); - mUI.showAutoFocusInProgress(); + private void startPassiveFocus() { + // TODO: make mFocusController final and remove null check. + if (mFocusController == null) { + return; + } + + // TODO: Some passive focus scans may trigger on a location + // instead of the center of the screen. + mFocusController.showPassiveFocusAt( + (int) (mPreviewArea.width() / 2.0f), + (int) (mPreviewArea.height() / 2.0f)); } /** @@ -808,33 +797,17 @@ public class CaptureModule extends CameraModule switch (state) { case PASSIVE_SCAN: - mMainHandler.removeCallbacks(mHideAutoFocusTargetRunnable); - mMainHandler.post(new Runnable() { - @Override - public void run() { - setAutoFocusTargetPassive(); - } - }); + startPassiveFocus(); break; case ACTIVE_SCAN: - mTapToFocusWaitForActiveScan = false; break; case PASSIVE_FOCUSED: case PASSIVE_UNFOCUSED: - mMainHandler.post(new Runnable() { - @Override - public void run() { - mUI.setPassiveFocusSuccess(state == AutoFocusState.PASSIVE_FOCUSED); - } - }); + mFocusController.clearFocusIndicator(); break; case ACTIVE_FOCUSED: case ACTIVE_UNFOCUSED: - if (!mTapToFocusWaitForActiveScan) { - mFocusedAtEnd = state != AutoFocusState.ACTIVE_UNFOCUSED; - mMainHandler.removeCallbacks(mHideAutoFocusTargetRunnable); - mMainHandler.post(mHideAutoFocusTargetRunnable); - } + mFocusController.clearFocusIndicator(); break; } @@ -1274,6 +1247,12 @@ public class CaptureModule extends CameraModule mState = ModuleState.WATCH_FOR_NEXT_FRAME_AFTER_PREVIEW_STARTED; Log.d(TAG, "starting preview ..."); + + // TODO: make mFocusController final and remove null check. + if (mFocusController != null) { + camera.setFocusDistanceListener(mFocusController); + } + // TODO: Consider rolling these two calls into one. camera.startPreview(new Surface(mFrameDistributor.getInputSurfaceTexture()), new CaptureReadyCallback() { diff --git a/src/com/android/camera/CaptureModuleUI.java b/src/com/android/camera/CaptureModuleUI.java index 67fb12f4c..6d7166066 100644 --- a/src/com/android/camera/CaptureModuleUI.java +++ b/src/com/android/camera/CaptureModuleUI.java @@ -32,6 +32,7 @@ import com.android.camera.ui.PreviewOverlay; import com.android.camera.ui.PreviewOverlay.OnZoomChangedListener; import com.android.camera.ui.PreviewStatusListener; import com.android.camera.ui.ProgressOverlay; +import com.android.camera.ui.focus.FocusRing; import com.android.camera2.R; /** @@ -58,7 +59,7 @@ public class CaptureModuleUI implements return true; } }; - private final FocusOverlayManager.FocusUI mFocusUI; + private final FocusRing mFocusRing; private final CountDownView mCountdownView; private int mPreviewAreaWidth; @@ -139,7 +140,7 @@ public class CaptureModuleUI implements mPreviewOverlay = (PreviewOverlay) mRootView.findViewById(R.id.preview_overlay); mProgressOverlay = (ProgressOverlay) mRootView.findViewById(R.id.progress_overlay); - mFocusUI = (FocusOverlayManager.FocusUI) mRootView.findViewById(R.id.focus_overlay); + mFocusRing = (FocusRing) mRootView.findViewById(R.id.focus_ring); mCountdownView = (CountDownView) mRootView.findViewById(R.id.count_down_view); } @@ -185,35 +186,12 @@ public class CaptureModuleUI implements return mPreviewView.getTransform(m); } - public void showAutoFocusInProgress() { - mFocusUI.onFocusStarted(); - } - - public void showAutoFocusSuccess() { - mFocusUI.onFocusSucceeded(); - } - - public void showAutoFocusFailure() { - mFocusUI.onFocusFailed(); - } - - public void setPassiveFocusSuccess(boolean success) { - mFocusUI.setPassiveFocusSuccess(success); + public FocusRing getFocusRing() { + return mFocusRing; } public void showDebugMessage(String message) { - mFocusUI.showDebugMessage(message); - } - - public void setAutoFocusTarget(int x, int y, boolean isPassiveScan, int afSize, int aeSize) { - mFocusUI.setFocusPosition(x, y, isPassiveScan, afSize, aeSize); - } - - public void clearAutoFocusIndicator() { - mFocusUI.clearFocus(); - } - - public void clearAutoFocusIndicator(boolean waitUntilProgressIsHidden) { + /* NoOp */ } /** diff --git a/src/com/android/camera/FocusOverlayManager.java b/src/com/android/camera/FocusOverlayManager.java index 514d76a85..92372e4d3 100644 --- a/src/com/android/camera/FocusOverlayManager.java +++ b/src/com/android/camera/FocusOverlayManager.java @@ -16,12 +16,9 @@ package com.android.camera; -import android.annotation.TargetApi; -import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.hardware.Camera.Area; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -35,6 +32,8 @@ import com.android.camera.settings.SettingsManager; import com.android.camera.ui.PreviewStatusListener; import com.android.camera.ui.TouchCoordinate; import com.android.camera.util.ApiHelper; +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; import com.android.ex.camera2.portability.CameraCapabilities; @@ -89,7 +88,7 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha private boolean mMeteringAreaSupported; private boolean mLockAeAwbNeeded; private boolean mAeAwbLock; - private final Matrix mMatrix; + private CameraCoordinateTransformer mCoordinateTransformer; private boolean mMirror; // true if the camera is front-facing. private int mDisplayOrientation; @@ -104,7 +103,7 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha private final Handler mHandler; Listener mListener; private boolean mPreviousMoving; - private final FocusUI mUI; + private final FocusRing mFocusRing; private final Rect mPreviewRect = new Rect(0, 0, 0, 0); private boolean mFocusLocked; @@ -112,20 +111,6 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha private TouchCoordinate mTouchCoordinate; private long mTouchTime; - public interface FocusUI { - public boolean hasFaces(); - public void clearFocus(); - public void setFocusPosition(int x, int y, boolean isPassiveScan, int aFsize, int aEsize); - public void setFocusPosition(int x, int y, boolean isPassiveScan); - public void onFocusStarted(); - public void onFocusSucceeded(); - public void onFocusFailed(); - public void setPassiveFocusSuccess(boolean success); - public void showDebugMessage(String message); - public void pauseFaceDetection(); - public void resumeFaceDetection(); - } - public interface Listener { public void autoFocus(); public void cancelAutoFocus(); @@ -170,16 +155,15 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha public FocusOverlayManager(AppController appController, List<CameraCapabilities.FocusMode> defaultFocusModes, CameraCapabilities capabilities, - Listener listener, boolean mirror, Looper looper, FocusUI ui) { + Listener listener, boolean mirror, Looper looper, FocusRing focusRing) { mAppController = appController; mSettingsManager = appController.getSettingsManager(); mHandler = new MainHandler(this, looper); - mMatrix = new Matrix(); mDefaultFocusModes = new ArrayList<CameraCapabilities.FocusMode>(defaultFocusModes); updateCapabilities(capabilities); mListener = listener; setMirror(mirror); - mUI = ui; + mFocusRing = focusRing; mFocusLocked = false; } @@ -202,7 +186,8 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha public void setPreviewRect(Rect previewRect) { if (!mPreviewRect.equals(previewRect)) { mPreviewRect.set(previewRect); - setMatrix(); + resetCoordinateTransformer(); + mInitialized = true; } } @@ -211,34 +196,27 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha setPreviewRect(CameraUtil.rectFToRect(previewArea)); } - /** 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"); } } + private void lockAeAwbIfNeeded() { if (mLockAeAwbNeeded && !mAeAwbLock) { mAeAwbLock = true; @@ -300,7 +278,6 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha } else { mState = STATE_FAIL; } - updateFocusUI(); capture(); } else if (mState == STATE_FOCUSING) { // This happens when (1) user is half-pressing the focus key or @@ -311,7 +288,6 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha } else { mState = STATE_FAIL; } - updateFocusUI(); // If this is triggered by touch focus, cancel focus after a // while. if (mFocusArea != null) { @@ -333,13 +309,6 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha return; } - - // Ignore if the camera has detected some faces. - if (mUI.hasFaces()) { - mUI.clearFocus(); - return; - } - // Ignore if we have requested autofocus. This method only handles // continuous autofocus. if (mState != STATE_IDLE) { @@ -349,26 +318,23 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha // animate on false->true trasition only b/8219520 if (moving && !mPreviousMoving) { // Auto focus at the center of the preview. - mUI.setFocusPosition(mPreviewRect.centerX(), mPreviewRect.centerY(), true, - getAFRegionEdge(), getAERegionEdge()); - mUI.onFocusStarted(); - } else if (!moving) { - mUI.onFocusSucceeded(); + mFocusRing.startPassiveFocus(); + } else if (!moving && mFocusRing.isPassiveFocusRunning()) { + mFocusRing.stopFocusAnimations(); } mPreviousMoving = moving; } /** Returns width of auto focus region in pixels. */ - private int getAFRegionEdge() { + private int getAFRegionSizePx() { return (int) (Math.min(mPreviewRect.width(), mPreviewRect.height()) * AF_REGION_BOX); } /** Returns width of metering region in pixels. */ - private int getAERegionEdge() { + private int getAERegionSizePx() { return (int) (Math.min(mPreviewRect.width(), mPreviewRect.height()) * AE_REGION_BOX); } - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private void initializeFocusAreas(int x, int y) { if (mFocusArea == null) { mFocusArea = new ArrayList<Area>(); @@ -376,10 +342,9 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha } // Convert the coordinates to driver format. - calculateTapArea(x, y, getAFRegionEdge(), mFocusArea.get(0).rect); + 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<Area>(); @@ -387,7 +352,7 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha } // Convert the coordinates to driver format. - calculateTapArea(x, y, getAERegionEdge(), mMeteringArea.get(0).rect); + mMeteringArea.get(0).rect = computeCameraRectFromPreviewCoordinates(x, y, getAERegionSizePx()); } public void onSingleTapUp(int x, int y) { @@ -413,8 +378,9 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha initializeMeteringAreas(x, y); } - // Use margin to set the focus indicator to the touched area. - mUI.setFocusPosition(x, y, false, getAFRegionEdge(), getAERegionEdge()); + mFocusRing.startActiveFocus(); + mFocusRing.setFocusLocation(x, y); + // Log manual tap to focus. mTouchCoordinate = new TouchCoordinate(x, y, mPreviewRect.width(), mPreviewRect.height()); mTouchTime = System.currentTimeMillis(); @@ -427,7 +393,6 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha if (mFocusAreaSupported) { autoFocus(); } else { // Just show the indicator in all other cases. - updateFocusUI(); // Reset the metering area in 4 seconds. mHandler.removeMessages(RESET_TOUCH_FOCUS); mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY_MILLIS); @@ -445,7 +410,6 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha public void onPreviewStopped() { // If auto focus was in progress, it would have been stopped. mState = STATE_IDLE; - updateFocusUI(); } public void onCameraReleased() { @@ -468,10 +432,6 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha private void autoFocus(int focusingState) { mListener.autoFocus(); mState = focusingState; - // Pause the face view because the driver will keep sending face - // callbacks after the focus completes. - mUI.pauseFaceDetection(); - updateFocusUI(); mHandler.removeMessages(RESET_TOUCH_FOCUS); } @@ -498,10 +458,8 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha // driver is not reset. resetTouchFocus(); mListener.cancelAutoFocus(); - mUI.resumeFaceDetection(); mState = STATE_IDLE; mFocusLocked = false; - updateFocusUI(); mHandler.removeMessages(RESET_TOUCH_FOCUS); } @@ -567,42 +525,11 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha return mMeteringArea; } - public void updateFocusUI() { - if (!mInitialized) { - // Show only focus indicator or face indicator. - return; - } - 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) { - mUI.onFocusStarted(); - } else { - if (mFocusMode == CameraCapabilities.FocusMode.CONTINUOUS_PICTURE) { - // TODO: check HAL behavior and decide if this can be removed. - mUI.onFocusSucceeded(); - } else if (mState == STATE_SUCCESS) { - mUI.onFocusSucceeded(); - } else if (mState == STATE_FAIL) { - mUI.onFocusFailed(); - } - } - } - public void resetTouchFocus() { if (!mInitialized) { return; } - // Put focus indicator to the center. clear reset position - mUI.clearFocus(); - // Initialize mFocusArea. mFocusArea = null; mMeteringArea = null; // This will cause current module to call getFocusAreas() and @@ -616,15 +543,14 @@ public class FocusOverlayManager implements PreviewStatusListener.PreviewAreaCha } } - private void calculateTapArea(int x, int y, int size, Rect rect) { + 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 + size, top + size); - mMatrix.mapRect(rectF); - CameraUtil.rectFToRect(rectF, rect); + return CameraUtil.rectFToRect(mCoordinateTransformer.toCameraSpace(rectF)); } /* package */ int getFocusState() { diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java index 659068dc3..735b4c9c9 100644 --- a/src/com/android/camera/PhotoModule.java +++ b/src/com/android/camera/PhotoModule.java @@ -41,7 +41,6 @@ import android.os.MessageQueue; import android.os.SystemClock; import android.provider.MediaStore; import android.view.KeyEvent; -import android.view.OrientationEventListener; import android.view.View; import com.android.camera.PhotoModule.NamedImages.NamedEntity; @@ -52,7 +51,6 @@ import com.android.camera.app.MediaSaver; import com.android.camera.app.MemoryManager; import com.android.camera.app.MemoryManager.MemoryListener; import com.android.camera.app.MotionManager; -import com.android.camera.app.OrientationManager; import com.android.camera.debug.Log; import com.android.camera.exif.ExifInterface; import com.android.camera.exif.ExifTag; @@ -1051,7 +1049,6 @@ public class PhotoModule Log.v(TAG, "mPictureDisplayedToJpegCallbackTime = " + mPictureDisplayedToJpegCallbackTime + "ms"); - mFocusManager.updateFocusUI(); // Ensure focus indicator is hidden. if (!mIsImageCaptureIntent) { setupPreview(); } @@ -1658,7 +1655,7 @@ public class PhotoModule mFocusManager = new FocusOverlayManager(mAppController, defaultFocusModes, mCameraCapabilities, this, mMirror, mActivity.getMainLooper(), - mUI.getFocusUI()); + mUI.getFocusRing()); mMotionManager = getServices().getMotionManager(); if (mMotionManager != null) { mMotionManager.addListener(mFocusManager); diff --git a/src/com/android/camera/PhotoUI.java b/src/com/android/camera/PhotoUI.java index dac4cb3e3..6e59bf9d9 100644 --- a/src/com/android/camera/PhotoUI.java +++ b/src/com/android/camera/PhotoUI.java @@ -24,7 +24,6 @@ import android.graphics.RectF; import android.graphics.SurfaceTexture; import android.hardware.Camera.Face; import android.os.AsyncTask; -import android.os.Build; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; @@ -32,13 +31,13 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; -import com.android.camera.FocusOverlayManager.FocusUI; import com.android.camera.debug.DebugPropertyHelper; import com.android.camera.debug.Log; import com.android.camera.ui.CountDownView; import com.android.camera.ui.FaceView; import com.android.camera.ui.PreviewOverlay; import com.android.camera.ui.PreviewStatusListener; +import com.android.camera.ui.focus.FocusRing; import com.android.camera.util.ApiHelper; import com.android.camera.util.CameraUtil; import com.android.camera.util.GservicesHelper; @@ -58,7 +57,7 @@ public class PhotoUI implements PreviewStatusListener, private static final float UNSET = 0f; private final PreviewOverlay mPreviewOverlay; - private final FocusUI mFocusUI; + private final FocusRing mFocusRing; private final CameraActivity mActivity; private final PhotoController mController; @@ -232,7 +231,7 @@ public class PhotoUI implements PreviewStatusListener, mActivity.getLayoutInflater().inflate(R.layout.photo_module, moduleRoot, true); initIndicators(); - mFocusUI = (FocusUI) mRootView.findViewById(R.id.focus_overlay); + mFocusRing = (FocusRing) mRootView.findViewById(R.id.focus_ring); mPreviewOverlay = (PreviewOverlay) mRootView.findViewById(R.id.preview_overlay); mCountdownView = (CountDownView) mRootView.findViewById(R.id.count_down_view); // Show faces if we are in debug mode. @@ -282,8 +281,8 @@ public class PhotoUI implements PreviewStatusListener, } - public FocusUI getFocusUI() { - return mFocusUI; + public FocusRing getFocusRing() { + return mFocusRing; } public void updatePreviewAspectRatio(float aspectRatio) { diff --git a/src/com/android/camera/SoundPlayer.java b/src/com/android/camera/SoundPlayer.java index afb928b5c..ff3f37f7a 100644 --- a/src/com/android/camera/SoundPlayer.java +++ b/src/com/android/camera/SoundPlayer.java @@ -32,6 +32,7 @@ public class SoundPlayer { 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. @@ -78,13 +79,17 @@ public class SoundPlayer { * 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 12143c220..03024f94d 100644 --- a/src/com/android/camera/VideoModule.java +++ b/src/com/android/camera/VideoModule.java @@ -485,7 +485,7 @@ public class VideoModule extends CameraModule } mFocusManager = new FocusOverlayManager(mAppController, defaultFocusModes, mCameraCapabilities, this, mMirror, - mActivity.getMainLooper(), mUI.getFocusUI()); + mActivity.getMainLooper(), mUI.getFocusRing()); } mAppController.addPreviewAreaSizeChangedListener(mFocusManager); } @@ -1324,7 +1324,7 @@ public class VideoModule extends CameraModule Log.i(TAG, "startVideoRecording: " + Thread.currentThread()); mUI.cancelAnimations(); mUI.setSwipingEnabled(false); - mUI.showFocusUI(false); + mUI.hidePassiveFocusIndicator(); mUI.showVideoRecordingHints(false); mAppController.getCameraAppUI().hideCaptureIndicator(); @@ -1446,7 +1446,7 @@ public class VideoModule extends CameraModule Log.v(TAG, "stopVideoRecording"); mUI.setSwipingEnabled(true); - mUI.showFocusUI(true); + mUI.showPassiveFocusIndicator(); mUI.showVideoRecordingHints(true); boolean fail = false; diff --git a/src/com/android/camera/VideoUI.java b/src/com/android/camera/VideoUI.java index 2cc02a7eb..76daf3aec 100644 --- a/src/com/android/camera/VideoUI.java +++ b/src/com/android/camera/VideoUI.java @@ -29,17 +29,15 @@ import android.widget.TextView; import com.android.camera.app.OrientationManager; import com.android.camera.debug.Log; -import com.android.camera.ui.FocusOverlay; import com.android.camera.ui.PreviewOverlay; import com.android.camera.ui.PreviewStatusListener; import com.android.camera.ui.RotateLayout; +import com.android.camera.ui.focus.FocusRing; import com.android.camera.widget.VideoRecordingHints; import com.android.camera2.R; import com.android.ex.camera2.portability.CameraCapabilities; import com.android.ex.camera2.portability.CameraSettings; -import java.util.List; - public class VideoUI implements PreviewStatusListener { private static final Log.Tag TAG = new Log.Tag("VideoUI"); @@ -48,7 +46,7 @@ public class VideoUI implements PreviewStatusListener { // module fields private final CameraActivity mActivity; private final View mRootView; - private final FocusOverlay mFocusUI; + private final FocusRing mFocusRing; // An review image having same size as preview. It is displayed when // recording is stopped in capture intent. private ImageView mReviewImage; @@ -108,7 +106,7 @@ public class VideoUI implements PreviewStatusListener { initializeMiscControls(); mAnimationManager = new AnimationManager(); - mFocusUI = (FocusOverlay) mRootView.findViewById(R.id.focus_overlay); + mFocusRing = (FocusRing) mRootView.findViewById(R.id.focus_ring); mVideoHints = (VideoRecordingHints) mRootView.findViewById(R.id.video_shooting_hints); } @@ -126,8 +124,8 @@ public class VideoUI implements PreviewStatusListener { setAspectRatio(aspectRatio); } - public FocusOverlayManager.FocusUI getFocusUI() { - return mFocusUI; + public FocusRing getFocusRing() { + return mFocusRing; } /** @@ -237,16 +235,25 @@ public class VideoUI implements PreviewStatusListener { } /** - * Shows or hides focus UI. - * - * @param show shows focus UI when true, hides it otherwise + * Hide the focus indicator. + */ + public void hidePassiveFocusIndicator() { + if (mFocusRing != null) { + Log.v(TAG, "mFocusRing.stopFocusAnimations()"); + mFocusRing.stopFocusAnimations(); + } + } + + /** + * Show the passive focus indicator. */ - public void showFocusUI(boolean show) { - if (mFocusUI != null) { - mFocusUI.setVisibility(show ? View.VISIBLE : View.INVISIBLE); + public void showPassiveFocusIndicator() { + if (mFocusRing != null) { + mFocusRing.startPassiveFocus(); } } + /** * Shows or hides video recording hints. * diff --git a/src/com/android/camera/app/CameraAppUI.java b/src/com/android/camera/app/CameraAppUI.java index 5104604b9..e79f754c1 100644 --- a/src/com/android/camera/app/CameraAppUI.java +++ b/src/com/android/camera/app/CameraAppUI.java @@ -58,6 +58,7 @@ import com.android.camera.ui.PreviewOverlay; import com.android.camera.ui.PreviewStatusListener; import com.android.camera.ui.StickyBottomCaptureLayout; import com.android.camera.ui.TouchCoordinate; +import com.android.camera.ui.focus.FocusRing; import com.android.camera.util.ApiHelper; import com.android.camera.util.CameraUtil; import com.android.camera.util.Gusterpolator; @@ -513,7 +514,7 @@ public class CameraAppUI implements ModeListView.ModeSwitchListener, private BottomBar mBottomBar; private ModeOptionsOverlay mModeOptionsOverlay; private IndicatorIconController mIndicatorIconController; - private View mFocusOverlay; + private FocusRing mFocusRing; private FrameLayout mTutorialsPlaceHolderWrapper; private StickyBottomCaptureLayout mStickyBottomCaptureLayout; private TextureViewHelper mTextureViewHelper; @@ -1263,7 +1264,7 @@ public class CameraAppUI implements ModeListView.ModeSwitchListener, mController.getSettingsManager().addListener(mIndicatorIconController); mModeOptionsToggle = mCameraRootView.findViewById(R.id.mode_options_toggle); - mFocusOverlay = mCameraRootView.findViewById(R.id.focus_overlay); + mFocusRing = (FocusRing) mCameraRootView.findViewById(R.id.focus_ring); mTutorialsPlaceHolderWrapper = (FrameLayout) mCameraRootView .findViewById(R.id.tutorials_placeholder_wrapper); mStickyBottomCaptureLayout = (StickyBottomCaptureLayout) mAppRootView @@ -1307,7 +1308,9 @@ public class CameraAppUI implements ModeListView.ModeSwitchListener, setShutterButtonEnabled(true); mPreviewStatusListener = null; mPreviewOverlay.reset(); - mFocusOverlay.setVisibility(View.INVISIBLE); + + Log.v(TAG, "mFocusRing.stopFocusAnimations()"); + mFocusRing.stopFocusAnimations(); } /** diff --git a/src/com/android/camera/one/AbstractOneCamera.java b/src/com/android/camera/one/AbstractOneCamera.java index 15276ecca..b9514b96f 100644 --- a/src/com/android/camera/one/AbstractOneCamera.java +++ b/src/com/android/camera/one/AbstractOneCamera.java @@ -32,6 +32,7 @@ import java.util.TimeZone; public abstract class AbstractOneCamera implements OneCamera { protected FocusStateListener mFocusStateListener; protected ReadyStateChangedListener mReadyStateChangedListener; + protected FocusDistanceListener mFocusDistanceListener; /** * Number of characters from the end of the device serial number used to @@ -45,6 +46,11 @@ public abstract class AbstractOneCamera implements OneCamera { } @Override + public void setFocusDistanceListener(FocusDistanceListener listener) { + mFocusDistanceListener = listener; + } + + @Override public void setReadyStateChangedListener(ReadyStateChangedListener listener) { mReadyStateChangedListener = listener; } diff --git a/src/com/android/camera/one/OneCamera.java b/src/com/android/camera/one/OneCamera.java index a45f83433..d4700717a 100644 --- a/src/com/android/camera/one/OneCamera.java +++ b/src/com/android/camera/one/OneCamera.java @@ -208,6 +208,33 @@ public interface OneCamera { } /** + * Classes implementing this interface will be called when the focus + * distance of the physical lens changes. + */ + public static interface FocusDistanceListener { + /** + * Called when physical lens distance on the camera changes. + * + * @param diopter the lens diopter from the last known position. + * @param isActive whether the lens is moving. + */ + public void onFocusDistance(float diopter, boolean isActive); + } + + /** + * Single instance of the current camera AF state. + */ + public static class FocusState { + public final float diopter; + public final boolean isActive; + + public FocusState(float diopter, boolean isActive) { + this.diopter = diopter; + this.isActive = isActive; + } + } + + /** * Parameters to be given to capture requests. */ public static abstract class CaptureParameters { @@ -333,6 +360,12 @@ public interface OneCamera { public void setFocusStateListener(FocusStateListener listener); /** + * Sets or replaces a listener that is called whenever the focus state of + * the camera changes. + */ + public void setFocusDistanceListener(FocusDistanceListener listener); + + /** * Sets or replaces a listener that is called whenever the state of the * camera changes to be either ready or not ready to take another picture. */ diff --git a/src/com/android/camera/one/v2/ImageCaptureManager.java b/src/com/android/camera/one/v2/ImageCaptureManager.java index 4240a893d..09b3e180a 100644 --- a/src/com/android/camera/one/v2/ImageCaptureManager.java +++ b/src/com/android/camera/one/v2/ImageCaptureManager.java @@ -432,42 +432,7 @@ public class ImageCaptureManager extends CameraCaptureSession.CaptureCallback im @Override public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, final CaptureResult partialResult) { - long frameNumber = partialResult.getFrameNumber(); - - // Update mMetadata for whichever keys are present, if this frame is - // supplying newer values. - for (final Key<?> key : partialResult.getKeys()) { - Pair<Long, Object> oldEntry = mMetadata.get(key); - final Object oldValue = (oldEntry != null) ? oldEntry.second : null; - - boolean newerValueAlreadyExists = oldEntry != null - && frameNumber < oldEntry.first; - if (newerValueAlreadyExists) { - continue; - } - - final Object newValue = partialResult.get(key); - mMetadata.put(key, new Pair<Long, Object>(frameNumber, newValue)); - - // If the value has changed, call the appropriate listeners, if - // any exist. - if (oldValue == newValue || !mMetadataChangeListeners.containsKey(key)) { - continue; - } - - for (final MetadataChangeListener listener : - mMetadataChangeListeners.get(key)) { - Log.v(TAG, "Dispatching to metadata change listener for key: " - + key.toString()); - mListenerHandler.post(new Runnable() { - @Override - public void run() { - listener.onImageMetadataChange(key, oldValue, newValue, - partialResult); - } - }); - } - } + updateMetadataChangeListeners(partialResult); } @Override @@ -475,6 +440,8 @@ public class ImageCaptureManager extends CameraCaptureSession.CaptureCallback im final TotalCaptureResult result) { final long timestamp = result.get(TotalCaptureResult.SENSOR_TIMESTAMP); + updateMetadataChangeListeners(result); + // Detect camera thread stall. long now = SystemClock.uptimeMillis(); if (now - mDebugLastOnCaptureCompletedMillis < DEBUG_INTERFRAME_STALL_WARNING) { @@ -497,6 +464,43 @@ public class ImageCaptureManager extends CameraCaptureSession.CaptureCallback im tryExecutePendingCaptureRequest(timestamp); } + private void updateMetadataChangeListeners(final CaptureResult result) { + long frameNumber = result.getFrameNumber(); + + // Update mMetadata for whichever keys are present, if this frame is + // supplying newer values. + for (final Key<?> key : result.getKeys()) { + Pair<Long, Object> oldEntry = mMetadata.get(key); + final Object oldValue = (oldEntry != null) ? oldEntry.second : null; + + boolean newerValueAlreadyExists = oldEntry != null + && frameNumber < oldEntry.first; + if (newerValueAlreadyExists) { + continue; + } + + final Object newValue = result.get(key); + mMetadata.put(key, new Pair<Long, Object>(frameNumber, newValue)); + + // If the value has changed, call the appropriate listeners, if + // any exist. + if (oldValue == newValue || !mMetadataChangeListeners.containsKey(key)) { + continue; + } + + for (final MetadataChangeListener listener : + mMetadataChangeListeners.get(key)) { + mListenerHandler.post(new Runnable() { + @Override + public void run() { + listener.onImageMetadataChange(key, oldValue, newValue, + result); + } + }); + } + } + } + private boolean doMetaDataSwap(final TotalCaptureResult newMetadata, final long timestamp) { mEvictionHandler.get().onFrameCaptureResultAvailable(timestamp, newMetadata); diff --git a/src/com/android/camera/one/v2/OneCameraImpl.java b/src/com/android/camera/one/v2/OneCameraImpl.java index d70c3183b..2837f626c 100644 --- a/src/com/android/camera/one/v2/OneCameraImpl.java +++ b/src/com/android/camera/one/v2/OneCameraImpl.java @@ -223,6 +223,11 @@ public class OneCameraImpl extends AbstractOneCamera { AutoFocusHelper.logExtraFocusInfo(result); } + Float diopter = result.get(CaptureResult.LENS_FOCUS_DISTANCE); + if(diopter != null && mFocusDistanceListener != null) { + mFocusDistanceListener.onFocusDistance(diopter, true); + } + if (request.getTag() == RequestTag.CAPTURE) { // Add the capture result to the latest in-flight // capture. If all the data for that capture is diff --git a/src/com/android/camera/one/v2/OneCameraZslImpl.java b/src/com/android/camera/one/v2/OneCameraZslImpl.java index 97faf0f83..d53e89452 100644 --- a/src/com/android/camera/one/v2/OneCameraZslImpl.java +++ b/src/com/android/camera/one/v2/OneCameraZslImpl.java @@ -355,6 +355,27 @@ public class OneCameraZslImpl extends AbstractOneCamera { mMediaActionSound.load(MediaActionSound.SHUTTER_CLICK); } + @Override + public void setFocusDistanceListener(FocusDistanceListener focusDistanceListener) { + if(mFocusDistanceListener == null) { + mCaptureManager.addMetadataChangeListener(CaptureResult.LENS_FOCUS_DISTANCE, + new ImageCaptureManager.MetadataChangeListener() { + @Override + public void onImageMetadataChange(Key<?> key, Object oldValue, + Object newValue, + CaptureResult result) { + Integer state = result.get(CaptureResult.LENS_STATE); + if (newValue != null && state != null) { + mFocusDistanceListener.onFocusDistance((float) newValue, state == 1); + } else if (newValue != null) { + mFocusDistanceListener.onFocusDistance((float) newValue, true); + } + } + }); + } + mFocusDistanceListener = focusDistanceListener; + } + /** * @return The largest supported picture size. */ diff --git a/src/com/android/camera/one/v2/SimpleJpegOneCameraFactory.java b/src/com/android/camera/one/v2/SimpleJpegOneCameraFactory.java index 813ca9c66..e48d35715 100644 --- a/src/com/android/camera/one/v2/SimpleJpegOneCameraFactory.java +++ b/src/com/android/camera/one/v2/SimpleJpegOneCameraFactory.java @@ -16,18 +16,6 @@ package com.android.camera.one.v2; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - import android.graphics.ImageFormat; import android.graphics.Rect; import android.hardware.camera2.CameraCharacteristics; @@ -57,6 +45,7 @@ import com.android.camera.async.SafeCloseable; import com.android.camera.async.Updatable; import com.android.camera.one.CameraDirectionProvider; import com.android.camera.one.OneCamera; +import com.android.camera.one.OneCamera.FocusState; import com.android.camera.one.v2.camera2proxy.CameraCaptureSessionProxy; import com.android.camera.one.v2.camera2proxy.CameraDeviceProxy; import com.android.camera.one.v2.camera2proxy.CameraDeviceRequestBuilderFactory; @@ -98,6 +87,18 @@ import com.android.camera.session.CaptureSession; import com.android.camera.util.ScopedFactory; import com.android.camera.util.Size; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + /** */ public class SimpleJpegOneCameraFactory { @@ -111,6 +112,7 @@ public class SimpleJpegOneCameraFactory { public final FutureResult<GenericOneCameraImpl.PictureTaker> pictureTaker; public final FutureResult<GenericOneCameraImpl.ManualAutoFocus> manualAutoFocus; public final ConcurrentState<Integer> afState; + public final ConcurrentState<FocusState> focusState; public final ConcurrentState<Boolean> readyState; public final ConcurrentState<Float> zoomState; public final ConcurrentState<Boolean> previewStartSuccess; @@ -123,6 +125,7 @@ public class SimpleJpegOneCameraFactory { FutureResult<GenericOneCameraImpl.PictureTaker> pictureTaker, FutureResult<GenericOneCameraImpl.ManualAutoFocus> manualAutoFocus, ConcurrentState<Integer> afState, + ConcurrentState<FocusState> focusState, ConcurrentState<Boolean> readyState, ConcurrentState<Float> zoomState, ConcurrentState<Boolean> previewStartSuccess, Size pictureSize) { @@ -132,6 +135,7 @@ public class SimpleJpegOneCameraFactory { this.pictureTaker = pictureTaker; this.manualAutoFocus = manualAutoFocus; this.afState = afState; + this.focusState = focusState; this.readyState = readyState; this.zoomState = zoomState; this.previewStartSuccess = previewStartSuccess; @@ -175,12 +179,13 @@ public class SimpleJpegOneCameraFactory { FutureResult<GenericOneCameraImpl.PictureTaker> pictureTakerFutureResult = new FutureResult<>(); FutureResult<GenericOneCameraImpl.ManualAutoFocus> manualAutoFocusFutureResult = new FutureResult<>(); ConcurrentState<Integer> afState = new ConcurrentState<>(); + ConcurrentState<FocusState> focusState = new ConcurrentState<>(); ConcurrentState<Boolean> readyState = new ConcurrentState<>(); ConcurrentState<Float> zoomState = new ConcurrentState<>(); ConcurrentState<Boolean> previewStartSuccess = new ConcurrentState<>(); return new CameraScope(device, characteristics, mainHandler, pictureTakerFutureResult, - manualAutoFocusFutureResult, afState, + manualAutoFocusFutureResult, afState, focusState, readyState, zoomState, previewStartSuccess, pictureSize); } @@ -216,6 +221,10 @@ public class SimpleJpegOneCameraFactory { return new ListenableConcurrentState<>(scope.afState, provideMainHandlerExecutor(scope)); } + private static Listenable<FocusState> provideFocusStateListenable(CameraScope scope) { + return new ListenableConcurrentState<>(scope.focusState, provideMainHandlerExecutor(scope)); + } + private static Listenable<Boolean> provideReadyStateListenable(CameraScope scope) { return new ListenableConcurrentState<>(scope.readyState, provideMainHandlerExecutor(scope)); } @@ -519,6 +528,7 @@ public class SimpleJpegOneCameraFactory { GenericOneCameraImpl.PictureTaker pictureTaker = providePictureTaker(scope); GenericOneCameraImpl.ManualAutoFocus manualAutoFocus = provideManualAutoFocus(scope); Listenable<Integer> afStateListenable = provideAFStateListenable(scope); + Listenable<FocusState> focusStateListenable = provideFocusStateListenable(scope); Listenable<Boolean> readyStateListenable = provideReadyStateListenable(scope); float maxZoom = provideMaxZoom(scope.characteristics); Updatable<Float> zoom = provideZoom(scope); @@ -530,7 +540,7 @@ public class SimpleJpegOneCameraFactory { Listenable<Boolean> previewStartSuccessListenable = providePreviewStartSuccessListenable(scope); return new GenericOneCameraImpl(closeListeners, pictureTaker, manualAutoFocus, - afStateListenable, readyStateListenable, maxZoom, zoom, + afStateListenable, focusStateListenable, readyStateListenable, maxZoom, zoom, supportedPreviewSizes, fullSizeAspectRatio, direction, previewSizeSelector, previewStartSuccessListenable, surfaceRunnableScopedFactory); } diff --git a/src/com/android/camera/one/v2/common/GenericOneCameraImpl.java b/src/com/android/camera/one/v2/common/GenericOneCameraImpl.java index a19fd2844..1357b5cae 100644 --- a/src/com/android/camera/one/v2/common/GenericOneCameraImpl.java +++ b/src/com/android/camera/one/v2/common/GenericOneCameraImpl.java @@ -16,9 +16,6 @@ package com.android.camera.one.v2.common; -import java.util.HashSet; -import java.util.Set; - import android.content.Context; import android.view.Surface; @@ -32,6 +29,9 @@ import com.android.camera.util.Callback; import com.android.camera.util.ScopedFactory; import com.android.camera.util.Size; +import java.util.HashSet; +import java.util.Set; + /** * A generic, composable {@link OneCamera}. * <p> @@ -61,6 +61,7 @@ public class GenericOneCameraImpl implements OneCamera { private final PictureTaker mPictureTaker; private final ManualAutoFocus mManualAutoFocus; private final Listenable<Integer> mAFStateListenable; + private final Listenable<FocusState> mFocusStateListenable; private final Listenable<Boolean> mReadyStateListenable; private final float mMaxZoom; private final Updatable<Float> mZoom; @@ -71,10 +72,17 @@ public class GenericOneCameraImpl implements OneCamera { private final Listenable<Boolean> mPreviewStartSuccessListenable; private final ScopedFactory<Surface, Runnable> mPreviewScopeEntrance; - public GenericOneCameraImpl(Set<SafeCloseable> closeListeners, PictureTaker pictureTaker, - ManualAutoFocus manualAutoFocus, Listenable<Integer> afStateProvider, - Listenable<Boolean> readyStateListenable, float maxZoom, Updatable<Float> zoom, - Size[] supportedPreviewSizes, float fullSizeAspectRatio, Facing direction, + public GenericOneCameraImpl(Set<SafeCloseable> closeListeners, + PictureTaker pictureTaker, + ManualAutoFocus manualAutoFocus, + Listenable<Integer> afStateProvider, + Listenable<FocusState> focusStateProvider, + Listenable<Boolean> readyStateListenable, + float maxZoom, + Updatable<Float> zoom, + Size[] supportedPreviewSizes, + float fullSizeAspectRatio, + Facing direction, PreviewSizeSelector previewSizeSelector, Listenable<Boolean> previewStartSuccessListenable, ScopedFactory<Surface, Runnable> previewScopeEntrance) { @@ -89,6 +97,7 @@ public class GenericOneCameraImpl implements OneCamera { mPictureTaker = pictureTaker; mManualAutoFocus = manualAutoFocus; mAFStateListenable = afStateProvider; + mFocusStateListenable = focusStateProvider; mReadyStateListenable = readyStateListenable; mZoom = zoom; } @@ -128,6 +137,16 @@ public class GenericOneCameraImpl implements OneCamera { } @Override + public void setFocusDistanceListener(final FocusDistanceListener listener) { + mFocusStateListenable.setCallback(new Callback<FocusState>() { + @Override + public void onCallback(FocusState focusState) { + listener.onFocusDistance(focusState.diopter, focusState.isActive); + } + }); + } + + @Override public void setReadyStateChangedListener(final ReadyStateChangedListener listener) { mReadyStateListenable.setCallback(new Callback<Boolean>() { @Override diff --git a/src/com/android/camera/ui/FocusOverlay.java b/src/com/android/camera/ui/FocusOverlay.java deleted file mode 100644 index bd913d4f6..000000000 --- a/src/com/android/camera/ui/FocusOverlay.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (C) 2013 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; - -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.View; - -import com.android.camera.FocusOverlayManager; -import com.android.camera.debug.DebugPropertyHelper; -import com.android.camera.debug.Log; -import com.android.camera2.R; - -/** - * Displays a focus indicator. - */ -public class FocusOverlay extends View implements FocusOverlayManager.FocusUI { - private static final Log.Tag TAG = new Log.Tag("FocusOverlay"); - - /** System Properties switch to enable debugging focus UI. */ - private static final boolean CAPTURE_DEBUG_UI = DebugPropertyHelper.showCaptureDebugUI(); - - private final static int FOCUS_DURATION_MS = 500; - private final static int FOCUS_INDICATOR_ROTATION_DEGREES = 50; - - private final Drawable mFocusIndicator; - private Drawable mFocusOuterRing; - private final Rect mBounds = new Rect(); - private final ValueAnimator mFocusAnimation = new ValueAnimator(); - - private Paint mDebugSolidPaint; - private Paint mDebugCornersPaint; - private Paint mDebugTextPaint; - private int mDebugStartColor; - private int mDebugSuccessColor; - private int mDebugFailColor; - private Rect mFocusDebugSolidRect; - private Rect mFocusDebugCornersRect; - private boolean mIsPassiveScan; - private String mDebugMessage; - - private int mPositionX; - private int mPositionY; - private int mAngle; - private final int mFocusIndicatorSize; - private boolean mShowIndicator; - private final int mFocusOuterRingSize; - - public FocusOverlay(Context context, AttributeSet attrs) { - super(context, attrs); - mFocusIndicator = getResources().getDrawable(R.drawable.focus_ring_touch_inner); - mFocusIndicatorSize = getResources().getDimensionPixelSize(R.dimen.focus_inner_ring_size); - mFocusOuterRing = getResources().getDrawable(R.drawable.focus_ring_touch_outer); - mFocusOuterRingSize = getResources().getDimensionPixelSize(R.dimen.focus_outer_ring_size); - - if (CAPTURE_DEBUG_UI) { - Resources res = getResources(); - mDebugStartColor = res.getColor(R.color.focus_debug); - mDebugSuccessColor = res.getColor(R.color.focus_debug_success); - mDebugFailColor = res.getColor(R.color.focus_debug_fail); - mDebugTextPaint= new Paint(); - mDebugTextPaint.setColor(res.getColor(R.color.focus_debug_text)); - mDebugTextPaint.setStyle(Paint.Style.FILL); - mDebugSolidPaint = new Paint(); - mDebugSolidPaint.setColor(res.getColor(R.color.focus_debug)); - mDebugSolidPaint.setAntiAlias(true); - mDebugSolidPaint.setStyle(Paint.Style.STROKE); - mDebugSolidPaint.setStrokeWidth(res.getDimension(R.dimen.focus_debug_stroke)); - mDebugCornersPaint = new Paint(mDebugSolidPaint); - mDebugCornersPaint.setColor(res.getColor(R.color.focus_debug)); - mFocusDebugSolidRect = new Rect(); - mFocusDebugCornersRect = new Rect(); - } - } - - @Override - public boolean hasFaces() { - // TODO: Add face detection support. - return false; - } - - @Override - public void clearFocus() { - mShowIndicator = false; - if (CAPTURE_DEBUG_UI) { - setVisibility(INVISIBLE); - } - } - - @Override - public void setFocusPosition(int x, int y, boolean isPassiveScan) { - setFocusPosition(x, y, isPassiveScan, 0, 0); - } - - @Override - public void setFocusPosition(int x, int y, boolean isPassiveScan, int aFsize, int aEsize) { - mIsPassiveScan = isPassiveScan; - mPositionX = x; - mPositionY = y; - mBounds.set(x - mFocusIndicatorSize / 2, y - mFocusIndicatorSize / 2, - x + mFocusIndicatorSize / 2, y + mFocusIndicatorSize / 2); - mFocusIndicator.setBounds(mBounds); - mFocusOuterRing.setBounds(x - mFocusOuterRingSize / 2, y - mFocusOuterRingSize / 2, - x + mFocusOuterRingSize / 2, y + mFocusOuterRingSize / 2); - - if (CAPTURE_DEBUG_UI) { - mFocusOuterRing.setBounds(0, 0, 0, 0); - if (isPassiveScan) { - // Use AE rect only. - mFocusDebugSolidRect.setEmpty(); - int avg = (aFsize + aEsize) / 2; - mFocusDebugCornersRect.set(x - avg / 2, y - avg / 2, x + avg / 2, y + avg / 2); - } else { - mFocusDebugSolidRect.set(x - aFsize / 2, y - aFsize / 2, x + aFsize / 2, - y + aFsize / 2); - // If AE region is different size than AF region and active scan. - if (aFsize != aEsize) { - mFocusDebugCornersRect.set(x - aEsize / 2, y - aEsize / 2, x + aEsize / 2, - y + aEsize / 2); - } else { - mFocusDebugCornersRect.setEmpty(); - } - } - mDebugSolidPaint.setColor(mDebugStartColor); - mDebugCornersPaint.setColor(mDebugStartColor); - } - - if (getVisibility() != VISIBLE) { - setVisibility(VISIBLE); - } - invalidate(); - } - - /** - * This is called in: - * <ul> - * <li>API1 non-CAF after autoFocus().</li> - * <li>API1 CAF mode for onAutoFocusMoving(true).</li> - * <li>API2 for transition to ACTIVE_SCANNING or PASSIVE_SCANNING.</li> - * <ul> - * TODO after PhotoModule/GcamModule deprecation: Do not use this for CAF. - */ - @Override - public void onFocusStarted() { - mShowIndicator = true; - mFocusAnimation.setIntValues(0, FOCUS_INDICATOR_ROTATION_DEGREES); - mFocusAnimation.setDuration(FOCUS_DURATION_MS); - mFocusAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - mAngle = (Integer) animation.getAnimatedValue(); - invalidate(); - } - }); - mFocusAnimation.start(); - if (CAPTURE_DEBUG_UI) { - mDebugMessage = null; - } - } - - /** - * This is called in: - * <ul> - * <li>API1 non-CAF for onAutoFocus(true).</li> - * <li>API2 non-CAF for transition to FOCUSED_LOCKED.</li> - * <li>API1 CAF mode for onAutoFocusMoving(false).</li> - * <ul> - * TODO after PhotoModule/GcamModule deprecation: Do not use this for CAF. - */ - @Override - public void onFocusSucceeded() { - mFocusAnimation.cancel(); - mShowIndicator = false; - if (CAPTURE_DEBUG_UI && !mIsPassiveScan) { - mDebugSolidPaint.setColor(mDebugSuccessColor); - } - invalidate(); - } - - /** - * This is called in: - * <ul> - * <li>API1 non-CAF for onAutoFocus(false).</li> - * <li>API2 non-CAF for transition to NOT_FOCUSED_LOCKED.</li> - * <ul> - */ - @Override - public void onFocusFailed() { - mFocusAnimation.cancel(); - mShowIndicator = false; - if (CAPTURE_DEBUG_UI && !mIsPassiveScan) { - mDebugSolidPaint.setColor(mDebugFailColor); - } - invalidate(); - } - - /** - * This is called in: - * API2 for CAF state changes to PASSIVE_FOCUSED or PASSIVE_UNFOCUSED. - */ - @Override - public void setPassiveFocusSuccess(boolean success) { - mFocusAnimation.cancel(); - mShowIndicator = false; - if (CAPTURE_DEBUG_UI) { - mDebugCornersPaint.setColor(success ? mDebugSuccessColor : mDebugFailColor); - } - invalidate(); - } - - @Override - public void showDebugMessage(String message) { - if (CAPTURE_DEBUG_UI) { - mDebugMessage = message; - } - } - - @Override - public void pauseFaceDetection() { - // TODO: Add face detection support. - } - - @Override - public void resumeFaceDetection() { - // TODO: Add face detection support. - } - - @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); - - if (mShowIndicator) { - mFocusOuterRing.draw(canvas); - canvas.save(); - canvas.rotate(mAngle, mPositionX, mPositionY); - mFocusIndicator.draw(canvas); - canvas.restore(); - } - if (CAPTURE_DEBUG_UI) { - canvas.drawRect(mFocusDebugSolidRect, mDebugSolidPaint); - float delta = 0.1f * mFocusDebugCornersRect.width(); - float left = mFocusDebugCornersRect.left; - float top = mFocusDebugCornersRect.top; - float right = mFocusDebugCornersRect.right; - float bot = mFocusDebugCornersRect.bottom; - - canvas.drawLines(new float[]{left, top + delta, left, top, left, top, left + delta, top}, mDebugCornersPaint); - canvas.drawLines(new float[]{right, top + delta, right, top, right, top, right - delta, top}, mDebugCornersPaint); - canvas.drawLines(new float[]{left, bot - delta, left, bot, left, bot, left + delta, bot}, mDebugCornersPaint); - canvas.drawLines(new float[]{right, bot - delta, right, bot, right, bot, right - delta, bot}, mDebugCornersPaint); - - if (mDebugMessage != null) { - mDebugTextPaint.setTextSize(40); - canvas.drawText(mDebugMessage, left - 4, bot + 44, mDebugTextPaint); - } - } - } -} 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..858b11649 --- /dev/null +++ b/src/com/android/camera/ui/focus/AutoFocusRing.java @@ -0,0 +1,102 @@ +/* + * 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.debug.Log.Tag; +import com.android.camera.ui.motion.InterpolateUtils; +import com.android.camera.ui.motion.Invalidator; + +/** + * Passive focus ring animation renderer. + */ +public class AutoFocusRing extends FocusRingRenderer { + private static final Tag TAG = new 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. + */ + 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..06640601b --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusController.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.os.Handler; + +import com.android.camera.debug.Log.Tag; +import com.android.camera.one.OneCamera.FocusDistanceListener; + +/** + * The focus controller interacts with the focus ring UI element. + */ +public class FocusController implements FocusDistanceListener { + private static final Tag TAG = new Tag("FocusController"); + + private final FocusRing mFocusRing; + private final Handler mHandler; + private final FocusSound mFocusSound; + + public FocusController(FocusRing focusRing, FocusSound focusSound, Handler handler) { + mFocusRing = focusRing; + mHandler = handler; + mFocusSound = focusSound; + } + + /** + * 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) { + mHandler.post(new Runnable() { + @Override + public void run() { + 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) { + mHandler.post(new Runnable() { + @Override + public void run() { + mFocusRing.startActiveFocus(); + mFocusRing.setFocusLocation(viewX, viewY); + mFocusSound.play(); + } + }); + } + + /** + * Stop any currently executing focus animation. + */ + public void clearFocusIndicator() { + mHandler.post(new Runnable() { + @Override + public void run() { + mFocusRing.stopFocusAnimations(); + } + }); + } + + @Override + public void onFocusDistance(final float diopter, final boolean isActive) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (isActive || mFocusRing.isPassiveFocusRunning() || + mFocusRing.isActiveFocusRunning()) { + mFocusRing.setFocusDiopter(diopter); + } + } + }); + } +} 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..362caa5ce --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusRing.java @@ -0,0 +1,55 @@ +/* + * 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; + +/** + * 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 lens diopter of the focus ring. + */ + public void setFocusDiopter(float diopter); +}
\ 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..c94b42197 --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusRingRenderer.java @@ -0,0 +1,239 @@ +/* + * 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 com.android.camera.debug.Log; +import com.android.camera.debug.Log.Tag; +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. + */ +public abstract class FocusRingRenderer implements DynamicAnimation { + private static final Tag TAG = new 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..3d57fc11f --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusRingView.java @@ -0,0 +1,183 @@ +/* + * 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.util.AttributeSet; +import android.view.View; + +import com.android.camera.debug.Log.Tag; +import com.android.camera.ui.motion.AnimationClock.SystemTimeClock; +import com.android.camera.ui.motion.DynamicAnimator; +import com.android.camera.ui.motion.InterpolateUtils; +import com.android.camera.ui.motion.Invalidator; +import com.android.camera2.R; + +/** + * Custom view for running the focus ring animations. + */ +public class FocusRingView extends View implements Invalidator, FocusRing { + private static final Tag TAG = new Tag("FocusRingView"); + + // A Diopter of 0.0f ish is infinity. + // A Diopter of about 15f or so is focused "as close as possible" + // Diopter max is computed from device testing, TODO: Replace with LENS_FOCUS_RANGE + // https://developer.android.com/reference/android/hardware/camera2/CaptureResult.html + // TODO: Refactor diopter to radius computation outside this class. + private static final float DIOPTER_MIN = 0.0f; + private static final float DIOPTER_MAX = 15.0f; + private static final float DIOPTER_MEDIAN = (DIOPTER_MAX - DIOPTER_MIN) / 2.0f; + + private static final float FADE_IN_DURATION_MILLIS = 1000f; + private static final float FADE_OUT_DURATION_MILLIS = 250f; + + private final AutoFocusRing autoFocusRing; + private final ManualFocusRing manualFocusRing; + private final DynamicAnimator animator; + + private final int mFocusCircleMinSize; + private final int mFocusCircleMaxSize; + + private FocusRingRenderer currentFocusAnimation; + private boolean isFirstDraw = true; + private float mLastDiopter = DIOPTER_MEDIAN; + + public FocusRingView(Context context, AttributeSet attrs) { + super(context, attrs); + + Resources res = getResources(); + Paint mPaint = makePaint(res, R.color.focus_color); + + mFocusCircleMinSize = res.getDimensionPixelSize(R.dimen.focus_circle_min_size); + mFocusCircleMaxSize = res.getDimensionPixelSize(R.dimen.focus_circle_max_size); + + animator = new DynamicAnimator(this, new SystemTimeClock()); + + autoFocusRing = new AutoFocusRing(animator, mPaint, + FADE_IN_DURATION_MILLIS, + FADE_OUT_DURATION_MILLIS); + manualFocusRing = new ManualFocusRing(animator, mPaint, + FADE_OUT_DURATION_MILLIS); + + animator.animations.add(autoFocusRing); + animator.animations.add(manualFocusRing); + } + + @Override + public boolean isPassiveFocusRunning() { + return autoFocusRing.isActive(); + } + + @Override + public boolean isActiveFocusRunning() { + return manualFocusRing.isActive(); + } + + @Override + public void startPassiveFocus() { + animator.invalidate(); + long tMs = animator.getTimeMillis(); + + if (manualFocusRing.isActive() && !manualFocusRing.isExiting()) { + manualFocusRing.stop(tMs); + } + + float lastRadius = radiusForDiopter(mLastDiopter); + autoFocusRing.start(tMs, lastRadius, lastRadius); + currentFocusAnimation = autoFocusRing; + } + + @Override + public void startActiveFocus() { + animator.invalidate(); + long tMs = animator.getTimeMillis(); + + if (autoFocusRing.isActive() && !autoFocusRing.isExiting()) { + autoFocusRing.stop(tMs); + } + + manualFocusRing.start(tMs, 0.0f, radiusForDiopter(mLastDiopter)); + currentFocusAnimation = manualFocusRing; + } + + @Override + public void stopFocusAnimations() { + long tMs = animator.getTimeMillis(); + if (manualFocusRing.isActive() && !manualFocusRing.isExiting() + && !manualFocusRing.isEntering()) { + manualFocusRing.exit(tMs); + } + + if (autoFocusRing.isActive() && !autoFocusRing.isExiting()) { + autoFocusRing.exit(tMs); + } + } + + @Override + public void setFocusLocation(float viewX, float viewY) { + autoFocusRing.setCenterX((int) viewX); + autoFocusRing.setCenterY((int) viewY); + manualFocusRing.setCenterX((int) viewX); + manualFocusRing.setCenterY((int) viewY); + } + + @Override + public void setFocusDiopter(float diopter) { + long tMs = animator.getTimeMillis(); + // Some devices return zero for invalid or "unknown" diopter values. + if (currentFocusAnimation != null && diopter > 0.1f) { + currentFocusAnimation.setRadius(tMs, radiusForDiopter(diopter)); + mLastDiopter = diopter; + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (isFirstDraw) { + isFirstDraw = false; + centerAutofocusRing(); + } + + animator.draw(canvas); + } + + private void centerAutofocusRing() { + float screenW = this.getWidth(); + float screenH = this.getHeight(); + + autoFocusRing.setCenterX((int) (screenW / 2f)); + autoFocusRing.setCenterY((int) (screenH / 2f)); + } + + private float radiusForDiopter(float diopter) { + return InterpolateUtils.scale(diopter, DIOPTER_MIN, DIOPTER_MAX, mFocusCircleMinSize, + mFocusCircleMaxSize); + } + + 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/ManualFocusRing.java b/src/com/android/camera/ui/focus/ManualFocusRing.java new file mode 100644 index 000000000..e3b10d59b --- /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. + */ +public class ManualFocusRing extends FocusRingRenderer { + /** + * The manual focus ring encapsulates the animation logic for visualizing + * a focus event when triggered by a physical screen touch. + * + * @param invalidator the object to invalidate while running. + * @param ringPaint the paint to draw the ring with. + * @param exitDurationMillis the fade out time in milliseconds. + */ + public ManualFocusRing(Invalidator invalidator, Paint ringPaint, + float exitDurationMillis) { + super(invalidator, ringPaint, 0.0f, exitDurationMillis); + } + + @Override + public void draw(long t, long dt, Canvas canvas) { + float ringRadius = mRingRadius.update(dt); + processStates(t); + + if (!isActive()) { + return; + } + + mInvalidator.invalidate(); + int ringAlpha = 255; + + if (mFocusState == FocusState.STATE_FADE_OUT) { + float rFade = InterpolateUtils.unitRatio(t, mExitStartMillis, mExitDurationMillis); + ringAlpha = (int) InterpolateUtils.lerp(255, 0, mExitOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_HARD_STOP) { + float rFade = InterpolateUtils.unitRatio(t, mHardExitStartMillis, + mHardExitDurationMillis); + ringAlpha = (int) InterpolateUtils.lerp(255, 0, mExitOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_INACTIVE) { + ringAlpha = 0; + } + + mRingPaint.setAlpha(ringAlpha); + canvas.drawCircle(getCenterX(), getCenterY(), ringRadius, mRingPaint); + } + + private void processStates(long t) { + if (mFocusState == FocusState.STATE_INACTIVE) { + return; + } + + if (mFocusState == FocusState.STATE_ENTER + && (t > mEnterStartMillis + mEnterDurationMillis)) { + mFocusState = FocusState.STATE_ACTIVE; + } + + if (mFocusState == FocusState.STATE_ACTIVE && !mRingRadius.isActive()) { + mFocusState = FocusState.STATE_FADE_OUT; + mExitStartMillis = t; + } + + if (mFocusState == FocusState.STATE_FADE_OUT && t > mExitStartMillis + mExitDurationMillis) { + mFocusState = FocusState.STATE_INACTIVE; + } + + if (mFocusState == FocusState.STATE_HARD_STOP + && t > mHardExitStartMillis + mHardExitDurationMillis) { + mFocusState = FocusState.STATE_INACTIVE; + } + } +} diff --git a/src/com/android/camera/ui/motion/AnimationClock.java b/src/com/android/camera/ui/motion/AnimationClock.java new file mode 100644 index 000000000..d2504de6b --- /dev/null +++ b/src/com/android/camera/ui/motion/AnimationClock.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +import android.os.SystemClock; + +/** + * Wraps the SystemClock static time methods so they can be exercised in tests. + */ +public abstract class AnimationClock { + + public abstract long getTimeMillis(); + + /** + * Forwards calls to SystemClock.uptimeMillis() since it is the most consistent clock for + * animations. + */ + public static class SystemTimeClock extends AnimationClock { + + @Override + public long getTimeMillis() { + return SystemClock.uptimeMillis(); + } + } +} diff --git a/src/com/android/camera/ui/motion/DampedSpring.java b/src/com/android/camera/ui/motion/DampedSpring.java new file mode 100644 index 000000000..84cbfa6f8 --- /dev/null +++ b/src/com/android/camera/ui/motion/DampedSpring.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +/** + * This models a value after the behavior of a spring. The value tracks the current value, a target + * value, and the current velocity and applies both a directional force and a damping force to the + * value on each update call. + */ +public class DampedSpring { + public static final float DEFAULT_TIME_TO_90_PERCENT_MILLIS = 200.0f; + public static final float DEFAULT_SPRING_STIFFNESS = 3.75f; + public static final float EPSILON = 0.01f; + + private final float mSpringStiffness; + private final float mTimeTo90PercentMs; + + private float mTarget = 0f; + private float mVelocity = 0f; + private float mValue = 0f; + + public DampedSpring() { + this(DEFAULT_TIME_TO_90_PERCENT_MILLIS, DEFAULT_SPRING_STIFFNESS); + } + + public DampedSpring(float timeTo90PercentMs) { + this(timeTo90PercentMs, DEFAULT_SPRING_STIFFNESS); + } + + public DampedSpring(float timeTo90PercentMs, float springStiffness) { + // TODO: Assert timeTo90PercentMs >= 1ms, it might behave badly at low values. + // TODO: Assert springStiffness > 2.0f + + mTimeTo90PercentMs = timeTo90PercentMs; + mSpringStiffness = springStiffness; + + if (springStiffness > timeTo90PercentMs) { + throw new IllegalArgumentException("Creating a spring value with " + + "excessive stiffness will oscillate endlessly."); + } + } + + /** + * @return the current value. + */ + public float getValue() { + return mValue; + } + + /** + * @param value the value to set this instance's current state too. + */ + public void setValue(float value) { + mValue = value; + } + + /** + * @return the current target value. + */ + public float getTarget() { + return mTarget; + } + + /** + * Set a target value. The current value will maintain any existing velocity values and will + * move towards the new target value. To forcibly stopAt the value use the stopAt() method. + * + * @param value the new value to move the current value towards. + */ + public void setTarget(float value) { + mTarget = value; + } + + /** + * Update the current value, moving it towards the actual value over the given + * time delta (in milliseconds) since the last update. This works off of the + * principle of a critically damped spring such that any given current value + * will move elastically towards the target value. The current value maintains + * and applies velocity, acceleration, and a damping force to give a continuous, + * smooth transition towards the target value. + * + * @param dtMs the time since the last update, or zero. + * @return the current value after the update occurs. + */ + public float update(float dtMs) { + float dt = dtMs / mTimeTo90PercentMs; + float dts = dt * mSpringStiffness; + + // If the dts > 1, and the velocity is zero, the force will exceed the + // distance to the target value and it will overshoot the value, causing + // weird behavior and unintended oscillation. since a critically damped + // spring should never overshoot the value, simply the current value to the + // target value. + if (dts > 1.0f || dts < 0.0f) { + stop(); + return mValue; + } + + float delta = (mTarget - mValue); + float force = delta - 2.0f * mVelocity; + + mVelocity += force * dts; + mValue += mVelocity * dts; + + // If we get close enough to the actual value, simply set the current value + // to the current target value and stop. + if (!isActive()) { + stop(); + } + + return mValue; + } + + /** + * @return true if this instance has velocity or it is not at the target value. + */ + public boolean isActive() { + boolean hasVelocity = Math.abs(mVelocity) >= EPSILON; + boolean atTarget = Math.abs(mTarget - mValue) < EPSILON; + return hasVelocity || !atTarget; + } + + /** + * Stop the spring motion wherever it is currently at. Sets target to the + * current value and sets the velocity to zero. + */ + public void stop() { + mTarget = mValue; + mVelocity = 0.0f; + } +} diff --git a/src/com/android/camera/ui/motion/DynamicAnimation.java b/src/com/android/camera/ui/motion/DynamicAnimation.java new file mode 100644 index 000000000..57d5a1021 --- /dev/null +++ b/src/com/android/camera/ui/motion/DynamicAnimation.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +import android.graphics.Canvas; + +/** + * Rendering object that can be driven by an animator instance. + */ +public interface DynamicAnimation { + + /** + * Check to determine if this animation is currently in a stable state. + * + * @return true if the animation is stable, false if it should continue to be redrawn. + */ + boolean isActive(); + + /** + * Update and draw the animation onto the given canvas. + * + * @param t current animation frame time. + * @param dt delta since the last update. + * @param canvas the canvas to draw the animation onto. + */ + void draw(long t, long dt, Canvas canvas); +} diff --git a/src/com/android/camera/ui/motion/DynamicAnimator.java b/src/com/android/camera/ui/motion/DynamicAnimator.java new file mode 100644 index 000000000..542ac1e37 --- /dev/null +++ b/src/com/android/camera/ui/motion/DynamicAnimator.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +import android.graphics.Canvas; + +import java.util.ArrayList; +import java.util.List; + +/** + * Designed to handle the lifecycle of a view that needs a continuous update / + * redraw cycle that does not have a defined start / end time. + * + * Fixed length animations should NOT use this class. + */ +public class DynamicAnimator implements Invalidator { + + public final List<DynamicAnimation> animations = new ArrayList<>(); + + private final Invalidator mInvalidator; + private final AnimationClock mClock; + + private boolean mUpdateRequested = false; + private boolean mIsDrawing = false; + private long mLastDrawTimeMillis = 0; + private long mDrawTimeMillis = 0; + + public DynamicAnimator(Invalidator invalidator, AnimationClock clock) { + mInvalidator = invalidator; + mClock = clock; + } + + public void draw(Canvas canvas) { + mIsDrawing = true; + mUpdateRequested = false; + + mDrawTimeMillis = mClock.getTimeMillis(); + + if (mLastDrawTimeMillis <= 0) { + mLastDrawTimeMillis = mDrawTimeMillis; // On the initial draw, dt is zero. + } + + long dt = mDrawTimeMillis - mLastDrawTimeMillis; + mLastDrawTimeMillis = mDrawTimeMillis; + + // Run the animation + for (DynamicAnimation renderer : animations) { + if (renderer.isActive()) { + renderer.draw(mDrawTimeMillis, dt, canvas); + } + } + + // If either the update or the draw methods requested new frames, then + // invalidate the view which should give us another frame to work with. + // Otherwise, stopAt the last update time. + if (mUpdateRequested) { + mInvalidator.invalidate(); + } else { + mLastDrawTimeMillis = -1; + } + + mIsDrawing = false; + } + + /** + * If a scheduleNewFrame request comes in outside of the animation loop, + * and we didn't schedule a frame after the previous loop (or it's the + * first time we've used this instance), invalidate the view and set the + * last update time to the current time. Theoretically, a few milliseconds + * have elapsed before the view gets updated. + */ + @Override + public void invalidate() { + if (!mIsDrawing && !mUpdateRequested) { + mInvalidator.invalidate(); + mLastDrawTimeMillis = mClock.getTimeMillis(); + } + + mUpdateRequested = true; + } + + /** + * This will return the "best guess" for the most current animation frame + * time. If the loop is currently drawing, then it will return the time the + * draw began, and if an update is currently requested it will return the + * time that the update was requested at, and if neither of these are true + * it will return the current system clock time. + * + * This method will not trigger a new update. + */ + public long getTimeMillis() { + if (mIsDrawing) { + return mDrawTimeMillis; + } + + if (mUpdateRequested) { + return mLastDrawTimeMillis; + } + + return mClock.getTimeMillis(); + } +} diff --git a/src/com/android/camera/ui/motion/InterpolateUtils.java b/src/com/android/camera/ui/motion/InterpolateUtils.java new file mode 100644 index 000000000..3c3cd532f --- /dev/null +++ b/src/com/android/camera/ui/motion/InterpolateUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +/** + * Various static helper functions for interpolating between values. + */ +public class InterpolateUtils { + + private InterpolateUtils() { + } + + /** + * Linear interpolation from v0 to v1 as t goes from 0...1 + * + * @param v0 the value at t=0 + * @param v1 the value at t=1 + * @param t value in the range of 0 to 1. + * @return the value between v0 and v1 as a ratio between 0 and 1 defined by t. + */ + public static float lerp(float v0, float v1, float t) { + return v0 + t * (v1 - v0); + } + + /** + * Project a value that is within the in(Min/Max) number space into the to(Min/Max) number + * space. + * + * @param v value to scale into the 'to' number space. + * @param vMin min value of the values number space. + * @param vMax max value of the values number space. + * @param pMin min value of the projection number space. + * @param pMax max value of the projection number space. + * @return the ratio of the value in the source number space as a value in the to(Min/Max) + * number space. + */ + public static float scale(float v, float vMin, float vMax, float pMin, float pMax) { + return (pMax - pMin) * (v - vMin) / (vMax - vMin) + pMin; + } + + /** + * Value between 0 and 1 as a ratio between tBegin over tDuration + * with no upper bound. + */ + public static float unitRatio(long t, long tBegin, float tDuration) { + if (t <= tBegin) { + return 0.0f; + } + + return (t - tBegin) / tDuration; + } +} diff --git a/src/com/android/camera/ui/motion/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/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 c265f0033..aa8c5254f 100644 --- a/src/com/android/camera/util/CameraUtil.java +++ b/src/com/android/camera/util/CameraUtil.java @@ -41,7 +41,6 @@ import android.os.ParcelFileDescriptor; import android.telephony.TelephonyManager; import android.util.DisplayMetrics; import android.util.TypedValue; -import android.view.Display; import android.view.OrientationEventListener; import android.view.Surface; import android.view.View; @@ -736,7 +735,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); @@ -745,7 +744,7 @@ public class CameraUtil { public static Rect rectFToRect(RectF rectF) { Rect rect = new Rect(); - rectFToRect(rectF, rect); + inlineRectToRectF(rectF, rect); return rect; } @@ -765,21 +764,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) { synchronized (sImageFileNamer) { return sImageFileNamer.generateName(dateTaken); diff --git a/src/com/android/camera/widget/FilmstripView.java b/src/com/android/camera/widget/FilmstripView.java index 95888fe3d..d9b95b01d 100644 --- a/src/com/android/camera/widget/FilmstripView.java +++ b/src/com/android/camera/widget/FilmstripView.java @@ -702,6 +702,9 @@ public class FilmstripView extends ViewGroup { if (recycledViewsForType != null) { result = recycledViewsForType.poll(); } + if (result != null) { + result.setVisibility(View.GONE); + } return result; } |