ModeListView.java revision 5e5734b558013e23f21902ad96d0dc2949610b90
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.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
23import android.animation.ValueAnimator;
24import android.content.Context;
25import android.graphics.Bitmap;
26import android.graphics.Canvas;
27import android.graphics.Paint;
28import android.graphics.PorterDuff;
29import android.graphics.PorterDuffXfermode;
30import android.graphics.RectF;
31import android.os.AsyncTask;
32import android.os.SystemClock;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.util.SparseArray;
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.app.CameraAppUI;
44import com.android.camera.util.CameraUtil;
45import com.android.camera.util.Gusterpolator;
46import com.android.camera.widget.AnimationEffects;
47import com.android.camera.widget.SettingsButton;
48import com.android.camera2.R;
49
50import java.util.ArrayList;
51import java.util.LinkedList;
52import java.util.List;
53
54/**
55 * ModeListView class displays all camera modes and settings in the form
56 * of a list. A swipe to the right will bring up this list. Then tapping on
57 * any of the items in the list will take the user to that corresponding mode
58 * with an animation. To dismiss this list, simply swipe left or select a mode.
59 */
60public class ModeListView extends FrameLayout
61        implements PreviewStatusListener.PreviewAreaChangedListener,
62        ModeSelectorItem.VisibleWidthChangedListener {
63
64    private static final String TAG = "ModeListView";
65
66    // Animation Durations
67    private static final int DEFAULT_DURATION_MS = 200;
68    private static final int FLY_IN_DURATION_MS = 0;
69    private static final int HOLD_DURATION_MS = 0;
70    private static final int FLY_OUT_DURATION_MS = 850;
71    private static final int START_DELAY_MS = 100;
72    private static final int TOTAL_DURATION_MS = FLY_IN_DURATION_MS + HOLD_DURATION_MS
73            + FLY_OUT_DURATION_MS;
74    private static final int HIDE_SHIMMY_DELAY_MS = 1000;
75    // Assumption for time since last scroll when no data point for last scroll.
76    private static final int SCROLL_INTERVAL_MS = 50;
77    // Last 20% percent of the drawer opening should be slow to ensure soft landing.
78    private static final float SLOW_ZONE_PERCENTAGE = 0.2f;
79
80    private static final float ROWS_TO_SHOW_IN_LANDSCAPE = 4.5f;
81    private static final int NO_ITEM_SELECTED = -1;
82
83    // Scrolling states
84    private static final int FULLY_HIDDEN = 0;
85    private static final int FULLY_SHOWN = 1;
86    private static final int ACCORDION_ANIMATION = 2;
87    private static final int SCROLLING = 3;
88    private static final int MODE_SELECTED = 4;
89
90    // Scrolling delay between non-focused item and focused item
91    private static final int DELAY_MS = 30;
92    // If the fling velocity exceeds this threshold, snap to full screen at a constant
93    // speed. Unit: pixel/ms.
94    private static final float VELOCITY_THRESHOLD = 2f;
95
96    /**
97     * A factor to change the UI responsiveness on a scroll.
98     * e.g. A scroll factor of 0.5 means UI will move half as fast as the finger.
99     */
100    private static final float SCROLL_FACTOR = 0.5f;
101    // 30% transparent black background.
102    private static final int BACKGROUND_TRANSPARENTCY = (int) (0.3f * 255);
103    private static final int PREVIEW_DOWN_SAMPLE_FACTOR = 4;
104    // Threshold, below which snap back will happen.
105    private static final float SNAP_BACK_THRESHOLD_RATIO = 0.33f;
106
107    private final GestureDetector mGestureDetector;
108    private final RectF mPreviewArea = new RectF();
109    private final RectF mUncoveredPreviewArea = new RectF();
110
111    private long mLastScrollTime;
112    private int mListBackgroundColor;
113    private LinearLayout mListView;
114    private SettingsButton mSettingsButton;
115    private int mState = FULLY_HIDDEN;
116    private int mTotalModes;
117    private ModeSelectorItem[] mModeSelectorItems;
118    private AnimatorSet mAnimatorSet;
119    private int mFocusItem = NO_ITEM_SELECTED;
120    private ModeListAnimationEffects mCurrentEffect = null;
121    private ModeListOpenListener mModeListOpenListener;
122    private ModeListVisibilityChangedListener mVisibilityChangedListener;
123    private CameraAppUI.CameraModuleScreenShotProvider mScreenShotProvider = null;
124    private int[] mInputPixels;
125    private int[] mOutputPixels;
126
127    private boolean mAdjustPositionWhenUncoveredPreviewAreaChanges = false;
128    private View mChildViewTouched = null;
129    private MotionEvent mLastChildTouchEvent = null;
130    private int mVisibleWidth = 0;
131
132    // Width and height of this view. They get updated in onLayout()
133    // Unit for width and height are pixels.
134    private int mWidth;
135    private int mHeight;
136    private float mScrollTrendX = 0f;
137    private float mScrollTrendY = 0f;
138    private ModeSwitchListener mModeSwitchListener = null;
139    private ArrayList<Integer> mSupportedModes;
140    private final LinkedList<TimeBasedPosition> mPositionHistory
141            = new LinkedList<TimeBasedPosition>();
142    private long mCurrentTime;
143    private float mVelocityX; // Unit: pixel/ms.
144    private final Animator.AnimatorListener mModeListAnimatorListener =
145            new Animator.AnimatorListener() {
146        private boolean mCancelled = false;
147
148        @Override
149        public void onAnimationStart(Animator animation) {
150            mCancelled = false;
151            setVisibility(VISIBLE);
152        }
153
154        @Override
155        public void onAnimationEnd(Animator animation) {
156            mAnimatorSet = null;
157            if (mCancelled) {
158                return;
159            }
160            if (mState == ACCORDION_ANIMATION || mState == FULLY_HIDDEN) {
161                resetModeSelectors();
162                setVisibility(INVISIBLE);
163                mState = FULLY_HIDDEN;
164            }
165        }
166
167        @Override
168        public void onAnimationCancel(Animator animation) {
169            mCancelled = true;
170        }
171
172        @Override
173        public void onAnimationRepeat(Animator animation) {
174
175        }
176    };
177    private long mLastDownTime = 0;
178
179    /**
180     * Abstract class for animation effects that are specific for mode list.
181     */
182    private abstract class ModeListAnimationEffects extends AnimationEffects {
183        public void onWindowFocusChanged(boolean hasFocus) {
184            // Default to do nothing.
185        }
186
187        /**
188         * Specifies how the UI elements should respond when mode list opens.
189         * Range: [0f, 1f]. 0f means no change in the UI elements other than
190         * mode drawer itself (i.e. No background dimming, etc). 1f means the
191         * change in the surrounding UI elements should stay in sync with the
192         * mode drawer opening.
193         */
194        public float getModeListOpenFactor() {
195            return 1f;
196        }
197
198        /**
199         * Sets the action (i.e. a runnable to run) at the end of the animation
200         * effects.
201         *
202         * @param runnable the action for the end of animation effects.
203         */
204        public abstract void setAnimationEndAction(Runnable runnable);
205    }
206
207    @Override
208    public void onPreviewAreaChanged(RectF previewArea) {
209        mPreviewArea.set(previewArea);
210    }
211
212    private final CameraAppUI.UncoveredPreviewAreaSizeChangedListener
213            mUncoveredPreviewAreaSizeChangedListener =
214            new CameraAppUI.UncoveredPreviewAreaSizeChangedListener() {
215
216                @Override
217                public void uncoveredPreviewAreaChanged(RectF uncoveredPreviewArea) {
218                    mUncoveredPreviewArea.set(uncoveredPreviewArea);
219                    mSettingsButton.uncoveredPreviewAreaChanged(uncoveredPreviewArea);
220                    if (mAdjustPositionWhenUncoveredPreviewAreaChanges) {
221                        mAdjustPositionWhenUncoveredPreviewAreaChanges = false;
222                        centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight());
223                    }
224                }
225            };
226
227    public interface ModeSwitchListener {
228        public void onModeSelected(int modeIndex);
229        public int getCurrentModeIndex();
230        public void onSettingsSelected();
231    }
232
233    public interface ModeListOpenListener {
234        /**
235         * Mode list will open to full screen after current animation.
236         */
237        public void onOpenFullScreen();
238
239        /**
240         * Updates the listener with the current progress of mode drawer opening.
241         *
242         * @param progress progress of the mode drawer opening, ranging [0f, 1f]
243         *                 0 means mode drawer is fully closed, 1 indicates a fully
244         *                 open mode drawer.
245         */
246        public void onModeListOpenProgress(float progress);
247
248        /**
249         * Gets called when mode list is completely closed.
250         */
251        public void onModeListClosed();
252    }
253
254    public static abstract class ModeListVisibilityChangedListener {
255        private Boolean mCurrentVisibility = null;
256
257        /** Whether the mode list is (partially or fully) visible. */
258        public abstract void onVisibilityChanged(boolean visible);
259
260        /**
261         * Internal method to be called by the mode list whenever a visibility
262         * even occurs.
263         * <p>
264         * Do not call {@link #onVisibilityChanged(boolean)} directly, as this
265         * is only called when the visibility has actually changed and not on
266         * each visibility event.
267         *
268         * @param visible whether the mode drawer is currently visible.
269         */
270        private void onVisibilityEvent(boolean visible) {
271            if (mCurrentVisibility == null || mCurrentVisibility != visible) {
272                mCurrentVisibility = visible;
273                onVisibilityChanged(visible);
274            }
275        }
276    }
277
278    /**
279     * This class aims to help store time and position in pairs.
280     */
281    private static class TimeBasedPosition {
282        private final float mPosition;
283        private final long mTimeStamp;
284        public TimeBasedPosition(float position, long time) {
285            mPosition = position;
286            mTimeStamp = time;
287        }
288
289        public float getPosition() {
290            return mPosition;
291        }
292
293        public long getTimeStamp() {
294            return mTimeStamp;
295        }
296    }
297
298    /**
299     * This is a highly customized interpolator. The purpose of having this subclass
300     * is to encapsulate intricate animation timing, so that the actual animation
301     * implementation can be re-used with other interpolators to achieve different
302     * animation effects.
303     *
304     * The accordion animation consists of three stages:
305     * 1) Animate into the screen within a pre-specified fly in duration.
306     * 2) Hold in place for a certain amount of time (Optional).
307     * 3) Animate out of the screen within the given time.
308     *
309     * The accordion animator is initialized with 3 parameter: 1) initial position,
310     * 2) how far out the view should be before flying back out,  3) end position.
311     * The interpolation output should be [0f, 0.5f] during animation between 1)
312     * to 2), and [0.5f, 1f] for flying from 2) to 3).
313     */
314    private final TimeInterpolator mAccordionInterpolator = new TimeInterpolator() {
315        @Override
316        public float getInterpolation(float input) {
317
318            float flyInDuration = (float) FLY_OUT_DURATION_MS / (float) TOTAL_DURATION_MS;
319            float holdDuration = (float) (FLY_OUT_DURATION_MS + HOLD_DURATION_MS)
320                    / (float) TOTAL_DURATION_MS;
321            if (input == 0) {
322                return 0;
323            } else if (input < flyInDuration) {
324                // Stage 1, project result to [0f, 0.5f]
325                input /= flyInDuration;
326                float result = Gusterpolator.INSTANCE.getInterpolation(input);
327                return result * 0.5f;
328            } else if (input < holdDuration) {
329                // Stage 2
330                return 0.5f;
331            } else {
332                // Stage 3, project result to [0.5f, 1f]
333                input -= holdDuration;
334                input /= (1 - holdDuration);
335                float result = Gusterpolator.INSTANCE.getInterpolation(input);
336                return 0.5f + result * 0.5f;
337            }
338        }
339    };
340
341    /**
342     * The listener that is used to notify when gestures occur.
343     * Here we only listen to a subset of gestures.
344     */
345    private final GestureDetector.OnGestureListener mOnGestureListener
346            = new GestureDetector.SimpleOnGestureListener(){
347        @Override
348        public boolean onScroll(MotionEvent e1, MotionEvent e2,
349                                float distanceX, float distanceY) {
350
351            if (mState == ACCORDION_ANIMATION) {
352                if (mCurrentEffect != null) {
353                    // Scroll happens during accordion animation.
354                    mCurrentEffect.cancelAnimation();
355                }
356            } else if (mState == FULLY_HIDDEN) {
357                resetModeSelectors();
358                setVisibility(VISIBLE);
359            }
360            mState = SCROLLING;
361            // Scroll based on the scrolling distance on the currently focused
362            // item.
363            scroll(mFocusItem, distanceX * SCROLL_FACTOR, distanceY * SCROLL_FACTOR);
364            mLastScrollTime = System.currentTimeMillis();
365            return true;
366        }
367
368        @Override
369        public boolean onSingleTapUp(MotionEvent ev) {
370            if (mState != FULLY_SHOWN) {
371                // Only allows tap to choose mode when the list is fully shown
372                return false;
373            }
374
375            // If the tap is not inside the mode drawer area, snap back.
376            if(!isTouchInsideList(ev)) {
377                snapBack(true);
378                return false;
379            }
380            return true;
381        }
382
383        @Override
384        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
385            // Cache velocity in the unit pixel/ms.
386            mVelocityX = velocityX / 1000f * SCROLL_FACTOR;
387            return true;
388        }
389    };
390
391    /**
392     * Gets called when a mode item in the mode drawer is clicked.
393     *
394     * @param selectedItem the item being clicked
395     */
396    private void onItemSelected(ModeSelectorItem selectedItem) {
397
398        final int modeId = selectedItem.getModeId();
399        // Un-highlight all the modes.
400        for (int i = 0; i < mModeSelectorItems.length; i++) {
401            mModeSelectorItems[i].setHighlighted(false);
402            mModeSelectorItems[i].setSelected(false);
403        }
404        // Select the focused item.
405        selectedItem.setSelected(true);
406        mState = MODE_SELECTED;
407        PeepholeAnimationEffect effect = new PeepholeAnimationEffect();
408        effect.setSize(mWidth, mHeight);
409        effect.setAnimationEndAction(new Runnable() {
410            @Override
411            public void run() {
412                setVisibility(INVISIBLE);
413                snapBack(false);
414            }
415        });
416
417        // Calculate the position of the icon in the selected item, and
418        // start animation from that position.
419        int[] location = new int[2];
420        // Gets icon's center position in relative to the window.
421        selectedItem.getIconCenterLocationInWindow(location);
422        int iconX = location[0];
423        int iconY = location[1];
424        // Gets current view's top left position relative to the window.
425        getLocationInWindow(location);
426        // Calculate icon location relative to this view
427        iconX -= location[0];
428        iconY -= location[1];
429
430        effect.setAnimationStartingPosition(iconX, iconY);
431        if (mScreenShotProvider != null) {
432            effect.setBackground(mScreenShotProvider
433                    .getPreviewFrame(PREVIEW_DOWN_SAMPLE_FACTOR), mPreviewArea);
434            effect.setBackgroundOverlay(mScreenShotProvider.getPreviewOverlayAndControls());
435        }
436        mCurrentEffect = effect;
437        invalidate();
438
439        // Post mode selection runnable to the end of the message queue
440        // so that current UI changes can finish before mode initialization
441        // clogs up UI thread.
442        post(new Runnable() {
443            @Override
444            public void run() {
445                onModeSelected(modeId);
446            }
447        });
448    }
449
450    /**
451     * Checks whether a touch event is inside of the bounds of the mode list.
452     *
453     * @param ev touch event to be checked
454     * @return whether the touch is inside the bounds of the mode list
455     */
456    private boolean isTouchInsideList(MotionEvent ev) {
457        // Ignore the tap if it happens outside of the mode list linear layout.
458        float x = ev.getX() - mListView.getX();
459        float y = ev.getY() - mListView.getY();
460        if (x < 0 || x > mListView.getWidth() || y < 0 || y > mListView.getHeight()) {
461            return false;
462        }
463        return true;
464    }
465
466    public ModeListView(Context context, AttributeSet attrs) {
467        super(context, attrs);
468        mGestureDetector = new GestureDetector(context, mOnGestureListener);
469        mListBackgroundColor = getResources().getColor(R.color.mode_list_background);
470    }
471
472    public CameraAppUI.UncoveredPreviewAreaSizeChangedListener
473            getUncoveredPreviewAreaSizeChangedListener() {
474        return mUncoveredPreviewAreaSizeChangedListener;
475    }
476
477    /**
478     * Sets the alpha on the list background. This is called whenever the list
479     * is scrolling or animating, so that background can adjust its dimness.
480     *
481     * @param alpha new alpha to be applied on list background color
482     */
483    private void setBackgroundAlpha(int alpha) {
484        // Make sure alpha is valid.
485        alpha = alpha & 0xFF;
486        // Change alpha on the background color.
487        mListBackgroundColor = mListBackgroundColor & 0xFFFFFF;
488        mListBackgroundColor = mListBackgroundColor | (alpha << 24);
489        // Set new color to list background.
490        setBackgroundColor(mListBackgroundColor);
491    }
492
493    /**
494     * Initialize mode list with a list of indices of supported modes.
495     *
496     * @param modeIndexList a list of indices of supported modes
497     */
498    public void init(List<Integer> modeIndexList) {
499        int[] modeSequence = getResources()
500                .getIntArray(R.array.camera_modes_in_nav_drawer_if_supported);
501        int[] visibleModes = getResources()
502                .getIntArray(R.array.camera_modes_always_visible);
503
504        // Mark the supported modes in a boolean array to preserve the
505        // sequence of the modes
506        SparseArray<Boolean> modeIsSupported = new SparseArray<Boolean>();
507        for (int i = 0; i < modeIndexList.size(); i++) {
508            int mode = modeIndexList.get(i);
509            modeIsSupported.put(mode, true);
510        }
511        for (int i = 0; i < visibleModes.length; i++) {
512            int mode = visibleModes[i];
513            modeIsSupported.put(mode, true);
514        }
515
516        // Put the indices of supported modes into an array preserving their
517        // display order.
518        mSupportedModes = new ArrayList<Integer>();
519        for (int i = 0; i < modeSequence.length; i++) {
520            int mode = modeSequence[i];
521            if (modeIsSupported.get(mode, false)) {
522                mSupportedModes.add(mode);
523            }
524        }
525        mTotalModes = mSupportedModes.size();
526        initializeModeSelectorItems();
527        mSettingsButton = (SettingsButton) findViewById(R.id.settings_button);
528        mSettingsButton.setOnClickListener(new OnClickListener() {
529            @Override
530            public void onClick(View v) {
531                // Post this callback to make sure current user interaction has
532                // been reflected in the UI. Specifically, the pressed state gets
533                // unset after click happens. In order to ensure the pressed state
534                // gets unset in UI before getting in the low frame rate settings
535                // activity launch stage, the settings selected callback is posted.
536                post(new Runnable() {
537                    @Override
538                    public void run() {
539                        mModeSwitchListener.onSettingsSelected();
540                    }
541                });
542            }
543        });
544        // The mode list is initialized to be all the way closed.
545        onModeListOpenRatioUpdate(0);
546    }
547
548    /**
549     * Sets the screen shot provider for getting a preview frame and a bitmap
550     * of the controls and overlay.
551     */
552    public void setCameraModuleScreenShotProvider(
553            CameraAppUI.CameraModuleScreenShotProvider provider) {
554        mScreenShotProvider = provider;
555    }
556
557    private void initializeModeSelectorItems() {
558        mModeSelectorItems = new ModeSelectorItem[mTotalModes];
559        // Inflate the mode selector items and add them to a linear layout
560        LayoutInflater inflater = (LayoutInflater) getContext()
561                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
562        mListView = (LinearLayout) findViewById(R.id.mode_list);
563        for (int i = 0; i < mTotalModes; i++) {
564            final ModeSelectorItem selectorItem =
565                    (ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null);
566            mListView.addView(selectorItem);
567            // Sets the top padding of the top item to 0.
568            if (i == 0) {
569                selectorItem.setPadding(selectorItem.getPaddingLeft(), 0,
570                        selectorItem.getPaddingRight(), selectorItem.getPaddingBottom());
571            }
572            // Sets the bottom padding of the bottom item to 0.
573            if (i == mTotalModes - 1) {
574                selectorItem.setPadding(selectorItem.getPaddingLeft(), selectorItem.getPaddingTop(),
575                        selectorItem.getPaddingRight(), 0);
576            }
577
578            int modeId = getModeIndex(i);
579            selectorItem.setHighlightColor(getResources()
580                    .getColor(CameraUtil.getCameraThemeColorId(modeId, getContext())));
581
582            // Set image
583            selectorItem.setImageResource(CameraUtil.getCameraModeIconResId(modeId, getContext()));
584
585            // Set text
586            selectorItem.setText(CameraUtil.getCameraModeText(modeId, getContext()));
587
588            // Set content description (for a11y)
589            selectorItem.setContentDescription(CameraUtil
590                    .getCameraModeContentDescription(modeId, getContext()));
591            selectorItem.setModeId(modeId);
592            selectorItem.setOnClickListener(new OnClickListener() {
593                @Override
594                public void onClick(View v) {
595                    onItemSelected(selectorItem);
596                }
597            });
598
599            mModeSelectorItems[i] = selectorItem;
600        }
601        // During drawer opening/closing, we change the visible width of the mode
602        // items in sequence, so we listen to the last item's visible width change
603        // for a good timing to do corresponding UI adjustments.
604        mModeSelectorItems[mTotalModes - 1].setVisibleWidthChangedListener(this);
605        resetModeSelectors();
606    }
607
608    /**
609     * Maps between the UI mode selector index to the actual mode id.
610     *
611     * @param modeSelectorIndex the index of the UI item
612     * @return the index of the corresponding camera mode
613     */
614    private int getModeIndex(int modeSelectorIndex) {
615        if (modeSelectorIndex < mTotalModes && modeSelectorIndex >= 0) {
616            return mSupportedModes.get(modeSelectorIndex);
617        }
618        Log.e(TAG, "Invalid mode selector index: " + modeSelectorIndex + ", total modes: "
619                + mTotalModes);
620        return getResources().getInteger(R.integer.camera_mode_photo);
621    }
622
623    /** Notify ModeSwitchListener, if any, of the mode change. */
624    private void onModeSelected(int modeIndex) {
625        if (mModeSwitchListener != null) {
626            mModeSwitchListener.onModeSelected(modeIndex);
627        }
628    }
629
630    /**
631     * Sets a listener that listens to receive mode switch event.
632     *
633     * @param listener a listener that gets notified when mode changes.
634     */
635    public void setModeSwitchListener(ModeSwitchListener listener) {
636        mModeSwitchListener = listener;
637    }
638
639    /**
640     * Sets a listener that gets notified when the mode list is open full screen.
641     *
642     * @param listener a listener that listens to mode list open events
643     */
644    public void setModeListOpenListener(ModeListOpenListener listener) {
645        mModeListOpenListener = listener;
646    }
647
648    /**
649     * Sets or replaces a listener that is called when the visibility of the
650     * mode list changed.
651     */
652    public void setVisibilityChangedListener(ModeListVisibilityChangedListener listener) {
653        mVisibilityChangedListener = listener;
654    }
655
656    @Override
657    public boolean onTouchEvent(MotionEvent ev) {
658        if (mCurrentEffect != null && mCurrentEffect.onTouchEvent(ev)) {
659            return true;
660        }
661
662        if (mState == ACCORDION_ANIMATION && MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
663            // If shimmy is on-going, reject the first down event, so that it can be handled
664            // by the view underneath. If a swipe is detected, the same series of touch will
665            // re-enter this function, in which case we will consume the touch events.
666            if (mLastDownTime != ev.getDownTime()) {
667                mLastDownTime = ev.getDownTime();
668                return false;
669            }
670        }
671        super.onTouchEvent(ev);
672        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
673            mVelocityX = 0;
674            if (mState == FULLY_SHOWN) {
675                mFocusItem = NO_ITEM_SELECTED;
676                setSwipeMode(false);
677                // If the down event happens inside the mode list, find out which
678                // mode item is being touched and forward all the subsequent touch
679                // events to that mode item for its pressed state and click handling.
680                if (isTouchInsideList(ev)) {
681                    mChildViewTouched = mModeSelectorItems[getFocusItem(ev.getX(), ev.getY())];
682                }
683
684            } else {
685                mFocusItem = getFocusItem(ev.getX(), ev.getY());
686                setSwipeMode(true);
687            }
688        } else if (mState == ACCORDION_ANIMATION) {
689            // This is a swipe during accordion animation
690            mFocusItem = getFocusItem(ev.getX(), ev.getY());
691            setSwipeMode(true);
692
693        }
694        forwardTouchEventToChild(ev);
695        // Pass all touch events to gesture detector for gesture handling.
696        mGestureDetector.onTouchEvent(ev);
697        if (ev.getActionMasked() == MotionEvent.ACTION_UP ||
698                ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
699            snap();
700            mFocusItem = NO_ITEM_SELECTED;
701            // Reset the touch forward recipient at the end of a touch event series,
702            // i.e. when an up or a cancel event is received.
703            mChildViewTouched = null;
704        }
705        return true;
706    }
707
708    /**
709     * Forward touch events to a recipient child view. Before feeding the motion
710     * event into the child view, the event needs to be converted in child view's
711     * coordinates.
712     */
713    private void forwardTouchEventToChild(MotionEvent ev) {
714        if (mChildViewTouched != null) {
715            float x = ev.getX() - mListView.getX();
716            float y = ev.getY() - mListView.getY();
717            x -= mChildViewTouched.getLeft();
718            y -= mChildViewTouched.getTop();
719
720            mLastChildTouchEvent = MotionEvent.obtain(ev);
721            mLastChildTouchEvent.setLocation(x, y);
722            mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
723        }
724    }
725
726    /**
727     * Sets the swipe mode to indicate whether this is a swiping in
728     * or out, and therefore we can have different animations.
729     *
730     * @param swipeIn indicates whether the swipe should reveal/hide the list.
731     */
732    private void setSwipeMode(boolean swipeIn) {
733        for (int i = 0 ; i < mModeSelectorItems.length; i++) {
734            mModeSelectorItems[i].onSwipeModeChanged(swipeIn);
735        }
736    }
737
738    @Override
739    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
740        super.onLayout(changed, left, top, right, bottom);
741        mWidth = right - left;
742        mHeight = bottom - top - getPaddingTop() - getPaddingBottom();
743        if (mCurrentEffect != null) {
744            mCurrentEffect.setSize(mWidth, mHeight);
745        }
746    }
747
748    /**
749     * Here we calculate the children size based on the orientation, change
750     * their layout parameters if needed before propagating onMeasure call
751     * to the children, so the newly changed params will take effect in this
752     * pass.
753     *
754     * @param widthMeasureSpec Horizontal space requirements as imposed by the
755     *        parent
756     * @param heightMeasureSpec Vertical space requirements as imposed by the
757     *        parent
758     */
759    @Override
760    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
761        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
762        centerModeDrawerInUncoveredPreview(MeasureSpec.getSize(widthMeasureSpec),
763                MeasureSpec.getSize(heightMeasureSpec));
764    }
765
766    @Override
767    public void draw(Canvas canvas) {
768        if (mCurrentEffect != null) {
769            mCurrentEffect.drawBackground(canvas);
770            super.draw(canvas);
771            mCurrentEffect.drawForeground(canvas);
772        } else {
773            super.draw(canvas);
774        }
775    }
776
777    /**
778     * This shows the mode switcher and starts the accordion animation with a delay.
779     * If the view does not currently have focus, (e.g. There are popups on top of
780     * it.) start the delayed accordion animation when it gains focus. Otherwise,
781     * start the animation with a delay right away.
782     */
783    public void showModeSwitcherHint() {
784        if (mState != FULLY_HIDDEN) {
785            return;
786        }
787        mState = ACCORDION_ANIMATION;
788        mCurrentEffect = new ShimmyAnimationEffects();
789        mCurrentEffect.startAnimation();
790    }
791
792    /**
793     * Resets the visible width of all the mode selectors to 0.
794     */
795    private void resetModeSelectors() {
796        for (int i = 0; i < mModeSelectorItems.length; i++) {
797            mModeSelectorItems[i].setVisibleWidth(0);
798        }
799    }
800
801    private boolean isRunningAccordionAnimation() {
802        return mAnimatorSet != null && mAnimatorSet.isRunning();
803    }
804
805    /**
806     * Calculate the mode selector item in the list that is at position (x, y).
807     * If the position is above the top item or below the bottom item, return
808     * the top item or bottom item respectively.
809     *
810     * @param x horizontal position
811     * @param y vertical position
812     * @return index of the item that is at position (x, y)
813     */
814    private int getFocusItem(float x, float y) {
815        // Convert coordinates into child view's coordinates.
816        x -= mListView.getX();
817        y -= mListView.getY();
818
819        for (int i = 0; i < mModeSelectorItems.length; i++) {
820            if (y <= mModeSelectorItems[i].getBottom()) {
821                return i;
822            }
823        }
824        return mModeSelectorItems.length - 1;
825    }
826
827    @Override
828    public void onWindowFocusChanged(boolean hasFocus) {
829        super.onWindowFocusChanged(hasFocus);
830        if (mCurrentEffect != null) {
831            mCurrentEffect.onWindowFocusChanged(hasFocus);
832        }
833    }
834
835    @Override
836    public void onVisibilityChanged(View v, int visibility) {
837        super.onVisibilityChanged(v, visibility);
838        if (visibility == VISIBLE) {
839            centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight());
840            // Highlight current module
841            if (mModeSwitchListener != null) {
842                int modeId = mModeSwitchListener.getCurrentModeIndex();
843                int parentMode = CameraUtil.getCameraModeParentModeId(modeId, getContext());
844                // Find parent mode in the nav drawer.
845                for (int i = 0; i < mSupportedModes.size(); i++) {
846                    if (mSupportedModes.get(i) == parentMode) {
847                        mModeSelectorItems[i].setSelected(true);
848                    }
849                }
850            }
851        } else {
852            if (mModeSelectorItems != null) {
853                // When becoming invisible/gone after initializing mode selector items.
854                for (int i = 0; i < mModeSelectorItems.length; i++) {
855                    mModeSelectorItems[i].setHighlighted(false);
856                    mModeSelectorItems[i].setSelected(false);
857                }
858            }
859            if (mModeListOpenListener != null) {
860                mModeListOpenListener.onModeListClosed();
861            }
862        }
863        if (mVisibilityChangedListener != null) {
864            mVisibilityChangedListener.onVisibilityEvent(getVisibility() == VISIBLE);
865        }
866    }
867
868    /**
869     * Center mode drawer in the portion of camera preview that is not covered by
870     * bottom bar.
871     */
872    // TODO: Combine SettingsButton logic into here if UX design does not change
873    // for another week.
874    private void centerModeDrawerInUncoveredPreview(int measuredWidth, int measuredHeight) {
875
876        // Assuming the preview is centered in the space aside from bottom bar.
877        float previewAreaWidth = mUncoveredPreviewArea.right + mUncoveredPreviewArea.left;
878        float previewAreaHeight = mUncoveredPreviewArea.top + mUncoveredPreviewArea.bottom;
879        if (measuredWidth > measuredHeight && previewAreaWidth < previewAreaHeight
880                || measuredWidth < measuredHeight && previewAreaWidth > previewAreaHeight) {
881            // Cached preview area is stale, update mode drawer position on next
882            // layout pass.
883            mAdjustPositionWhenUncoveredPreviewAreaChanges = true;
884        } else {
885            // Align left:
886            mListView.setTranslationX(mUncoveredPreviewArea.left);
887            // Align center vertical:
888            mListView.setTranslationY(mUncoveredPreviewArea.centerY()
889                    - mListView.getMeasuredHeight() / 2);
890        }
891    }
892
893    private void scroll(int itemId, float deltaX, float deltaY) {
894        // Scrolling trend on X and Y axis, to track the trend by biasing
895        // towards latest touch events.
896        mScrollTrendX = mScrollTrendX * 0.3f + deltaX * 0.7f;
897        mScrollTrendY = mScrollTrendY * 0.3f + deltaY * 0.7f;
898
899        // TODO: Change how the curve is calculated below when UX finalize their design.
900        mCurrentTime = SystemClock.uptimeMillis();
901        float longestWidth;
902        if (itemId != NO_ITEM_SELECTED) {
903            longestWidth = mModeSelectorItems[itemId].getVisibleWidth();
904        } else {
905            longestWidth = mModeSelectorItems[0].getVisibleWidth();
906        }
907        float newPosition = longestWidth - deltaX;
908        int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
909        newPosition = Math.min(newPosition, getMaxMovementBasedOnPosition((int) longestWidth,
910                maxVisibleWidth));
911        newPosition = Math.max(newPosition, 0);
912        insertNewPosition(newPosition, mCurrentTime);
913
914        for (int i = 0; i < mModeSelectorItems.length; i++) {
915            mModeSelectorItems[i].setVisibleWidth(calculateVisibleWidthForItem(i,
916                    (int) newPosition));
917        }
918    }
919
920    /**
921     * Calculate the width of a specified item based on its position relative to
922     * the item with longest width.
923     */
924    private int calculateVisibleWidthForItem(int itemId, int longestWidth) {
925        if (itemId == mFocusItem || mFocusItem == NO_ITEM_SELECTED) {
926            return longestWidth;
927        }
928
929        int delay = Math.abs(itemId - mFocusItem) * DELAY_MS;
930        return (int) getPosition(mCurrentTime - delay,
931                mModeSelectorItems[itemId].getVisibleWidth());
932    }
933
934    /**
935     * Insert new position and time stamp into the history position list, and
936     * remove stale position items.
937     *
938     * @param position latest position of the focus item
939     * @param time  current time in milliseconds
940     */
941    private void insertNewPosition(float position, long time) {
942        // TODO: Consider re-using stale position objects rather than
943        // always creating new position objects.
944        mPositionHistory.add(new TimeBasedPosition(position, time));
945
946        // Positions that are from too long ago will not be of any use for
947        // future position interpolation. So we need to remove those positions
948        // from the list.
949        long timeCutoff = time - (mTotalModes - 1) * DELAY_MS;
950        while (mPositionHistory.size() > 0) {
951            // Remove all the position items that are prior to the cutoff time.
952            TimeBasedPosition historyPosition = mPositionHistory.getFirst();
953            if (historyPosition.getTimeStamp() < timeCutoff) {
954                mPositionHistory.removeFirst();
955            } else {
956                break;
957            }
958        }
959    }
960
961    /**
962     * Gets the interpolated position at the specified time. This involves going
963     * through the recorded positions until a {@link TimeBasedPosition} is found
964     * such that the position the recorded before the given time, and the
965     * {@link TimeBasedPosition} after that is recorded no earlier than the given
966     * time. These two positions are then interpolated to get the position at the
967     * specified time.
968     */
969    private float getPosition(long time, float currentPosition) {
970        int i;
971        for (i = 0; i < mPositionHistory.size(); i++) {
972            TimeBasedPosition historyPosition = mPositionHistory.get(i);
973            if (historyPosition.getTimeStamp() > time) {
974                // Found the winner. Now interpolate between position i and position i - 1
975                if (i == 0) {
976                    // Slowly approaching to the destination if there isn't enough data points
977                    float weight = 0.2f;
978                    return historyPosition.getPosition() * weight + (1f - weight) * currentPosition;
979                } else {
980                    TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1);
981                    // Start interpolation
982                    float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) /
983                            (float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp());
984                    float position = fraction * (historyPosition.getPosition()
985                            - prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition();
986                    return position;
987                }
988            }
989        }
990        // It should never get here.
991        Log.e(TAG, "Invalid time input for getPosition(). time: " + time);
992        if (mPositionHistory.size() == 0) {
993            Log.e(TAG, "TimeBasedPosition history size is 0");
994        } else {
995            Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp()
996            + " , last position recorded at " + mPositionHistory.getLast().getTimeStamp());
997        }
998        assert (i < mPositionHistory.size());
999        return i;
1000    }
1001
1002    private void reset() {
1003        resetModeSelectors();
1004        mScrollTrendX = 0f;
1005        mScrollTrendY = 0f;
1006        mCurrentEffect = null;
1007        setVisibility(INVISIBLE);
1008    }
1009
1010    /**
1011     * When visible width of list is changed, the background of the list needs
1012     * to darken/lighten correspondingly.
1013     */
1014    public void onVisibleWidthChanged(int visibleWidth) {
1015        mVisibleWidth = visibleWidth;
1016        float factor = 1f;
1017        if (mCurrentEffect != null) {
1018            factor = mCurrentEffect.getModeListOpenFactor();
1019        }
1020
1021        // When the longest mode item is entirely shown (across the screen), the
1022        // background should be 50% transparent.
1023        int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
1024        visibleWidth = Math.min(maxVisibleWidth, visibleWidth);
1025        if (visibleWidth != maxVisibleWidth) {
1026            // No longer full screen.
1027            cancelForwardingTouchEvent();
1028        }
1029        float openRatio = (float) visibleWidth / maxVisibleWidth;
1030        onModeListOpenRatioUpdate(openRatio * factor);
1031    }
1032
1033    /**
1034     * Gets called when UI elements such as background and gear icon need to adjust
1035     * their appearance based on the percentage of the mode list opening.
1036     *
1037     * @param openRatio percentage of the mode list opening, ranging [0f, 1f]
1038     */
1039    private void onModeListOpenRatioUpdate(float openRatio) {
1040        for (int i = 0; i < mModeSelectorItems.length; i++) {
1041            mModeSelectorItems[i].setTextAlpha(openRatio);
1042        }
1043        setBackgroundAlpha((int) (BACKGROUND_TRANSPARENTCY * openRatio));
1044        if (mModeListOpenListener != null) {
1045            mModeListOpenListener.onModeListOpenProgress(openRatio);
1046        }
1047        if (mSettingsButton != null) {
1048            mSettingsButton.setAlpha(openRatio);
1049        }
1050    }
1051
1052    /**
1053     * Cancels the touch event forwarding by sending a cancel event to the recipient
1054     * view and resetting the touch forward recipient to ensure no more events
1055     * can be forwarded in the current series of the touch events.
1056     */
1057    private void cancelForwardingTouchEvent() {
1058        if (mChildViewTouched != null) {
1059            mLastChildTouchEvent.setAction(MotionEvent.ACTION_CANCEL);
1060            mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
1061            mChildViewTouched = null;
1062        }
1063    }
1064
1065    @Override
1066    public void onWindowVisibilityChanged(int visibility) {
1067        super.onWindowVisibilityChanged(visibility);
1068        if (visibility != VISIBLE) {
1069            // Reset mode list if the window is no longer visible.
1070            reset();
1071            mState = FULLY_HIDDEN;
1072        }
1073    }
1074
1075    /**
1076     * The list view should either snap back or snap to full screen after a gesture.
1077     * This function is called when an up or cancel event is received, and then based
1078     * on the current position of the list and the gesture we can decide which way
1079     * to snap.
1080     */
1081    private void snap() {
1082        if (mState == SCROLLING) {
1083            int itemId = Math.max(0, mFocusItem);
1084            if (mModeSelectorItems[itemId].getVisibleWidth()
1085                    < mModeSelectorItems[itemId].getMaxVisibleWidth() * SNAP_BACK_THRESHOLD_RATIO) {
1086                snapBack();
1087            } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) {
1088                snapBack();
1089            } else {
1090                snapToFullScreen();
1091            }
1092        }
1093    }
1094
1095    /**
1096     * Snaps back out of the screen.
1097     *
1098     * @param withAnimation whether snapping back should be animated
1099     */
1100    public void snapBack(boolean withAnimation) {
1101        if (withAnimation) {
1102            if (mVelocityX > -VELOCITY_THRESHOLD * SCROLL_FACTOR) {
1103                animateListToWidth(0);
1104            } else {
1105                animateListToWidthAtVelocity(mVelocityX, 0);
1106            }
1107            mState = FULLY_HIDDEN;
1108        } else {
1109            setVisibility(INVISIBLE);
1110            resetModeSelectors();
1111            mState = FULLY_HIDDEN;
1112        }
1113    }
1114
1115    /**
1116     * Snaps the mode list back out with animation.
1117     */
1118    private void snapBack() {
1119        snapBack(true);
1120    }
1121
1122    private void snapToFullScreen() {
1123        int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
1124        int fullWidth = mModeSelectorItems[focusItem].getMaxVisibleWidth();
1125        if (mVelocityX <= VELOCITY_THRESHOLD) {
1126            animateListToWidth(fullWidth);
1127        } else {
1128            // If the fling velocity exceeds this threshold, snap to full screen
1129            // at a constant speed.
1130            animateListToWidthAtVelocity(VELOCITY_THRESHOLD, fullWidth);
1131        }
1132        mState = FULLY_SHOWN;
1133        if (mModeListOpenListener != null) {
1134            mModeListOpenListener.onOpenFullScreen();
1135        }
1136    }
1137
1138    /**
1139     * Overloaded function to provide a simple way to start animation. Animation
1140     * will use default duration, and a value of <code>null</code> for interpolator
1141     * means linear interpolation will be used.
1142     *
1143     * @param width a set of values that the animation will animate between over time
1144     */
1145    private void animateListToWidth(int... width) {
1146        animateListToWidth(0, DEFAULT_DURATION_MS, null, width);
1147    }
1148
1149    /**
1150     * Animate the mode list between the given set of visible width.
1151     *
1152     * @param delay start delay between consecutive mode item. If delay < 0, the
1153     *              leader in the animation will be the bottom item.
1154     * @param duration duration for the animation of each mode item
1155     * @param interpolator interpolator to be used by the animation
1156     * @param width a set of values that the animation will animate between over time
1157     */
1158    private Animator animateListToWidth(int delay, int duration,
1159                                    TimeInterpolator interpolator, int... width) {
1160        if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
1161            mAnimatorSet.end();
1162        }
1163
1164        ArrayList<Animator> animators = new ArrayList<Animator>();
1165        boolean animateModeItemsInOrder = true;
1166        if (delay < 0) {
1167            animateModeItemsInOrder = false;
1168            delay *= -1;
1169        }
1170        int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
1171        for (int i = 0; i < mTotalModes; i++) {
1172            ObjectAnimator animator;
1173            if (animateModeItemsInOrder) {
1174                animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
1175                    "visibleWidth", width);
1176            } else {
1177                animator = ObjectAnimator.ofInt(mModeSelectorItems[mTotalModes - 1 -i],
1178                        "visibleWidth", width);
1179            }
1180            animator.setDuration(duration);
1181            animator.setStartDelay(i * delay);
1182            animators.add(animator);
1183        }
1184
1185        mAnimatorSet = new AnimatorSet();
1186        mAnimatorSet.playTogether(animators);
1187        mAnimatorSet.setInterpolator(interpolator);
1188        mAnimatorSet.addListener(mModeListAnimatorListener);
1189        mAnimatorSet.start();
1190
1191        return mAnimatorSet;
1192    }
1193
1194    /**
1195     * Animate the mode list to the given width at a constant velocity.
1196     *
1197     * @param velocity the velocity that animation will be at
1198     * @param width final width of the list
1199     */
1200    private void animateListToWidthAtVelocity(float velocity, int width) {
1201        if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
1202            mAnimatorSet.end();
1203        }
1204
1205        ArrayList<Animator> animators = new ArrayList<Animator>();
1206        int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
1207        for (int i = 0; i < mTotalModes; i++) {
1208            ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
1209                    "visibleWidth", width);
1210            int duration = (int) (width / velocity);
1211            animator.setDuration(duration);
1212            animators.add(animator);
1213        }
1214
1215        mAnimatorSet = new AnimatorSet();
1216        mAnimatorSet.playTogether(animators);
1217        mAnimatorSet.setInterpolator(null);
1218        mAnimatorSet.addListener(mModeListAnimatorListener);
1219        mAnimatorSet.start();
1220    }
1221
1222    /**
1223     * Called when the back key is pressed.
1224     *
1225     * @return Whether the UI responded to the key event.
1226     */
1227    public boolean onBackPressed() {
1228        if (mState == FULLY_SHOWN) {
1229            snapBack();
1230            return true;
1231        } else {
1232            return false;
1233        }
1234    }
1235
1236    public void startModeSelectionAnimation() {
1237        if (mState != MODE_SELECTED || mCurrentEffect == null) {
1238            setVisibility(INVISIBLE);
1239            snapBack(false);
1240            mCurrentEffect = null;
1241        } else {
1242            mCurrentEffect.startAnimation();
1243        }
1244
1245    }
1246
1247    public float getMaxMovementBasedOnPosition(int lastVisibleWidth, int maxWidth) {
1248        int timeElapsed = (int) (System.currentTimeMillis() - mLastScrollTime);
1249        if (timeElapsed > SCROLL_INTERVAL_MS) {
1250            timeElapsed = SCROLL_INTERVAL_MS;
1251        }
1252        float position;
1253        int slowZone = (int) (maxWidth * SLOW_ZONE_PERCENTAGE);
1254        if (lastVisibleWidth < (maxWidth - slowZone)) {
1255            position = VELOCITY_THRESHOLD * (float) timeElapsed + lastVisibleWidth;
1256        } else {
1257            float percentageIntoSlowZone = (lastVisibleWidth - (maxWidth - slowZone)) / slowZone;
1258            float velocity = (1 - percentageIntoSlowZone) * VELOCITY_THRESHOLD;
1259            position = velocity * (float) timeElapsed + lastVisibleWidth;
1260        }
1261        position = Math.min(maxWidth, position);
1262        return position;
1263    }
1264
1265    private class PeepholeAnimationEffect extends ModeListAnimationEffects {
1266
1267        private final static int UNSET = -1;
1268        private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 300;
1269
1270        private final Paint mMaskPaint = new Paint();
1271        private final Paint mBackgroundPaint = new Paint();
1272        private final RectF mBackgroundDrawArea = new RectF();
1273
1274        private int mWidth;
1275        private int mHeight;
1276        private int mPeepHoleCenterX = UNSET;
1277        private int mPeepHoleCenterY = UNSET;
1278        private float mRadius = 0f;
1279        private ValueAnimator mPeepHoleAnimator;
1280        private Runnable mEndAction;
1281        private Bitmap mBackground;
1282        private Bitmap mBlurredBackground;
1283        private Bitmap mBackgroundOverlay;
1284
1285        public PeepholeAnimationEffect() {
1286            mMaskPaint.setAlpha(0);
1287            mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
1288        }
1289
1290        @Override
1291        public void setSize(int width, int height) {
1292            mWidth = width;
1293            mHeight = height;
1294        }
1295
1296        @Override
1297        public boolean onTouchEvent(MotionEvent event) {
1298            return true;
1299        }
1300
1301        @Override
1302        public void drawForeground(Canvas canvas) {
1303            // Draw the circle in clear mode
1304            if (mPeepHoleAnimator != null) {
1305                // Draw a transparent circle using clear mode
1306                canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint);
1307            }
1308        }
1309
1310        public void setAnimationStartingPosition(int x, int y) {
1311            mPeepHoleCenterX = x;
1312            mPeepHoleCenterY = y;
1313        }
1314
1315        /**
1316         * Sets the bitmap to be drawn in the background and the drawArea to draw
1317         * the bitmap. In the meantime, start processing the image in a background
1318         * thread to get a blurred background image.
1319         *
1320         * @param background image to be drawn in the background
1321         * @param drawArea area to draw the background image
1322         */
1323        public void setBackground(Bitmap background, RectF drawArea) {
1324            mBackground = background;
1325            mBackgroundDrawArea.set(drawArea);
1326            new BlurTask().execute(Bitmap.createScaledBitmap(background, background.getWidth(),
1327                    background.getHeight(), true));
1328        }
1329
1330        /**
1331         * Sets the overlay image to be drawn on top of the background.
1332         */
1333        public void setBackgroundOverlay(Bitmap overlay) {
1334            mBackgroundOverlay = overlay;
1335        }
1336
1337        /**
1338         * This gets called when a blurred image of the background is generated.
1339         * Start an animation to fade in the blur.
1340         *
1341         * @param blur blurred image of the background.
1342         */
1343        public void setBlurredBackground(Bitmap blur) {
1344            mBlurredBackground = blur;
1345            // Start fade in.
1346            ObjectAnimator alpha = ObjectAnimator.ofInt(mBackgroundPaint, "alpha", 80, 255);
1347            alpha.setDuration(250);
1348            alpha.setInterpolator(Gusterpolator.INSTANCE);
1349            alpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1350                @Override
1351                public void onAnimationUpdate(ValueAnimator animation) {
1352                    invalidate();
1353                }
1354            });
1355            alpha.start();
1356            invalidate();
1357        }
1358
1359        @Override
1360        public void drawBackground(Canvas canvas) {
1361            if (mBackground != null && mBackgroundOverlay != null) {
1362                canvas.drawARGB(255, 0, 0, 0);
1363                canvas.drawBitmap(mBackground, null, mBackgroundDrawArea, null);
1364                if (mBlurredBackground != null) {
1365                    canvas.drawBitmap(mBlurredBackground, null, mBackgroundDrawArea, mBackgroundPaint);
1366                }
1367                canvas.drawBitmap(mBackgroundOverlay, 0, 0, null);
1368            }
1369        }
1370
1371        @Override
1372        public void startAnimation() {
1373            if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) {
1374                return;
1375            }
1376            if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) {
1377                mPeepHoleCenterX = mWidth / 2;
1378                mPeepHoleCenterY = mHeight / 2;
1379            }
1380
1381            int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX);
1382            int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY);
1383            int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge
1384                    + verticalDistanceToFarEdge * verticalDistanceToFarEdge));
1385            int startRadius = getResources().getDimensionPixelSize(
1386                    R.dimen.mode_selector_icon_block_width) / 2;
1387
1388            mPeepHoleAnimator = ValueAnimator.ofFloat(0, endRadius);
1389            mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS);
1390            mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE);
1391            mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1392                @Override
1393                public void onAnimationUpdate(ValueAnimator animation) {
1394                    // Modify mask by enlarging the hole
1395                    mRadius = (Float) mPeepHoleAnimator.getAnimatedValue();
1396                    invalidate();
1397                }
1398            });
1399
1400            mPeepHoleAnimator.addListener(new Animator.AnimatorListener() {
1401                @Override
1402                public void onAnimationStart(Animator animation) {
1403
1404                }
1405
1406                @Override
1407                public void onAnimationEnd(Animator animation) {
1408                    endAnimation();
1409                }
1410
1411                @Override
1412                public void onAnimationCancel(Animator animation) {
1413
1414                }
1415
1416                @Override
1417                public void onAnimationRepeat(Animator animation) {
1418
1419                }
1420            });
1421            mPeepHoleAnimator.start();
1422        }
1423
1424        @Override
1425        public void endAnimation() {
1426            if (mEndAction != null) {
1427                post(mEndAction);
1428                mEndAction = null;
1429                post(new Runnable() {
1430                    @Override
1431                    public void run() {
1432                        mPeepHoleAnimator = null;
1433                        mRadius = 0;
1434                        mPeepHoleCenterX = UNSET;
1435                        mPeepHoleCenterY = UNSET;
1436                        mCurrentEffect = null;
1437                    }
1438                });
1439            } else {
1440                mPeepHoleAnimator = null;
1441                mRadius = 0;
1442                mPeepHoleCenterX = UNSET;
1443                mPeepHoleCenterY = UNSET;
1444                mCurrentEffect = null;
1445            }
1446        }
1447
1448        @Override
1449        public void setAnimationEndAction(Runnable runnable) {
1450            mEndAction = runnable;
1451        }
1452
1453        private class BlurTask extends AsyncTask<Bitmap, Integer, Bitmap> {
1454
1455            // Gaussian blur mask size.
1456            private static final int MASK_SIZE = 7;
1457            @Override
1458            protected Bitmap doInBackground(Bitmap... params) {
1459
1460                Bitmap intermediateBitmap = params[0];
1461                int factor = 4;
1462                Bitmap lowResPreview = Bitmap.createScaledBitmap(intermediateBitmap,
1463                        intermediateBitmap.getWidth() / factor,
1464                        intermediateBitmap.getHeight() / factor, true);
1465
1466                int width = lowResPreview.getWidth();
1467                int height = lowResPreview.getHeight();
1468
1469                if (mInputPixels == null || mInputPixels.length < width * height) {
1470                    mInputPixels = new int[width * height];
1471                    mOutputPixels = new int[width * height];
1472                }
1473                lowResPreview.getPixels(mInputPixels, 0, width, 0, 0, width, height);
1474                CameraUtil.blur(mInputPixels, mOutputPixels, width, height, MASK_SIZE);
1475                lowResPreview.setPixels(mOutputPixels, 0, width, 0, 0, width, height);
1476
1477                intermediateBitmap.recycle();
1478                return Bitmap.createScaledBitmap(lowResPreview, width * factor,
1479                        height * factor, true);
1480            }
1481
1482            @Override
1483            protected void onPostExecute(Bitmap bitmap) {
1484                setBlurredBackground(bitmap);
1485            }
1486        };
1487    }
1488
1489    /**
1490     * Shimmy animation effects handles the specifics for shimmy animation, including
1491     * setting up to show mode drawer (without text) and hide it with shimmy animation.
1492     */
1493    private class ShimmyAnimationEffects extends ModeListAnimationEffects {
1494        private boolean mStartHidingShimmyWhenWindowGainsFocus = false;
1495        private Animator mAnimator = null;
1496        private float mModeListOpenFactor = 0f;
1497        private final Runnable mHideShimmy = new Runnable() {
1498            @Override
1499            public void run() {
1500                startHidingShimmy();
1501            }
1502        };
1503        private Runnable mEndAction = null;
1504
1505        @Override
1506        public void setSize(int width, int height) {
1507            // Do nothing.
1508        }
1509
1510        @Override
1511        public void drawForeground(Canvas canvas) {
1512            // Do nothing.
1513        }
1514
1515        @Override
1516        public void startAnimation() {
1517            setVisibility(VISIBLE);
1518            mSettingsButton.setVisibility(INVISIBLE);
1519            onModeListOpenRatioUpdate(0);
1520            int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
1521            for (int i = 0; i < mModeSelectorItems.length; i++) {
1522                mModeSelectorItems[i].setVisibleWidth(maxVisibleWidth);
1523            }
1524            if (hasWindowFocus()) {
1525                hideShimmyWithDelay();
1526            } else {
1527                mStartHidingShimmyWhenWindowGainsFocus = true;
1528            }
1529        }
1530
1531        private void hideShimmyWithDelay() {
1532            postDelayed(mHideShimmy, HIDE_SHIMMY_DELAY_MS);
1533        }
1534
1535        @Override
1536        public void onWindowFocusChanged(boolean hasFocus) {
1537            if (mStartHidingShimmyWhenWindowGainsFocus && hasFocus) {
1538                mStartHidingShimmyWhenWindowGainsFocus = false;
1539                hideShimmyWithDelay();
1540            }
1541        }
1542
1543        /**
1544         * This starts the accordion animation, unless it's already running, in which
1545         * case the start animation call will be ignored.
1546         */
1547        private void startHidingShimmy() {
1548            int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
1549            mAnimator = animateListToWidth(START_DELAY_MS * (-1), TOTAL_DURATION_MS,
1550                    Gusterpolator.INSTANCE, maxVisibleWidth, 0);
1551            mAnimator.addListener(new Animator.AnimatorListener() {
1552                private boolean mCanceled = false;
1553                @Override
1554                public void onAnimationStart(Animator animation) {
1555                    // Do nothing.
1556                }
1557
1558                @Override
1559                public void onAnimationEnd(Animator animation) {
1560                    endAnimation();
1561                }
1562
1563                @Override
1564                public void onAnimationCancel(Animator animation) {
1565                    mCanceled = true;
1566                }
1567
1568                @Override
1569                public void onAnimationRepeat(Animator animation) {
1570                    // Do nothing.
1571                }
1572            });
1573        }
1574
1575        @Override
1576        public boolean cancelAnimation() {
1577            removeCallbacks(mHideShimmy);
1578            if (mAnimator != null && mAnimator.isRunning()) {
1579                mAnimator.cancel();
1580            }
1581            endAnimation();
1582            return true;
1583        }
1584
1585        @Override
1586        public void endAnimation() {
1587            mAnimator = null;
1588            mSettingsButton.setVisibility(VISIBLE);
1589            if (mEndAction != null) {
1590                post(mEndAction);
1591            }
1592            final ValueAnimator openFactorAnimator = ValueAnimator.ofFloat(mModeListOpenFactor, 1f);
1593            openFactorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1594                @Override
1595                public void onAnimationUpdate(ValueAnimator animation) {
1596                    mModeListOpenFactor = (Float) openFactorAnimator.getAnimatedValue();
1597                    onVisibleWidthChanged(mVisibleWidth);
1598                }
1599            });
1600            openFactorAnimator.addListener(new Animator.AnimatorListener() {
1601                @Override
1602                public void onAnimationStart(Animator animation) {
1603                    // Do nothing.
1604                }
1605
1606                @Override
1607                public void onAnimationEnd(Animator animation) {
1608                    mModeListOpenFactor = 1f;
1609                    mCurrentEffect = null;
1610                }
1611
1612                @Override
1613                public void onAnimationCancel(Animator animation) {
1614                    // Do nothing.
1615                }
1616
1617                @Override
1618                public void onAnimationRepeat(Animator animation) {
1619                    // Do nothing.
1620                }
1621            });
1622            openFactorAnimator.start();
1623        }
1624
1625        @Override
1626        public float getModeListOpenFactor() {
1627            return mModeListOpenFactor;
1628        }
1629
1630        @Override
1631        public void setAnimationEndAction(Runnable runnable) {
1632            mEndAction = runnable;
1633        }
1634    }
1635}
1636