ModeListView.java revision d2710487e724660cbb890ebf5eb887b4a93281c1
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            mSettingsButton.setVisibility(VISIBLE);
706            // If successfully finish hiding shimmy, then we should go back to
707            // fully hidden state.
708            if (success) {
709                ModeListView.this.enableA11yOnModeSelectorItems();
710                mModeListOpenFactor = 1;
711                mCurrentStateManager.setCurrentState(new FullyHiddenState());
712                return;
713            }
714
715            // If the animation was canceled before it's finished, animate the mode
716            // list open factor from 0 to 1 to ensure a smooth visual transition.
717            final ValueAnimator openFactorAnimator = ValueAnimator.ofFloat(mModeListOpenFactor, 1f);
718            openFactorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
719                @Override
720                public void onAnimationUpdate(ValueAnimator animation) {
721                    mModeListOpenFactor = (Float) openFactorAnimator.getAnimatedValue();
722                    onVisibleWidthChanged(mVisibleWidth);
723                }
724            });
725            openFactorAnimator.addListener(new Animator.AnimatorListener() {
726                @Override
727                public void onAnimationStart(Animator animation) {
728                    // Do nothing.
729                }
730
731                @Override
732                public void onAnimationEnd(Animator animation) {
733                    mModeListOpenFactor = 1f;
734                }
735
736                @Override
737                public void onAnimationCancel(Animator animation) {
738                    // Do nothing.
739                }
740
741                @Override
742                public void onAnimationRepeat(Animator animation) {
743                    // Do nothing.
744                }
745            });
746            openFactorAnimator.start();
747        }
748
749        @Override
750        public void hide() {
751            cancelAnimation();
752            mCurrentStateManager.setCurrentState(new FullyHiddenState());
753        }
754
755        @Override
756        public void hideAnimated() {
757            cancelAnimation();
758            animateListToWidth(0).addListener(new AnimatorListenerAdapter() {
759                @Override
760                public void onAnimationEnd(Animator animation) {
761                    mCurrentStateManager.setCurrentState(new FullyHiddenState());
762                }
763            });
764        }
765    }
766
767    /**
768     * When the mode list is being scrolled, it will be in ScrollingState. From
769     * this state, the mode list could transition to fully hidden, fully open
770     * depending on which direction the scrolling goes.
771     */
772    private class ScrollingState extends ModeListState {
773        private Animator mAnimator = null;
774
775        public ScrollingState() {
776            setVisibility(VISIBLE);
777        }
778
779        @Override
780        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
781            // Scroll based on the scrolling distance on the currently focused
782            // item.
783            scroll(mFocusItem, distanceX * SCROLL_FACTOR,
784                    distanceY * SCROLL_FACTOR);
785            return true;
786        }
787
788        @Override
789        public boolean shouldHandleTouchEvent(MotionEvent ev) {
790            // If the snap back/to full screen animation is on going, ignore any
791            // touch.
792            if (mAnimator != null) {
793                return false;
794            }
795            return true;
796        }
797
798        @Override
799        public boolean onTouchEvent(MotionEvent ev) {
800            if (ev.getActionMasked() == MotionEvent.ACTION_UP ||
801                    ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
802                final boolean shouldSnapBack = shouldSnapBack();
803                if (shouldSnapBack) {
804                    mAnimator = snapBack();
805                } else {
806                    mAnimator = snapToFullScreen();
807                }
808                mAnimator.addListener(new Animator.AnimatorListener() {
809                    @Override
810                    public void onAnimationStart(Animator animation) {
811
812                    }
813
814                    @Override
815                    public void onAnimationEnd(Animator animation) {
816                        mAnimator = null;
817                        mFocusItem = NO_ITEM_SELECTED;
818                        if (shouldSnapBack) {
819                            mCurrentStateManager.setCurrentState(new FullyHiddenState());
820                        } else {
821                            mCurrentStateManager.setCurrentState(new FullyShownState());
822                            UsageStatistics.instance().controlUsed(
823                                    eventprotos.ControlEvent.ControlType.MENU_FULL_FROM_SCROLL);
824                        }
825                    }
826
827                    @Override
828                    public void onAnimationCancel(Animator animation) {
829
830                    }
831
832                    @Override
833                    public void onAnimationRepeat(Animator animation) {
834
835                    }
836                });
837            }
838            return true;
839        }
840    }
841
842    /**
843     * Mode list gets in this state when a mode item has been selected/clicked.
844     * There will be an animation with the blurred preview fading in, a potential
845     * pause to wait for the new mode to be ready, and then the new mode will
846     * be revealed through a pinhole animation. After all the animations finish,
847     * mode list will transition into fully hidden state.
848     */
849    private class SelectedState extends ModeListState {
850        public SelectedState(ModeSelectorItem selectedItem) {
851            final int modeId = selectedItem.getModeId();
852            // Un-highlight all the modes.
853            for (int i = 0; i < mModeSelectorItems.length; i++) {
854                mModeSelectorItems[i].setSelected(false);
855            }
856
857            PeepholeAnimationEffect effect = new PeepholeAnimationEffect();
858            effect.setSize(mWidth, mHeight);
859
860            // Calculate the position of the icon in the selected item, and
861            // start animation from that position.
862            int[] location = new int[2];
863            // Gets icon's center position in relative to the window.
864            selectedItem.getIconCenterLocationInWindow(location);
865            int iconX = location[0];
866            int iconY = location[1];
867            // Gets current view's top left position relative to the window.
868            getLocationInWindow(location);
869            // Calculate icon location relative to this view
870            iconX -= location[0];
871            iconY -= location[1];
872
873            effect.setAnimationStartingPosition(iconX, iconY);
874            effect.setModeSpecificColor(selectedItem.getHighlightColor());
875            if (mScreenShotProvider != null) {
876                effect.setBackground(mScreenShotProvider
877                        .getPreviewFrame(PREVIEW_DOWN_SAMPLE_FACTOR),
878                        mCaptureLayoutHelper.getPreviewRect());
879                effect.setBackgroundOverlay(mScreenShotProvider.getPreviewOverlayAndControls());
880            }
881            mCurrentAnimationEffects = effect;
882            effect.startFadeoutAnimation(null, selectedItem, iconX, iconY, modeId);
883            invalidate();
884        }
885
886        @Override
887        public boolean shouldHandleTouchEvent(MotionEvent ev) {
888            return false;
889        }
890
891        @Override
892        public void startModeSelectionAnimation() {
893            mCurrentAnimationEffects.startAnimation(new AnimatorListenerAdapter() {
894                @Override
895                public void onAnimationEnd(Animator animation) {
896                    mCurrentAnimationEffects = null;
897                    mCurrentStateManager.setCurrentState(new FullyHiddenState());
898                }
899            });
900        }
901
902        @Override
903        public void hide() {
904            if (!mCurrentAnimationEffects.cancelAnimation()) {
905                mCurrentAnimationEffects = null;
906                mCurrentStateManager.setCurrentState(new FullyHiddenState());
907            }
908        }
909    }
910
911    public interface ModeSwitchListener {
912        public void onModeButtonPressed(int modeIndex);
913        public void onModeSelected(int modeIndex);
914        public int getCurrentModeIndex();
915        public void onSettingsSelected();
916    }
917
918    public interface ModeListOpenListener {
919        /**
920         * Mode list will open to full screen after current animation.
921         */
922        public void onOpenFullScreen();
923
924        /**
925         * Updates the listener with the current progress of mode drawer opening.
926         *
927         * @param progress progress of the mode drawer opening, ranging [0f, 1f]
928         *                 0 means mode drawer is fully closed, 1 indicates a fully
929         *                 open mode drawer.
930         */
931        public void onModeListOpenProgress(float progress);
932
933        /**
934         * Gets called when mode list is completely closed.
935         */
936        public void onModeListClosed();
937    }
938
939    public static abstract class ModeListVisibilityChangedListener {
940        private Boolean mCurrentVisibility = null;
941
942        /** Whether the mode list is (partially or fully) visible. */
943        public abstract void onVisibilityChanged(boolean visible);
944
945        /**
946         * Internal method to be called by the mode list whenever a visibility
947         * even occurs.
948         * <p>
949         * Do not call {@link #onVisibilityChanged(boolean)} directly, as this
950         * is only called when the visibility has actually changed and not on
951         * each visibility event.
952         *
953         * @param visible whether the mode drawer is currently visible.
954         */
955        private void onVisibilityEvent(boolean visible) {
956            if (mCurrentVisibility == null || mCurrentVisibility != visible) {
957                mCurrentVisibility = visible;
958                onVisibilityChanged(visible);
959            }
960        }
961    }
962
963    /**
964     * This class aims to help store time and position in pairs.
965     */
966    private static class TimeBasedPosition {
967        private final float mPosition;
968        private final long mTimeStamp;
969        public TimeBasedPosition(float position, long time) {
970            mPosition = position;
971            mTimeStamp = time;
972        }
973
974        public float getPosition() {
975            return mPosition;
976        }
977
978        public long getTimeStamp() {
979            return mTimeStamp;
980        }
981    }
982
983    /**
984     * This is a highly customized interpolator. The purpose of having this subclass
985     * is to encapsulate intricate animation timing, so that the actual animation
986     * implementation can be re-used with other interpolators to achieve different
987     * animation effects.
988     *
989     * The accordion animation consists of three stages:
990     * 1) Animate into the screen within a pre-specified fly in duration.
991     * 2) Hold in place for a certain amount of time (Optional).
992     * 3) Animate out of the screen within the given time.
993     *
994     * The accordion animator is initialized with 3 parameter: 1) initial position,
995     * 2) how far out the view should be before flying back out,  3) end position.
996     * The interpolation output should be [0f, 0.5f] during animation between 1)
997     * to 2), and [0.5f, 1f] for flying from 2) to 3).
998     */
999    private final TimeInterpolator mAccordionInterpolator = new TimeInterpolator() {
1000        @Override
1001        public float getInterpolation(float input) {
1002
1003            float flyInDuration = (float) FLY_OUT_DURATION_MS / (float) TOTAL_DURATION_MS;
1004            float holdDuration = (float) (FLY_OUT_DURATION_MS + HOLD_DURATION_MS)
1005                    / (float) TOTAL_DURATION_MS;
1006            if (input == 0) {
1007                return 0;
1008            } else if (input < flyInDuration) {
1009                // Stage 1, project result to [0f, 0.5f]
1010                input /= flyInDuration;
1011                float result = Gusterpolator.INSTANCE.getInterpolation(input);
1012                return result * 0.5f;
1013            } else if (input < holdDuration) {
1014                // Stage 2
1015                return 0.5f;
1016            } else {
1017                // Stage 3, project result to [0.5f, 1f]
1018                input -= holdDuration;
1019                input /= (1 - holdDuration);
1020                float result = Gusterpolator.INSTANCE.getInterpolation(input);
1021                return 0.5f + result * 0.5f;
1022            }
1023        }
1024    };
1025
1026    /**
1027     * The listener that is used to notify when gestures occur.
1028     * Here we only listen to a subset of gestures.
1029     */
1030    private final GestureDetector.OnGestureListener mOnGestureListener
1031            = new GestureDetector.SimpleOnGestureListener(){
1032        @Override
1033        public boolean onScroll(MotionEvent e1, MotionEvent e2,
1034                                float distanceX, float distanceY) {
1035            mCurrentStateManager.getCurrentState().onScroll(e1, e2, distanceX, distanceY);
1036            mLastScrollTime = System.currentTimeMillis();
1037            return true;
1038        }
1039
1040        @Override
1041        public boolean onSingleTapUp(MotionEvent ev) {
1042            mCurrentStateManager.getCurrentState().onSingleTapUp(ev);
1043            return true;
1044        }
1045
1046        @Override
1047        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
1048            // Cache velocity in the unit pixel/ms.
1049            mVelocityX = velocityX / 1000f * SCROLL_FACTOR;
1050            mCurrentStateManager.getCurrentState().onFling(e1, e2, velocityX, velocityY);
1051            return true;
1052        }
1053
1054        @Override
1055        public boolean onDown(MotionEvent ev) {
1056            mVelocityX = 0;
1057            mCurrentStateManager.getCurrentState().onDown(ev);
1058            return true;
1059        }
1060    };
1061
1062    /**
1063     * Gets called when a mode item in the mode drawer is clicked.
1064     *
1065     * @param selectedItem the item being clicked
1066     */
1067    private void onItemSelected(ModeSelectorItem selectedItem) {
1068        int modeId = selectedItem.getModeId();
1069        mModeSwitchListener.onModeButtonPressed(modeId);
1070
1071        mCurrentStateManager.getCurrentState().onItemSelected(selectedItem);
1072    }
1073
1074    /**
1075     * Checks whether a touch event is inside of the bounds of the mode list.
1076     *
1077     * @param ev touch event to be checked
1078     * @return whether the touch is inside the bounds of the mode list
1079     */
1080    private boolean isTouchInsideList(MotionEvent ev) {
1081        // Ignore the tap if it happens outside of the mode list linear layout.
1082        float x = ev.getX() - mListView.getX();
1083        float y = ev.getY() - mListView.getY();
1084        if (x < 0 || x > mListView.getWidth() || y < 0 || y > mListView.getHeight()) {
1085            return false;
1086        }
1087        return true;
1088    }
1089
1090    public ModeListView(Context context, AttributeSet attrs) {
1091        super(context, attrs);
1092        mGestureDetector = new GestureDetector(context, mOnGestureListener);
1093        mListBackgroundColor = getResources().getColor(R.color.mode_list_background);
1094        mSettingsButtonMargin = getResources().getDimensionPixelSize(
1095                R.dimen.mode_list_settings_icon_margin);
1096    }
1097
1098    private void disableA11yOnModeSelectorItems() {
1099        for (View selectorItem : mModeSelectorItems) {
1100            selectorItem.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
1101        }
1102    }
1103
1104    private void enableA11yOnModeSelectorItems() {
1105        for (View selectorItem : mModeSelectorItems) {
1106            selectorItem.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
1107        }
1108    }
1109
1110    /**
1111     * Sets the alpha on the list background. This is called whenever the list
1112     * is scrolling or animating, so that background can adjust its dimness.
1113     *
1114     * @param alpha new alpha to be applied on list background color
1115     */
1116    private void setBackgroundAlpha(int alpha) {
1117        // Make sure alpha is valid.
1118        alpha = alpha & 0xFF;
1119        // Change alpha on the background color.
1120        mListBackgroundColor = mListBackgroundColor & 0xFFFFFF;
1121        mListBackgroundColor = mListBackgroundColor | (alpha << 24);
1122        // Set new color to list background.
1123        setBackgroundColor(mListBackgroundColor);
1124    }
1125
1126    /**
1127     * Initialize mode list with a list of indices of supported modes.
1128     *
1129     * @param modeIndexList a list of indices of supported modes
1130     */
1131    public void init(List<Integer> modeIndexList) {
1132        int[] modeSequence = getResources()
1133                .getIntArray(R.array.camera_modes_in_nav_drawer_if_supported);
1134        int[] visibleModes = getResources()
1135                .getIntArray(R.array.camera_modes_always_visible);
1136
1137        // Mark the supported modes in a boolean array to preserve the
1138        // sequence of the modes
1139        SparseBooleanArray modeIsSupported = new SparseBooleanArray();
1140        for (int i = 0; i < modeIndexList.size(); i++) {
1141            int mode = modeIndexList.get(i);
1142            modeIsSupported.put(mode, true);
1143        }
1144        for (int i = 0; i < visibleModes.length; i++) {
1145            int mode = visibleModes[i];
1146            modeIsSupported.put(mode, true);
1147        }
1148
1149        // Put the indices of supported modes into an array preserving their
1150        // display order.
1151        mSupportedModes = new ArrayList<Integer>();
1152        for (int i = 0; i < modeSequence.length; i++) {
1153            int mode = modeSequence[i];
1154            if (modeIsSupported.get(mode, false)) {
1155                mSupportedModes.add(mode);
1156            }
1157        }
1158        mTotalModes = mSupportedModes.size();
1159        initializeModeSelectorItems();
1160        mSettingsButton = findViewById(R.id.settings_button);
1161        mSettingsButton.setOnClickListener(new OnClickListener() {
1162            @Override
1163            public void onClick(View v) {
1164                // Post this callback to make sure current user interaction has
1165                // been reflected in the UI. Specifically, the pressed state gets
1166                // unset after click happens. In order to ensure the pressed state
1167                // gets unset in UI before getting in the low frame rate settings
1168                // activity launch stage, the settings selected callback is posted.
1169                post(new Runnable() {
1170                    @Override
1171                    public void run() {
1172                        mModeSwitchListener.onSettingsSelected();
1173                    }
1174                });
1175            }
1176        });
1177        // The mode list is initialized to be all the way closed.
1178        onModeListOpenRatioUpdate(0);
1179        if (mCurrentStateManager.getCurrentState() == null) {
1180            mCurrentStateManager.setCurrentState(new FullyHiddenState());
1181        }
1182    }
1183
1184    /**
1185     * Sets the screen shot provider for getting a preview frame and a bitmap
1186     * of the controls and overlay.
1187     */
1188    public void setCameraModuleScreenShotProvider(
1189            CameraAppUI.CameraModuleScreenShotProvider provider) {
1190        mScreenShotProvider = provider;
1191    }
1192
1193    private void initializeModeSelectorItems() {
1194        mModeSelectorItems = new ModeSelectorItem[mTotalModes];
1195        // Inflate the mode selector items and add them to a linear layout
1196        LayoutInflater inflater = AndroidServices.instance().provideLayoutInflater();
1197        mListView = (LinearLayout) findViewById(R.id.mode_list);
1198        for (int i = 0; i < mTotalModes; i++) {
1199            final ModeSelectorItem selectorItem =
1200                    (ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null);
1201            mListView.addView(selectorItem);
1202            // Sets the top padding of the top item to 0.
1203            if (i == 0) {
1204                selectorItem.setPadding(selectorItem.getPaddingLeft(), 0,
1205                        selectorItem.getPaddingRight(), selectorItem.getPaddingBottom());
1206            }
1207            // Sets the bottom padding of the bottom item to 0.
1208            if (i == mTotalModes - 1) {
1209                selectorItem.setPadding(selectorItem.getPaddingLeft(), selectorItem.getPaddingTop(),
1210                        selectorItem.getPaddingRight(), 0);
1211            }
1212
1213            int modeId = getModeIndex(i);
1214            selectorItem.setHighlightColor(getResources()
1215                    .getColor(CameraUtil.getCameraThemeColorId(modeId, getContext())));
1216
1217            // Set image
1218            selectorItem.setImageResource(CameraUtil.getCameraModeIconResId(modeId, getContext()));
1219
1220            // Set text
1221            selectorItem.setText(CameraUtil.getCameraModeText(modeId, getContext()));
1222
1223            // Set content description (for a11y)
1224            selectorItem.setContentDescription(CameraUtil
1225                    .getCameraModeContentDescription(modeId, getContext()));
1226            selectorItem.setModeId(modeId);
1227            selectorItem.setOnClickListener(new OnClickListener() {
1228                @Override
1229                public void onClick(View v) {
1230                    onItemSelected(selectorItem);
1231                }
1232            });
1233
1234            mModeSelectorItems[i] = selectorItem;
1235        }
1236        // During drawer opening/closing, we change the visible width of the mode
1237        // items in sequence, so we listen to the last item's visible width change
1238        // for a good timing to do corresponding UI adjustments.
1239        mModeSelectorItems[mTotalModes - 1].setVisibleWidthChangedListener(this);
1240        resetModeSelectors();
1241    }
1242
1243    /**
1244     * Maps between the UI mode selector index to the actual mode id.
1245     *
1246     * @param modeSelectorIndex the index of the UI item
1247     * @return the index of the corresponding camera mode
1248     */
1249    private int getModeIndex(int modeSelectorIndex) {
1250        if (modeSelectorIndex < mTotalModes && modeSelectorIndex >= 0) {
1251            return mSupportedModes.get(modeSelectorIndex);
1252        }
1253        Log.e(TAG, "Invalid mode selector index: " + modeSelectorIndex + ", total modes: " +
1254                mTotalModes);
1255        return getResources().getInteger(R.integer.camera_mode_photo);
1256    }
1257
1258    /** Notify ModeSwitchListener, if any, of the mode change. */
1259    private void onModeSelected(int modeIndex) {
1260        if (mModeSwitchListener != null) {
1261            mModeSwitchListener.onModeSelected(modeIndex);
1262        }
1263    }
1264
1265    /**
1266     * Sets a listener that listens to receive mode switch event.
1267     *
1268     * @param listener a listener that gets notified when mode changes.
1269     */
1270    public void setModeSwitchListener(ModeSwitchListener listener) {
1271        mModeSwitchListener = listener;
1272    }
1273
1274    /**
1275     * Sets a listener that gets notified when the mode list is open full screen.
1276     *
1277     * @param listener a listener that listens to mode list open events
1278     */
1279    public void setModeListOpenListener(ModeListOpenListener listener) {
1280        mModeListOpenListener = listener;
1281    }
1282
1283    /**
1284     * Sets or replaces a listener that is called when the visibility of the
1285     * mode list changed.
1286     */
1287    public void setVisibilityChangedListener(ModeListVisibilityChangedListener listener) {
1288        mVisibilityChangedListener = listener;
1289    }
1290
1291    @Override
1292    public boolean onTouchEvent(MotionEvent ev) {
1293        // Reset touch forward recipient
1294        if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
1295            mChildViewTouched = null;
1296        }
1297
1298        if (!mCurrentStateManager.getCurrentState().shouldHandleTouchEvent(ev)) {
1299            return false;
1300        }
1301        getParent().requestDisallowInterceptTouchEvent(true);
1302        super.onTouchEvent(ev);
1303
1304        // Pass all touch events to gesture detector for gesture handling.
1305        mGestureDetector.onTouchEvent(ev);
1306        mCurrentStateManager.getCurrentState().onTouchEvent(ev);
1307        return true;
1308    }
1309
1310    /**
1311     * Forward touch events to a recipient child view. Before feeding the motion
1312     * event into the child view, the event needs to be converted in child view's
1313     * coordinates.
1314     */
1315    private void forwardTouchEventToChild(MotionEvent ev) {
1316        if (mChildViewTouched != null) {
1317            float x = ev.getX() - mListView.getX();
1318            float y = ev.getY() - mListView.getY();
1319            x -= mChildViewTouched.getLeft();
1320            y -= mChildViewTouched.getTop();
1321
1322            mLastChildTouchEvent = MotionEvent.obtain(ev);
1323            mLastChildTouchEvent.setLocation(x, y);
1324            mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
1325        }
1326    }
1327
1328    /**
1329     * Sets the swipe mode to indicate whether this is a swiping in
1330     * or out, and therefore we can have different animations.
1331     *
1332     * @param swipeIn indicates whether the swipe should reveal/hide the list.
1333     */
1334    private void setSwipeMode(boolean swipeIn) {
1335        for (int i = 0 ; i < mModeSelectorItems.length; i++) {
1336            mModeSelectorItems[i].onSwipeModeChanged(swipeIn);
1337        }
1338    }
1339
1340    @Override
1341    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1342        super.onLayout(changed, left, top, right, bottom);
1343        mWidth = right - left;
1344        mHeight = bottom - top - getPaddingTop() - getPaddingBottom();
1345
1346        updateModeListLayout();
1347
1348        if (mCurrentStateManager.getCurrentState().getCurrentAnimationEffects() != null) {
1349            mCurrentStateManager.getCurrentState().getCurrentAnimationEffects().setSize(
1350                    mWidth, mHeight);
1351        }
1352    }
1353
1354    /**
1355     * Sets a capture layout helper to query layout rect from.
1356     */
1357    public void setCaptureLayoutHelper(CaptureLayoutHelper helper) {
1358        mCaptureLayoutHelper = helper;
1359    }
1360
1361    @Override
1362    public void onPreviewAreaChanged(RectF previewArea) {
1363        if (getVisibility() == View.VISIBLE && !hasWindowFocus()) {
1364            // When the preview area has changed, to avoid visual disruption we
1365            // only make corresponding UI changes when mode list does not have
1366            // window focus.
1367            updateModeListLayout();
1368        }
1369    }
1370
1371    private void updateModeListLayout() {
1372        if (mCaptureLayoutHelper == null) {
1373            Log.e(TAG, "Capture layout helper needs to be set first.");
1374            return;
1375        }
1376        // Center mode drawer in the portion of camera preview that is not covered by
1377        // bottom bar.
1378        RectF uncoveredPreviewArea = mCaptureLayoutHelper.getUncoveredPreviewRect();
1379        // Align left:
1380        mListView.setTranslationX(uncoveredPreviewArea.left);
1381        // Align center vertical:
1382        mListView.setTranslationY(uncoveredPreviewArea.centerY()
1383                - mListView.getMeasuredHeight() / 2);
1384
1385        updateSettingsButtonLayout(uncoveredPreviewArea);
1386    }
1387
1388    private void updateSettingsButtonLayout(RectF uncoveredPreviewArea) {
1389        if (mWidth > mHeight) {
1390            // Align to the top right.
1391            mSettingsButton.setTranslationX(uncoveredPreviewArea.right - mSettingsButtonMargin
1392                    - mSettingsButton.getMeasuredWidth());
1393            mSettingsButton.setTranslationY(uncoveredPreviewArea.top + mSettingsButtonMargin);
1394        } else {
1395            // Align to the bottom right.
1396            mSettingsButton.setTranslationX(uncoveredPreviewArea.right - mSettingsButtonMargin
1397                    - mSettingsButton.getMeasuredWidth());
1398            mSettingsButton.setTranslationY(uncoveredPreviewArea.bottom - mSettingsButtonMargin
1399                    - mSettingsButton.getMeasuredHeight());
1400        }
1401        if (mSettingsCling != null) {
1402            mSettingsCling.updatePosition(mSettingsButton);
1403        }
1404    }
1405
1406    @Override
1407    public void draw(Canvas canvas) {
1408        ModeListState currentState = mCurrentStateManager.getCurrentState();
1409        AnimationEffects currentEffects = currentState.getCurrentAnimationEffects();
1410        if (currentEffects != null) {
1411            currentEffects.drawBackground(canvas);
1412            if (currentEffects.shouldDrawSuper()) {
1413                super.draw(canvas);
1414            }
1415            currentEffects.drawForeground(canvas);
1416        } else {
1417            super.draw(canvas);
1418        }
1419    }
1420
1421    /**
1422     * Sets whether a cling for settings button should be shown. If not, remove
1423     * the cling from view hierarchy if any. If a cling should be shown, inflate
1424     * the cling into this view group.
1425     *
1426     * @param show whether the cling needs to be shown.
1427     */
1428    public void setShouldShowSettingsCling(boolean show) {
1429        if (show) {
1430            if (mSettingsCling == null) {
1431                inflate(getContext(), R.layout.settings_cling, this);
1432                mSettingsCling = (SettingsCling) findViewById(R.id.settings_cling);
1433            }
1434        } else {
1435            if (mSettingsCling != null) {
1436                // Remove settings cling from view hierarchy.
1437                removeView(mSettingsCling);
1438                mSettingsCling = null;
1439            }
1440        }
1441    }
1442
1443    /**
1444     * Show or hide cling for settings button. The cling will only be shown if
1445     * settings button has never been clicked. Otherwise, cling will be null,
1446     * and will not show even if this method is called to show it.
1447     */
1448    private void showSettingsClingIfEnabled(boolean show) {
1449        if (mSettingsCling != null) {
1450            int visibility = show ? VISIBLE : INVISIBLE;
1451            mSettingsCling.setVisibility(visibility);
1452        }
1453    }
1454
1455    /**
1456     * This shows the mode switcher and starts the accordion animation with a delay.
1457     * If the view does not currently have focus, (e.g. There are popups on top of
1458     * it.) start the delayed accordion animation when it gains focus. Otherwise,
1459     * start the animation with a delay right away.
1460     */
1461    public void showModeSwitcherHint() {
1462        mCurrentStateManager.getCurrentState().showSwitcherHint();
1463    }
1464
1465    /**
1466     * Hide the mode list immediately (provided the current state allows it).
1467     */
1468    public void hide() {
1469        mCurrentStateManager.getCurrentState().hide();
1470    }
1471
1472    /**
1473     * Hide the mode list with an animation.
1474     */
1475    public void hideAnimated() {
1476        mCurrentStateManager.getCurrentState().hideAnimated();
1477    }
1478
1479    /**
1480     * Resets the visible width of all the mode selectors to 0.
1481     */
1482    private void resetModeSelectors() {
1483        for (int i = 0; i < mModeSelectorItems.length; i++) {
1484            mModeSelectorItems[i].setVisibleWidth(0);
1485        }
1486    }
1487
1488    private boolean isRunningAccordionAnimation() {
1489        return mAnimatorSet != null && mAnimatorSet.isRunning();
1490    }
1491
1492    /**
1493     * Calculate the mode selector item in the list that is at position (x, y).
1494     * If the position is above the top item or below the bottom item, return
1495     * the top item or bottom item respectively.
1496     *
1497     * @param x horizontal position
1498     * @param y vertical position
1499     * @return index of the item that is at position (x, y)
1500     */
1501    private int getFocusItem(float x, float y) {
1502        // Convert coordinates into child view's coordinates.
1503        x -= mListView.getX();
1504        y -= mListView.getY();
1505
1506        for (int i = 0; i < mModeSelectorItems.length; i++) {
1507            if (y <= mModeSelectorItems[i].getBottom()) {
1508                return i;
1509            }
1510        }
1511        return mModeSelectorItems.length - 1;
1512    }
1513
1514    @Override
1515    public void onWindowFocusChanged(boolean hasFocus) {
1516        super.onWindowFocusChanged(hasFocus);
1517        mCurrentStateManager.getCurrentState().onWindowFocusChanged(hasFocus);
1518    }
1519
1520    @Override
1521    public void onVisibilityChanged(View v, int visibility) {
1522        super.onVisibilityChanged(v, visibility);
1523        if (visibility == VISIBLE) {
1524            // Highlight current module
1525            if (mModeSwitchListener != null) {
1526                int modeId = mModeSwitchListener.getCurrentModeIndex();
1527                int parentMode = CameraUtil.getCameraModeParentModeId(modeId, getContext());
1528                // Find parent mode in the nav drawer.
1529                for (int i = 0; i < mSupportedModes.size(); i++) {
1530                    if (mSupportedModes.get(i) == parentMode) {
1531                        mModeSelectorItems[i].setSelected(true);
1532                    }
1533                }
1534            }
1535            updateModeListLayout();
1536        } else {
1537            if (mModeSelectorItems != null) {
1538                // When becoming invisible/gone after initializing mode selector items.
1539                for (int i = 0; i < mModeSelectorItems.length; i++) {
1540                    mModeSelectorItems[i].setSelected(false);
1541                }
1542            }
1543            if (mModeListOpenListener != null) {
1544                mModeListOpenListener.onModeListClosed();
1545            }
1546        }
1547
1548        if (mVisibilityChangedListener != null) {
1549            mVisibilityChangedListener.onVisibilityEvent(getVisibility() == VISIBLE);
1550        }
1551    }
1552
1553    @Override
1554    public void setVisibility(int visibility) {
1555        ModeListState currentState = mCurrentStateManager.getCurrentState();
1556        if (currentState != null && !currentState.shouldHandleVisibilityChange(visibility)) {
1557            return;
1558        }
1559        super.setVisibility(visibility);
1560    }
1561
1562    private void scroll(int itemId, float deltaX, float deltaY) {
1563        // Scrolling trend on X and Y axis, to track the trend by biasing
1564        // towards latest touch events.
1565        mScrollTrendX = mScrollTrendX * 0.3f + deltaX * 0.7f;
1566        mScrollTrendY = mScrollTrendY * 0.3f + deltaY * 0.7f;
1567
1568        // TODO: Change how the curve is calculated below when UX finalize their design.
1569        mCurrentTime = SystemClock.uptimeMillis();
1570        float longestWidth;
1571        if (itemId != NO_ITEM_SELECTED) {
1572            longestWidth = mModeSelectorItems[itemId].getVisibleWidth();
1573        } else {
1574            longestWidth = mModeSelectorItems[0].getVisibleWidth();
1575        }
1576        float newPosition = longestWidth - deltaX;
1577        int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
1578        newPosition = Math.min(newPosition, getMaxMovementBasedOnPosition((int) longestWidth,
1579                maxVisibleWidth));
1580        newPosition = Math.max(newPosition, 0);
1581        insertNewPosition(newPosition, mCurrentTime);
1582
1583        for (int i = 0; i < mModeSelectorItems.length; i++) {
1584            mModeSelectorItems[i].setVisibleWidth((int) newPosition);
1585        }
1586    }
1587
1588    /**
1589     * Insert new position and time stamp into the history position list, and
1590     * remove stale position items.
1591     *
1592     * @param position latest position of the focus item
1593     * @param time  current time in milliseconds
1594     */
1595    private void insertNewPosition(float position, long time) {
1596        // TODO: Consider re-using stale position objects rather than
1597        // always creating new position objects.
1598        mPositionHistory.add(new TimeBasedPosition(position, time));
1599
1600        // Positions that are from too long ago will not be of any use for
1601        // future position interpolation. So we need to remove those positions
1602        // from the list.
1603        long timeCutoff = time - (mTotalModes - 1) * DELAY_MS;
1604        while (mPositionHistory.size() > 0) {
1605            // Remove all the position items that are prior to the cutoff time.
1606            TimeBasedPosition historyPosition = mPositionHistory.getFirst();
1607            if (historyPosition.getTimeStamp() < timeCutoff) {
1608                mPositionHistory.removeFirst();
1609            } else {
1610                break;
1611            }
1612        }
1613    }
1614
1615    /**
1616     * Gets the interpolated position at the specified time. This involves going
1617     * through the recorded positions until a {@link TimeBasedPosition} is found
1618     * such that the position the recorded before the given time, and the
1619     * {@link TimeBasedPosition} after that is recorded no earlier than the given
1620     * time. These two positions are then interpolated to get the position at the
1621     * specified time.
1622     */
1623    private float getPosition(long time, float currentPosition) {
1624        int i;
1625        for (i = 0; i < mPositionHistory.size(); i++) {
1626            TimeBasedPosition historyPosition = mPositionHistory.get(i);
1627            if (historyPosition.getTimeStamp() > time) {
1628                // Found the winner. Now interpolate between position i and position i - 1
1629                if (i == 0) {
1630                    // Slowly approaching to the destination if there isn't enough data points
1631                    float weight = 0.2f;
1632                    return historyPosition.getPosition() * weight + (1f - weight) * currentPosition;
1633                } else {
1634                    TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1);
1635                    // Start interpolation
1636                    float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) /
1637                            (float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp());
1638                    float position = fraction * (historyPosition.getPosition()
1639                            - prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition();
1640                    return position;
1641                }
1642            }
1643        }
1644        // It should never get here.
1645        Log.e(TAG, "Invalid time input for getPosition(). time: " + time);
1646        if (mPositionHistory.size() == 0) {
1647            Log.e(TAG, "TimeBasedPosition history size is 0");
1648        } else {
1649            Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp()
1650            + " , last position recorded at " + mPositionHistory.getLast().getTimeStamp());
1651        }
1652        assert (i < mPositionHistory.size());
1653        return i;
1654    }
1655
1656    private void reset() {
1657        resetModeSelectors();
1658        mScrollTrendX = 0f;
1659        mScrollTrendY = 0f;
1660        setVisibility(INVISIBLE);
1661    }
1662
1663    /**
1664     * When visible width of list is changed, the background of the list needs
1665     * to darken/lighten correspondingly.
1666     */
1667    @Override
1668    public void onVisibleWidthChanged(int visibleWidth) {
1669        mVisibleWidth = visibleWidth;
1670
1671        // When the longest mode item is entirely shown (across the screen), the
1672        // background should be 50% transparent.
1673        int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
1674        visibleWidth = Math.min(maxVisibleWidth, visibleWidth);
1675        if (visibleWidth != maxVisibleWidth) {
1676            // No longer full screen.
1677            cancelForwardingTouchEvent();
1678        }
1679        float openRatio = (float) visibleWidth / maxVisibleWidth;
1680        onModeListOpenRatioUpdate(openRatio * mModeListOpenFactor);
1681    }
1682
1683    /**
1684     * Gets called when UI elements such as background and gear icon need to adjust
1685     * their appearance based on the percentage of the mode list opening.
1686     *
1687     * @param openRatio percentage of the mode list opening, ranging [0f, 1f]
1688     */
1689    private void onModeListOpenRatioUpdate(float openRatio) {
1690        for (int i = 0; i < mModeSelectorItems.length; i++) {
1691            mModeSelectorItems[i].setTextAlpha(openRatio);
1692        }
1693        setBackgroundAlpha((int) (BACKGROUND_TRANSPARENTCY * openRatio));
1694        if (mModeListOpenListener != null) {
1695            mModeListOpenListener.onModeListOpenProgress(openRatio);
1696        }
1697        if (mSettingsButton != null) {
1698            mSettingsButton.setAlpha(openRatio);
1699        }
1700    }
1701
1702    /**
1703     * Cancels the touch event forwarding by sending a cancel event to the recipient
1704     * view and resetting the touch forward recipient to ensure no more events
1705     * can be forwarded in the current series of the touch events.
1706     */
1707    private void cancelForwardingTouchEvent() {
1708        if (mChildViewTouched != null) {
1709            mLastChildTouchEvent.setAction(MotionEvent.ACTION_CANCEL);
1710            mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
1711            mChildViewTouched = null;
1712        }
1713    }
1714
1715    @Override
1716    public void onWindowVisibilityChanged(int visibility) {
1717        super.onWindowVisibilityChanged(visibility);
1718        if (visibility != VISIBLE) {
1719            mCurrentStateManager.getCurrentState().hide();
1720        }
1721    }
1722
1723    /**
1724     * Defines how the list view should respond to a menu button pressed
1725     * event.
1726     */
1727    public boolean onMenuPressed() {
1728        return mCurrentStateManager.getCurrentState().onMenuPressed();
1729    }
1730
1731    /**
1732     * The list view should either snap back or snap to full screen after a gesture.
1733     * This function is called when an up or cancel event is received, and then based
1734     * on the current position of the list and the gesture we can decide which way
1735     * to snap.
1736     */
1737    private void snap() {
1738        if (shouldSnapBack()) {
1739            snapBack();
1740        } else {
1741            snapToFullScreen();
1742        }
1743    }
1744
1745    private boolean shouldSnapBack() {
1746        int itemId = Math.max(0, mFocusItem);
1747        if (Math.abs(mVelocityX) > VELOCITY_THRESHOLD) {
1748            // Fling to open / close
1749            return mVelocityX < 0;
1750        } else if (mModeSelectorItems[itemId].getVisibleWidth()
1751                < mModeSelectorItems[itemId].getMaxVisibleWidth() * SNAP_BACK_THRESHOLD_RATIO) {
1752            return true;
1753        } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) {
1754            return true;
1755        } else {
1756            return false;
1757        }
1758    }
1759
1760    /**
1761     * Snaps back out of the screen.
1762     *
1763     * @param withAnimation whether snapping back should be animated
1764     */
1765    public Animator snapBack(boolean withAnimation) {
1766        if (withAnimation) {
1767            if (mVelocityX > -VELOCITY_THRESHOLD * SCROLL_FACTOR) {
1768                return animateListToWidth(0);
1769            } else {
1770                return animateListToWidthAtVelocity(mVelocityX, 0);
1771            }
1772        } else {
1773            setVisibility(INVISIBLE);
1774            resetModeSelectors();
1775            return null;
1776        }
1777    }
1778
1779    /**
1780     * Snaps the mode list back out with animation.
1781     */
1782    private Animator snapBack() {
1783        return snapBack(true);
1784    }
1785
1786    private Animator snapToFullScreen() {
1787        Animator animator;
1788        int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
1789        int fullWidth = mModeSelectorItems[focusItem].getMaxVisibleWidth();
1790        if (mVelocityX <= VELOCITY_THRESHOLD) {
1791            animator = animateListToWidth(fullWidth);
1792        } else {
1793            // If the fling velocity exceeds this threshold, snap to full screen
1794            // at a constant speed.
1795            animator = animateListToWidthAtVelocity(VELOCITY_THRESHOLD, fullWidth);
1796        }
1797        if (mModeListOpenListener != null) {
1798            mModeListOpenListener.onOpenFullScreen();
1799        }
1800        return animator;
1801    }
1802
1803    /**
1804     * Overloaded function to provide a simple way to start animation. Animation
1805     * will use default duration, and a value of <code>null</code> for interpolator
1806     * means linear interpolation will be used.
1807     *
1808     * @param width a set of values that the animation will animate between over time
1809     */
1810    private Animator animateListToWidth(int... width) {
1811        return animateListToWidth(0, DEFAULT_DURATION_MS, null, width);
1812    }
1813
1814    /**
1815     * Animate the mode list between the given set of visible width.
1816     *
1817     * @param delay start delay between consecutive mode item. If delay < 0, the
1818     *              leader in the animation will be the bottom item.
1819     * @param duration duration for the animation of each mode item
1820     * @param interpolator interpolator to be used by the animation
1821     * @param width a set of values that the animation will animate between over time
1822     */
1823    private Animator animateListToWidth(int delay, int duration,
1824                                    TimeInterpolator interpolator, int... width) {
1825        if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
1826            mAnimatorSet.end();
1827        }
1828
1829        ArrayList<Animator> animators = new ArrayList<Animator>();
1830        boolean animateModeItemsInOrder = true;
1831        if (delay < 0) {
1832            animateModeItemsInOrder = false;
1833            delay *= -1;
1834        }
1835        for (int i = 0; i < mTotalModes; i++) {
1836            ObjectAnimator animator;
1837            if (animateModeItemsInOrder) {
1838                animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
1839                    "visibleWidth", width);
1840            } else {
1841                animator = ObjectAnimator.ofInt(mModeSelectorItems[mTotalModes - 1 -i],
1842                        "visibleWidth", width);
1843            }
1844            animator.setDuration(duration);
1845            animators.add(animator);
1846        }
1847
1848        mAnimatorSet = new AnimatorSet();
1849        mAnimatorSet.playTogether(animators);
1850        mAnimatorSet.setInterpolator(interpolator);
1851        mAnimatorSet.start();
1852
1853        return mAnimatorSet;
1854    }
1855
1856    /**
1857     * Animate the mode list to the given width at a constant velocity.
1858     *
1859     * @param velocity the velocity that animation will be at
1860     * @param width final width of the list
1861     */
1862    private Animator animateListToWidthAtVelocity(float velocity, int width) {
1863        if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
1864            mAnimatorSet.end();
1865        }
1866
1867        ArrayList<Animator> animators = new ArrayList<Animator>();
1868        for (int i = 0; i < mTotalModes; i++) {
1869            ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
1870                    "visibleWidth", width);
1871            int duration = (int) (width / velocity);
1872            animator.setDuration(duration);
1873            animators.add(animator);
1874        }
1875
1876        mAnimatorSet = new AnimatorSet();
1877        mAnimatorSet.playTogether(animators);
1878        mAnimatorSet.setInterpolator(null);
1879        mAnimatorSet.start();
1880
1881        return mAnimatorSet;
1882    }
1883
1884    /**
1885     * Called when the back key is pressed.
1886     *
1887     * @return Whether the UI responded to the key event.
1888     */
1889    public boolean onBackPressed() {
1890        return mCurrentStateManager.getCurrentState().onBackPressed();
1891    }
1892
1893    public void startModeSelectionAnimation() {
1894        mCurrentStateManager.getCurrentState().startModeSelectionAnimation();
1895    }
1896
1897    public float getMaxMovementBasedOnPosition(int lastVisibleWidth, int maxWidth) {
1898        int timeElapsed = (int) (System.currentTimeMillis() - mLastScrollTime);
1899        if (timeElapsed > SCROLL_INTERVAL_MS) {
1900            timeElapsed = SCROLL_INTERVAL_MS;
1901        }
1902        float position;
1903        int slowZone = (int) (maxWidth * SLOW_ZONE_PERCENTAGE);
1904        if (lastVisibleWidth < (maxWidth - slowZone)) {
1905            position = VELOCITY_THRESHOLD * timeElapsed + lastVisibleWidth;
1906        } else {
1907            float percentageIntoSlowZone = (lastVisibleWidth - (maxWidth - slowZone)) / slowZone;
1908            float velocity = (1 - percentageIntoSlowZone) * VELOCITY_THRESHOLD;
1909            position = velocity * timeElapsed + lastVisibleWidth;
1910        }
1911        position = Math.min(maxWidth, position);
1912        return position;
1913    }
1914
1915    private class PeepholeAnimationEffect extends AnimationEffects {
1916
1917        private final static int UNSET = -1;
1918        private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 500;
1919
1920        private final Paint mMaskPaint = new Paint();
1921        private final RectF mBackgroundDrawArea = new RectF();
1922
1923        private int mPeepHoleCenterX = UNSET;
1924        private int mPeepHoleCenterY = UNSET;
1925        private float mRadius = 0f;
1926        private ValueAnimator mPeepHoleAnimator;
1927        private ValueAnimator mFadeOutAlphaAnimator;
1928        private ValueAnimator mRevealAlphaAnimator;
1929        private Bitmap mBackground;
1930        private Bitmap mBackgroundOverlay;
1931
1932        private Paint mCirclePaint = new Paint();
1933        private Paint mCoverPaint = new Paint();
1934
1935        private TouchCircleDrawable mCircleDrawable;
1936
1937        public PeepholeAnimationEffect() {
1938            mMaskPaint.setAlpha(0);
1939            mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
1940
1941            mCirclePaint.setColor(0);
1942            mCirclePaint.setAlpha(0);
1943
1944            mCoverPaint.setColor(0);
1945            mCoverPaint.setAlpha(0);
1946
1947            setupAnimators();
1948        }
1949
1950        private void setupAnimators() {
1951            mFadeOutAlphaAnimator = ValueAnimator.ofInt(0, 255);
1952            mFadeOutAlphaAnimator.setDuration(100);
1953            mFadeOutAlphaAnimator.setInterpolator(Gusterpolator.INSTANCE);
1954            mFadeOutAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1955                @Override
1956                public void onAnimationUpdate(ValueAnimator animation) {
1957                    mCoverPaint.setAlpha((Integer) animation.getAnimatedValue());
1958                    invalidate();
1959                }
1960            });
1961            mFadeOutAlphaAnimator.addListener(new AnimatorListenerAdapter() {
1962                @Override
1963                public void onAnimationStart(Animator animation) {
1964                    // Sets a HW layer on the view for the animation.
1965                    setLayerType(LAYER_TYPE_HARDWARE, null);
1966                }
1967
1968                @Override
1969                public void onAnimationEnd(Animator animation) {
1970                    // Sets the layer type back to NONE as a workaround for b/12594617.
1971                    setLayerType(LAYER_TYPE_NONE, null);
1972                }
1973            });
1974
1975            /////////////////
1976
1977            mRevealAlphaAnimator = ValueAnimator.ofInt(255, 0);
1978            mRevealAlphaAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS);
1979            mRevealAlphaAnimator.setInterpolator(Gusterpolator.INSTANCE);
1980            mRevealAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1981                @Override
1982                public void onAnimationUpdate(ValueAnimator animation) {
1983                    int alpha = (Integer) animation.getAnimatedValue();
1984                    mCirclePaint.setAlpha(alpha);
1985                    mCoverPaint.setAlpha(alpha);
1986                }
1987            });
1988            mRevealAlphaAnimator.addListener(new AnimatorListenerAdapter() {
1989                @Override
1990                public void onAnimationStart(Animator animation) {
1991                    // Sets a HW layer on the view for the animation.
1992                    setLayerType(LAYER_TYPE_HARDWARE, null);
1993                }
1994
1995                @Override
1996                public void onAnimationEnd(Animator animation) {
1997                    // Sets the layer type back to NONE as a workaround for b/12594617.
1998                    setLayerType(LAYER_TYPE_NONE, null);
1999                }
2000            });
2001
2002            ////////////////
2003
2004            int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX);
2005            int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY);
2006            int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge
2007                    + verticalDistanceToFarEdge * verticalDistanceToFarEdge));
2008            int startRadius = getResources().getDimensionPixelSize(
2009                    R.dimen.mode_selector_icon_block_width) / 2;
2010
2011            mPeepHoleAnimator = ValueAnimator.ofFloat(startRadius, endRadius);
2012            mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS);
2013            mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE);
2014            mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2015                @Override
2016                public void onAnimationUpdate(ValueAnimator animation) {
2017                    // Modify mask by enlarging the hole
2018                    mRadius = (Float) mPeepHoleAnimator.getAnimatedValue();
2019                    invalidate();
2020                }
2021            });
2022            mPeepHoleAnimator.addListener(new AnimatorListenerAdapter() {
2023                @Override
2024                public void onAnimationStart(Animator animation) {
2025                    // Sets a HW layer on the view for the animation.
2026                    setLayerType(LAYER_TYPE_HARDWARE, null);
2027                }
2028
2029                @Override
2030                public void onAnimationEnd(Animator animation) {
2031                    // Sets the layer type back to NONE as a workaround for b/12594617.
2032                    setLayerType(LAYER_TYPE_NONE, null);
2033                }
2034            });
2035
2036            ////////////////
2037            int size = getContext().getResources()
2038                    .getDimensionPixelSize(R.dimen.mode_selector_icon_block_width);
2039            mCircleDrawable = new TouchCircleDrawable(getContext().getResources());
2040            mCircleDrawable.setSize(size, size);
2041            mCircleDrawable.setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
2042                @Override
2043                public void onAnimationUpdate(ValueAnimator animation) {
2044                    invalidate();
2045                }
2046            });
2047        }
2048
2049        @Override
2050        public void setSize(int width, int height) {
2051            mWidth = width;
2052            mHeight = height;
2053        }
2054
2055        @Override
2056        public boolean onTouchEvent(MotionEvent event) {
2057            return true;
2058        }
2059
2060        @Override
2061        public void drawForeground(Canvas canvas) {
2062            // Draw the circle in clear mode
2063            if (mPeepHoleAnimator != null) {
2064                // Draw a transparent circle using clear mode
2065                canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint);
2066                canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mCirclePaint);
2067            }
2068        }
2069
2070        public void setAnimationStartingPosition(int x, int y) {
2071            mPeepHoleCenterX = x;
2072            mPeepHoleCenterY = y;
2073        }
2074
2075        public void setModeSpecificColor(int color) {
2076            mCirclePaint.setColor(color & 0x00ffffff);
2077        }
2078
2079        /**
2080         * Sets the bitmap to be drawn in the background and the drawArea to draw
2081         * the bitmap.
2082         *
2083         * @param background image to be drawn in the background
2084         * @param drawArea area to draw the background image
2085         */
2086        public void setBackground(Bitmap background, RectF drawArea) {
2087            mBackground = background;
2088            mBackgroundDrawArea.set(drawArea);
2089        }
2090
2091        /**
2092         * Sets the overlay image to be drawn on top of the background.
2093         */
2094        public void setBackgroundOverlay(Bitmap overlay) {
2095            mBackgroundOverlay = overlay;
2096        }
2097
2098        @Override
2099        public void drawBackground(Canvas canvas) {
2100            if (mBackground != null && mBackgroundOverlay != null) {
2101                canvas.drawBitmap(mBackground, null, mBackgroundDrawArea, null);
2102                canvas.drawPaint(mCoverPaint);
2103                canvas.drawBitmap(mBackgroundOverlay, 0, 0, null);
2104
2105                if (mCircleDrawable != null) {
2106                    mCircleDrawable.draw(canvas);
2107                }
2108            }
2109        }
2110
2111        @Override
2112        public boolean shouldDrawSuper() {
2113            // No need to draw super when mBackgroundOverlay is being drawn, as
2114            // background overlay already contains what's drawn in super.
2115            return (mBackground == null || mBackgroundOverlay == null);
2116        }
2117
2118        public void startFadeoutAnimation(Animator.AnimatorListener listener,
2119                final ModeSelectorItem selectedItem,
2120                int x, int y, final int modeId) {
2121            mCoverPaint.setColor(0);
2122            mCoverPaint.setAlpha(0);
2123
2124            mCircleDrawable.setIconDrawable(
2125                    selectedItem.getIcon().getIconDrawableClone(),
2126                    selectedItem.getIcon().getIconDrawableSize());
2127            mCircleDrawable.setCenter(new Point(x, y));
2128            mCircleDrawable.setColor(selectedItem.getHighlightColor());
2129            mCircleDrawable.setAnimatorListener(new AnimatorListenerAdapter() {
2130                @Override
2131                public void onAnimationEnd(Animator animation) {
2132                    // Post mode selection runnable to the end of the message queue
2133                    // so that current UI changes can finish before mode initialization
2134                    // clogs up UI thread.
2135                    post(new Runnable() {
2136                        @Override
2137                        public void run() {
2138                            // Select the focused item.
2139                            selectedItem.setSelected(true);
2140                            onModeSelected(modeId);
2141                        }
2142                    });
2143                }
2144            });
2145
2146            // add fade out animator to a set, so we can freely add
2147            // the listener without having to worry about listener dupes
2148            AnimatorSet s = new AnimatorSet();
2149            s.play(mFadeOutAlphaAnimator);
2150            if (listener != null) {
2151                s.addListener(listener);
2152            }
2153            mCircleDrawable.animate();
2154            s.start();
2155        }
2156
2157        @Override
2158        public void startAnimation(Animator.AnimatorListener listener) {
2159            if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) {
2160                return;
2161            }
2162            if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) {
2163                mPeepHoleCenterX = mWidth / 2;
2164                mPeepHoleCenterY = mHeight / 2;
2165            }
2166
2167            mCirclePaint.setAlpha(255);
2168            mCoverPaint.setAlpha(255);
2169
2170            // add peephole and reveal animators to a set, so we can
2171            // freely add the listener without having to worry about
2172            // listener dupes
2173            AnimatorSet s = new AnimatorSet();
2174            s.play(mPeepHoleAnimator).with(mRevealAlphaAnimator);
2175            if (listener != null) {
2176                s.addListener(listener);
2177            }
2178            s.start();
2179        }
2180
2181        @Override
2182        public void endAnimation() {
2183        }
2184
2185        @Override
2186        public boolean cancelAnimation() {
2187            if (mPeepHoleAnimator == null || !mPeepHoleAnimator.isRunning()) {
2188                return false;
2189            } else {
2190                mPeepHoleAnimator.cancel();
2191                return true;
2192            }
2193        }
2194    }
2195}
2196