2 * Copyright (C) 2013 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.camera.ui;
19 import android.animation.Animator;
20 import android.animation.AnimatorSet;
21 import android.animation.ObjectAnimator;
22 import android.animation.TimeInterpolator;
23 import android.animation.ValueAnimator;
24 import android.content.Context;
25 import android.graphics.Bitmap;
26 import android.graphics.Canvas;
27 import android.graphics.Paint;
28 import android.graphics.PorterDuff;
29 import android.graphics.PorterDuffXfermode;
30 import android.graphics.RectF;
31 import android.os.AsyncTask;
32 import android.os.SystemClock;
33 import android.util.AttributeSet;
34 import android.util.SparseArray;
35 import android.view.GestureDetector;
36 import android.view.LayoutInflater;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.widget.FrameLayout;
40 import android.widget.LinearLayout;
42 import com.android.camera.app.CameraAppUI;
43 import com.android.camera.debug.Log;
44 import com.android.camera.util.CameraUtil;
45 import com.android.camera.util.Gusterpolator;
46 import com.android.camera.widget.AnimationEffects;
47 import com.android.camera.widget.SettingsButton;
48 import com.android.camera2.R;
50 import java.util.ArrayList;
51 import java.util.LinkedList;
52 import java.util.List;
55 * ModeListView class displays all camera modes and settings in the form
56 * of a list. A swipe to the right will bring up this list. Then tapping on
57 * any of the items in the list will take the user to that corresponding mode
58 * with an animation. To dismiss this list, simply swipe left or select a mode.
60 public class ModeListView extends FrameLayout
61 implements PreviewStatusListener.PreviewAreaChangedListener,
62 ModeSelectorItem.VisibleWidthChangedListener {
64 private static final Log.Tag TAG = new Log.Tag("ModeListView");
66 // Animation Durations
67 private static final int DEFAULT_DURATION_MS = 200;
68 private static final int FLY_IN_DURATION_MS = 0;
69 private static final int HOLD_DURATION_MS = 0;
70 private static final int FLY_OUT_DURATION_MS = 850;
71 private static final int START_DELAY_MS = 100;
72 private static final int TOTAL_DURATION_MS = FLY_IN_DURATION_MS + HOLD_DURATION_MS
73 + FLY_OUT_DURATION_MS;
74 private static final int HIDE_SHIMMY_DELAY_MS = 1000;
75 // Assumption for time since last scroll when no data point for last scroll.
76 private static final int SCROLL_INTERVAL_MS = 50;
77 // Last 20% percent of the drawer opening should be slow to ensure soft landing.
78 private static final float SLOW_ZONE_PERCENTAGE = 0.2f;
80 private static final int NO_ITEM_SELECTED = -1;
82 // Scrolling delay between non-focused item and focused item
83 private static final int DELAY_MS = 30;
84 // If the fling velocity exceeds this threshold, snap to full screen at a constant
85 // speed. Unit: pixel/ms.
86 private static final float VELOCITY_THRESHOLD = 2f;
89 * A factor to change the UI responsiveness on a scroll.
90 * e.g. A scroll factor of 0.5 means UI will move half as fast as the finger.
92 private static final float SCROLL_FACTOR = 0.5f;
93 // 60% opaque black background.
94 private static final int BACKGROUND_TRANSPARENTCY = (int) (0.6f * 255);
95 private static final int PREVIEW_DOWN_SAMPLE_FACTOR = 4;
96 // Threshold, below which snap back will happen.
97 private static final float SNAP_BACK_THRESHOLD_RATIO = 0.33f;
99 private final GestureDetector mGestureDetector;
100 private final RectF mPreviewArea = new RectF();
101 private final RectF mUncoveredPreviewArea = new RectF();
103 private final CurrentStateManager mCurrentStateManager = new CurrentStateManager();
104 private long mLastScrollTime;
105 private int mListBackgroundColor;
106 private LinearLayout mListView;
107 private SettingsButton mSettingsButton;
108 private int mTotalModes;
109 private ModeSelectorItem[] mModeSelectorItems;
110 private AnimatorSet mAnimatorSet;
111 private int mFocusItem = NO_ITEM_SELECTED;
112 private ModeListOpenListener mModeListOpenListener;
113 private ModeListVisibilityChangedListener mVisibilityChangedListener;
114 private CameraAppUI.CameraModuleScreenShotProvider mScreenShotProvider = null;
115 private int[] mInputPixels;
116 private int[] mOutputPixels;
117 private float mModeListOpenFactor = 1f;
119 private boolean mAdjustPositionWhenUncoveredPreviewAreaChanges = false;
120 private View mChildViewTouched = null;
121 private MotionEvent mLastChildTouchEvent = null;
122 private int mVisibleWidth = 0;
124 // Width and height of this view. They get updated in onLayout()
125 // Unit for width and height are pixels.
128 private float mScrollTrendX = 0f;
129 private float mScrollTrendY = 0f;
130 private ModeSwitchListener mModeSwitchListener = null;
131 private ArrayList<Integer> mSupportedModes;
132 private final LinkedList<TimeBasedPosition> mPositionHistory
133 = new LinkedList<TimeBasedPosition>();
134 private long mCurrentTime;
135 private float mVelocityX; // Unit: pixel/ms.
136 private long mLastDownTime = 0;
138 private class CurrentStateManager {
139 private ModeListState mCurrentState;
141 ModeListState getCurrentState() {
142 return mCurrentState;
145 void setCurrentState(ModeListState state) {
146 mCurrentState = state;
147 state.onCurrentState();
152 * ModeListState defines a set of functions through which the view could manage
153 * or change the states. Sub-classes could selectively override these functions
154 * accordingly to respect the specific requirements for each state. By overriding
155 * these methods, state transition can also be achieved.
157 private abstract class ModeListState implements GestureDetector.OnGestureListener {
158 protected AnimationEffects mCurrentAnimationEffects = null;
161 * Called by the state manager when this state instance becomes the current
164 public void onCurrentState() {
169 * If supported, this should show the mode switcher and starts the accordion
170 * animation with a delay. If the view does not currently have focus, (e.g.
171 * There are popups on top of it.) start the delayed accordion animation
172 * when it gains focus. Otherwise, start the animation with a delay right
175 public void showSwitcherHint() {
180 * Gets the currently running animation effects for the current state.
182 public AnimationEffects getCurrentAnimationEffects() {
183 return mCurrentAnimationEffects;
187 * Returns true if the touch event should be handled, false otherwise.
189 * @param ev motion event to be handled
190 * @return true if the event should be handled, false otherwise.
192 public boolean shouldHandleTouchEvent(MotionEvent ev) {
197 * Handles touch event. This will be called if
198 * {@link ModeListState#shouldHandleTouchEvent(android.view.MotionEvent)}
199 * returns {@code true}
201 * @param ev touch event to be handled
202 * @return always true
204 public boolean onTouchEvent(MotionEvent ev) {
209 * Gets called when the window focus has changed.
211 * @param hasFocus whether current window has focus
213 public void onWindowFocusChanged(boolean hasFocus) {
214 // Default to do nothing.
218 * Gets called when back key is pressed.
220 * @return true if handled, false otherwise.
222 public boolean onBackPressed() {
227 * Gets called when menu key is pressed.
229 * @return true if handled, false otherwise.
231 public boolean onMenuPressed() {
236 * Gets called when there is a {@link View#setVisibility(int)} call to
237 * change the visibility of the mode drawer. Visibility change does not
238 * always make sense, for example there can be an outside call to make
239 * the mode drawer visible when it is in the fully hidden state. The logic
240 * is that the mode drawer can only be made visible when user swipe it in.
242 * @param visibility the proposed visibility change
243 * @return true if the visibility change is valid and therefore should be
244 * handled, false otherwise.
246 public boolean shouldHandleVisibilityChange(int visibility) {
251 * If supported, this should start blurring the camera preview and
252 * start the mode switch.
254 * @param selectedItem mode item that has been selected
256 public void onItemSelected(ModeSelectorItem selectedItem) {
261 * This gets called when mode switch has finished and UI needs to
262 * pinhole into the new mode through animation.
264 public void startModeSelectionAnimation() {
269 * Hide the mode drawer and switch to fully hidden state.
275 /***************GestureListener implementation*****************/
277 public boolean onDown(MotionEvent e) {
282 public void onShowPress(MotionEvent e) {
287 public boolean onSingleTapUp(MotionEvent e) {
292 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
297 public void onLongPress(MotionEvent e) {
302 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
308 * Fully hidden state. Transitioning to ScrollingState and ShimmyState are supported
311 private class FullyHiddenState extends ModeListState {
312 private Animator mAnimator = null;
313 private boolean mShouldBeVisible = false;
315 public FullyHiddenState() {
320 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
321 mShouldBeVisible = true;
322 // Change visibility, and switch to scrolling state.
323 resetModeSelectors();
324 mCurrentStateManager.setCurrentState(new ScrollingState());
329 public void showSwitcherHint() {
330 mShouldBeVisible = true;
331 mCurrentStateManager.setCurrentState(new ShimmyState());
335 public boolean shouldHandleTouchEvent(MotionEvent ev) {
340 public boolean onTouchEvent(MotionEvent ev) {
341 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
342 mFocusItem = getFocusItem(ev.getX(), ev.getY());
349 public boolean onMenuPressed() {
350 if (mAnimator != null) {
358 public boolean shouldHandleVisibilityChange(int visibility) {
359 if (mAnimator != null) {
362 if (visibility == VISIBLE && !mShouldBeVisible) {
368 * Snaps open the mode list and go to the fully shown state.
370 private void snapOpenAndShow() {
371 mShouldBeVisible = true;
372 setVisibility(VISIBLE);
374 mAnimator = snapToFullScreen();
375 if (mAnimator != null) {
376 mAnimator.addListener(new Animator.AnimatorListener() {
378 public void onAnimationStart(Animator animation) {
383 public void onAnimationEnd(Animator animation) {
385 mCurrentStateManager.setCurrentState(new FullyShownState());
389 public void onAnimationCancel(Animator animation) {
394 public void onAnimationRepeat(Animator animation) {
399 mCurrentStateManager.setCurrentState(new FullyShownState());
404 public void onCurrentState() {
405 announceForAccessibility(
406 getContext().getResources().getString(R.string.accessibility_mode_list_hidden));
412 * Fully shown state. This state represents when the mode list is entirely shown
413 * on screen without any on-going animation. Transitions from this state could be
414 * to ScrollingState, SelectedState, or FullyHiddenState.
416 private class FullyShownState extends ModeListState {
417 private Animator mAnimator = null;
420 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
421 // Go to scrolling state.
424 cancelForwardingTouchEvent();
425 mCurrentStateManager.setCurrentState(new ScrollingState());
431 public boolean shouldHandleTouchEvent(MotionEvent ev) {
432 if (mAnimator != null && mAnimator.isRunning()) {
439 public boolean onTouchEvent(MotionEvent ev) {
440 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
441 mFocusItem = NO_ITEM_SELECTED;
443 // If the down event happens inside the mode list, find out which
444 // mode item is being touched and forward all the subsequent touch
445 // events to that mode item for its pressed state and click handling.
446 if (isTouchInsideList(ev)) {
447 mChildViewTouched = mModeSelectorItems[getFocusItem(ev.getX(), ev.getY())];
450 forwardTouchEventToChild(ev);
456 public boolean onSingleTapUp(MotionEvent ev) {
457 // If the tap is not inside the mode drawer area, snap back.
458 if(!isTouchInsideList(ev)) {
466 public boolean onBackPressed() {
472 public boolean onMenuPressed() {
478 public void onItemSelected(ModeSelectorItem selectedItem) {
479 mCurrentStateManager.setCurrentState(new SelectedState(selectedItem));
483 * Snaps back the mode list and go to the fully hidden state.
485 private void snapBackAndHide() {
486 mAnimator = snapBack(true);
487 if (mAnimator != null) {
488 mAnimator.addListener(new Animator.AnimatorListener() {
490 public void onAnimationStart(Animator animation) {
495 public void onAnimationEnd(Animator animation) {
497 mCurrentStateManager.setCurrentState(new FullyHiddenState());
501 public void onAnimationCancel(Animator animation) {
506 public void onAnimationRepeat(Animator animation) {
511 mCurrentStateManager.setCurrentState(new FullyHiddenState());
517 if (mAnimator != null) {
520 mCurrentStateManager.setCurrentState(new FullyHiddenState());
525 public void onCurrentState() {
526 announceForAccessibility(
527 getContext().getResources().getString(R.string.accessibility_mode_list_shown));
533 * Shimmy state handles the specifics for shimmy animation, including
534 * setting up to show mode drawer (without text) and hide it with shimmy animation.
536 * This state can be interrupted when scrolling or mode selection happened,
537 * in which case the state will transition into ScrollingState, or SelectedState.
538 * Otherwise, after shimmy finishes successfully, a transition to fully hidden
541 private class ShimmyState extends ModeListState {
543 private boolean mStartHidingShimmyWhenWindowGainsFocus = false;
544 private Animator mAnimator = null;
545 private final Runnable mHideShimmy = new Runnable() {
552 public ShimmyState() {
553 setVisibility(VISIBLE);
554 mSettingsButton.setVisibility(INVISIBLE);
555 mModeListOpenFactor = 0f;
556 onModeListOpenRatioUpdate(0);
557 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
558 for (int i = 0; i < mModeSelectorItems.length; i++) {
559 mModeSelectorItems[i].setVisibleWidth(maxVisibleWidth);
561 if (hasWindowFocus()) {
562 hideShimmyWithDelay();
564 mStartHidingShimmyWhenWindowGainsFocus = true;
569 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
570 // Scroll happens during accordion animation.
572 // Go to scrolling state
573 mCurrentStateManager.setCurrentState(new ScrollingState());
578 public boolean shouldHandleTouchEvent(MotionEvent ev) {
579 if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
580 if (isTouchInsideList(ev) &&
581 ev.getX() <= mModeSelectorItems[0].getMaxVisibleWidth()) {
582 mChildViewTouched = mModeSelectorItems[getFocusItem(ev.getX(), ev.getY())];
585 // If shimmy is on-going, reject the first down event, so that it can be handled
586 // by the view underneath. If a swipe is detected, the same series of touch will
587 // re-enter this function, in which case we will consume the touch events.
588 if (mLastDownTime != ev.getDownTime()) {
589 mLastDownTime = ev.getDownTime();
597 public boolean onTouchEvent(MotionEvent ev) {
598 if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
599 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
600 mFocusItem = getFocusItem(ev.getX(), ev.getY());
604 forwardTouchEventToChild(ev);
609 public void onItemSelected(ModeSelectorItem selectedItem) {
611 mCurrentStateManager.setCurrentState(new SelectedState(selectedItem));
614 private void hideShimmyWithDelay() {
615 postDelayed(mHideShimmy, HIDE_SHIMMY_DELAY_MS);
619 public void onWindowFocusChanged(boolean hasFocus) {
620 if (mStartHidingShimmyWhenWindowGainsFocus && hasFocus) {
621 mStartHidingShimmyWhenWindowGainsFocus = false;
622 hideShimmyWithDelay();
627 * This starts the accordion animation, unless it's already running, in which
628 * case the start animation call will be ignored.
630 private void startHidingShimmy() {
631 if (mAnimator != null) {
634 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
635 mAnimator = animateListToWidth(START_DELAY_MS * (-1), TOTAL_DURATION_MS,
636 Gusterpolator.INSTANCE, maxVisibleWidth, 0);
637 mAnimator.addListener(new Animator.AnimatorListener() {
638 private boolean mSuccess = true;
640 public void onAnimationStart(Animator animation) {
645 public void onAnimationEnd(Animator animation) {
647 ShimmyState.this.onAnimationEnd(mSuccess);
651 public void onAnimationCancel(Animator animation) {
656 public void onAnimationRepeat(Animator animation) {
663 * Cancels the pending/on-going animation.
665 private void cancelAnimation() {
666 removeCallbacks(mHideShimmy);
667 if (mAnimator != null && mAnimator.isRunning()) {
671 onAnimationEnd(false);
676 * Gets called when the animation finishes or gets canceled.
678 * @param success indicates whether the animation finishes successfully
680 private void onAnimationEnd(boolean success) {
681 mSettingsButton.setVisibility(VISIBLE);
682 // If successfully finish hiding shimmy, then we should go back to
683 // fully hidden state.
685 mModeListOpenFactor = 1;
686 mCurrentStateManager.setCurrentState(new FullyHiddenState());
690 // If the animation was canceled before it's finished, animate the mode
691 // list open factor from 0 to 1 to ensure a smooth visual transition.
692 final ValueAnimator openFactorAnimator = ValueAnimator.ofFloat(mModeListOpenFactor, 1f);
693 openFactorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
695 public void onAnimationUpdate(ValueAnimator animation) {
696 mModeListOpenFactor = (Float) openFactorAnimator.getAnimatedValue();
697 onVisibleWidthChanged(mVisibleWidth);
700 openFactorAnimator.addListener(new Animator.AnimatorListener() {
702 public void onAnimationStart(Animator animation) {
707 public void onAnimationEnd(Animator animation) {
708 mModeListOpenFactor = 1f;
712 public void onAnimationCancel(Animator animation) {
717 public void onAnimationRepeat(Animator animation) {
721 openFactorAnimator.start();
727 mCurrentStateManager.setCurrentState(new FullyHiddenState());
731 public void onCurrentState() {
732 announceForAccessibility(
733 getContext().getResources().getString(R.string.accessibility_mode_list_shimmy));
739 * When the mode list is being scrolled, it will be in ScrollingState. From
740 * this state, the mode list could transition to fully hidden, fully open
741 * depending on which direction the scrolling goes.
743 private class ScrollingState extends ModeListState {
744 private Animator mAnimator = null;
746 public ScrollingState() {
747 setVisibility(VISIBLE);
751 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
752 // Scroll based on the scrolling distance on the currently focused
754 scroll(mFocusItem, distanceX * SCROLL_FACTOR,
755 distanceY * SCROLL_FACTOR);
760 public boolean shouldHandleTouchEvent(MotionEvent ev) {
761 // If the snap back/to full screen animation is on going, ignore any
763 if (mAnimator != null) {
770 public boolean onTouchEvent(MotionEvent ev) {
771 if (ev.getActionMasked() == MotionEvent.ACTION_UP ||
772 ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
773 final boolean shouldSnapBack = shouldSnapBack();
774 if (shouldSnapBack) {
775 mAnimator = snapBack();
777 mAnimator = snapToFullScreen();
779 mAnimator.addListener(new Animator.AnimatorListener() {
781 public void onAnimationStart(Animator animation) {
786 public void onAnimationEnd(Animator animation) {
788 mFocusItem = NO_ITEM_SELECTED;
789 if (shouldSnapBack) {
790 mCurrentStateManager.setCurrentState(new FullyHiddenState());
792 mCurrentStateManager.setCurrentState(new FullyShownState());
797 public void onAnimationCancel(Animator animation) {
802 public void onAnimationRepeat(Animator animation) {
812 * Mode list gets in this state when a mode item has been selected/clicked.
813 * There will be an animation with the blurred preview fading in, a potential
814 * pause to wait for the new mode to be ready, and then the new mode will
815 * be revealed through a pinhole animation. After all the animations finish,
816 * mode list will transition into fully hidden state.
818 private class SelectedState extends ModeListState {
820 public SelectedState(ModeSelectorItem selectedItem) {
821 final int modeId = selectedItem.getModeId();
822 // Un-highlight all the modes.
823 for (int i = 0; i < mModeSelectorItems.length; i++) {
824 mModeSelectorItems[i].setHighlighted(false);
825 mModeSelectorItems[i].setSelected(false);
827 // Select the focused item.
828 selectedItem.setSelected(true);
829 PeepholeAnimationEffect effect = new PeepholeAnimationEffect();
830 effect.setSize(mWidth, mHeight);
832 // Calculate the position of the icon in the selected item, and
833 // start animation from that position.
834 int[] location = new int[2];
835 // Gets icon's center position in relative to the window.
836 selectedItem.getIconCenterLocationInWindow(location);
837 int iconX = location[0];
838 int iconY = location[1];
839 // Gets current view's top left position relative to the window.
840 getLocationInWindow(location);
841 // Calculate icon location relative to this view
842 iconX -= location[0];
843 iconY -= location[1];
845 effect.setAnimationStartingPosition(iconX, iconY);
846 if (mScreenShotProvider != null) {
847 effect.setBackground(mScreenShotProvider
848 .getPreviewFrame(PREVIEW_DOWN_SAMPLE_FACTOR), mPreviewArea);
849 effect.setBackgroundOverlay(mScreenShotProvider.getPreviewOverlayAndControls());
851 mCurrentAnimationEffects = effect;
854 // Post mode selection runnable to the end of the message queue
855 // so that current UI changes can finish before mode initialization
856 // clogs up UI thread.
857 post(new Runnable() {
860 onModeSelected(modeId);
866 public boolean shouldHandleTouchEvent(MotionEvent ev) {
871 public void startModeSelectionAnimation() {
872 mCurrentAnimationEffects.startAnimation(new Animator.AnimatorListener() {
874 public void onAnimationStart(Animator animation) {
879 public void onAnimationEnd(Animator animation) {
880 mCurrentAnimationEffects = null;
881 mCurrentStateManager.setCurrentState(new FullyHiddenState());
885 public void onAnimationCancel(Animator animation) {
890 public void onAnimationRepeat(Animator animation) {
898 public void onPreviewAreaChanged(RectF previewArea) {
899 mPreviewArea.set(previewArea);
902 private final CameraAppUI.UncoveredPreviewAreaSizeChangedListener
903 mUncoveredPreviewAreaSizeChangedListener =
904 new CameraAppUI.UncoveredPreviewAreaSizeChangedListener() {
907 public void uncoveredPreviewAreaChanged(RectF uncoveredPreviewArea) {
908 mUncoveredPreviewArea.set(uncoveredPreviewArea);
909 mSettingsButton.uncoveredPreviewAreaChanged(uncoveredPreviewArea);
910 if (mAdjustPositionWhenUncoveredPreviewAreaChanges) {
911 mAdjustPositionWhenUncoveredPreviewAreaChanges = false;
912 centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight());
917 public interface ModeSwitchListener {
918 public void onModeSelected(int modeIndex);
919 public int getCurrentModeIndex();
920 public void onSettingsSelected();
923 public interface ModeListOpenListener {
925 * Mode list will open to full screen after current animation.
927 public void onOpenFullScreen();
930 * Updates the listener with the current progress of mode drawer opening.
932 * @param progress progress of the mode drawer opening, ranging [0f, 1f]
933 * 0 means mode drawer is fully closed, 1 indicates a fully
936 public void onModeListOpenProgress(float progress);
939 * Gets called when mode list is completely closed.
941 public void onModeListClosed();
944 public static abstract class ModeListVisibilityChangedListener {
945 private Boolean mCurrentVisibility = null;
947 /** Whether the mode list is (partially or fully) visible. */
948 public abstract void onVisibilityChanged(boolean visible);
951 * Internal method to be called by the mode list whenever a visibility
954 * Do not call {@link #onVisibilityChanged(boolean)} directly, as this
955 * is only called when the visibility has actually changed and not on
956 * each visibility event.
958 * @param visible whether the mode drawer is currently visible.
960 private void onVisibilityEvent(boolean visible) {
961 if (mCurrentVisibility == null || mCurrentVisibility != visible) {
962 mCurrentVisibility = visible;
963 onVisibilityChanged(visible);
969 * This class aims to help store time and position in pairs.
971 private static class TimeBasedPosition {
972 private final float mPosition;
973 private final long mTimeStamp;
974 public TimeBasedPosition(float position, long time) {
975 mPosition = position;
979 public float getPosition() {
983 public long getTimeStamp() {
989 * This is a highly customized interpolator. The purpose of having this subclass
990 * is to encapsulate intricate animation timing, so that the actual animation
991 * implementation can be re-used with other interpolators to achieve different
994 * The accordion animation consists of three stages:
995 * 1) Animate into the screen within a pre-specified fly in duration.
996 * 2) Hold in place for a certain amount of time (Optional).
997 * 3) Animate out of the screen within the given time.
999 * The accordion animator is initialized with 3 parameter: 1) initial position,
1000 * 2) how far out the view should be before flying back out, 3) end position.
1001 * The interpolation output should be [0f, 0.5f] during animation between 1)
1002 * to 2), and [0.5f, 1f] for flying from 2) to 3).
1004 private final TimeInterpolator mAccordionInterpolator = new TimeInterpolator() {
1006 public float getInterpolation(float input) {
1008 float flyInDuration = (float) FLY_OUT_DURATION_MS / (float) TOTAL_DURATION_MS;
1009 float holdDuration = (float) (FLY_OUT_DURATION_MS + HOLD_DURATION_MS)
1010 / (float) TOTAL_DURATION_MS;
1013 } else if (input < flyInDuration) {
1014 // Stage 1, project result to [0f, 0.5f]
1015 input /= flyInDuration;
1016 float result = Gusterpolator.INSTANCE.getInterpolation(input);
1017 return result * 0.5f;
1018 } else if (input < holdDuration) {
1022 // Stage 3, project result to [0.5f, 1f]
1023 input -= holdDuration;
1024 input /= (1 - holdDuration);
1025 float result = Gusterpolator.INSTANCE.getInterpolation(input);
1026 return 0.5f + result * 0.5f;
1032 * The listener that is used to notify when gestures occur.
1033 * Here we only listen to a subset of gestures.
1035 private final GestureDetector.OnGestureListener mOnGestureListener
1036 = new GestureDetector.SimpleOnGestureListener(){
1038 public boolean onScroll(MotionEvent e1, MotionEvent e2,
1039 float distanceX, float distanceY) {
1040 mCurrentStateManager.getCurrentState().onScroll(e1, e2, distanceX, distanceY);
1041 mLastScrollTime = System.currentTimeMillis();
1046 public boolean onSingleTapUp(MotionEvent ev) {
1047 mCurrentStateManager.getCurrentState().onSingleTapUp(ev);
1052 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
1053 // Cache velocity in the unit pixel/ms.
1054 mVelocityX = velocityX / 1000f * SCROLL_FACTOR;
1055 mCurrentStateManager.getCurrentState().onFling(e1, e2, velocityX, velocityY);
1060 public boolean onDown(MotionEvent ev) {
1062 mCurrentStateManager.getCurrentState().onDown(ev);
1068 * Gets called when a mode item in the mode drawer is clicked.
1070 * @param selectedItem the item being clicked
1072 private void onItemSelected(ModeSelectorItem selectedItem) {
1073 mCurrentStateManager.getCurrentState().onItemSelected(selectedItem);
1077 * Checks whether a touch event is inside of the bounds of the mode list.
1079 * @param ev touch event to be checked
1080 * @return whether the touch is inside the bounds of the mode list
1082 private boolean isTouchInsideList(MotionEvent ev) {
1083 // Ignore the tap if it happens outside of the mode list linear layout.
1084 float x = ev.getX() - mListView.getX();
1085 float y = ev.getY() - mListView.getY();
1086 if (x < 0 || x > mListView.getWidth() || y < 0 || y > mListView.getHeight()) {
1092 public ModeListView(Context context, AttributeSet attrs) {
1093 super(context, attrs);
1094 mGestureDetector = new GestureDetector(context, mOnGestureListener);
1095 mListBackgroundColor = getResources().getColor(R.color.mode_list_background);
1098 public CameraAppUI.UncoveredPreviewAreaSizeChangedListener
1099 getUncoveredPreviewAreaSizeChangedListener() {
1100 return mUncoveredPreviewAreaSizeChangedListener;
1104 * Sets the alpha on the list background. This is called whenever the list
1105 * is scrolling or animating, so that background can adjust its dimness.
1107 * @param alpha new alpha to be applied on list background color
1109 private void setBackgroundAlpha(int alpha) {
1110 // Make sure alpha is valid.
1111 alpha = alpha & 0xFF;
1112 // Change alpha on the background color.
1113 mListBackgroundColor = mListBackgroundColor & 0xFFFFFF;
1114 mListBackgroundColor = mListBackgroundColor | (alpha << 24);
1115 // Set new color to list background.
1116 setBackgroundColor(mListBackgroundColor);
1120 * Initialize mode list with a list of indices of supported modes.
1122 * @param modeIndexList a list of indices of supported modes
1124 public void init(List<Integer> modeIndexList) {
1125 int[] modeSequence = getResources()
1126 .getIntArray(R.array.camera_modes_in_nav_drawer_if_supported);
1127 int[] visibleModes = getResources()
1128 .getIntArray(R.array.camera_modes_always_visible);
1130 // Mark the supported modes in a boolean array to preserve the
1131 // sequence of the modes
1132 SparseArray<Boolean> modeIsSupported = new SparseArray<Boolean>();
1133 for (int i = 0; i < modeIndexList.size(); i++) {
1134 int mode = modeIndexList.get(i);
1135 modeIsSupported.put(mode, true);
1137 for (int i = 0; i < visibleModes.length; i++) {
1138 int mode = visibleModes[i];
1139 modeIsSupported.put(mode, true);
1142 // Put the indices of supported modes into an array preserving their
1144 mSupportedModes = new ArrayList<Integer>();
1145 for (int i = 0; i < modeSequence.length; i++) {
1146 int mode = modeSequence[i];
1147 if (modeIsSupported.get(mode, false)) {
1148 mSupportedModes.add(mode);
1151 mTotalModes = mSupportedModes.size();
1152 initializeModeSelectorItems();
1153 mSettingsButton = (SettingsButton) findViewById(R.id.settings_button);
1154 mSettingsButton.setOnClickListener(new OnClickListener() {
1156 public void onClick(View v) {
1157 // Post this callback to make sure current user interaction has
1158 // been reflected in the UI. Specifically, the pressed state gets
1159 // unset after click happens. In order to ensure the pressed state
1160 // gets unset in UI before getting in the low frame rate settings
1161 // activity launch stage, the settings selected callback is posted.
1162 post(new Runnable() {
1165 mModeSwitchListener.onSettingsSelected();
1170 // The mode list is initialized to be all the way closed.
1171 onModeListOpenRatioUpdate(0);
1172 if (mCurrentStateManager.getCurrentState() == null) {
1173 mCurrentStateManager.setCurrentState(new FullyHiddenState());
1178 * Sets the screen shot provider for getting a preview frame and a bitmap
1179 * of the controls and overlay.
1181 public void setCameraModuleScreenShotProvider(
1182 CameraAppUI.CameraModuleScreenShotProvider provider) {
1183 mScreenShotProvider = provider;
1186 private void initializeModeSelectorItems() {
1187 mModeSelectorItems = new ModeSelectorItem[mTotalModes];
1188 // Inflate the mode selector items and add them to a linear layout
1189 LayoutInflater inflater = (LayoutInflater) getContext()
1190 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1191 mListView = (LinearLayout) findViewById(R.id.mode_list);
1192 for (int i = 0; i < mTotalModes; i++) {
1193 final ModeSelectorItem selectorItem =
1194 (ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null);
1195 mListView.addView(selectorItem);
1196 // Sets the top padding of the top item to 0.
1198 selectorItem.setPadding(selectorItem.getPaddingLeft(), 0,
1199 selectorItem.getPaddingRight(), selectorItem.getPaddingBottom());
1201 // Sets the bottom padding of the bottom item to 0.
1202 if (i == mTotalModes - 1) {
1203 selectorItem.setPadding(selectorItem.getPaddingLeft(), selectorItem.getPaddingTop(),
1204 selectorItem.getPaddingRight(), 0);
1207 int modeId = getModeIndex(i);
1208 selectorItem.setHighlightColor(getResources()
1209 .getColor(CameraUtil.getCameraThemeColorId(modeId, getContext())));
1212 selectorItem.setImageResource(CameraUtil.getCameraModeIconResId(modeId, getContext()));
1215 selectorItem.setText(CameraUtil.getCameraModeText(modeId, getContext()));
1217 // Set content description (for a11y)
1218 selectorItem.setContentDescription(CameraUtil
1219 .getCameraModeContentDescription(modeId, getContext()));
1220 selectorItem.setModeId(modeId);
1221 selectorItem.setOnClickListener(new OnClickListener() {
1223 public void onClick(View v) {
1224 onItemSelected(selectorItem);
1228 mModeSelectorItems[i] = selectorItem;
1230 // During drawer opening/closing, we change the visible width of the mode
1231 // items in sequence, so we listen to the last item's visible width change
1232 // for a good timing to do corresponding UI adjustments.
1233 mModeSelectorItems[mTotalModes - 1].setVisibleWidthChangedListener(this);
1234 resetModeSelectors();
1238 * Maps between the UI mode selector index to the actual mode id.
1240 * @param modeSelectorIndex the index of the UI item
1241 * @return the index of the corresponding camera mode
1243 private int getModeIndex(int modeSelectorIndex) {
1244 if (modeSelectorIndex < mTotalModes && modeSelectorIndex >= 0) {
1245 return mSupportedModes.get(modeSelectorIndex);
1247 Log.e(TAG, "Invalid mode selector index: " + modeSelectorIndex + ", total modes: " +
1249 return getResources().getInteger(R.integer.camera_mode_photo);
1252 /** Notify ModeSwitchListener, if any, of the mode change. */
1253 private void onModeSelected(int modeIndex) {
1254 if (mModeSwitchListener != null) {
1255 mModeSwitchListener.onModeSelected(modeIndex);
1260 * Sets a listener that listens to receive mode switch event.
1262 * @param listener a listener that gets notified when mode changes.
1264 public void setModeSwitchListener(ModeSwitchListener listener) {
1265 mModeSwitchListener = listener;
1269 * Sets a listener that gets notified when the mode list is open full screen.
1271 * @param listener a listener that listens to mode list open events
1273 public void setModeListOpenListener(ModeListOpenListener listener) {
1274 mModeListOpenListener = listener;
1278 * Sets or replaces a listener that is called when the visibility of the
1279 * mode list changed.
1281 public void setVisibilityChangedListener(ModeListVisibilityChangedListener listener) {
1282 mVisibilityChangedListener = listener;
1286 public boolean onTouchEvent(MotionEvent ev) {
1287 // Reset touch forward recipient
1288 if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
1289 mChildViewTouched = null;
1292 if (!mCurrentStateManager.getCurrentState().shouldHandleTouchEvent(ev)) {
1295 getParent().requestDisallowInterceptTouchEvent(true);
1296 super.onTouchEvent(ev);
1298 // Pass all touch events to gesture detector for gesture handling.
1299 mGestureDetector.onTouchEvent(ev);
1300 mCurrentStateManager.getCurrentState().onTouchEvent(ev);
1305 * Forward touch events to a recipient child view. Before feeding the motion
1306 * event into the child view, the event needs to be converted in child view's
1309 private void forwardTouchEventToChild(MotionEvent ev) {
1310 if (mChildViewTouched != null) {
1311 float x = ev.getX() - mListView.getX();
1312 float y = ev.getY() - mListView.getY();
1313 x -= mChildViewTouched.getLeft();
1314 y -= mChildViewTouched.getTop();
1316 mLastChildTouchEvent = MotionEvent.obtain(ev);
1317 mLastChildTouchEvent.setLocation(x, y);
1318 mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
1323 * Sets the swipe mode to indicate whether this is a swiping in
1324 * or out, and therefore we can have different animations.
1326 * @param swipeIn indicates whether the swipe should reveal/hide the list.
1328 private void setSwipeMode(boolean swipeIn) {
1329 for (int i = 0 ; i < mModeSelectorItems.length; i++) {
1330 mModeSelectorItems[i].onSwipeModeChanged(swipeIn);
1335 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1336 super.onLayout(changed, left, top, right, bottom);
1337 mWidth = right - left;
1338 mHeight = bottom - top - getPaddingTop() - getPaddingBottom();
1339 if (mCurrentStateManager.getCurrentState().getCurrentAnimationEffects() != null) {
1340 mCurrentStateManager.getCurrentState().getCurrentAnimationEffects().setSize(
1346 * Here we calculate the children size based on the orientation, change
1347 * their layout parameters if needed before propagating onMeasure call
1348 * to the children, so the newly changed params will take effect in this
1351 * @param widthMeasureSpec Horizontal space requirements as imposed by the
1353 * @param heightMeasureSpec Vertical space requirements as imposed by the
1357 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1358 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1359 centerModeDrawerInUncoveredPreview(MeasureSpec.getSize(widthMeasureSpec),
1360 MeasureSpec.getSize(heightMeasureSpec));
1364 public void draw(Canvas canvas) {
1365 ModeListState currentState = mCurrentStateManager.getCurrentState();
1366 if (currentState.getCurrentAnimationEffects() != null) {
1367 currentState.getCurrentAnimationEffects().drawBackground(canvas);
1369 currentState.getCurrentAnimationEffects().drawForeground(canvas);
1376 * This shows the mode switcher and starts the accordion animation with a delay.
1377 * If the view does not currently have focus, (e.g. There are popups on top of
1378 * it.) start the delayed accordion animation when it gains focus. Otherwise,
1379 * start the animation with a delay right away.
1381 public void showModeSwitcherHint() {
1382 mCurrentStateManager.getCurrentState().showSwitcherHint();
1386 * Resets the visible width of all the mode selectors to 0.
1388 private void resetModeSelectors() {
1389 for (int i = 0; i < mModeSelectorItems.length; i++) {
1390 mModeSelectorItems[i].setVisibleWidth(0);
1394 private boolean isRunningAccordionAnimation() {
1395 return mAnimatorSet != null && mAnimatorSet.isRunning();
1399 * Calculate the mode selector item in the list that is at position (x, y).
1400 * If the position is above the top item or below the bottom item, return
1401 * the top item or bottom item respectively.
1403 * @param x horizontal position
1404 * @param y vertical position
1405 * @return index of the item that is at position (x, y)
1407 private int getFocusItem(float x, float y) {
1408 // Convert coordinates into child view's coordinates.
1409 x -= mListView.getX();
1410 y -= mListView.getY();
1412 for (int i = 0; i < mModeSelectorItems.length; i++) {
1413 if (y <= mModeSelectorItems[i].getBottom()) {
1417 return mModeSelectorItems.length - 1;
1421 public void onWindowFocusChanged(boolean hasFocus) {
1422 super.onWindowFocusChanged(hasFocus);
1423 mCurrentStateManager.getCurrentState().onWindowFocusChanged(hasFocus);
1427 public void onVisibilityChanged(View v, int visibility) {
1428 super.onVisibilityChanged(v, visibility);
1429 if (visibility == VISIBLE) {
1430 centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight());
1431 // Highlight current module
1432 if (mModeSwitchListener != null) {
1433 int modeId = mModeSwitchListener.getCurrentModeIndex();
1434 int parentMode = CameraUtil.getCameraModeParentModeId(modeId, getContext());
1435 // Find parent mode in the nav drawer.
1436 for (int i = 0; i < mSupportedModes.size(); i++) {
1437 if (mSupportedModes.get(i) == parentMode) {
1438 mModeSelectorItems[i].setSelected(true);
1443 if (mModeSelectorItems != null) {
1444 // When becoming invisible/gone after initializing mode selector items.
1445 for (int i = 0; i < mModeSelectorItems.length; i++) {
1446 mModeSelectorItems[i].setHighlighted(false);
1447 mModeSelectorItems[i].setSelected(false);
1450 if (mModeListOpenListener != null) {
1451 mModeListOpenListener.onModeListClosed();
1454 if (mVisibilityChangedListener != null) {
1455 mVisibilityChangedListener.onVisibilityEvent(getVisibility() == VISIBLE);
1460 public void setVisibility(int visibility) {
1461 ModeListState currentState = mCurrentStateManager.getCurrentState();
1462 if (currentState != null && !currentState.shouldHandleVisibilityChange(visibility)) {
1465 super.setVisibility(visibility);
1469 * Center mode drawer in the portion of camera preview that is not covered by
1472 // TODO: Combine SettingsButton logic into here if UX design does not change
1473 // for another week.
1474 private void centerModeDrawerInUncoveredPreview(int measuredWidth, int measuredHeight) {
1476 // Assuming the preview is centered in the space aside from bottom bar.
1477 float previewAreaWidth = mUncoveredPreviewArea.right + mUncoveredPreviewArea.left;
1478 float previewAreaHeight = mUncoveredPreviewArea.top + mUncoveredPreviewArea.bottom;
1479 if (measuredWidth > measuredHeight && previewAreaWidth < previewAreaHeight
1480 || measuredWidth < measuredHeight && previewAreaWidth > previewAreaHeight) {
1481 // Cached preview area is stale, update mode drawer position on next
1483 mAdjustPositionWhenUncoveredPreviewAreaChanges = true;
1486 mListView.setTranslationX(mUncoveredPreviewArea.left);
1487 // Align center vertical:
1488 mListView.setTranslationY(mUncoveredPreviewArea.centerY()
1489 - mListView.getMeasuredHeight() / 2);
1493 private void scroll(int itemId, float deltaX, float deltaY) {
1494 // Scrolling trend on X and Y axis, to track the trend by biasing
1495 // towards latest touch events.
1496 mScrollTrendX = mScrollTrendX * 0.3f + deltaX * 0.7f;
1497 mScrollTrendY = mScrollTrendY * 0.3f + deltaY * 0.7f;
1499 // TODO: Change how the curve is calculated below when UX finalize their design.
1500 mCurrentTime = SystemClock.uptimeMillis();
1502 if (itemId != NO_ITEM_SELECTED) {
1503 longestWidth = mModeSelectorItems[itemId].getVisibleWidth();
1505 longestWidth = mModeSelectorItems[0].getVisibleWidth();
1507 float newPosition = longestWidth - deltaX;
1508 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
1509 newPosition = Math.min(newPosition, getMaxMovementBasedOnPosition((int) longestWidth,
1511 newPosition = Math.max(newPosition, 0);
1512 insertNewPosition(newPosition, mCurrentTime);
1514 for (int i = 0; i < mModeSelectorItems.length; i++) {
1515 mModeSelectorItems[i].setVisibleWidth(calculateVisibleWidthForItem(i,
1516 (int) newPosition));
1521 * Calculate the width of a specified item based on its position relative to
1522 * the item with longest width.
1524 private int calculateVisibleWidthForItem(int itemId, int longestWidth) {
1525 if (itemId == mFocusItem || mFocusItem == NO_ITEM_SELECTED) {
1526 return longestWidth;
1529 int delay = Math.abs(itemId - mFocusItem) * DELAY_MS;
1530 return (int) getPosition(mCurrentTime - delay,
1531 mModeSelectorItems[itemId].getVisibleWidth());
1535 * Insert new position and time stamp into the history position list, and
1536 * remove stale position items.
1538 * @param position latest position of the focus item
1539 * @param time current time in milliseconds
1541 private void insertNewPosition(float position, long time) {
1542 // TODO: Consider re-using stale position objects rather than
1543 // always creating new position objects.
1544 mPositionHistory.add(new TimeBasedPosition(position, time));
1546 // Positions that are from too long ago will not be of any use for
1547 // future position interpolation. So we need to remove those positions
1549 long timeCutoff = time - (mTotalModes - 1) * DELAY_MS;
1550 while (mPositionHistory.size() > 0) {
1551 // Remove all the position items that are prior to the cutoff time.
1552 TimeBasedPosition historyPosition = mPositionHistory.getFirst();
1553 if (historyPosition.getTimeStamp() < timeCutoff) {
1554 mPositionHistory.removeFirst();
1562 * Gets the interpolated position at the specified time. This involves going
1563 * through the recorded positions until a {@link TimeBasedPosition} is found
1564 * such that the position the recorded before the given time, and the
1565 * {@link TimeBasedPosition} after that is recorded no earlier than the given
1566 * time. These two positions are then interpolated to get the position at the
1569 private float getPosition(long time, float currentPosition) {
1571 for (i = 0; i < mPositionHistory.size(); i++) {
1572 TimeBasedPosition historyPosition = mPositionHistory.get(i);
1573 if (historyPosition.getTimeStamp() > time) {
1574 // Found the winner. Now interpolate between position i and position i - 1
1576 // Slowly approaching to the destination if there isn't enough data points
1577 float weight = 0.2f;
1578 return historyPosition.getPosition() * weight + (1f - weight) * currentPosition;
1580 TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1);
1581 // Start interpolation
1582 float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) /
1583 (float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp());
1584 float position = fraction * (historyPosition.getPosition()
1585 - prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition();
1590 // It should never get here.
1591 Log.e(TAG, "Invalid time input for getPosition(). time: " + time);
1592 if (mPositionHistory.size() == 0) {
1593 Log.e(TAG, "TimeBasedPosition history size is 0");
1595 Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp()
1596 + " , last position recorded at " + mPositionHistory.getLast().getTimeStamp());
1598 assert (i < mPositionHistory.size());
1602 private void reset() {
1603 resetModeSelectors();
1606 setVisibility(INVISIBLE);
1610 * When visible width of list is changed, the background of the list needs
1611 * to darken/lighten correspondingly.
1613 public void onVisibleWidthChanged(int visibleWidth) {
1614 mVisibleWidth = visibleWidth;
1616 // When the longest mode item is entirely shown (across the screen), the
1617 // background should be 50% transparent.
1618 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
1619 visibleWidth = Math.min(maxVisibleWidth, visibleWidth);
1620 if (visibleWidth != maxVisibleWidth) {
1621 // No longer full screen.
1622 cancelForwardingTouchEvent();
1624 float openRatio = (float) visibleWidth / maxVisibleWidth;
1625 onModeListOpenRatioUpdate(openRatio * mModeListOpenFactor);
1629 * Gets called when UI elements such as background and gear icon need to adjust
1630 * their appearance based on the percentage of the mode list opening.
1632 * @param openRatio percentage of the mode list opening, ranging [0f, 1f]
1634 private void onModeListOpenRatioUpdate(float openRatio) {
1635 for (int i = 0; i < mModeSelectorItems.length; i++) {
1636 mModeSelectorItems[i].setTextAlpha(openRatio);
1638 setBackgroundAlpha((int) (BACKGROUND_TRANSPARENTCY * openRatio));
1639 if (mModeListOpenListener != null) {
1640 mModeListOpenListener.onModeListOpenProgress(openRatio);
1642 if (mSettingsButton != null) {
1643 mSettingsButton.setAlpha(openRatio);
1648 * Cancels the touch event forwarding by sending a cancel event to the recipient
1649 * view and resetting the touch forward recipient to ensure no more events
1650 * can be forwarded in the current series of the touch events.
1652 private void cancelForwardingTouchEvent() {
1653 if (mChildViewTouched != null) {
1654 mLastChildTouchEvent.setAction(MotionEvent.ACTION_CANCEL);
1655 mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
1656 mChildViewTouched = null;
1661 public void onWindowVisibilityChanged(int visibility) {
1662 super.onWindowVisibilityChanged(visibility);
1663 if (visibility != VISIBLE) {
1664 mCurrentStateManager.getCurrentState().hide();
1669 * Defines how the list view should respond to a menu button pressed
1672 public boolean onMenuPressed() {
1673 return mCurrentStateManager.getCurrentState().onMenuPressed();
1677 * The list view should either snap back or snap to full screen after a gesture.
1678 * This function is called when an up or cancel event is received, and then based
1679 * on the current position of the list and the gesture we can decide which way
1682 private void snap() {
1683 if (shouldSnapBack()) {
1690 private boolean shouldSnapBack() {
1691 int itemId = Math.max(0, mFocusItem);
1692 if (Math.abs(mVelocityX) > VELOCITY_THRESHOLD) {
1693 // Fling to open / close
1694 return mVelocityX < 0;
1695 } else if (mModeSelectorItems[itemId].getVisibleWidth()
1696 < mModeSelectorItems[itemId].getMaxVisibleWidth() * SNAP_BACK_THRESHOLD_RATIO) {
1698 } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) {
1706 * Snaps back out of the screen.
1708 * @param withAnimation whether snapping back should be animated
1710 public Animator snapBack(boolean withAnimation) {
1711 if (withAnimation) {
1712 if (mVelocityX > -VELOCITY_THRESHOLD * SCROLL_FACTOR) {
1713 return animateListToWidth(0);
1715 return animateListToWidthAtVelocity(mVelocityX, 0);
1718 setVisibility(INVISIBLE);
1719 resetModeSelectors();
1725 * Snaps the mode list back out with animation.
1727 private Animator snapBack() {
1728 return snapBack(true);
1731 private Animator snapToFullScreen() {
1733 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
1734 int fullWidth = mModeSelectorItems[focusItem].getMaxVisibleWidth();
1735 if (mVelocityX <= VELOCITY_THRESHOLD) {
1736 animator = animateListToWidth(fullWidth);
1738 // If the fling velocity exceeds this threshold, snap to full screen
1739 // at a constant speed.
1740 animator = animateListToWidthAtVelocity(VELOCITY_THRESHOLD, fullWidth);
1742 if (mModeListOpenListener != null) {
1743 mModeListOpenListener.onOpenFullScreen();
1749 * Overloaded function to provide a simple way to start animation. Animation
1750 * will use default duration, and a value of <code>null</code> for interpolator
1751 * means linear interpolation will be used.
1753 * @param width a set of values that the animation will animate between over time
1755 private Animator animateListToWidth(int... width) {
1756 return animateListToWidth(0, DEFAULT_DURATION_MS, null, width);
1760 * Animate the mode list between the given set of visible width.
1762 * @param delay start delay between consecutive mode item. If delay < 0, the
1763 * leader in the animation will be the bottom item.
1764 * @param duration duration for the animation of each mode item
1765 * @param interpolator interpolator to be used by the animation
1766 * @param width a set of values that the animation will animate between over time
1768 private Animator animateListToWidth(int delay, int duration,
1769 TimeInterpolator interpolator, int... width) {
1770 if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
1774 ArrayList<Animator> animators = new ArrayList<Animator>();
1775 boolean animateModeItemsInOrder = true;
1777 animateModeItemsInOrder = false;
1780 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
1781 for (int i = 0; i < mTotalModes; i++) {
1782 ObjectAnimator animator;
1783 if (animateModeItemsInOrder) {
1784 animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
1785 "visibleWidth", width);
1787 animator = ObjectAnimator.ofInt(mModeSelectorItems[mTotalModes - 1 -i],
1788 "visibleWidth", width);
1790 animator.setDuration(duration);
1791 animator.setStartDelay(i * delay);
1792 animators.add(animator);
1795 mAnimatorSet = new AnimatorSet();
1796 mAnimatorSet.playTogether(animators);
1797 mAnimatorSet.setInterpolator(interpolator);
1798 mAnimatorSet.start();
1800 return mAnimatorSet;
1804 * Animate the mode list to the given width at a constant velocity.
1806 * @param velocity the velocity that animation will be at
1807 * @param width final width of the list
1809 private Animator animateListToWidthAtVelocity(float velocity, int width) {
1810 if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
1814 ArrayList<Animator> animators = new ArrayList<Animator>();
1815 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
1816 for (int i = 0; i < mTotalModes; i++) {
1817 ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
1818 "visibleWidth", width);
1819 int duration = (int) (width / velocity);
1820 animator.setDuration(duration);
1821 animators.add(animator);
1824 mAnimatorSet = new AnimatorSet();
1825 mAnimatorSet.playTogether(animators);
1826 mAnimatorSet.setInterpolator(null);
1827 mAnimatorSet.start();
1829 return mAnimatorSet;
1833 * Called when the back key is pressed.
1835 * @return Whether the UI responded to the key event.
1837 public boolean onBackPressed() {
1838 return mCurrentStateManager.getCurrentState().onBackPressed();
1841 public void startModeSelectionAnimation() {
1842 mCurrentStateManager.getCurrentState().startModeSelectionAnimation();
1845 public float getMaxMovementBasedOnPosition(int lastVisibleWidth, int maxWidth) {
1846 int timeElapsed = (int) (System.currentTimeMillis() - mLastScrollTime);
1847 if (timeElapsed > SCROLL_INTERVAL_MS) {
1848 timeElapsed = SCROLL_INTERVAL_MS;
1851 int slowZone = (int) (maxWidth * SLOW_ZONE_PERCENTAGE);
1852 if (lastVisibleWidth < (maxWidth - slowZone)) {
1853 position = VELOCITY_THRESHOLD * (float) timeElapsed + lastVisibleWidth;
1855 float percentageIntoSlowZone = (lastVisibleWidth - (maxWidth - slowZone)) / slowZone;
1856 float velocity = (1 - percentageIntoSlowZone) * VELOCITY_THRESHOLD;
1857 position = velocity * (float) timeElapsed + lastVisibleWidth;
1859 position = Math.min(maxWidth, position);
1863 private class PeepholeAnimationEffect extends AnimationEffects {
1865 private final static int UNSET = -1;
1866 private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 300;
1868 private final Paint mMaskPaint = new Paint();
1869 private final Paint mBackgroundPaint = new Paint();
1870 private final RectF mBackgroundDrawArea = new RectF();
1873 private int mHeight;
1874 private int mPeepHoleCenterX = UNSET;
1875 private int mPeepHoleCenterY = UNSET;
1876 private float mRadius = 0f;
1877 private ValueAnimator mPeepHoleAnimator;
1878 private Bitmap mBackground;
1879 private Bitmap mBlurredBackground;
1880 private Bitmap mBackgroundOverlay;
1882 public PeepholeAnimationEffect() {
1883 mMaskPaint.setAlpha(0);
1884 mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
1888 public void setSize(int width, int height) {
1894 public boolean onTouchEvent(MotionEvent event) {
1899 public void drawForeground(Canvas canvas) {
1900 // Draw the circle in clear mode
1901 if (mPeepHoleAnimator != null) {
1902 // Draw a transparent circle using clear mode
1903 canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint);
1907 public void setAnimationStartingPosition(int x, int y) {
1908 mPeepHoleCenterX = x;
1909 mPeepHoleCenterY = y;
1913 * Sets the bitmap to be drawn in the background and the drawArea to draw
1914 * the bitmap. In the meantime, start processing the image in a background
1915 * thread to get a blurred background image.
1917 * @param background image to be drawn in the background
1918 * @param drawArea area to draw the background image
1920 public void setBackground(Bitmap background, RectF drawArea) {
1921 mBackground = background;
1922 mBackgroundDrawArea.set(drawArea);
1923 new BlurTask().execute(Bitmap.createScaledBitmap(background, background.getWidth(),
1924 background.getHeight(), true));
1928 * Sets the overlay image to be drawn on top of the background.
1930 public void setBackgroundOverlay(Bitmap overlay) {
1931 mBackgroundOverlay = overlay;
1935 * This gets called when a blurred image of the background is generated.
1936 * Start an animation to fade in the blur.
1938 * @param blur blurred image of the background.
1940 public void setBlurredBackground(Bitmap blur) {
1941 mBlurredBackground = blur;
1943 ObjectAnimator alpha = ObjectAnimator.ofInt(mBackgroundPaint, "alpha", 80, 255);
1944 alpha.setDuration(250);
1945 alpha.setInterpolator(Gusterpolator.INSTANCE);
1946 alpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1948 public void onAnimationUpdate(ValueAnimator animation) {
1957 public void drawBackground(Canvas canvas) {
1958 if (mBackground != null && mBackgroundOverlay != null) {
1959 canvas.drawARGB(255, 0, 0, 0);
1960 canvas.drawBitmap(mBackground, null, mBackgroundDrawArea, null);
1961 if (mBlurredBackground != null) {
1962 canvas.drawBitmap(mBlurredBackground, null, mBackgroundDrawArea, mBackgroundPaint);
1964 canvas.drawBitmap(mBackgroundOverlay, 0, 0, null);
1969 public void startAnimation(Animator.AnimatorListener listener) {
1970 if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) {
1973 if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) {
1974 mPeepHoleCenterX = mWidth / 2;
1975 mPeepHoleCenterY = mHeight / 2;
1978 int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX);
1979 int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY);
1980 int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge
1981 + verticalDistanceToFarEdge * verticalDistanceToFarEdge));
1982 int startRadius = getResources().getDimensionPixelSize(
1983 R.dimen.mode_selector_icon_block_width) / 2;
1985 mPeepHoleAnimator = ValueAnimator.ofFloat(0, endRadius);
1986 mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS);
1987 mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE);
1988 mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1990 public void onAnimationUpdate(ValueAnimator animation) {
1991 // Modify mask by enlarging the hole
1992 mRadius = (Float) mPeepHoleAnimator.getAnimatedValue();
1997 if (listener != null) {
1998 mPeepHoleAnimator.addListener(listener);
2000 mPeepHoleAnimator.start();
2004 public void endAnimation() {
2007 private class BlurTask extends AsyncTask<Bitmap, Integer, Bitmap> {
2009 // Gaussian blur mask size.
2010 private static final int MASK_SIZE = 7;
2012 protected Bitmap doInBackground(Bitmap... params) {
2014 Bitmap intermediateBitmap = params[0];
2016 Bitmap lowResPreview = Bitmap.createScaledBitmap(intermediateBitmap,
2017 intermediateBitmap.getWidth() / factor,
2018 intermediateBitmap.getHeight() / factor, true);
2020 int width = lowResPreview.getWidth();
2021 int height = lowResPreview.getHeight();
2023 if (mInputPixels == null || mInputPixels.length < width * height) {
2024 mInputPixels = new int[width * height];
2025 mOutputPixels = new int[width * height];
2027 lowResPreview.getPixels(mInputPixels, 0, width, 0, 0, width, height);
2028 CameraUtil.blur(mInputPixels, mOutputPixels, width, height, MASK_SIZE);
2029 lowResPreview.setPixels(mOutputPixels, 0, width, 0, 0, width, height);
2031 intermediateBitmap.recycle();
2032 return Bitmap.createScaledBitmap(lowResPreview, width * factor,
2033 height * factor, true);
2037 protected void onPostExecute(Bitmap bitmap) {
2038 setBlurredBackground(bitmap);