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