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