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