• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 
17 package com.android.camera.ui;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.animation.TimeInterpolator;
24 import android.animation.ValueAnimator;
25 import android.content.Context;
26 import android.graphics.Bitmap;
27 import android.graphics.Canvas;
28 import android.graphics.Paint;
29 import android.graphics.Point;
30 import android.graphics.PorterDuff;
31 import android.graphics.PorterDuffXfermode;
32 import android.graphics.RectF;
33 import android.os.SystemClock;
34 import android.util.AttributeSet;
35 import android.util.SparseBooleanArray;
36 import android.view.GestureDetector;
37 import android.view.LayoutInflater;
38 import android.view.MotionEvent;
39 import android.view.View;
40 import android.widget.FrameLayout;
41 import android.widget.LinearLayout;
42 
43 import com.android.camera.CaptureLayoutHelper;
44 import com.android.camera.app.CameraAppUI;
45 import com.android.camera.debug.Log;
46 import com.android.camera.util.AndroidServices;
47 import com.android.camera.util.CameraUtil;
48 import com.android.camera.util.Gusterpolator;
49 import com.android.camera.stats.UsageStatistics;
50 import com.android.camera.widget.AnimationEffects;
51 import com.android.camera.widget.SettingsCling;
52 import com.android.camera2.R;
53 import com.google.common.logging.eventprotos;
54 
55 import java.util.ArrayList;
56 import java.util.LinkedList;
57 import java.util.List;
58 
59 /**
60  * ModeListView class displays all camera modes and settings in the form
61  * of a list. A swipe to the right will bring up this list. Then tapping on
62  * any of the items in the list will take the user to that corresponding mode
63  * with an animation. To dismiss this list, simply swipe left or select a mode.
64  */
65 public class ModeListView extends FrameLayout
66         implements ModeSelectorItem.VisibleWidthChangedListener,
67         PreviewStatusListener.PreviewAreaChangedListener {
68 
69     private static final Log.Tag TAG = new Log.Tag("ModeListView");
70 
71     // Animation Durations
72     private static final int DEFAULT_DURATION_MS = 200;
73     private static final int FLY_IN_DURATION_MS = 0;
74     private static final int HOLD_DURATION_MS = 0;
75     private static final int FLY_OUT_DURATION_MS = 850;
76     private static final int START_DELAY_MS = 100;
77     private static final int TOTAL_DURATION_MS = FLY_IN_DURATION_MS + HOLD_DURATION_MS
78             + FLY_OUT_DURATION_MS;
79     private static final int HIDE_SHIMMY_DELAY_MS = 1000;
80     // Assumption for time since last scroll when no data point for last scroll.
81     private static final int SCROLL_INTERVAL_MS = 50;
82     // Last 20% percent of the drawer opening should be slow to ensure soft landing.
83     private static final float SLOW_ZONE_PERCENTAGE = 0.2f;
84 
85     private static final int NO_ITEM_SELECTED = -1;
86 
87     // Scrolling delay between non-focused item and focused item
88     private static final int DELAY_MS = 30;
89     // If the fling velocity exceeds this threshold, snap to full screen at a constant
90     // speed. Unit: pixel/ms.
91     private static final float VELOCITY_THRESHOLD = 2f;
92 
93     /**
94      * A factor to change the UI responsiveness on a scroll.
95      * e.g. A scroll factor of 0.5 means UI will move half as fast as the finger.
96      */
97     private static final float SCROLL_FACTOR = 0.5f;
98     // 60% opaque black background.
99     private static final int BACKGROUND_TRANSPARENTCY = (int) (0.6f * 255);
100     private static final int PREVIEW_DOWN_SAMPLE_FACTOR = 4;
101     // Threshold, below which snap back will happen.
102     private static final float SNAP_BACK_THRESHOLD_RATIO = 0.33f;
103 
104     private final GestureDetector mGestureDetector;
105     private final CurrentStateManager mCurrentStateManager = new CurrentStateManager();
106     private final int mSettingsButtonMargin;
107     private long mLastScrollTime;
108     private int mListBackgroundColor;
109     private LinearLayout mListView;
110     private View mSettingsButton;
111     private int mTotalModes;
112     private ModeSelectorItem[] mModeSelectorItems;
113     private AnimatorSet mAnimatorSet;
114     private int mFocusItem = NO_ITEM_SELECTED;
115     private ModeListOpenListener mModeListOpenListener;
116     private ModeListVisibilityChangedListener mVisibilityChangedListener;
117     private CameraAppUI.CameraModuleScreenShotProvider mScreenShotProvider = null;
118     private int[] mInputPixels;
119     private int[] mOutputPixels;
120     private float mModeListOpenFactor = 1f;
121 
122     private View mChildViewTouched = null;
123     private MotionEvent mLastChildTouchEvent = null;
124     private int mVisibleWidth = 0;
125 
126     // Width and height of this view. They get updated in onLayout()
127     // Unit for width and height are pixels.
128     private int mWidth;
129     private int mHeight;
130     private float mScrollTrendX = 0f;
131     private float mScrollTrendY = 0f;
132     private ModeSwitchListener mModeSwitchListener = null;
133     private ArrayList<Integer> mSupportedModes;
134     private final LinkedList<TimeBasedPosition> mPositionHistory
135             = new LinkedList<TimeBasedPosition>();
136     private long mCurrentTime;
137     private float mVelocityX; // Unit: pixel/ms.
138     private long mLastDownTime = 0;
139     private CaptureLayoutHelper mCaptureLayoutHelper = null;
140     private SettingsCling mSettingsCling = null;
141 
142     private class CurrentStateManager {
143         private ModeListState mCurrentState;
144 
getCurrentState()145         ModeListState getCurrentState() {
146             return mCurrentState;
147         }
148 
setCurrentState(ModeListState state)149         void setCurrentState(ModeListState state) {
150             mCurrentState = state;
151             state.onCurrentState();
152         }
153     }
154 
155     /**
156      * ModeListState defines a set of functions through which the view could manage
157      * or change the states. Sub-classes could selectively override these functions
158      * accordingly to respect the specific requirements for each state. By overriding
159      * these methods, state transition can also be achieved.
160      */
161     private abstract class ModeListState implements GestureDetector.OnGestureListener {
162         protected AnimationEffects mCurrentAnimationEffects = null;
163 
164         /**
165          * Called by the state manager when this state instance becomes the current
166          * mode list state.
167          */
onCurrentState()168         public void onCurrentState() {
169             // Do nothing.
170             showSettingsClingIfEnabled(false);
171         }
172 
173         /**
174          * If supported, this should show the mode switcher and starts the accordion
175          * animation with a delay. If the view does not currently have focus, (e.g.
176          * There are popups on top of it.) start the delayed accordion animation
177          * when it gains focus. Otherwise, start the animation with a delay right
178          * away.
179          */
showSwitcherHint()180         public void showSwitcherHint() {
181             // Do nothing.
182         }
183 
184         /**
185          * Gets the currently running animation effects for the current state.
186          */
getCurrentAnimationEffects()187         public AnimationEffects getCurrentAnimationEffects() {
188             return mCurrentAnimationEffects;
189         }
190 
191         /**
192          * Returns true if the touch event should be handled, false otherwise.
193          *
194          * @param ev motion event to be handled
195          * @return true if the event should be handled, false otherwise.
196          */
shouldHandleTouchEvent(MotionEvent ev)197         public boolean shouldHandleTouchEvent(MotionEvent ev) {
198             return true;
199         }
200 
201         /**
202          * Handles touch event. This will be called if
203          * {@link ModeListState#shouldHandleTouchEvent(android.view.MotionEvent)}
204          * returns {@code true}
205          *
206          * @param ev touch event to be handled
207          * @return always true
208          */
onTouchEvent(MotionEvent ev)209         public boolean onTouchEvent(MotionEvent ev) {
210             return true;
211         }
212 
213         /**
214          * Gets called when the window focus has changed.
215          *
216          * @param hasFocus whether current window has focus
217          */
onWindowFocusChanged(boolean hasFocus)218         public void onWindowFocusChanged(boolean hasFocus) {
219             // Default to do nothing.
220         }
221 
222         /**
223          * Gets called when back key is pressed.
224          *
225          * @return true if handled, false otherwise.
226          */
onBackPressed()227         public boolean onBackPressed() {
228             return false;
229         }
230 
231         /**
232          * Gets called when menu key is pressed.
233          *
234          * @return true if handled, false otherwise.
235          */
onMenuPressed()236         public boolean onMenuPressed() {
237             return false;
238         }
239 
240         /**
241          * Gets called when there is a {@link View#setVisibility(int)} call to
242          * change the visibility of the mode drawer. Visibility change does not
243          * always make sense, for example there can be an outside call to make
244          * the mode drawer visible when it is in the fully hidden state. The logic
245          * is that the mode drawer can only be made visible when user swipe it in.
246          *
247          * @param visibility the proposed visibility change
248          * @return true if the visibility change is valid and therefore should be
249          *         handled, false otherwise.
250          */
shouldHandleVisibilityChange(int visibility)251         public boolean shouldHandleVisibilityChange(int visibility) {
252             return true;
253         }
254 
255         /**
256          * If supported, this should start blurring the camera preview and
257          * start the mode switch.
258          *
259          * @param selectedItem mode item that has been selected
260          */
onItemSelected(ModeSelectorItem selectedItem)261         public void onItemSelected(ModeSelectorItem selectedItem) {
262             // Do nothing.
263         }
264 
265         /**
266          * This gets called when mode switch has finished and UI needs to
267          * pinhole into the new mode through animation.
268          */
startModeSelectionAnimation()269         public void startModeSelectionAnimation() {
270             // Do nothing.
271         }
272 
273         /**
274          * Hide the mode drawer and switch to fully hidden state.
275          */
hide()276         public void hide() {
277             // Do nothing.
278         }
279 
280         /**
281          * Hide the mode drawer (with animation, if supported)
282          * and switch to fully hidden state.
283          * Default is to simply call {@link #hide()}.
284          */
hideAnimated()285         public void hideAnimated() {
286             hide();
287         }
288 
289         /***************GestureListener implementation*****************/
290         @Override
onDown(MotionEvent e)291         public boolean onDown(MotionEvent e) {
292             return false;
293         }
294 
295         @Override
onShowPress(MotionEvent e)296         public void onShowPress(MotionEvent e) {
297             // Do nothing.
298         }
299 
300         @Override
onSingleTapUp(MotionEvent e)301         public boolean onSingleTapUp(MotionEvent e) {
302             return false;
303         }
304 
305         @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)306         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
307             return false;
308         }
309 
310         @Override
onLongPress(MotionEvent e)311         public void onLongPress(MotionEvent e) {
312             // Do nothing.
313         }
314 
315         @Override
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)316         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
317             return false;
318         }
319     }
320 
321     /**
322      * Fully hidden state. Transitioning to ScrollingState and ShimmyState are supported
323      * in this state.
324      */
325     private class FullyHiddenState extends ModeListState {
326         private Animator mAnimator = null;
327         private boolean mShouldBeVisible = false;
328 
FullyHiddenState()329         public FullyHiddenState() {
330             reset();
331         }
332 
333         @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)334         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
335             mShouldBeVisible = true;
336             // Change visibility, and switch to scrolling state.
337             resetModeSelectors();
338             mCurrentStateManager.setCurrentState(new ScrollingState());
339             return true;
340         }
341 
342         @Override
showSwitcherHint()343         public void showSwitcherHint() {
344             mShouldBeVisible = true;
345             mCurrentStateManager.setCurrentState(new ShimmyState());
346         }
347 
348         @Override
shouldHandleTouchEvent(MotionEvent ev)349         public boolean shouldHandleTouchEvent(MotionEvent ev) {
350             return true;
351         }
352 
353         @Override
onTouchEvent(MotionEvent ev)354         public boolean onTouchEvent(MotionEvent ev) {
355             if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
356                 mFocusItem = getFocusItem(ev.getX(), ev.getY());
357                 setSwipeMode(true);
358             }
359             return true;
360         }
361 
362         @Override
onMenuPressed()363         public boolean onMenuPressed() {
364             if (mAnimator != null) {
365                 return false;
366             }
367             snapOpenAndShow();
368             return true;
369         }
370 
371         @Override
shouldHandleVisibilityChange(int visibility)372         public boolean shouldHandleVisibilityChange(int visibility) {
373             if (mAnimator != null) {
374                 return false;
375             }
376             if (visibility == VISIBLE && !mShouldBeVisible) {
377                 return false;
378             }
379             return true;
380         }
381         /**
382          * Snaps open the mode list and go to the fully shown state.
383          */
snapOpenAndShow()384         private void snapOpenAndShow() {
385             mShouldBeVisible = true;
386             setVisibility(VISIBLE);
387 
388             mAnimator = snapToFullScreen();
389             if (mAnimator != null) {
390                 mAnimator.addListener(new Animator.AnimatorListener() {
391                     @Override
392                     public void onAnimationStart(Animator animation) {
393 
394                     }
395 
396                     @Override
397                     public void onAnimationEnd(Animator animation) {
398                         mAnimator = null;
399                         mCurrentStateManager.setCurrentState(new FullyShownState());
400                     }
401 
402                     @Override
403                     public void onAnimationCancel(Animator animation) {
404 
405                     }
406 
407                     @Override
408                     public void onAnimationRepeat(Animator animation) {
409 
410                     }
411                 });
412             } else {
413                 mCurrentStateManager.setCurrentState(new FullyShownState());
414                 UsageStatistics.instance().controlUsed(
415                         eventprotos.ControlEvent.ControlType.MENU_FULL_FROM_HIDDEN);
416             }
417         }
418 
419         @Override
onCurrentState()420         public void onCurrentState() {
421             super.onCurrentState();
422             announceForAccessibility(
423                     getContext().getResources().getString(R.string.accessibility_mode_list_hidden));
424         }
425     }
426 
427     /**
428      * Fully shown state. This state represents when the mode list is entirely shown
429      * on screen without any on-going animation. Transitions from this state could be
430      * to ScrollingState, SelectedState, or FullyHiddenState.
431      */
432     private class FullyShownState extends ModeListState {
433         private Animator mAnimator = null;
434 
435         @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)436         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
437             // Go to scrolling state.
438             if (distanceX > 0) {
439                 // Swipe out
440                 cancelForwardingTouchEvent();
441                 mCurrentStateManager.setCurrentState(new ScrollingState());
442             }
443             return true;
444         }
445 
446         @Override
shouldHandleTouchEvent(MotionEvent ev)447         public boolean shouldHandleTouchEvent(MotionEvent ev) {
448             if (mAnimator != null && mAnimator.isRunning()) {
449                 return false;
450             }
451             return true;
452         }
453 
454         @Override
onTouchEvent(MotionEvent ev)455         public boolean onTouchEvent(MotionEvent ev) {
456             if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
457                 mFocusItem = NO_ITEM_SELECTED;
458                 setSwipeMode(false);
459                 // If the down event happens inside the mode list, find out which
460                 // mode item is being touched and forward all the subsequent touch
461                 // events to that mode item for its pressed state and click handling.
462                 if (isTouchInsideList(ev)) {
463                     mChildViewTouched = mModeSelectorItems[getFocusItem(ev.getX(), ev.getY())];
464                 }
465             }
466             forwardTouchEventToChild(ev);
467             return true;
468         }
469 
470 
471         @Override
onSingleTapUp(MotionEvent ev)472         public boolean onSingleTapUp(MotionEvent ev) {
473             // If the tap is not inside the mode drawer area, snap back.
474             if(!isTouchInsideList(ev)) {
475                 snapBackAndHide();
476                 return false;
477             }
478             return true;
479         }
480 
481         @Override
onBackPressed()482         public boolean onBackPressed() {
483             snapBackAndHide();
484             return true;
485         }
486 
487         @Override
onMenuPressed()488         public boolean onMenuPressed() {
489             snapBackAndHide();
490             return true;
491         }
492 
493         @Override
onItemSelected(ModeSelectorItem selectedItem)494         public void onItemSelected(ModeSelectorItem selectedItem) {
495             mCurrentStateManager.setCurrentState(new SelectedState(selectedItem));
496         }
497 
498         /**
499          * Snaps back the mode list and go to the fully hidden state.
500          */
snapBackAndHide()501         private void snapBackAndHide() {
502             mAnimator = snapBack(true);
503             if (mAnimator != null) {
504                 mAnimator.addListener(new Animator.AnimatorListener() {
505                     @Override
506                     public void onAnimationStart(Animator animation) {
507 
508                     }
509 
510                     @Override
511                     public void onAnimationEnd(Animator animation) {
512                         mAnimator = null;
513                         mCurrentStateManager.setCurrentState(new FullyHiddenState());
514                     }
515 
516                     @Override
517                     public void onAnimationCancel(Animator animation) {
518 
519                     }
520 
521                     @Override
522                     public void onAnimationRepeat(Animator animation) {
523 
524                     }
525                 });
526             } else {
527                 mCurrentStateManager.setCurrentState(new FullyHiddenState());
528             }
529         }
530 
531         @Override
hide()532         public void hide() {
533             if (mAnimator != null) {
534                 mAnimator.cancel();
535             } else {
536                 mCurrentStateManager.setCurrentState(new FullyHiddenState());
537             }
538         }
539 
540         @Override
onCurrentState()541         public void onCurrentState() {
542             announceForAccessibility(
543                     getContext().getResources().getString(R.string.accessibility_mode_list_shown));
544             showSettingsClingIfEnabled(true);
545         }
546     }
547 
548     /**
549      * Shimmy state handles the specifics for shimmy animation, including
550      * setting up to show mode drawer (without text) and hide it with shimmy animation.
551      *
552      * This state can be interrupted when scrolling or mode selection happened,
553      * in which case the state will transition into ScrollingState, or SelectedState.
554      * Otherwise, after shimmy finishes successfully, a transition to fully hidden
555      * state will happen.
556      */
557     private class ShimmyState extends ModeListState {
558 
559         private boolean mStartHidingShimmyWhenWindowGainsFocus = false;
560         private Animator mAnimator = null;
561         private final Runnable mHideShimmy = new Runnable() {
562             @Override
563             public void run() {
564                 startHidingShimmy();
565             }
566         };
567 
ShimmyState()568         public ShimmyState() {
569             setVisibility(VISIBLE);
570             mSettingsButton.setVisibility(INVISIBLE);
571             mModeListOpenFactor = 0f;
572             onModeListOpenRatioUpdate(0);
573             int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
574             for (int i = 0; i < mModeSelectorItems.length; i++) {
575                 mModeSelectorItems[i].setVisibleWidth(maxVisibleWidth);
576             }
577             if (hasWindowFocus()) {
578                 hideShimmyWithDelay();
579             } else {
580                 mStartHidingShimmyWhenWindowGainsFocus = true;
581             }
582         }
583 
584         @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)585         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
586             // Scroll happens during accordion animation.
587             cancelAnimation();
588             cancelForwardingTouchEvent();
589             // Go to scrolling state
590             mCurrentStateManager.setCurrentState(new ScrollingState());
591             UsageStatistics.instance().controlUsed(
592                     eventprotos.ControlEvent.ControlType.MENU_SCROLL_FROM_SHIMMY);
593             return true;
594         }
595 
596         @Override
shouldHandleTouchEvent(MotionEvent ev)597         public boolean shouldHandleTouchEvent(MotionEvent ev) {
598             if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
599                 if (isTouchInsideList(ev) &&
600                         ev.getX() <= mModeSelectorItems[0].getMaxVisibleWidth()) {
601                     mChildViewTouched = mModeSelectorItems[getFocusItem(ev.getX(), ev.getY())];
602                     return true;
603                 }
604                 // If shimmy is on-going, reject the first down event, so that it can be handled
605                 // by the view underneath. If a swipe is detected, the same series of touch will
606                 // re-enter this function, in which case we will consume the touch events.
607                 if (mLastDownTime != ev.getDownTime()) {
608                     mLastDownTime = ev.getDownTime();
609                     return false;
610                 }
611             }
612             return true;
613         }
614 
615         @Override
onTouchEvent(MotionEvent ev)616         public boolean onTouchEvent(MotionEvent ev) {
617             if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
618                 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
619                     mFocusItem = getFocusItem(ev.getX(), ev.getY());
620                     setSwipeMode(true);
621                 }
622             }
623             forwardTouchEventToChild(ev);
624             return true;
625         }
626 
627         @Override
onItemSelected(ModeSelectorItem selectedItem)628         public void onItemSelected(ModeSelectorItem selectedItem) {
629             cancelAnimation();
630             mCurrentStateManager.setCurrentState(new SelectedState(selectedItem));
631         }
632 
hideShimmyWithDelay()633         private void hideShimmyWithDelay() {
634             postDelayed(mHideShimmy, HIDE_SHIMMY_DELAY_MS);
635         }
636 
637         @Override
onWindowFocusChanged(boolean hasFocus)638         public void onWindowFocusChanged(boolean hasFocus) {
639             if (mStartHidingShimmyWhenWindowGainsFocus && hasFocus) {
640                 mStartHidingShimmyWhenWindowGainsFocus = false;
641                 hideShimmyWithDelay();
642             }
643         }
644 
645         /**
646          * This starts the accordion animation, unless it's already running, in which
647          * case the start animation call will be ignored.
648          */
startHidingShimmy()649         private void startHidingShimmy() {
650             if (mAnimator != null) {
651                 return;
652             }
653             int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
654             mAnimator = animateListToWidth(START_DELAY_MS * (-1), TOTAL_DURATION_MS,
655                     Gusterpolator.INSTANCE, maxVisibleWidth, 0);
656             mAnimator.addListener(new Animator.AnimatorListener() {
657                 private boolean mSuccess = true;
658                 @Override
659                 public void onAnimationStart(Animator animation) {
660                     // Do nothing.
661                 }
662 
663                 @Override
664                 public void onAnimationEnd(Animator animation) {
665                     mAnimator = null;
666                     ShimmyState.this.onAnimationEnd(mSuccess);
667                 }
668 
669                 @Override
670                 public void onAnimationCancel(Animator animation) {
671                     mSuccess = false;
672                 }
673 
674                 @Override
675                 public void onAnimationRepeat(Animator animation) {
676                     // Do nothing.
677                 }
678             });
679         }
680 
681         /**
682          * Cancels the pending/on-going animation.
683          */
cancelAnimation()684         private void cancelAnimation() {
685             removeCallbacks(mHideShimmy);
686             if (mAnimator != null && mAnimator.isRunning()) {
687                 mAnimator.cancel();
688             } else {
689                 mAnimator = null;
690                 onAnimationEnd(false);
691             }
692         }
693 
694         @Override
onCurrentState()695         public void onCurrentState() {
696             super.onCurrentState();
697             ModeListView.this.disableA11yOnModeSelectorItems();
698         }
699         /**
700          * Gets called when the animation finishes or gets canceled.
701          *
702          * @param success indicates whether the animation finishes successfully
703          */
onAnimationEnd(boolean success)704         private void onAnimationEnd(boolean success) {
705             if (mSettingsButton.getLayerType() == View.LAYER_TYPE_HARDWARE) {
706                 Log.v(TAG, "Disabling hardware layer for the Settings Button. (onAnimationEnd)");
707                 mSettingsButton.setLayerType(View.LAYER_TYPE_NONE, null);
708             }
709 
710             mSettingsButton.setVisibility(VISIBLE);
711             // If successfully finish hiding shimmy, then we should go back to
712             // fully hidden state.
713             if (success) {
714                 ModeListView.this.enableA11yOnModeSelectorItems();
715                 mModeListOpenFactor = 1;
716                 mCurrentStateManager.setCurrentState(new FullyHiddenState());
717                 return;
718             }
719 
720             // If the animation was canceled before it's finished, animate the mode
721             // list open factor from 0 to 1 to ensure a smooth visual transition.
722             final ValueAnimator openFactorAnimator = ValueAnimator.ofFloat(mModeListOpenFactor, 1f);
723             openFactorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
724                 @Override
725                 public void onAnimationUpdate(ValueAnimator animation) {
726                     mModeListOpenFactor = (Float) openFactorAnimator.getAnimatedValue();
727                     onVisibleWidthChanged(mVisibleWidth);
728                 }
729             });
730             openFactorAnimator.addListener(new Animator.AnimatorListener() {
731                 @Override
732                 public void onAnimationStart(Animator animation) {
733                     // Do nothing.
734                 }
735 
736                 @Override
737                 public void onAnimationEnd(Animator animation) {
738                     mModeListOpenFactor = 1f;
739                 }
740 
741                 @Override
742                 public void onAnimationCancel(Animator animation) {
743                     // Do nothing.
744                 }
745 
746                 @Override
747                 public void onAnimationRepeat(Animator animation) {
748                     // Do nothing.
749                 }
750             });
751             openFactorAnimator.start();
752         }
753 
754         @Override
hide()755         public void hide() {
756             cancelAnimation();
757             mCurrentStateManager.setCurrentState(new FullyHiddenState());
758         }
759 
760         @Override
hideAnimated()761         public void hideAnimated() {
762             cancelAnimation();
763             animateListToWidth(0).addListener(new AnimatorListenerAdapter() {
764                 @Override
765                 public void onAnimationEnd(Animator animation) {
766                     mCurrentStateManager.setCurrentState(new FullyHiddenState());
767                 }
768             });
769         }
770     }
771 
772     /**
773      * When the mode list is being scrolled, it will be in ScrollingState. From
774      * this state, the mode list could transition to fully hidden, fully open
775      * depending on which direction the scrolling goes.
776      */
777     private class ScrollingState extends ModeListState {
778         private Animator mAnimator = null;
779 
ScrollingState()780         public ScrollingState() {
781             setVisibility(VISIBLE);
782         }
783 
784         @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)785         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
786             // Scroll based on the scrolling distance on the currently focused
787             // item.
788             scroll(mFocusItem, distanceX * SCROLL_FACTOR,
789                     distanceY * SCROLL_FACTOR);
790             return true;
791         }
792 
793         @Override
shouldHandleTouchEvent(MotionEvent ev)794         public boolean shouldHandleTouchEvent(MotionEvent ev) {
795             // If the snap back/to full screen animation is on going, ignore any
796             // touch.
797             if (mAnimator != null) {
798                 return false;
799             }
800             return true;
801         }
802 
803         @Override
onTouchEvent(MotionEvent ev)804         public boolean onTouchEvent(MotionEvent ev) {
805             if (ev.getActionMasked() == MotionEvent.ACTION_UP ||
806                     ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
807                 final boolean shouldSnapBack = shouldSnapBack();
808                 if (shouldSnapBack) {
809                     mAnimator = snapBack();
810                 } else {
811                     mAnimator = snapToFullScreen();
812                 }
813                 mAnimator.addListener(new Animator.AnimatorListener() {
814                     @Override
815                     public void onAnimationStart(Animator animation) {
816 
817                     }
818 
819                     @Override
820                     public void onAnimationEnd(Animator animation) {
821                         mAnimator = null;
822                         mFocusItem = NO_ITEM_SELECTED;
823                         if (shouldSnapBack) {
824                             mCurrentStateManager.setCurrentState(new FullyHiddenState());
825                         } else {
826                             mCurrentStateManager.setCurrentState(new FullyShownState());
827                             UsageStatistics.instance().controlUsed(
828                                     eventprotos.ControlEvent.ControlType.MENU_FULL_FROM_SCROLL);
829                         }
830                     }
831 
832                     @Override
833                     public void onAnimationCancel(Animator animation) {
834 
835                     }
836 
837                     @Override
838                     public void onAnimationRepeat(Animator animation) {
839 
840                     }
841                 });
842             }
843             return true;
844         }
845     }
846 
847     /**
848      * Mode list gets in this state when a mode item has been selected/clicked.
849      * There will be an animation with the blurred preview fading in, a potential
850      * pause to wait for the new mode to be ready, and then the new mode will
851      * be revealed through a pinhole animation. After all the animations finish,
852      * mode list will transition into fully hidden state.
853      */
854     private class SelectedState extends ModeListState {
SelectedState(ModeSelectorItem selectedItem)855         public SelectedState(ModeSelectorItem selectedItem) {
856             final int modeId = selectedItem.getModeId();
857             // Un-highlight all the modes.
858             for (int i = 0; i < mModeSelectorItems.length; i++) {
859                 mModeSelectorItems[i].setSelected(false);
860             }
861 
862             PeepholeAnimationEffect effect = new PeepholeAnimationEffect();
863             effect.setSize(mWidth, mHeight);
864 
865             // Calculate the position of the icon in the selected item, and
866             // start animation from that position.
867             int[] location = new int[2];
868             // Gets icon's center position in relative to the window.
869             selectedItem.getIconCenterLocationInWindow(location);
870             int iconX = location[0];
871             int iconY = location[1];
872             // Gets current view's top left position relative to the window.
873             getLocationInWindow(location);
874             // Calculate icon location relative to this view
875             iconX -= location[0];
876             iconY -= location[1];
877 
878             effect.setAnimationStartingPosition(iconX, iconY);
879             effect.setModeSpecificColor(selectedItem.getHighlightColor());
880             if (mScreenShotProvider != null) {
881                 effect.setBackground(mScreenShotProvider
882                         .getPreviewFrame(PREVIEW_DOWN_SAMPLE_FACTOR),
883                         mCaptureLayoutHelper.getPreviewRect());
884                 effect.setBackgroundOverlay(mScreenShotProvider.getPreviewOverlayAndControls());
885             }
886             mCurrentAnimationEffects = effect;
887             effect.startFadeoutAnimation(null, selectedItem, iconX, iconY, modeId);
888             invalidate();
889         }
890 
891         @Override
shouldHandleTouchEvent(MotionEvent ev)892         public boolean shouldHandleTouchEvent(MotionEvent ev) {
893             return false;
894         }
895 
896         @Override
startModeSelectionAnimation()897         public void startModeSelectionAnimation() {
898             mCurrentAnimationEffects.startAnimation(new AnimatorListenerAdapter() {
899                 @Override
900                 public void onAnimationEnd(Animator animation) {
901                     mCurrentAnimationEffects = null;
902                     mCurrentStateManager.setCurrentState(new FullyHiddenState());
903                 }
904             });
905         }
906 
907         @Override
hide()908         public void hide() {
909             if (!mCurrentAnimationEffects.cancelAnimation()) {
910                 mCurrentAnimationEffects = null;
911                 mCurrentStateManager.setCurrentState(new FullyHiddenState());
912             }
913         }
914     }
915 
916     public interface ModeSwitchListener {
onModeButtonPressed(int modeIndex)917         public void onModeButtonPressed(int modeIndex);
onModeSelected(int modeIndex)918         public void onModeSelected(int modeIndex);
getCurrentModeIndex()919         public int getCurrentModeIndex();
onSettingsSelected()920         public void onSettingsSelected();
921     }
922 
923     public interface ModeListOpenListener {
924         /**
925          * Mode list will open to full screen after current animation.
926          */
onOpenFullScreen()927         public void onOpenFullScreen();
928 
929         /**
930          * Updates the listener with the current progress of mode drawer opening.
931          *
932          * @param progress progress of the mode drawer opening, ranging [0f, 1f]
933          *                 0 means mode drawer is fully closed, 1 indicates a fully
934          *                 open mode drawer.
935          */
onModeListOpenProgress(float progress)936         public void onModeListOpenProgress(float progress);
937 
938         /**
939          * Gets called when mode list is completely closed.
940          */
onModeListClosed()941         public void onModeListClosed();
942     }
943 
944     public static abstract class ModeListVisibilityChangedListener {
945         private Boolean mCurrentVisibility = null;
946 
947         /** Whether the mode list is (partially or fully) visible. */
onVisibilityChanged(boolean visible)948         public abstract void onVisibilityChanged(boolean visible);
949 
950         /**
951          * Internal method to be called by the mode list whenever a visibility
952          * even occurs.
953          * <p>
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.
957          *
958          * @param visible whether the mode drawer is currently visible.
959          */
onVisibilityEvent(boolean visible)960         private void onVisibilityEvent(boolean visible) {
961             if (mCurrentVisibility == null || mCurrentVisibility != visible) {
962                 mCurrentVisibility = visible;
963                 onVisibilityChanged(visible);
964             }
965         }
966     }
967 
968     /**
969      * This class aims to help store time and position in pairs.
970      */
971     private static class TimeBasedPosition {
972         private final float mPosition;
973         private final long mTimeStamp;
TimeBasedPosition(float position, long time)974         public TimeBasedPosition(float position, long time) {
975             mPosition = position;
976             mTimeStamp = time;
977         }
978 
getPosition()979         public float getPosition() {
980             return mPosition;
981         }
982 
getTimeStamp()983         public long getTimeStamp() {
984             return mTimeStamp;
985         }
986     }
987 
988     /**
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
992      * animation effects.
993      *
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.
998      *
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).
1003      */
1004     private final TimeInterpolator mAccordionInterpolator = new TimeInterpolator() {
1005         @Override
1006         public float getInterpolation(float input) {
1007 
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;
1011             if (input == 0) {
1012                 return 0;
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) {
1019                 // Stage 2
1020                 return 0.5f;
1021             } else {
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;
1027             }
1028         }
1029     };
1030 
1031     /**
1032      * The listener that is used to notify when gestures occur.
1033      * Here we only listen to a subset of gestures.
1034      */
1035     private final GestureDetector.OnGestureListener mOnGestureListener
1036             = new GestureDetector.SimpleOnGestureListener(){
1037         @Override
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();
1042             return true;
1043         }
1044 
1045         @Override
1046         public boolean onSingleTapUp(MotionEvent ev) {
1047             mCurrentStateManager.getCurrentState().onSingleTapUp(ev);
1048             return true;
1049         }
1050 
1051         @Override
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);
1056             return true;
1057         }
1058 
1059         @Override
1060         public boolean onDown(MotionEvent ev) {
1061             mVelocityX = 0;
1062             mCurrentStateManager.getCurrentState().onDown(ev);
1063             return true;
1064         }
1065     };
1066 
1067     /**
1068      * Gets called when a mode item in the mode drawer is clicked.
1069      *
1070      * @param selectedItem the item being clicked
1071      */
onItemSelected(ModeSelectorItem selectedItem)1072     private void onItemSelected(ModeSelectorItem selectedItem) {
1073         int modeId = selectedItem.getModeId();
1074         mModeSwitchListener.onModeButtonPressed(modeId);
1075 
1076         mCurrentStateManager.getCurrentState().onItemSelected(selectedItem);
1077     }
1078 
1079     /**
1080      * Checks whether a touch event is inside of the bounds of the mode list.
1081      *
1082      * @param ev touch event to be checked
1083      * @return whether the touch is inside the bounds of the mode list
1084      */
isTouchInsideList(MotionEvent ev)1085     private boolean isTouchInsideList(MotionEvent ev) {
1086         // Ignore the tap if it happens outside of the mode list linear layout.
1087         float x = ev.getX() - mListView.getX();
1088         float y = ev.getY() - mListView.getY();
1089         if (x < 0 || x > mListView.getWidth() || y < 0 || y > mListView.getHeight()) {
1090             return false;
1091         }
1092         return true;
1093     }
1094 
ModeListView(Context context, AttributeSet attrs)1095     public ModeListView(Context context, AttributeSet attrs) {
1096         super(context, attrs);
1097         mGestureDetector = new GestureDetector(context, mOnGestureListener);
1098         mListBackgroundColor = getResources().getColor(R.color.mode_list_background);
1099         mSettingsButtonMargin = getResources().getDimensionPixelSize(
1100                 R.dimen.mode_list_settings_icon_margin);
1101     }
1102 
disableA11yOnModeSelectorItems()1103     private void disableA11yOnModeSelectorItems() {
1104         for (View selectorItem : mModeSelectorItems) {
1105             selectorItem.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
1106         }
1107     }
1108 
enableA11yOnModeSelectorItems()1109     private void enableA11yOnModeSelectorItems() {
1110         for (View selectorItem : mModeSelectorItems) {
1111             selectorItem.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
1112         }
1113     }
1114 
1115     /**
1116      * Sets the alpha on the list background. This is called whenever the list
1117      * is scrolling or animating, so that background can adjust its dimness.
1118      *
1119      * @param alpha new alpha to be applied on list background color
1120      */
setBackgroundAlpha(int alpha)1121     private void setBackgroundAlpha(int alpha) {
1122         // Make sure alpha is valid.
1123         alpha = alpha & 0xFF;
1124         // Change alpha on the background color.
1125         mListBackgroundColor = mListBackgroundColor & 0xFFFFFF;
1126         mListBackgroundColor = mListBackgroundColor | (alpha << 24);
1127         // Set new color to list background.
1128         setBackgroundColor(mListBackgroundColor);
1129     }
1130 
1131     /**
1132      * Initialize mode list with a list of indices of supported modes.
1133      *
1134      * @param modeIndexList a list of indices of supported modes
1135      */
init(List<Integer> modeIndexList)1136     public void init(List<Integer> modeIndexList) {
1137         int[] modeSequence = getResources()
1138                 .getIntArray(R.array.camera_modes_in_nav_drawer_if_supported);
1139         int[] visibleModes = getResources()
1140                 .getIntArray(R.array.camera_modes_always_visible);
1141 
1142         // Mark the supported modes in a boolean array to preserve the
1143         // sequence of the modes
1144         SparseBooleanArray modeIsSupported = new SparseBooleanArray();
1145         for (int i = 0; i < modeIndexList.size(); i++) {
1146             int mode = modeIndexList.get(i);
1147             modeIsSupported.put(mode, true);
1148         }
1149         for (int i = 0; i < visibleModes.length; i++) {
1150             int mode = visibleModes[i];
1151             modeIsSupported.put(mode, true);
1152         }
1153 
1154         // Put the indices of supported modes into an array preserving their
1155         // display order.
1156         mSupportedModes = new ArrayList<Integer>();
1157         for (int i = 0; i < modeSequence.length; i++) {
1158             int mode = modeSequence[i];
1159             if (modeIsSupported.get(mode, false)) {
1160                 mSupportedModes.add(mode);
1161             }
1162         }
1163         mTotalModes = mSupportedModes.size();
1164         initializeModeSelectorItems();
1165         mSettingsButton = findViewById(R.id.settings_button);
1166         mSettingsButton.setOnClickListener(new OnClickListener() {
1167             @Override
1168             public void onClick(View v) {
1169                 // Post this callback to make sure current user interaction has
1170                 // been reflected in the UI. Specifically, the pressed state gets
1171                 // unset after click happens. In order to ensure the pressed state
1172                 // gets unset in UI before getting in the low frame rate settings
1173                 // activity launch stage, the settings selected callback is posted.
1174                 post(new Runnable() {
1175                     @Override
1176                     public void run() {
1177                         mModeSwitchListener.onSettingsSelected();
1178                     }
1179                 });
1180             }
1181         });
1182         // The mode list is initialized to be all the way closed.
1183         onModeListOpenRatioUpdate(0);
1184         if (mCurrentStateManager.getCurrentState() == null) {
1185             mCurrentStateManager.setCurrentState(new FullyHiddenState());
1186         }
1187     }
1188 
1189     /**
1190      * Sets the screen shot provider for getting a preview frame and a bitmap
1191      * of the controls and overlay.
1192      */
setCameraModuleScreenShotProvider( CameraAppUI.CameraModuleScreenShotProvider provider)1193     public void setCameraModuleScreenShotProvider(
1194             CameraAppUI.CameraModuleScreenShotProvider provider) {
1195         mScreenShotProvider = provider;
1196     }
1197 
initializeModeSelectorItems()1198     private void initializeModeSelectorItems() {
1199         mModeSelectorItems = new ModeSelectorItem[mTotalModes];
1200         // Inflate the mode selector items and add them to a linear layout
1201         LayoutInflater inflater = AndroidServices.instance().provideLayoutInflater();
1202         mListView = (LinearLayout) findViewById(R.id.mode_list);
1203         for (int i = 0; i < mTotalModes; i++) {
1204             final ModeSelectorItem selectorItem =
1205                     (ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null);
1206             mListView.addView(selectorItem);
1207             // Sets the top padding of the top item to 0.
1208             if (i == 0) {
1209                 selectorItem.setPadding(selectorItem.getPaddingLeft(), 0,
1210                         selectorItem.getPaddingRight(), selectorItem.getPaddingBottom());
1211             }
1212             // Sets the bottom padding of the bottom item to 0.
1213             if (i == mTotalModes - 1) {
1214                 selectorItem.setPadding(selectorItem.getPaddingLeft(), selectorItem.getPaddingTop(),
1215                         selectorItem.getPaddingRight(), 0);
1216             }
1217 
1218             int modeId = getModeIndex(i);
1219             selectorItem.setHighlightColor(getResources()
1220                     .getColor(CameraUtil.getCameraThemeColorId(modeId, getContext())));
1221 
1222             // Set image
1223             selectorItem.setImageResource(CameraUtil.getCameraModeIconResId(modeId, getContext()));
1224 
1225             // Set text
1226             selectorItem.setText(CameraUtil.getCameraModeText(modeId, getContext()));
1227 
1228             // Set content description (for a11y)
1229             selectorItem.setContentDescription(CameraUtil
1230                     .getCameraModeContentDescription(modeId, getContext()));
1231             selectorItem.setModeId(modeId);
1232             selectorItem.setOnClickListener(new OnClickListener() {
1233                 @Override
1234                 public void onClick(View v) {
1235                     onItemSelected(selectorItem);
1236                 }
1237             });
1238 
1239             mModeSelectorItems[i] = selectorItem;
1240         }
1241         // During drawer opening/closing, we change the visible width of the mode
1242         // items in sequence, so we listen to the last item's visible width change
1243         // for a good timing to do corresponding UI adjustments.
1244         mModeSelectorItems[mTotalModes - 1].setVisibleWidthChangedListener(this);
1245         resetModeSelectors();
1246     }
1247 
1248     /**
1249      * Maps between the UI mode selector index to the actual mode id.
1250      *
1251      * @param modeSelectorIndex the index of the UI item
1252      * @return the index of the corresponding camera mode
1253      */
getModeIndex(int modeSelectorIndex)1254     private int getModeIndex(int modeSelectorIndex) {
1255         if (modeSelectorIndex < mTotalModes && modeSelectorIndex >= 0) {
1256             return mSupportedModes.get(modeSelectorIndex);
1257         }
1258         Log.e(TAG, "Invalid mode selector index: " + modeSelectorIndex + ", total modes: " +
1259                 mTotalModes);
1260         return getResources().getInteger(R.integer.camera_mode_photo);
1261     }
1262 
1263     /** Notify ModeSwitchListener, if any, of the mode change. */
onModeSelected(int modeIndex)1264     private void onModeSelected(int modeIndex) {
1265         if (mModeSwitchListener != null) {
1266             mModeSwitchListener.onModeSelected(modeIndex);
1267         }
1268     }
1269 
1270     /**
1271      * Sets a listener that listens to receive mode switch event.
1272      *
1273      * @param listener a listener that gets notified when mode changes.
1274      */
setModeSwitchListener(ModeSwitchListener listener)1275     public void setModeSwitchListener(ModeSwitchListener listener) {
1276         mModeSwitchListener = listener;
1277     }
1278 
1279     /**
1280      * Sets a listener that gets notified when the mode list is open full screen.
1281      *
1282      * @param listener a listener that listens to mode list open events
1283      */
setModeListOpenListener(ModeListOpenListener listener)1284     public void setModeListOpenListener(ModeListOpenListener listener) {
1285         mModeListOpenListener = listener;
1286     }
1287 
1288     /**
1289      * Sets or replaces a listener that is called when the visibility of the
1290      * mode list changed.
1291      */
setVisibilityChangedListener(ModeListVisibilityChangedListener listener)1292     public void setVisibilityChangedListener(ModeListVisibilityChangedListener listener) {
1293         mVisibilityChangedListener = listener;
1294     }
1295 
1296     @Override
onTouchEvent(MotionEvent ev)1297     public boolean onTouchEvent(MotionEvent ev) {
1298         // Reset touch forward recipient
1299         if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
1300             mChildViewTouched = null;
1301         }
1302 
1303         if (!mCurrentStateManager.getCurrentState().shouldHandleTouchEvent(ev)) {
1304             return false;
1305         }
1306         getParent().requestDisallowInterceptTouchEvent(true);
1307         super.onTouchEvent(ev);
1308 
1309         // Pass all touch events to gesture detector for gesture handling.
1310         mGestureDetector.onTouchEvent(ev);
1311         mCurrentStateManager.getCurrentState().onTouchEvent(ev);
1312         return true;
1313     }
1314 
1315     /**
1316      * Forward touch events to a recipient child view. Before feeding the motion
1317      * event into the child view, the event needs to be converted in child view's
1318      * coordinates.
1319      */
forwardTouchEventToChild(MotionEvent ev)1320     private void forwardTouchEventToChild(MotionEvent ev) {
1321         if (mChildViewTouched != null) {
1322             float x = ev.getX() - mListView.getX();
1323             float y = ev.getY() - mListView.getY();
1324             x -= mChildViewTouched.getLeft();
1325             y -= mChildViewTouched.getTop();
1326 
1327             mLastChildTouchEvent = MotionEvent.obtain(ev);
1328             mLastChildTouchEvent.setLocation(x, y);
1329             mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
1330         }
1331     }
1332 
1333     /**
1334      * Sets the swipe mode to indicate whether this is a swiping in
1335      * or out, and therefore we can have different animations.
1336      *
1337      * @param swipeIn indicates whether the swipe should reveal/hide the list.
1338      */
setSwipeMode(boolean swipeIn)1339     private void setSwipeMode(boolean swipeIn) {
1340         for (int i = 0 ; i < mModeSelectorItems.length; i++) {
1341             mModeSelectorItems[i].onSwipeModeChanged(swipeIn);
1342         }
1343     }
1344 
1345     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)1346     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1347         super.onLayout(changed, left, top, right, bottom);
1348         mWidth = right - left;
1349         mHeight = bottom - top - getPaddingTop() - getPaddingBottom();
1350 
1351         updateModeListLayout();
1352 
1353         if (mCurrentStateManager.getCurrentState().getCurrentAnimationEffects() != null) {
1354             mCurrentStateManager.getCurrentState().getCurrentAnimationEffects().setSize(
1355                     mWidth, mHeight);
1356         }
1357     }
1358 
1359     /**
1360      * Sets a capture layout helper to query layout rect from.
1361      */
setCaptureLayoutHelper(CaptureLayoutHelper helper)1362     public void setCaptureLayoutHelper(CaptureLayoutHelper helper) {
1363         mCaptureLayoutHelper = helper;
1364     }
1365 
1366     @Override
onPreviewAreaChanged(RectF previewArea)1367     public void onPreviewAreaChanged(RectF previewArea) {
1368         if (getVisibility() == View.VISIBLE && !hasWindowFocus()) {
1369             // When the preview area has changed, to avoid visual disruption we
1370             // only make corresponding UI changes when mode list does not have
1371             // window focus.
1372             updateModeListLayout();
1373         }
1374     }
1375 
updateModeListLayout()1376     private void updateModeListLayout() {
1377         if (mCaptureLayoutHelper == null) {
1378             Log.e(TAG, "Capture layout helper needs to be set first.");
1379             return;
1380         }
1381         // Center mode drawer in the portion of camera preview that is not covered by
1382         // bottom bar.
1383         RectF uncoveredPreviewArea = mCaptureLayoutHelper.getUncoveredPreviewRect();
1384         // Align left:
1385         mListView.setTranslationX(uncoveredPreviewArea.left);
1386         // Align center vertical:
1387         mListView.setTranslationY(uncoveredPreviewArea.centerY()
1388                 - mListView.getMeasuredHeight() / 2);
1389 
1390         updateSettingsButtonLayout(uncoveredPreviewArea);
1391     }
1392 
updateSettingsButtonLayout(RectF uncoveredPreviewArea)1393     private void updateSettingsButtonLayout(RectF uncoveredPreviewArea) {
1394         if (mWidth > mHeight) {
1395             // Align to the top right.
1396             mSettingsButton.setTranslationX(uncoveredPreviewArea.right - mSettingsButtonMargin
1397                     - mSettingsButton.getMeasuredWidth());
1398             mSettingsButton.setTranslationY(uncoveredPreviewArea.top + mSettingsButtonMargin);
1399         } else {
1400             // Align to the bottom right.
1401             mSettingsButton.setTranslationX(uncoveredPreviewArea.right - mSettingsButtonMargin
1402                     - mSettingsButton.getMeasuredWidth());
1403             mSettingsButton.setTranslationY(uncoveredPreviewArea.bottom - mSettingsButtonMargin
1404                     - mSettingsButton.getMeasuredHeight());
1405         }
1406         if (mSettingsCling != null) {
1407             mSettingsCling.updatePosition(mSettingsButton);
1408         }
1409     }
1410 
1411     @Override
draw(Canvas canvas)1412     public void draw(Canvas canvas) {
1413         ModeListState currentState = mCurrentStateManager.getCurrentState();
1414         AnimationEffects currentEffects = currentState.getCurrentAnimationEffects();
1415         if (currentEffects != null) {
1416             currentEffects.drawBackground(canvas);
1417             if (currentEffects.shouldDrawSuper()) {
1418                 super.draw(canvas);
1419             }
1420             currentEffects.drawForeground(canvas);
1421         } else {
1422             super.draw(canvas);
1423         }
1424     }
1425 
1426     /**
1427      * Sets whether a cling for settings button should be shown. If not, remove
1428      * the cling from view hierarchy if any. If a cling should be shown, inflate
1429      * the cling into this view group.
1430      *
1431      * @param show whether the cling needs to be shown.
1432      */
setShouldShowSettingsCling(boolean show)1433     public void setShouldShowSettingsCling(boolean show) {
1434         if (show) {
1435             if (mSettingsCling == null) {
1436                 inflate(getContext(), R.layout.settings_cling, this);
1437                 mSettingsCling = (SettingsCling) findViewById(R.id.settings_cling);
1438             }
1439         } else {
1440             if (mSettingsCling != null) {
1441                 // Remove settings cling from view hierarchy.
1442                 removeView(mSettingsCling);
1443                 mSettingsCling = null;
1444             }
1445         }
1446     }
1447 
1448     /**
1449      * Show or hide cling for settings button. The cling will only be shown if
1450      * settings button has never been clicked. Otherwise, cling will be null,
1451      * and will not show even if this method is called to show it.
1452      */
showSettingsClingIfEnabled(boolean show)1453     private void showSettingsClingIfEnabled(boolean show) {
1454         if (mSettingsCling != null) {
1455             int visibility = show ? VISIBLE : INVISIBLE;
1456             mSettingsCling.setVisibility(visibility);
1457         }
1458     }
1459 
1460     /**
1461      * This shows the mode switcher and starts the accordion animation with a delay.
1462      * If the view does not currently have focus, (e.g. There are popups on top of
1463      * it.) start the delayed accordion animation when it gains focus. Otherwise,
1464      * start the animation with a delay right away.
1465      */
showModeSwitcherHint()1466     public void showModeSwitcherHint() {
1467         mCurrentStateManager.getCurrentState().showSwitcherHint();
1468     }
1469 
1470     /**
1471      * Hide the mode list immediately (provided the current state allows it).
1472      */
hide()1473     public void hide() {
1474         mCurrentStateManager.getCurrentState().hide();
1475     }
1476 
1477     /**
1478      * Hide the mode list with an animation.
1479      */
hideAnimated()1480     public void hideAnimated() {
1481         mCurrentStateManager.getCurrentState().hideAnimated();
1482     }
1483 
1484     /**
1485      * Resets the visible width of all the mode selectors to 0.
1486      */
resetModeSelectors()1487     private void resetModeSelectors() {
1488         for (int i = 0; i < mModeSelectorItems.length; i++) {
1489             mModeSelectorItems[i].setVisibleWidth(0);
1490         }
1491     }
1492 
isRunningAccordionAnimation()1493     private boolean isRunningAccordionAnimation() {
1494         return mAnimatorSet != null && mAnimatorSet.isRunning();
1495     }
1496 
1497     /**
1498      * Calculate the mode selector item in the list that is at position (x, y).
1499      * If the position is above the top item or below the bottom item, return
1500      * the top item or bottom item respectively.
1501      *
1502      * @param x horizontal position
1503      * @param y vertical position
1504      * @return index of the item that is at position (x, y)
1505      */
getFocusItem(float x, float y)1506     private int getFocusItem(float x, float y) {
1507         // Convert coordinates into child view's coordinates.
1508         x -= mListView.getX();
1509         y -= mListView.getY();
1510 
1511         for (int i = 0; i < mModeSelectorItems.length; i++) {
1512             if (y <= mModeSelectorItems[i].getBottom()) {
1513                 return i;
1514             }
1515         }
1516         return mModeSelectorItems.length - 1;
1517     }
1518 
1519     @Override
onWindowFocusChanged(boolean hasFocus)1520     public void onWindowFocusChanged(boolean hasFocus) {
1521         super.onWindowFocusChanged(hasFocus);
1522         mCurrentStateManager.getCurrentState().onWindowFocusChanged(hasFocus);
1523     }
1524 
1525     @Override
onVisibilityChanged(View v, int visibility)1526     public void onVisibilityChanged(View v, int visibility) {
1527         super.onVisibilityChanged(v, visibility);
1528         if (visibility == VISIBLE) {
1529             // Highlight current module
1530             if (mModeSwitchListener != null) {
1531                 int modeId = mModeSwitchListener.getCurrentModeIndex();
1532                 int parentMode = CameraUtil.getCameraModeParentModeId(modeId, getContext());
1533                 // Find parent mode in the nav drawer.
1534                 for (int i = 0; i < mSupportedModes.size(); i++) {
1535                     if (mSupportedModes.get(i) == parentMode) {
1536                         mModeSelectorItems[i].setSelected(true);
1537                     }
1538                 }
1539             }
1540             updateModeListLayout();
1541         } else {
1542             if (mModeSelectorItems != null) {
1543                 // When becoming invisible/gone after initializing mode selector items.
1544                 for (int i = 0; i < mModeSelectorItems.length; i++) {
1545                     mModeSelectorItems[i].setSelected(false);
1546                 }
1547             }
1548             if (mModeListOpenListener != null) {
1549                 mModeListOpenListener.onModeListClosed();
1550             }
1551         }
1552 
1553         if (mVisibilityChangedListener != null) {
1554             mVisibilityChangedListener.onVisibilityEvent(getVisibility() == VISIBLE);
1555         }
1556     }
1557 
1558     @Override
setVisibility(int visibility)1559     public void setVisibility(int visibility) {
1560         ModeListState currentState = mCurrentStateManager.getCurrentState();
1561         if (currentState != null && !currentState.shouldHandleVisibilityChange(visibility)) {
1562             return;
1563         }
1564         super.setVisibility(visibility);
1565     }
1566 
scroll(int itemId, float deltaX, float deltaY)1567     private void scroll(int itemId, float deltaX, float deltaY) {
1568         // Scrolling trend on X and Y axis, to track the trend by biasing
1569         // towards latest touch events.
1570         mScrollTrendX = mScrollTrendX * 0.3f + deltaX * 0.7f;
1571         mScrollTrendY = mScrollTrendY * 0.3f + deltaY * 0.7f;
1572 
1573         // TODO: Change how the curve is calculated below when UX finalize their design.
1574         mCurrentTime = SystemClock.uptimeMillis();
1575         float longestWidth;
1576         if (itemId != NO_ITEM_SELECTED) {
1577             longestWidth = mModeSelectorItems[itemId].getVisibleWidth();
1578         } else {
1579             longestWidth = mModeSelectorItems[0].getVisibleWidth();
1580         }
1581         float newPosition = longestWidth - deltaX;
1582         int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
1583         newPosition = Math.min(newPosition, getMaxMovementBasedOnPosition((int) longestWidth,
1584                 maxVisibleWidth));
1585         newPosition = Math.max(newPosition, 0);
1586         insertNewPosition(newPosition, mCurrentTime);
1587 
1588         for (int i = 0; i < mModeSelectorItems.length; i++) {
1589             mModeSelectorItems[i].setVisibleWidth((int) newPosition);
1590         }
1591     }
1592 
1593     /**
1594      * Insert new position and time stamp into the history position list, and
1595      * remove stale position items.
1596      *
1597      * @param position latest position of the focus item
1598      * @param time  current time in milliseconds
1599      */
insertNewPosition(float position, long time)1600     private void insertNewPosition(float position, long time) {
1601         // TODO: Consider re-using stale position objects rather than
1602         // always creating new position objects.
1603         mPositionHistory.add(new TimeBasedPosition(position, time));
1604 
1605         // Positions that are from too long ago will not be of any use for
1606         // future position interpolation. So we need to remove those positions
1607         // from the list.
1608         long timeCutoff = time - (mTotalModes - 1) * DELAY_MS;
1609         while (mPositionHistory.size() > 0) {
1610             // Remove all the position items that are prior to the cutoff time.
1611             TimeBasedPosition historyPosition = mPositionHistory.getFirst();
1612             if (historyPosition.getTimeStamp() < timeCutoff) {
1613                 mPositionHistory.removeFirst();
1614             } else {
1615                 break;
1616             }
1617         }
1618     }
1619 
1620     /**
1621      * Gets the interpolated position at the specified time. This involves going
1622      * through the recorded positions until a {@link TimeBasedPosition} is found
1623      * such that the position the recorded before the given time, and the
1624      * {@link TimeBasedPosition} after that is recorded no earlier than the given
1625      * time. These two positions are then interpolated to get the position at the
1626      * specified time.
1627      */
getPosition(long time, float currentPosition)1628     private float getPosition(long time, float currentPosition) {
1629         int i;
1630         for (i = 0; i < mPositionHistory.size(); i++) {
1631             TimeBasedPosition historyPosition = mPositionHistory.get(i);
1632             if (historyPosition.getTimeStamp() > time) {
1633                 // Found the winner. Now interpolate between position i and position i - 1
1634                 if (i == 0) {
1635                     // Slowly approaching to the destination if there isn't enough data points
1636                     float weight = 0.2f;
1637                     return historyPosition.getPosition() * weight + (1f - weight) * currentPosition;
1638                 } else {
1639                     TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1);
1640                     // Start interpolation
1641                     float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) /
1642                             (float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp());
1643                     float position = fraction * (historyPosition.getPosition()
1644                             - prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition();
1645                     return position;
1646                 }
1647             }
1648         }
1649         // It should never get here.
1650         Log.e(TAG, "Invalid time input for getPosition(). time: " + time);
1651         if (mPositionHistory.size() == 0) {
1652             Log.e(TAG, "TimeBasedPosition history size is 0");
1653         } else {
1654             Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp()
1655             + " , last position recorded at " + mPositionHistory.getLast().getTimeStamp());
1656         }
1657         assert (i < mPositionHistory.size());
1658         return i;
1659     }
1660 
1661     private void reset() {
1662         resetModeSelectors();
1663         mScrollTrendX = 0f;
1664         mScrollTrendY = 0f;
1665         setVisibility(INVISIBLE);
1666     }
1667 
1668     /**
1669      * When visible width of list is changed, the background of the list needs
1670      * to darken/lighten correspondingly.
1671      */
1672     @Override
1673     public void onVisibleWidthChanged(int visibleWidth) {
1674         mVisibleWidth = visibleWidth;
1675 
1676         // When the longest mode item is entirely shown (across the screen), the
1677         // background should be 50% transparent.
1678         int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
1679         visibleWidth = Math.min(maxVisibleWidth, visibleWidth);
1680         if (visibleWidth != maxVisibleWidth) {
1681             // No longer full screen.
1682             cancelForwardingTouchEvent();
1683         }
1684         float openRatio = (float) visibleWidth / maxVisibleWidth;
1685         onModeListOpenRatioUpdate(openRatio * mModeListOpenFactor);
1686     }
1687 
1688     /**
1689      * Gets called when UI elements such as background and gear icon need to adjust
1690      * their appearance based on the percentage of the mode list opening.
1691      *
1692      * @param openRatio percentage of the mode list opening, ranging [0f, 1f]
1693      */
1694     private void onModeListOpenRatioUpdate(float openRatio) {
1695         for (int i = 0; i < mModeSelectorItems.length; i++) {
1696             mModeSelectorItems[i].setTextAlpha(openRatio);
1697         }
1698         setBackgroundAlpha((int) (BACKGROUND_TRANSPARENTCY * openRatio));
1699         if (mModeListOpenListener != null) {
1700             mModeListOpenListener.onModeListOpenProgress(openRatio);
1701         }
1702         if (mSettingsButton != null) {
1703             // Disable the hardware layer when the ratio reaches 0.0 or 1.0.
1704             if (openRatio >= 1.0f || openRatio <= 0.0f) {
1705                 if (mSettingsButton.getLayerType() == View.LAYER_TYPE_HARDWARE) {
1706                     Log.v(TAG, "Disabling hardware layer for the Settings Button. (via alpha)");
1707                     mSettingsButton.setLayerType(View.LAYER_TYPE_NONE, null);
1708                 }
1709             } else {
1710                 if (mSettingsButton.getLayerType() != View.LAYER_TYPE_HARDWARE) {
1711                     Log.v(TAG, "Enabling hardware layer for the Settings Button.");
1712                     mSettingsButton.setLayerType(View.LAYER_TYPE_HARDWARE, null);
1713                 }
1714             }
1715 
1716             mSettingsButton.setAlpha(openRatio);
1717         }
1718     }
1719 
1720     /**
1721      * Cancels the touch event forwarding by sending a cancel event to the recipient
1722      * view and resetting the touch forward recipient to ensure no more events
1723      * can be forwarded in the current series of the touch events.
1724      */
1725     private void cancelForwardingTouchEvent() {
1726         if (mChildViewTouched != null) {
1727             mLastChildTouchEvent.setAction(MotionEvent.ACTION_CANCEL);
1728             mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
1729             mChildViewTouched = null;
1730         }
1731     }
1732 
1733     @Override
1734     public void onWindowVisibilityChanged(int visibility) {
1735         super.onWindowVisibilityChanged(visibility);
1736         if (visibility != VISIBLE) {
1737             mCurrentStateManager.getCurrentState().hide();
1738         }
1739     }
1740 
1741     /**
1742      * Defines how the list view should respond to a menu button pressed
1743      * event.
1744      */
1745     public boolean onMenuPressed() {
1746         return mCurrentStateManager.getCurrentState().onMenuPressed();
1747     }
1748 
1749     /**
1750      * The list view should either snap back or snap to full screen after a gesture.
1751      * This function is called when an up or cancel event is received, and then based
1752      * on the current position of the list and the gesture we can decide which way
1753      * to snap.
1754      */
1755     private void snap() {
1756         if (shouldSnapBack()) {
1757             snapBack();
1758         } else {
1759             snapToFullScreen();
1760         }
1761     }
1762 
1763     private boolean shouldSnapBack() {
1764         int itemId = Math.max(0, mFocusItem);
1765         if (Math.abs(mVelocityX) > VELOCITY_THRESHOLD) {
1766             // Fling to open / close
1767             return mVelocityX < 0;
1768         } else if (mModeSelectorItems[itemId].getVisibleWidth()
1769                 < mModeSelectorItems[itemId].getMaxVisibleWidth() * SNAP_BACK_THRESHOLD_RATIO) {
1770             return true;
1771         } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) {
1772             return true;
1773         } else {
1774             return false;
1775         }
1776     }
1777 
1778     /**
1779      * Snaps back out of the screen.
1780      *
1781      * @param withAnimation whether snapping back should be animated
1782      */
1783     public Animator snapBack(boolean withAnimation) {
1784         if (withAnimation) {
1785             if (mVelocityX > -VELOCITY_THRESHOLD * SCROLL_FACTOR) {
1786                 return animateListToWidth(0);
1787             } else {
1788                 return animateListToWidthAtVelocity(mVelocityX, 0);
1789             }
1790         } else {
1791             setVisibility(INVISIBLE);
resetModeSelectors()1792             resetModeSelectors();
1793             return null;
1794         }
1795     }
1796 
1797     /**
1798      * Snaps the mode list back out with animation.
1799      */
snapBack()1800     private Animator snapBack() {
1801         return snapBack(true);
1802     }
1803 
snapToFullScreen()1804     private Animator snapToFullScreen() {
1805         Animator animator;
1806         int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
1807         int fullWidth = mModeSelectorItems[focusItem].getMaxVisibleWidth();
1808         if (mVelocityX <= VELOCITY_THRESHOLD) {
1809             animator = animateListToWidth(fullWidth);
1810         } else {
1811             // If the fling velocity exceeds this threshold, snap to full screen
1812             // at a constant speed.
1813             animator = animateListToWidthAtVelocity(VELOCITY_THRESHOLD, fullWidth);
1814         }
1815         if (mModeListOpenListener != null) {
1816             mModeListOpenListener.onOpenFullScreen();
1817         }
1818         return animator;
1819     }
1820 
1821     /**
1822      * Overloaded function to provide a simple way to start animation. Animation
1823      * will use default duration, and a value of <code>null</code> for interpolator
1824      * means linear interpolation will be used.
1825      *
1826      * @param width a set of values that the animation will animate between over time
1827      */
animateListToWidth(int... width)1828     private Animator animateListToWidth(int... width) {
1829         return animateListToWidth(0, DEFAULT_DURATION_MS, null, width);
1830     }
1831 
1832     /**
1833      * Animate the mode list between the given set of visible width.
1834      *
1835      * @param delay start delay between consecutive mode item. If delay < 0, the
1836      *              leader in the animation will be the bottom item.
1837      * @param duration duration for the animation of each mode item
1838      * @param interpolator interpolator to be used by the animation
1839      * @param width a set of values that the animation will animate between over time
1840      */
animateListToWidth(int delay, int duration, TimeInterpolator interpolator, int... width)1841     private Animator animateListToWidth(int delay, int duration,
1842                                     TimeInterpolator interpolator, int... width) {
1843         if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
1844             mAnimatorSet.end();
1845         }
1846 
1847         ArrayList<Animator> animators = new ArrayList<Animator>();
1848         boolean animateModeItemsInOrder = true;
1849         if (delay < 0) {
1850             animateModeItemsInOrder = false;
1851             delay *= -1;
1852         }
1853         for (int i = 0; i < mTotalModes; i++) {
1854             ObjectAnimator animator;
1855             if (animateModeItemsInOrder) {
1856                 animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
1857                     "visibleWidth", width);
1858             } else {
1859                 animator = ObjectAnimator.ofInt(mModeSelectorItems[mTotalModes - 1 -i],
1860                         "visibleWidth", width);
1861             }
1862             animator.setDuration(duration);
1863             animators.add(animator);
1864         }
1865 
1866         mAnimatorSet = new AnimatorSet();
1867         mAnimatorSet.playTogether(animators);
1868         mAnimatorSet.setInterpolator(interpolator);
1869         mAnimatorSet.start();
1870 
1871         return mAnimatorSet;
1872     }
1873 
1874     /**
1875      * Animate the mode list to the given width at a constant velocity.
1876      *
1877      * @param velocity the velocity that animation will be at
1878      * @param width final width of the list
1879      */
animateListToWidthAtVelocity(float velocity, int width)1880     private Animator animateListToWidthAtVelocity(float velocity, int width) {
1881         if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
1882             mAnimatorSet.end();
1883         }
1884 
1885         ArrayList<Animator> animators = new ArrayList<Animator>();
1886         for (int i = 0; i < mTotalModes; i++) {
1887             ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
1888                     "visibleWidth", width);
1889             int duration = (int) (width / velocity);
1890             animator.setDuration(duration);
1891             animators.add(animator);
1892         }
1893 
1894         mAnimatorSet = new AnimatorSet();
1895         mAnimatorSet.playTogether(animators);
1896         mAnimatorSet.setInterpolator(null);
1897         mAnimatorSet.start();
1898 
1899         return mAnimatorSet;
1900     }
1901 
1902     /**
1903      * Called when the back key is pressed.
1904      *
1905      * @return Whether the UI responded to the key event.
1906      */
onBackPressed()1907     public boolean onBackPressed() {
1908         return mCurrentStateManager.getCurrentState().onBackPressed();
1909     }
1910 
startModeSelectionAnimation()1911     public void startModeSelectionAnimation() {
1912         mCurrentStateManager.getCurrentState().startModeSelectionAnimation();
1913     }
1914 
getMaxMovementBasedOnPosition(int lastVisibleWidth, int maxWidth)1915     public float getMaxMovementBasedOnPosition(int lastVisibleWidth, int maxWidth) {
1916         int timeElapsed = (int) (System.currentTimeMillis() - mLastScrollTime);
1917         if (timeElapsed > SCROLL_INTERVAL_MS) {
1918             timeElapsed = SCROLL_INTERVAL_MS;
1919         }
1920         float position;
1921         int slowZone = (int) (maxWidth * SLOW_ZONE_PERCENTAGE);
1922         if (lastVisibleWidth < (maxWidth - slowZone)) {
1923             position = VELOCITY_THRESHOLD * timeElapsed + lastVisibleWidth;
1924         } else {
1925             float percentageIntoSlowZone = (lastVisibleWidth - (maxWidth - slowZone)) / slowZone;
1926             float velocity = (1 - percentageIntoSlowZone) * VELOCITY_THRESHOLD;
1927             position = velocity * timeElapsed + lastVisibleWidth;
1928         }
1929         position = Math.min(maxWidth, position);
1930         return position;
1931     }
1932 
1933     private class PeepholeAnimationEffect extends AnimationEffects {
1934 
1935         private final static int UNSET = -1;
1936         private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 500;
1937 
1938         private final Paint mMaskPaint = new Paint();
1939         private final RectF mBackgroundDrawArea = new RectF();
1940 
1941         private int mPeepHoleCenterX = UNSET;
1942         private int mPeepHoleCenterY = UNSET;
1943         private float mRadius = 0f;
1944         private ValueAnimator mPeepHoleAnimator;
1945         private ValueAnimator mFadeOutAlphaAnimator;
1946         private ValueAnimator mRevealAlphaAnimator;
1947         private Bitmap mBackground;
1948         private Bitmap mBackgroundOverlay;
1949 
1950         private Paint mCirclePaint = new Paint();
1951         private Paint mCoverPaint = new Paint();
1952 
1953         private TouchCircleDrawable mCircleDrawable;
1954 
PeepholeAnimationEffect()1955         public PeepholeAnimationEffect() {
1956             mMaskPaint.setAlpha(0);
1957             mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
1958 
1959             mCirclePaint.setColor(0);
1960             mCirclePaint.setAlpha(0);
1961 
1962             mCoverPaint.setColor(0);
1963             mCoverPaint.setAlpha(0);
1964 
1965             setupAnimators();
1966         }
1967 
setupAnimators()1968         private void setupAnimators() {
1969             mFadeOutAlphaAnimator = ValueAnimator.ofInt(0, 255);
1970             mFadeOutAlphaAnimator.setDuration(100);
1971             mFadeOutAlphaAnimator.setInterpolator(Gusterpolator.INSTANCE);
1972             mFadeOutAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1973                 @Override
1974                 public void onAnimationUpdate(ValueAnimator animation) {
1975                     mCoverPaint.setAlpha((Integer) animation.getAnimatedValue());
1976                     invalidate();
1977                 }
1978             });
1979             mFadeOutAlphaAnimator.addListener(new AnimatorListenerAdapter() {
1980                 @Override
1981                 public void onAnimationStart(Animator animation) {
1982                     // Sets a HW layer on the view for the animation.
1983                     setLayerType(LAYER_TYPE_HARDWARE, null);
1984                 }
1985 
1986                 @Override
1987                 public void onAnimationEnd(Animator animation) {
1988                     // Sets the layer type back to NONE as a workaround for b/12594617.
1989                     setLayerType(LAYER_TYPE_NONE, null);
1990                 }
1991             });
1992 
1993             /////////////////
1994 
1995             mRevealAlphaAnimator = ValueAnimator.ofInt(255, 0);
1996             mRevealAlphaAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS);
1997             mRevealAlphaAnimator.setInterpolator(Gusterpolator.INSTANCE);
1998             mRevealAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1999                 @Override
2000                 public void onAnimationUpdate(ValueAnimator animation) {
2001                     int alpha = (Integer) animation.getAnimatedValue();
2002                     mCirclePaint.setAlpha(alpha);
2003                     mCoverPaint.setAlpha(alpha);
2004                 }
2005             });
2006             mRevealAlphaAnimator.addListener(new AnimatorListenerAdapter() {
2007                 @Override
2008                 public void onAnimationStart(Animator animation) {
2009                     // Sets a HW layer on the view for the animation.
2010                     setLayerType(LAYER_TYPE_HARDWARE, null);
2011                 }
2012 
2013                 @Override
2014                 public void onAnimationEnd(Animator animation) {
2015                     // Sets the layer type back to NONE as a workaround for b/12594617.
2016                     setLayerType(LAYER_TYPE_NONE, null);
2017                 }
2018             });
2019 
2020             ////////////////
2021 
2022             int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX);
2023             int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY);
2024             int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge
2025                     + verticalDistanceToFarEdge * verticalDistanceToFarEdge));
2026             int startRadius = getResources().getDimensionPixelSize(
2027                     R.dimen.mode_selector_icon_block_width) / 2;
2028 
2029             mPeepHoleAnimator = ValueAnimator.ofFloat(startRadius, endRadius);
2030             mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS);
2031             mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE);
2032             mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2033                 @Override
2034                 public void onAnimationUpdate(ValueAnimator animation) {
2035                     // Modify mask by enlarging the hole
2036                     mRadius = (Float) mPeepHoleAnimator.getAnimatedValue();
2037                     invalidate();
2038                 }
2039             });
2040             mPeepHoleAnimator.addListener(new AnimatorListenerAdapter() {
2041                 @Override
2042                 public void onAnimationStart(Animator animation) {
2043                     // Sets a HW layer on the view for the animation.
2044                     setLayerType(LAYER_TYPE_HARDWARE, null);
2045                 }
2046 
2047                 @Override
2048                 public void onAnimationEnd(Animator animation) {
2049                     // Sets the layer type back to NONE as a workaround for b/12594617.
2050                     setLayerType(LAYER_TYPE_NONE, null);
2051                 }
2052             });
2053 
2054             ////////////////
2055             int size = getContext().getResources()
2056                     .getDimensionPixelSize(R.dimen.mode_selector_icon_block_width);
2057             mCircleDrawable = new TouchCircleDrawable(getContext().getResources());
2058             mCircleDrawable.setSize(size, size);
2059             mCircleDrawable.setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2060                 @Override
2061                 public void onAnimationUpdate(ValueAnimator animation) {
2062                     invalidate();
2063                 }
2064             });
2065         }
2066 
2067         @Override
setSize(int width, int height)2068         public void setSize(int width, int height) {
2069             mWidth = width;
2070             mHeight = height;
2071         }
2072 
2073         @Override
onTouchEvent(MotionEvent event)2074         public boolean onTouchEvent(MotionEvent event) {
2075             return true;
2076         }
2077 
2078         @Override
drawForeground(Canvas canvas)2079         public void drawForeground(Canvas canvas) {
2080             // Draw the circle in clear mode
2081             if (mPeepHoleAnimator != null) {
2082                 // Draw a transparent circle using clear mode
2083                 canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint);
2084                 canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mCirclePaint);
2085             }
2086         }
2087 
setAnimationStartingPosition(int x, int y)2088         public void setAnimationStartingPosition(int x, int y) {
2089             mPeepHoleCenterX = x;
2090             mPeepHoleCenterY = y;
2091         }
2092 
setModeSpecificColor(int color)2093         public void setModeSpecificColor(int color) {
2094             mCirclePaint.setColor(color & 0x00ffffff);
2095         }
2096 
2097         /**
2098          * Sets the bitmap to be drawn in the background and the drawArea to draw
2099          * the bitmap.
2100          *
2101          * @param background image to be drawn in the background
2102          * @param drawArea area to draw the background image
2103          */
setBackground(Bitmap background, RectF drawArea)2104         public void setBackground(Bitmap background, RectF drawArea) {
2105             mBackground = background;
2106             mBackgroundDrawArea.set(drawArea);
2107         }
2108 
2109         /**
2110          * Sets the overlay image to be drawn on top of the background.
2111          */
setBackgroundOverlay(Bitmap overlay)2112         public void setBackgroundOverlay(Bitmap overlay) {
2113             mBackgroundOverlay = overlay;
2114         }
2115 
2116         @Override
drawBackground(Canvas canvas)2117         public void drawBackground(Canvas canvas) {
2118             if (mBackground != null && mBackgroundOverlay != null) {
2119                 canvas.drawBitmap(mBackground, null, mBackgroundDrawArea, null);
2120                 canvas.drawPaint(mCoverPaint);
2121                 canvas.drawBitmap(mBackgroundOverlay, 0, 0, null);
2122 
2123                 if (mCircleDrawable != null) {
2124                     mCircleDrawable.draw(canvas);
2125                 }
2126             }
2127         }
2128 
2129         @Override
shouldDrawSuper()2130         public boolean shouldDrawSuper() {
2131             // No need to draw super when mBackgroundOverlay is being drawn, as
2132             // background overlay already contains what's drawn in super.
2133             return (mBackground == null || mBackgroundOverlay == null);
2134         }
2135 
startFadeoutAnimation(Animator.AnimatorListener listener, final ModeSelectorItem selectedItem, int x, int y, final int modeId)2136         public void startFadeoutAnimation(Animator.AnimatorListener listener,
2137                 final ModeSelectorItem selectedItem,
2138                 int x, int y, final int modeId) {
2139             mCoverPaint.setColor(0);
2140             mCoverPaint.setAlpha(0);
2141 
2142             mCircleDrawable.setIconDrawable(
2143                     selectedItem.getIcon().getIconDrawableClone(),
2144                     selectedItem.getIcon().getIconDrawableSize());
2145             mCircleDrawable.setCenter(new Point(x, y));
2146             mCircleDrawable.setColor(selectedItem.getHighlightColor());
2147             mCircleDrawable.setAnimatorListener(new AnimatorListenerAdapter() {
2148                 @Override
2149                 public void onAnimationEnd(Animator animation) {
2150                     // Post mode selection runnable to the end of the message queue
2151                     // so that current UI changes can finish before mode initialization
2152                     // clogs up UI thread.
2153                     post(new Runnable() {
2154                         @Override
2155                         public void run() {
2156                             // Select the focused item.
2157                             selectedItem.setSelected(true);
2158                             onModeSelected(modeId);
2159                         }
2160                     });
2161                 }
2162             });
2163 
2164             // add fade out animator to a set, so we can freely add
2165             // the listener without having to worry about listener dupes
2166             AnimatorSet s = new AnimatorSet();
2167             s.play(mFadeOutAlphaAnimator);
2168             if (listener != null) {
2169                 s.addListener(listener);
2170             }
2171             mCircleDrawable.animate();
2172             s.start();
2173         }
2174 
2175         @Override
startAnimation(Animator.AnimatorListener listener)2176         public void startAnimation(Animator.AnimatorListener listener) {
2177             if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) {
2178                 return;
2179             }
2180             if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) {
2181                 mPeepHoleCenterX = mWidth / 2;
2182                 mPeepHoleCenterY = mHeight / 2;
2183             }
2184 
2185             mCirclePaint.setAlpha(255);
2186             mCoverPaint.setAlpha(255);
2187 
2188             // add peephole and reveal animators to a set, so we can
2189             // freely add the listener without having to worry about
2190             // listener dupes
2191             AnimatorSet s = new AnimatorSet();
2192             s.play(mPeepHoleAnimator).with(mRevealAlphaAnimator);
2193             if (listener != null) {
2194                 s.addListener(listener);
2195             }
2196             s.start();
2197         }
2198 
2199         @Override
endAnimation()2200         public void endAnimation() {
2201         }
2202 
2203         @Override
cancelAnimation()2204         public boolean cancelAnimation() {
2205             if (mPeepHoleAnimator == null || !mPeepHoleAnimator.isRunning()) {
2206                 return false;
2207             } else {
2208                 mPeepHoleAnimator.cancel();
2209                 return true;
2210             }
2211         }
2212     }
2213 }
2214