Home | History | Annotate | Download | only in ui
      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 
    144         ModeListState getCurrentState() {
    145             return mCurrentState;
    146         }
    147 
    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          */
    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          */
    179         public void showSwitcherHint() {
    180             // Do nothing.
    181         }
    182 
    183         /**
    184          * Gets the currently running animation effects for the current state.
    185          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    268         public void startModeSelectionAnimation() {
    269             // Do nothing.
    270         }
    271 
    272         /**
    273          * Hide the mode drawer and switch to fully hidden state.
    274          */
    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          */
    284         public void hideAnimated() {
    285             hide();
    286         }
    287 
    288         /***************GestureListener implementation*****************/
    289         @Override
    290         public boolean onDown(MotionEvent e) {
    291             return false;
    292         }
    293 
    294         @Override
    295         public void onShowPress(MotionEvent e) {
    296             // Do nothing.
    297         }
    298 
    299         @Override
    300         public boolean onSingleTapUp(MotionEvent e) {
    301             return false;
    302         }
    303 
    304         @Override
    305         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    306             return false;
    307         }
    308 
    309         @Override
    310         public void onLongPress(MotionEvent e) {
    311             // Do nothing.
    312         }
    313 
    314         @Override
    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 
    328         public FullyHiddenState() {
    329             reset();
    330         }
    331 
    332         @Override
    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
    342         public void showSwitcherHint() {
    343             mShouldBeVisible = true;
    344             mCurrentStateManager.setCurrentState(new ShimmyState());
    345         }
    346 
    347         @Override
    348         public boolean shouldHandleTouchEvent(MotionEvent ev) {
    349             return true;
    350         }
    351 
    352         @Override
    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
    362         public boolean onMenuPressed() {
    363             if (mAnimator != null) {
    364                 return false;
    365             }
    366             snapOpenAndShow();
    367             return true;
    368         }
    369 
    370         @Override
    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          */
    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
    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
    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
    446         public boolean shouldHandleTouchEvent(MotionEvent ev) {
    447             if (mAnimator != null && mAnimator.isRunning()) {
    448                 return false;
    449             }
    450             return true;
    451         }
    452 
    453         @Override
    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
    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
    481         public boolean onBackPressed() {
    482             snapBackAndHide();
    483             return true;
    484         }
    485 
    486         @Override
    487         public boolean onMenuPressed() {
    488             snapBackAndHide();
    489             return true;
    490         }
    491 
    492         @Override
    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          */
    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
    531         public void hide() {
    532             if (mAnimator != null) {
    533                 mAnimator.cancel();
    534             } else {
    535                 mCurrentStateManager.setCurrentState(new FullyHiddenState());
    536             }
    537         }
    538 
    539         @Override
    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 
    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
    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
    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
    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
    627         public void onItemSelected(ModeSelectorItem selectedItem) {
    628             cancelAnimation();
    629             mCurrentStateManager.setCurrentState(new SelectedState(selectedItem));
    630         }
    631 
    632         private void hideShimmyWithDelay() {
    633             postDelayed(mHideShimmy, HIDE_SHIMMY_DELAY_MS);
    634         }
    635 
    636         @Override
    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          */
    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          */
    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
    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          */
    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
    749         public void hide() {
    750             cancelAnimation();
    751             mCurrentStateManager.setCurrentState(new FullyHiddenState());
    752         }
    753 
    754         @Override
    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 
    774         public ScrollingState() {
    775             setVisibility(VISIBLE);
    776         }
    777 
    778         @Override
    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
    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
    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 {
    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
    886         public boolean shouldHandleTouchEvent(MotionEvent ev) {
    887             return false;
    888         }
    889 
    890         @Override
    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
    902         public void hide() {
    903             if (!mCurrentAnimationEffects.cancelAnimation()) {
    904                 mCurrentAnimationEffects = null;
    905                 mCurrentStateManager.setCurrentState(new FullyHiddenState());
    906             }
    907         }
    908     }
    909 
    910     public interface ModeSwitchListener {
    911         public void onModeSelected(int modeIndex);
    912         public int getCurrentModeIndex();
    913         public void onSettingsSelected();
    914     }
    915 
    916     public interface ModeListOpenListener {
    917         /**
    918          * Mode list will open to full screen after current animation.
    919          */
    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          */
    929         public void onModeListOpenProgress(float progress);
    930 
    931         /**
    932          * Gets called when mode list is completely closed.
    933          */
    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. */
    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          */
    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;
    967         public TimeBasedPosition(float position, long time) {
    968             mPosition = position;
    969             mTimeStamp = time;
    970         }
    971 
    972         public float getPosition() {
    973             return mPosition;
    974         }
    975 
    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      */
   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      */
   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 
   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 
   1093     private void disableA11yOnModeSelectorItems() {
   1094         for (View selectorItem : mModeSelectorItems) {
   1095             selectorItem.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
   1096         }
   1097     }
   1098 
   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      */
   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      */
   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      */
   1183     public void setCameraModuleScreenShotProvider(
   1184             CameraAppUI.CameraModuleScreenShotProvider provider) {
   1185         mScreenShotProvider = provider;
   1186     }
   1187 
   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      */
   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. */
   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      */
   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      */
   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      */
   1283     public void setVisibilityChangedListener(ModeListVisibilityChangedListener listener) {
   1284         mVisibilityChangedListener = listener;
   1285     }
   1286 
   1287     @Override
   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      */
   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      */
   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
   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      */
   1353     public void setCaptureLayoutHelper(CaptureLayoutHelper helper) {
   1354         mCaptureLayoutHelper = helper;
   1355     }
   1356 
   1357     @Override
   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 
   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 
   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
   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      */
   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      */
   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      */
   1457     public void showModeSwitcherHint() {
   1458         mCurrentStateManager.getCurrentState().showSwitcherHint();
   1459     }
   1460 
   1461     /**
   1462      * Hide the mode list immediately (provided the current state allows it).
   1463      */
   1464     public void hide() {
   1465         mCurrentStateManager.getCurrentState().hide();
   1466     }
   1467 
   1468     /**
   1469      * Hide the mode list with an animation.
   1470      */
   1471     public void hideAnimated() {
   1472         mCurrentStateManager.getCurrentState().hideAnimated();
   1473     }
   1474 
   1475     /**
   1476      * Resets the visible width of all the mode selectors to 0.
   1477      */
   1478     private void resetModeSelectors() {
   1479         for (int i = 0; i < mModeSelectorItems.length; i++) {
   1480             mModeSelectorItems[i].setVisibleWidth(0);
   1481         }
   1482     }
   1483 
   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      */
   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
   1511     public void onWindowFocusChanged(boolean hasFocus) {
   1512         super.onWindowFocusChanged(hasFocus);
   1513         mCurrentStateManager.getCurrentState().onWindowFocusChanged(hasFocus);
   1514     }
   1515 
   1516     @Override
   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
   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 
   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      */
   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      */
   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      */
   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);
   1785             resetModeSelectors();
   1786             return null;
   1787         }
   1788     }
   1789 
   1790     /**
   1791      * Snaps the mode list back out with animation.
   1792      */
   1793     private Animator snapBack() {
   1794         return snapBack(true);
   1795     }
   1796 
   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      */
   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      */
   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      */
   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      */
   1902     public boolean onBackPressed() {
   1903         return mCurrentStateManager.getCurrentState().onBackPressed();
   1904     }
   1905 
   1906     public void startModeSelectionAnimation() {
   1907         mCurrentStateManager.getCurrentState().startModeSelectionAnimation();
   1908     }
   1909 
   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 
   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 
   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
   2063         public void setSize(int width, int height) {
   2064             mWidth = width;
   2065             mHeight = height;
   2066         }
   2067 
   2068         @Override
   2069         public boolean onTouchEvent(MotionEvent event) {
   2070             return true;
   2071         }
   2072 
   2073         @Override
   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 
   2083         public void setAnimationStartingPosition(int x, int y) {
   2084             mPeepHoleCenterX = x;
   2085             mPeepHoleCenterY = y;
   2086         }
   2087 
   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          */
   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          */
   2107         public void setBackgroundOverlay(Bitmap overlay) {
   2108             mBackgroundOverlay = overlay;
   2109         }
   2110 
   2111         @Override
   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
   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 
   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
   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
   2195         public void endAnimation() {
   2196         }
   2197 
   2198         @Override
   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