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