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