ModeListView.java revision 948de99e58b72e815224d4e12761863dfceffae0
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.PreviewAreaSizeChangedListener {
62
63    private static final String TAG = "ModeListView";
64
65    // Animation Durations
66    private static final int DEFAULT_DURATION_MS = 200;
67    private static final int FLY_IN_DURATION_MS = 850;
68    private static final int HOLD_DURATION_MS = 0;
69    private static final int FLY_OUT_DURATION_MS = 850;
70    private static final int START_DELAY_MS = 100;
71    private static final int TOTAL_DURATION_MS = FLY_IN_DURATION_MS + HOLD_DURATION_MS
72            + FLY_OUT_DURATION_MS;
73
74    private static final float ROWS_TO_SHOW_IN_LANDSCAPE = 4.5f;
75    private static final int NO_ITEM_SELECTED = -1;
76
77    // Scrolling states
78    private static final int IDLE = 0;
79    private static final int FULLY_SHOWN = 1;
80    private static final int ACCORDION_ANIMATION = 2;
81    private static final int SCROLLING = 3;
82    private static final int MODE_SELECTED = 4;
83
84    // Scrolling delay between non-focused item and focused item
85    private static final int DELAY_MS = 30;
86    // If the fling velocity exceeds this threshold, snap to full screen at a constant
87    // speed. Unit: pixel/ms.
88    private static final float VELOCITY_THRESHOLD = 2f;
89
90    /**
91     * A factor to change the UI responsiveness on a scroll.
92     * e.g. A scroll factor of 0.5 means UI will move half as fast as the finger.
93     */
94    private static final float SCROLL_FACTOR = 0.5f;
95    // 30% transparent black background.
96    private static final int BACKGROUND_TRANSPARENTCY = (int) (0.3f * 255);
97    private static final int PREVIEW_DOWN_SAMPLE_FACTOR = 4;
98    // Threshold, below which snap back will happen.
99    private static final float SNAP_BACK_THRESHOLD_RATIO = 0.33f;
100
101    private final GestureDetector mGestureDetector;
102    private final int mIconBlockWidth;
103    private final RectF mPreviewArea = new RectF();
104    private final RectF mUncoveredPreviewArea = new RectF();
105
106    private int mListBackgroundColor;
107    private LinearLayout mListView;
108    private SettingsButton mSettingsButton;
109    private int mState = IDLE;
110    private int mTotalModes;
111    private ModeSelectorItem[] mModeSelectorItems;
112    private AnimatorSet mAnimatorSet;
113    private int mFocusItem = NO_ITEM_SELECTED;
114    private AnimationEffects mCurrentEffect;
115    private ModeListOpenListener mModeListOpenListener;
116    private CameraAppUI.CameraModuleScreenShotProvider mScreenShotProvider = null;
117    private int[] mInputPixels;
118    private int[] mOutputPixels;
119
120    // Width and height of this view. They get updated in onLayout()
121    // Unit for width and height are pixels.
122    private int mWidth;
123    private int mHeight;
124    private float mScrollTrendX = 0f;
125    private float mScrollTrendY = 0f;
126    private ModeSwitchListener mModeSwitchListener = null;
127    private ArrayList<Integer> mSupportedModes;
128    private final LinkedList<TimeBasedPosition> mPositionHistory
129            = new LinkedList<TimeBasedPosition>();
130    private long mCurrentTime;
131    private float mVelocityX; // Unit: pixel/ms.
132    private final Animator.AnimatorListener mModeListAnimatorListener =
133            new Animator.AnimatorListener() {
134
135        @Override
136        public void onAnimationStart(Animator animation) {
137            setVisibility(VISIBLE);
138        }
139
140        @Override
141        public void onAnimationEnd(Animator animation) {
142            mAnimatorSet = null;
143            if (mState == ACCORDION_ANIMATION || mState == IDLE) {
144                resetModeSelectors();
145                setVisibility(INVISIBLE);
146                mState = IDLE;
147            }
148        }
149
150        @Override
151        public void onAnimationCancel(Animator animation) {
152        }
153
154        @Override
155        public void onAnimationRepeat(Animator animation) {
156
157        }
158    };
159    private boolean mAdjustPositionWhenUncoveredPreviewAreaChanges = false;
160
161    @Override
162    public void onPreviewAreaSizeChanged(RectF previewArea) {
163        mPreviewArea.set(previewArea);
164    }
165
166    private final CameraAppUI.UncoveredPreviewAreaSizeChangedListener
167            mUncoveredPreviewAreaSizeChangedListener =
168            new CameraAppUI.UncoveredPreviewAreaSizeChangedListener() {
169
170                @Override
171                public void uncoveredPreviewAreaChanged(RectF uncoveredPreviewArea) {
172                    mUncoveredPreviewArea.set(uncoveredPreviewArea);
173                    mSettingsButton.uncoveredPreviewAreaChanged(uncoveredPreviewArea);
174                    if (mAdjustPositionWhenUncoveredPreviewAreaChanges) {
175                        mAdjustPositionWhenUncoveredPreviewAreaChanges = false;
176                        centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight());
177                    }
178                }
179            };
180
181    public interface ModeSwitchListener {
182        public void onModeSelected(int modeIndex);
183        public int getCurrentModeIndex();
184        public void onSettingsSelected();
185    }
186
187    public interface ModeListOpenListener {
188        /**
189         * Mode list will open to full screen after current animation.
190         */
191        public void onOpenFullScreen();
192
193        /**
194         * Updates the listener with the current progress of mode drawer opening.
195         *
196         * @param progress progress of the mode drawer opening, ranging [0f, 1f]
197         *                 0 means mode drawer is fully closed, 1 indicates a fully
198         *                 open mode drawer.
199         */
200        public void onModeListOpenProgress(float progress);
201
202        /**
203         * Gets called when mode list is completely closed.
204         */
205        public void onModeListClosed();
206    }
207
208    /**
209     * This class aims to help store time and position in pairs.
210     */
211    private static class TimeBasedPosition {
212        private final float mPosition;
213        private final long mTimeStamp;
214        public TimeBasedPosition(float position, long time) {
215            mPosition = position;
216            mTimeStamp = time;
217        }
218
219        public float getPosition() {
220            return mPosition;
221        }
222
223        public long getTimeStamp() {
224            return mTimeStamp;
225        }
226    }
227
228    /**
229     * This is a highly customized interpolator. The purpose of having this subclass
230     * is to encapsulate intricate animation timing, so that the actual animation
231     * implementation can be re-used with other interpolators to achieve different
232     * animation effects.
233     *
234     * The accordion animation consists of three stages:
235     * 1) Animate into the screen within a pre-specified fly in duration.
236     * 2) Hold in place for a certain amount of time (Optional).
237     * 3) Animate out of the screen within the given time.
238     *
239     * The accordion animator is initialized with 3 parameter: 1) initial position,
240     * 2) how far out the view should be before flying back out,  3) end position.
241     * The interpolation output should be [0f, 0.5f] during animation between 1)
242     * to 2), and [0.5f, 1f] for flying from 2) to 3).
243     */
244    private final TimeInterpolator mAccordionInterpolator = new TimeInterpolator() {
245        @Override
246        public float getInterpolation(float input) {
247
248            float flyInDuration = (float) FLY_OUT_DURATION_MS / (float) TOTAL_DURATION_MS;
249            float holdDuration = (float) (FLY_OUT_DURATION_MS + HOLD_DURATION_MS)
250                    / (float) TOTAL_DURATION_MS;
251            if (input == 0) {
252                return 0;
253            }else if (input < flyInDuration) {
254                // Stage 1, project result to [0f, 0.5f]
255                input /= flyInDuration;
256                float result = Gusterpolator.INSTANCE.getInterpolation(input);
257                return result * 0.5f;
258            } else if (input < holdDuration) {
259                // Stage 2
260                return 0.5f;
261            } else {
262                // Stage 3, project result to [0.5f, 1f]
263                input -= holdDuration;
264                input /= (1 - holdDuration);
265                float result = Gusterpolator.INSTANCE.getInterpolation(input);
266                return 0.5f + result * 0.5f;
267            }
268        }
269    };
270
271    /**
272     * The listener that is used to notify when gestures occur.
273     * Here we only listen to a subset of gestures.
274     */
275    private final GestureDetector.OnGestureListener mOnGestureListener
276            = new GestureDetector.SimpleOnGestureListener(){
277        @Override
278        public boolean onScroll(MotionEvent e1, MotionEvent e2,
279                                float distanceX, float distanceY) {
280
281            if (mState == ACCORDION_ANIMATION) {
282                // Scroll happens during accordion animation.
283                if (isRunningAccordionAnimation()) {
284                    mAnimatorSet.cancel();
285                }
286                setVisibility(VISIBLE);
287            }
288
289            if (mState == IDLE) {
290                resetModeSelectors();
291                setVisibility(VISIBLE);
292            }
293
294            mState = SCROLLING;
295            // Scroll based on the scrolling distance on the currently focused
296            // item.
297            scroll(mFocusItem, distanceX * SCROLL_FACTOR, distanceY * SCROLL_FACTOR);
298            return true;
299        }
300
301        @Override
302        public boolean onSingleTapUp(MotionEvent ev) {
303            if (mState != FULLY_SHOWN) {
304                // Only allows tap to choose mode when the list is fully shown
305                return false;
306            }
307
308            // Ignore the tap if it happens outside of the mode list linear layout.
309            float x = ev.getX() - mListView.getX();
310            float y = ev.getY() - mListView.getY();
311            if (x < 0 || x > mListView.getWidth() || y < 0 || y > mListView.getHeight()) {
312                snapBack(true);
313                return false;
314            }
315
316            int index = getFocusItem(ev.getX(), ev.getY());
317            // Validate the selection
318            if (index != NO_ITEM_SELECTED) {
319                final int modeId = getModeIndex(index);
320                // Un-highlight all the modes.
321                for (int i = 0; i < mModeSelectorItems.length; i++) {
322                    mModeSelectorItems[i].setHighlighted(false);
323                }
324                // Select the focused item.
325                mModeSelectorItems[index].setSelected(true);
326                mState = MODE_SELECTED;
327                PeepholeAnimationEffect effect = new PeepholeAnimationEffect();
328                effect.setSize(mWidth, mHeight);
329                effect.setAnimationEndAction(new Runnable() {
330                    @Override
331                    public void run() {
332                        setVisibility(INVISIBLE);
333                        mCurrentEffect = null;
334                        snapBack(false);
335                    }
336                });
337
338                // Calculate the position of the icon in the selected item, and
339                // start animation from that position.
340                int[] location = new int[2];
341                // Gets icon's center position in relative to the window.
342                mModeSelectorItems[index].getIconCenterLocationInWindow(location);
343                int iconX = location[0];
344                int iconY = location[1];
345                // Gets current view's top left position relative to the window.
346                getLocationInWindow(location);
347                // Calculate icon location relative to this view
348                iconX -= location[0];
349                iconY -= location[1];
350
351                effect.setAnimationStartingPosition(iconX, iconY);
352                if (mScreenShotProvider != null) {
353                    effect.setBackground(mScreenShotProvider
354                            .getPreviewFrame(PREVIEW_DOWN_SAMPLE_FACTOR), mPreviewArea);
355                    effect.setBackgroundOverlay(mScreenShotProvider.getPreviewOverlayAndControls());
356                }
357                mCurrentEffect = effect;
358                invalidate();
359
360                // Post mode selection runnable to the end of the message queue
361                // so that current UI changes can finish before mode initialization
362                // clogs up UI thread.
363                post(new Runnable() {
364                    @Override
365                    public void run() {
366                        onModeSelected(modeId);
367                    }
368                });
369            }
370            return true;
371        }
372
373        @Override
374        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
375            // Cache velocity in the unit pixel/ms.
376            mVelocityX = velocityX / 1000f * SCROLL_FACTOR;
377            return true;
378        }
379    };
380
381    public ModeListView(Context context, AttributeSet attrs) {
382        super(context, attrs);
383        mGestureDetector = new GestureDetector(context, mOnGestureListener);
384        mIconBlockWidth = getResources()
385                .getDimensionPixelSize(R.dimen.mode_selector_icon_block_width);
386        mListBackgroundColor = getResources().getColor(R.color.mode_list_background);
387    }
388
389    public CameraAppUI.UncoveredPreviewAreaSizeChangedListener
390            getUncoveredPreviewAreaSizeChangedListener() {
391        return mUncoveredPreviewAreaSizeChangedListener;
392    }
393
394    /**
395     * Sets the alpha on the list background. This is called whenever the list
396     * is scrolling or animating, so that background can adjust its dimness.
397     *
398     * @param alpha new alpha to be applied on list background color
399     */
400    private void setBackgroundAlpha(int alpha) {
401        // Make sure alpha is valid.
402        alpha = alpha & 0xFF;
403        // Change alpha on the background color.
404        mListBackgroundColor = mListBackgroundColor & 0xFFFFFF;
405        mListBackgroundColor = mListBackgroundColor | (alpha << 24);
406        // Set new color to list background.
407        setBackgroundColor(mListBackgroundColor);
408    }
409
410    /**
411     * Initialize mode list with a list of indices of supported modes.
412     *
413     * @param modeIndexList a list of indices of supported modes
414     */
415    public void init(List<Integer> modeIndexList) {
416        int[] modeSequence = getResources()
417                .getIntArray(R.array.camera_modes_in_nav_drawer_if_supported);
418        int[] visibleModes = getResources()
419                .getIntArray(R.array.camera_modes_always_visible);
420
421        // Mark the supported modes in a boolean array to preserve the
422        // sequence of the modes
423        SparseArray<Boolean> modeIsSupported = new SparseArray<Boolean>();
424        for (int i = 0; i < modeIndexList.size(); i++) {
425            int mode = modeIndexList.get(i);
426            modeIsSupported.put(mode, true);
427        }
428        for (int i = 0; i < visibleModes.length; i++) {
429            int mode = visibleModes[i];
430            modeIsSupported.put(mode, true);
431        }
432
433        // Put the indices of supported modes into an array preserving their
434        // display order.
435        mSupportedModes = new ArrayList<Integer>();
436        for (int i = 0; i < modeSequence.length; i++) {
437            int mode = modeSequence[i];
438            if (modeIsSupported.get(mode, false)) {
439                mSupportedModes.add(mode);
440            }
441        }
442        mTotalModes = mSupportedModes.size();
443        initializeModeSelectorItems();
444        mSettingsButton = (SettingsButton) findViewById(R.id.settings_button);
445        mSettingsButton.setOnClickListener(new OnClickListener() {
446            @Override
447            public void onClick(View v) {
448                // Post this callback to make sure current user interaction has
449                // been reflected in the UI. Specifically, the pressed state gets
450                // unset after click happens. In order to ensure the pressed state
451                // gets unset in UI before getting in the low frame rate settings
452                // activity launch stage, the settings selected callback is posted.
453                post(new Runnable() {
454                    @Override
455                    public void run() {
456                        mModeSwitchListener.onSettingsSelected();
457                    }
458                });
459            }
460        });
461    }
462
463    /**
464     * Sets the screen shot provider for getting a preview frame and a bitmap
465     * of the controls and overlay.
466     */
467    public void setCameraModuleScreenShotProvider(
468            CameraAppUI.CameraModuleScreenShotProvider provider) {
469        mScreenShotProvider = provider;
470    }
471
472    private void initializeModeSelectorItems() {
473        mModeSelectorItems = new ModeSelectorItem[mTotalModes];
474        // Inflate the mode selector items and add them to a linear layout
475        LayoutInflater inflater = (LayoutInflater) getContext()
476                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
477        mListView = (LinearLayout) findViewById(R.id.mode_list);
478        for (int i = 0; i < mTotalModes; i++) {
479            ModeSelectorItem selectorItem =
480                    (ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null);
481            mListView.addView(selectorItem);
482            // Sets the top padding of the top item to 0.
483            if (i == 0) {
484                selectorItem.setPadding(selectorItem.getPaddingLeft(), 0,
485                        selectorItem.getPaddingRight(), selectorItem.getPaddingBottom());
486            }
487            // Sets the bottom padding of the bottom item to 0.
488            if (i == mTotalModes - 1) {
489                selectorItem.setPadding(selectorItem.getPaddingLeft(), selectorItem.getPaddingTop(),
490                        selectorItem.getPaddingRight(), 0);
491            }
492
493            int modeId = getModeIndex(i);
494            selectorItem.setHighlightColor(getResources()
495                    .getColor(CameraUtil.getCameraThemeColorId(modeId, getContext())));
496
497            // Set image
498            selectorItem.setImageResource(CameraUtil.getCameraModeIconResId(modeId, getContext()));
499
500            // Set text
501            selectorItem.setText(CameraUtil.getCameraModeText(modeId, getContext()));
502
503            // Set content description (for a11y)
504            selectorItem.setContentDescription(CameraUtil
505                    .getCameraModeContentDescription(modeId, getContext()));
506
507            mModeSelectorItems[i] = selectorItem;
508        }
509
510        resetModeSelectors();
511    }
512
513    /**
514     * Maps between the UI mode selector index to the actual mode id.
515     *
516     * @param modeSelectorIndex the index of the UI item
517     * @return the index of the corresponding camera mode
518     */
519    private int getModeIndex(int modeSelectorIndex) {
520        if (modeSelectorIndex < mTotalModes && modeSelectorIndex >= 0) {
521            return mSupportedModes.get(modeSelectorIndex);
522        }
523        Log.e(TAG, "Invalid mode selector index: " + modeSelectorIndex + ", total modes: "
524                + mTotalModes);
525        return getResources().getInteger(R.integer.camera_mode_photo);
526    }
527
528    /** Notify ModeSwitchListener, if any, of the mode change. */
529    private void onModeSelected(int modeIndex) {
530        if (mModeSwitchListener != null) {
531            mModeSwitchListener.onModeSelected(modeIndex);
532        }
533    }
534
535    /**
536     * Sets a listener that listens to receive mode switch event.
537     *
538     * @param listener a listener that gets notified when mode changes.
539     */
540    public void setModeSwitchListener(ModeSwitchListener listener) {
541        mModeSwitchListener = listener;
542    }
543
544    /**
545     * Sets a listener that gets notified when the mode list is open full screen.
546     *
547     * @param listener a listener that listens to mode list open events
548     */
549    public void setModeListOpenListener(ModeListOpenListener listener) {
550        mModeListOpenListener = listener;
551    }
552
553    @Override
554    public boolean onTouchEvent(MotionEvent ev) {
555        if (mCurrentEffect != null) {
556            return mCurrentEffect.onTouchEvent(ev);
557        }
558
559        super.onTouchEvent(ev);
560        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
561            mVelocityX = 0;
562            if  (mState == ACCORDION_ANIMATION) {
563                // Let taps go through to take a capture during the accordion
564                return false;
565            }
566            getParent().requestDisallowInterceptTouchEvent(true);
567            if (mState == FULLY_SHOWN) {
568                mFocusItem = NO_ITEM_SELECTED;
569                setSwipeMode(false);
570            } else {
571                mFocusItem = getFocusItem(ev.getX(), ev.getY());
572                setSwipeMode(true);
573            }
574        } else if (mState == ACCORDION_ANIMATION) {
575            // This is a swipe during accordion animation
576            mFocusItem = getFocusItem(ev.getX(), ev.getY());
577            setSwipeMode(true);
578
579        }
580        // Pass all touch events to gesture detector for gesture handling.
581        mGestureDetector.onTouchEvent(ev);
582        if (ev.getActionMasked() == MotionEvent.ACTION_UP ||
583                ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
584            snap();
585            mFocusItem = NO_ITEM_SELECTED;
586        }
587        return true;
588    }
589
590    /**
591     * Sets the swipe mode to indicate whether this is a swiping in
592     * or out, and therefore we can have different animations.
593     *
594     * @param swipeIn indicates whether the swipe should reveal/hide the list.
595     */
596    private void setSwipeMode(boolean swipeIn) {
597        for (int i = 0 ; i < mModeSelectorItems.length; i++) {
598            mModeSelectorItems[i].onSwipeModeChanged(swipeIn);
599        }
600    }
601
602    @Override
603    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
604        super.onLayout(changed, left, top, right, bottom);
605        mWidth = right - left;
606        mHeight = bottom - top - getPaddingTop() - getPaddingBottom();
607        if (mCurrentEffect != null) {
608            mCurrentEffect.setSize(mWidth, mHeight);
609        }
610    }
611
612    /**
613     * Here we calculate the children size based on the orientation, change
614     * their layout parameters if needed before propagating onMeasure call
615     * to the children, so the newly changed params will take effect in this
616     * pass.
617     *
618     * @param widthMeasureSpec Horizontal space requirements as imposed by the
619     *        parent
620     * @param heightMeasureSpec Vertical space requirements as imposed by the
621     *        parent
622     */
623    @Override
624    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
625        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
626        centerModeDrawerInUncoveredPreview(MeasureSpec.getSize(widthMeasureSpec),
627                MeasureSpec.getSize(heightMeasureSpec));
628    }
629
630    @Override
631    public void draw(Canvas canvas) {
632        if (mCurrentEffect != null) {
633            mCurrentEffect.drawBackground(canvas);
634            super.draw(canvas);
635            mCurrentEffect.drawForeground(canvas);
636        } else {
637            super.draw(canvas);
638        }
639    }
640
641    /**
642     * This starts the accordion animation, unless it's already running, in which
643     * case the start animation call will be ignored.
644     */
645    public void startAccordionAnimation() {
646        if (mState != IDLE) {
647            return;
648        }
649        if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
650            return;
651        }
652        mState = ACCORDION_ANIMATION;
653        resetModeSelectors();
654        animateListToWidth(START_DELAY_MS, TOTAL_DURATION_MS, mAccordionInterpolator,
655                0, mIconBlockWidth, 0);
656    }
657
658    /**
659     * This starts the accordion animation with a delay.
660     *
661     * @param delay delay in milliseconds before starting animation
662     */
663    public void startAccordionAnimationWithDelay(int delay) {
664        postDelayed(new Runnable() {
665            @Override
666            public void run() {
667                startAccordionAnimation();
668            }
669        }, delay);
670    }
671
672    /**
673     * Resets the visible width of all the mode selectors to 0.
674     */
675    private void resetModeSelectors() {
676        for (int i = 0; i < mModeSelectorItems.length; i++) {
677            mModeSelectorItems[i].setVisibleWidth(0);
678        }
679        // Visible width has been changed to 0
680        onVisibleWidthChanged(0);
681    }
682
683    private boolean isRunningAccordionAnimation() {
684        return mAnimatorSet != null && mAnimatorSet.isRunning();
685    }
686
687    /**
688     * Calculate the mode selector item in the list that is at position (x, y).
689     * If the position is above the top item or below the bottom item, return
690     * the top item or bottom item respectively.
691     *
692     * @param x horizontal position
693     * @param y vertical position
694     * @return index of the item that is at position (x, y)
695     */
696    private int getFocusItem(float x, float y) {
697        // Convert coordinates into child view's coordinates.
698        x -= mListView.getX();
699        y -= mListView.getY();
700
701        for (int i = 0; i < mModeSelectorItems.length; i++) {
702            if (y <= mModeSelectorItems[i].getBottom()) {
703                return i;
704            }
705        }
706        return mModeSelectorItems.length - 1;
707    }
708
709    @Override
710    public void onVisibilityChanged(View v, int visibility) {
711        super.onVisibilityChanged(v, visibility);
712        if (visibility == VISIBLE) {
713            centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight());
714            // Highlight current module
715            if (mModeSwitchListener != null) {
716                int modeId = mModeSwitchListener.getCurrentModeIndex();
717                int parentMode = CameraUtil.getCameraModeParentModeId(modeId, getContext());
718                // Find parent mode in the nav drawer.
719                for (int i = 0; i < mSupportedModes.size(); i++) {
720                    if (mSupportedModes.get(i) == parentMode) {
721                        mModeSelectorItems[i].setHighlighted(true);
722                    }
723                }
724            }
725        } else {
726            if (mModeSelectorItems != null) {
727                // When becoming invisible/gone after initializing mode selector items.
728                for (int i = 0; i < mModeSelectorItems.length; i++) {
729                    mModeSelectorItems[i].setHighlighted(false);
730                    mModeSelectorItems[i].setSelected(false);
731                }
732            }
733            if (mModeSwitchListener != null) {
734                mModeListOpenListener.onModeListClosed();
735            }
736        }
737    }
738
739    /**
740     * Center mode drawer in the portion of camera preview that is not covered by
741     * bottom bar.
742     */
743    // TODO: Combine SettingsButton logic into here if UX design does not change
744    // for another week.
745    private void centerModeDrawerInUncoveredPreview(int measuredWidth, int measuredHeight) {
746
747        // Assuming the preview is centered in the space aside from bottom bar.
748        float previewAreaWidth = mUncoveredPreviewArea.right + mUncoveredPreviewArea.left;
749        float previewAreaHeight = mUncoveredPreviewArea.top + mUncoveredPreviewArea.bottom;
750        if (measuredWidth > measuredHeight && previewAreaWidth < previewAreaHeight
751                || measuredWidth < measuredHeight && previewAreaWidth > previewAreaHeight) {
752            // Cached preview area is stale, update mode drawer position on next
753            // layout pass.
754            mAdjustPositionWhenUncoveredPreviewAreaChanges = true;
755        } else {
756            // Align left:
757            mListView.setTranslationX(mUncoveredPreviewArea.left);
758            // Align center vertical:
759            mListView.setTranslationY(mUncoveredPreviewArea.centerY()
760                    - mListView.getMeasuredHeight() / 2);
761        }
762    }
763
764    private void scroll(int itemId, float deltaX, float deltaY) {
765        // Scrolling trend on X and Y axis, to track the trend by biasing
766        // towards latest touch events.
767        mScrollTrendX = mScrollTrendX * 0.3f + deltaX * 0.7f;
768        mScrollTrendY = mScrollTrendY * 0.3f + deltaY * 0.7f;
769
770        // TODO: Change how the curve is calculated below when UX finalize their design.
771        mCurrentTime = SystemClock.uptimeMillis();
772        float longestWidth;
773        if (itemId != NO_ITEM_SELECTED) {
774            longestWidth = mModeSelectorItems[itemId].getVisibleWidth() - deltaX;
775        } else {
776            longestWidth = mModeSelectorItems[0].getVisibleWidth() - deltaX;
777        }
778        insertNewPosition(longestWidth, mCurrentTime);
779
780        for (int i = 0; i < mModeSelectorItems.length; i++) {
781            mModeSelectorItems[i].setVisibleWidth(calculateVisibleWidthForItem(i,
782                    (int) longestWidth));
783        }
784        if (longestWidth <= 0) {
785            reset();
786        }
787
788        itemId = itemId == NO_ITEM_SELECTED ? 0 : itemId;
789        onVisibleWidthChanged(mModeSelectorItems[itemId].getVisibleWidth());
790    }
791
792    /**
793     * Calculate the width of a specified item based on its position relative to
794     * the item with longest width.
795     */
796    private int calculateVisibleWidthForItem(int itemId, int longestWidth) {
797        if (itemId == mFocusItem || mFocusItem == NO_ITEM_SELECTED) {
798            return longestWidth;
799        }
800
801        int delay = Math.abs(itemId - mFocusItem) * DELAY_MS;
802        return (int) getPosition(mCurrentTime - delay);
803    }
804
805    /**
806     * Insert new position and time stamp into the history position list, and
807     * remove stale position items.
808     *
809     * @param position latest position of the focus item
810     * @param time  current time in milliseconds
811     */
812    private void insertNewPosition(float position, long time) {
813        // TODO: Consider re-using stale position objects rather than
814        // always creating new position objects.
815        mPositionHistory.add(new TimeBasedPosition(position, time));
816
817        // Positions that are from too long ago will not be of any use for
818        // future position interpolation. So we need to remove those positions
819        // from the list.
820        long timeCutoff = time - (mTotalModes - 1) * DELAY_MS;
821        while (mPositionHistory.size() > 0) {
822            // Remove all the position items that are prior to the cutoff time.
823            TimeBasedPosition historyPosition = mPositionHistory.getFirst();
824            if (historyPosition.getTimeStamp() < timeCutoff) {
825                mPositionHistory.removeFirst();
826            } else {
827                break;
828            }
829        }
830    }
831
832    /**
833     * Gets the interpolated position at the specified time. This involves going
834     * through the recorded positions until a {@link TimeBasedPosition} is found
835     * such that the position the recorded before the given time, and the
836     * {@link TimeBasedPosition} after that is recorded no earlier than the given
837     * time. These two positions are then interpolated to get the position at the
838     * specified time.
839     */
840    private float getPosition(long time) {
841        int i;
842        for (i = 0; i < mPositionHistory.size(); i++) {
843            TimeBasedPosition historyPosition = mPositionHistory.get(i);
844            if (historyPosition.getTimeStamp() > time) {
845                // Found the winner. Now interpolate between position i and position i - 1
846                if (i == 0) {
847                    return historyPosition.getPosition();
848                } else {
849                    TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1);
850                    // Start interpolation
851                    float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) /
852                            (float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp());
853                    float position = fraction * (historyPosition.getPosition()
854                            - prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition();
855                    return position;
856                }
857            }
858        }
859        // It should never get here.
860        Log.e(TAG, "Invalid time input for getPosition(). time: " + time);
861        if (mPositionHistory.size() == 0) {
862            Log.e(TAG, "TimeBasedPosition history size is 0");
863        } else {
864            Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp()
865            + " , last position recorded at " + mPositionHistory.getLast().getTimeStamp());
866        }
867        assert (i < mPositionHistory.size());
868        return i;
869    }
870
871    private void reset() {
872        resetModeSelectors();
873        mScrollTrendX = 0f;
874        mScrollTrendY = 0f;
875        mCurrentEffect = null;
876        setVisibility(INVISIBLE);
877    }
878
879    /**
880     * When visible width of list is changed, the background of the list needs
881     * to darken/lighten correspondingly.
882     */
883    private void onVisibleWidthChanged(int focusItemWidth) {
884        // When the longest mode item is entirely shown (across the screen), the
885        // background should be 50% transparent.
886        int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
887        focusItemWidth = Math.min(maxVisibleWidth, focusItemWidth);
888        float openRatio = (float) focusItemWidth / maxVisibleWidth;
889        setBackgroundAlpha((int) (BACKGROUND_TRANSPARENTCY * openRatio));
890        if (mModeListOpenListener != null) {
891            mModeListOpenListener.onModeListOpenProgress(openRatio);
892        }
893        if (mSettingsButton != null) {
894            mSettingsButton.setAlpha(openRatio);
895        }
896    }
897
898    @Override
899    public void onWindowVisibilityChanged(int visibility) {
900        super.onWindowVisibilityChanged(visibility);
901        if (visibility != VISIBLE) {
902            // Reset mode list if the window is no longer visible.
903            reset();
904            mState = IDLE;
905        }
906    }
907
908    /**
909     * The list view should either snap back or snap to full screen after a gesture.
910     * This function is called when an up or cancel event is received, and then based
911     * on the current position of the list and the gesture we can decide which way
912     * to snap.
913     */
914    private void snap() {
915        if (mState == SCROLLING) {
916            int itemId = Math.max(0, mFocusItem);
917            if (mModeSelectorItems[itemId].getVisibleWidth()
918                    < mModeSelectorItems[itemId].getMaxVisibleWidth() * SNAP_BACK_THRESHOLD_RATIO) {
919                snapBack();
920            } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) {
921                snapBack();
922            } else {
923                snapToFullScreen();
924            }
925        }
926    }
927
928    /**
929     * Snaps back out of the screen.
930     *
931     * @param withAnimation whether snapping back should be animated
932     */
933    public void snapBack(boolean withAnimation) {
934        if (withAnimation) {
935            if (mVelocityX > -VELOCITY_THRESHOLD * SCROLL_FACTOR) {
936                animateListToWidth(0);
937            } else {
938                animateListToWidthAtVelocity(mVelocityX, 0);
939            }
940            mState = IDLE;
941        } else {
942            setVisibility(INVISIBLE);
943            resetModeSelectors();
944            mState = IDLE;
945        }
946    }
947
948    /**
949     * Snaps the mode list back out with animation.
950     */
951    private void snapBack() {
952        snapBack(true);
953    }
954
955    private void snapToFullScreen() {
956        int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
957        int fullWidth = mModeSelectorItems[focusItem].getMaxVisibleWidth();
958        if (mVelocityX <= VELOCITY_THRESHOLD * SCROLL_FACTOR) {
959            animateListToWidth(fullWidth);
960        } else {
961            // If the fling velocity exceeds this threshold, snap to full screen
962            // at a constant speed.
963            animateListToWidthAtVelocity(mVelocityX, fullWidth);
964        }
965        mState = FULLY_SHOWN;
966        if (mModeListOpenListener != null) {
967            mModeListOpenListener.onOpenFullScreen();
968        }
969    }
970
971    /**
972     * Overloaded function to provide a simple way to start animation. Animation
973     * will use default duration, and a value of <code>null</code> for interpolator
974     * means linear interpolation will be used.
975     *
976     * @param width a set of values that the animation will animate between over time
977     */
978    private void animateListToWidth(int... width) {
979        animateListToWidth(0, DEFAULT_DURATION_MS, null, width);
980    }
981
982    /**
983     * Animate the mode list between the given set of visible width.
984     *
985     * @param delay start delay between consecutive mode item
986     * @param duration duration for the animation of each mode item
987     * @param interpolator interpolator to be used by the animation
988     * @param width a set of values that the animation will animate between over time
989     */
990    private void animateListToWidth(int delay, int duration,
991                                    TimeInterpolator interpolator, int... width) {
992        if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
993            mAnimatorSet.end();
994        }
995
996        ArrayList<Animator> animators = new ArrayList<Animator>();
997        int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
998        for (int i = 0; i < mTotalModes; i++) {
999            ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
1000                    "visibleWidth", width);
1001            animator.setDuration(duration);
1002            animator.setStartDelay(i * delay);
1003            animators.add(animator);
1004            if (i == focusItem) {
1005                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1006                    @Override
1007                    public void onAnimationUpdate(ValueAnimator animation) {
1008                        onVisibleWidthChanged((Integer) animation.getAnimatedValue());
1009                    }
1010                });
1011            }
1012        }
1013
1014        mAnimatorSet = new AnimatorSet();
1015        mAnimatorSet.playTogether(animators);
1016        mAnimatorSet.setInterpolator(interpolator);
1017        mAnimatorSet.addListener(mModeListAnimatorListener);
1018        mAnimatorSet.start();
1019    }
1020
1021    /**
1022     * Animate the mode list to the given width at a constant velocity.
1023     *
1024     * @param velocity the velocity that animation will be at
1025     * @param width final width of the list
1026     */
1027    private void animateListToWidthAtVelocity(float velocity, int width) {
1028        if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
1029            mAnimatorSet.end();
1030        }
1031
1032        ArrayList<Animator> animators = new ArrayList<Animator>();
1033        int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
1034        for (int i = 0; i < mTotalModes; i++) {
1035            ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
1036                    "visibleWidth", width);
1037            int duration = (int) ((float) width / velocity);
1038            animator.setDuration(duration);
1039            animators.add(animator);
1040            if (i == focusItem) {
1041                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1042                    @Override
1043                    public void onAnimationUpdate(ValueAnimator animation) {
1044                        onVisibleWidthChanged((Integer) animation.getAnimatedValue());
1045                    }
1046                });
1047            }
1048        }
1049
1050        mAnimatorSet = new AnimatorSet();
1051        mAnimatorSet.playTogether(animators);
1052        mAnimatorSet.setInterpolator(null);
1053        mAnimatorSet.addListener(mModeListAnimatorListener);
1054        mAnimatorSet.start();
1055    }
1056
1057    /**
1058     * Called when the back key is pressed.
1059     *
1060     * @return Whether the UI responded to the key event.
1061     */
1062    public boolean onBackPressed() {
1063        if (mState == FULLY_SHOWN) {
1064            snapBack();
1065            return true;
1066        } else {
1067            return false;
1068        }
1069    }
1070
1071    public void startModeSelectionAnimation() {
1072        if (mState != MODE_SELECTED || mCurrentEffect == null) {
1073            setVisibility(INVISIBLE);
1074            snapBack(false);
1075            mCurrentEffect = null;
1076        } else {
1077            mCurrentEffect.startAnimation();
1078        }
1079
1080    }
1081
1082    private class PeepholeAnimationEffect extends AnimationEffects {
1083
1084        private final static int UNSET = -1;
1085        private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 300;
1086
1087        private final Paint mMaskPaint = new Paint();
1088        private final Paint mBackgroundPaint = new Paint();
1089        private final RectF mBackgroundDrawArea = new RectF();
1090
1091        private int mWidth;
1092        private int mHeight;
1093        private int mPeepHoleCenterX = UNSET;
1094        private int mPeepHoleCenterY = UNSET;
1095        private float mRadius = 0f;
1096        private ValueAnimator mPeepHoleAnimator;
1097        private Runnable mEndAction;
1098        private Bitmap mBackground;
1099        private Bitmap mBlurredBackground;
1100        private Bitmap mBackgroundOverlay;
1101
1102        public PeepholeAnimationEffect() {
1103            mMaskPaint.setAlpha(0);
1104            mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
1105        }
1106
1107        @Override
1108        public void setSize(int width, int height) {
1109            mWidth = width;
1110            mHeight = height;
1111        }
1112
1113        @Override
1114        public void drawForeground(Canvas canvas) {
1115            // Draw the circle in clear mode
1116            if (mPeepHoleAnimator != null) {
1117                // Draw a transparent circle using clear mode
1118                canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint);
1119            }
1120        }
1121
1122        public void setAnimationStartingPosition(int x, int y) {
1123            mPeepHoleCenterX = x;
1124            mPeepHoleCenterY = y;
1125        }
1126
1127        /**
1128         * Sets the bitmap to be drawn in the background and the drawArea to draw
1129         * the bitmap. In the meantime, start processing the image in a background
1130         * thread to get a blurred background image.
1131         *
1132         * @param background image to be drawn in the background
1133         * @param drawArea area to draw the background image
1134         */
1135        public void setBackground(Bitmap background, RectF drawArea) {
1136            mBackground = background;
1137            mBackgroundDrawArea.set(drawArea);
1138            new BlurTask().execute(Bitmap.createScaledBitmap(background, background.getWidth(),
1139                    background.getHeight(), true));
1140        }
1141
1142        /**
1143         * Sets the overlay image to be drawn on top of the background.
1144         */
1145        public void setBackgroundOverlay(Bitmap overlay) {
1146            mBackgroundOverlay = overlay;
1147        }
1148
1149        /**
1150         * This gets called when a blurred image of the background is generated.
1151         * Start an animation to fade in the blur.
1152         *
1153         * @param blur blurred image of the background.
1154         */
1155        public void setBlurredBackground(Bitmap blur) {
1156            mBlurredBackground = blur;
1157            // Start fade in.
1158            ObjectAnimator alpha = ObjectAnimator.ofInt(mBackgroundPaint, "alpha", 80, 255);
1159            alpha.setDuration(250);
1160            alpha.setInterpolator(Gusterpolator.INSTANCE);
1161            alpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1162                @Override
1163                public void onAnimationUpdate(ValueAnimator animation) {
1164                    invalidate();
1165                }
1166            });
1167            alpha.start();
1168            invalidate();
1169        }
1170
1171        @Override
1172        public void drawBackground(Canvas canvas) {
1173            if (mBackground != null && mBackgroundOverlay != null) {
1174                canvas.drawARGB(255, 0, 0, 0);
1175                canvas.drawBitmap(mBackground, null, mBackgroundDrawArea, null);
1176                if (mBlurredBackground != null) {
1177                    canvas.drawBitmap(mBlurredBackground, null, mBackgroundDrawArea, mBackgroundPaint);
1178                }
1179                canvas.drawBitmap(mBackgroundOverlay, 0, 0, null);
1180            }
1181        }
1182
1183        @Override
1184        public void startAnimation() {
1185            if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) {
1186                return;
1187            }
1188            if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) {
1189                mPeepHoleCenterX = mWidth / 2;
1190                mPeepHoleCenterY = mHeight / 2;
1191            }
1192
1193            int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX);
1194            int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY);
1195            int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge
1196                    + verticalDistanceToFarEdge * verticalDistanceToFarEdge));
1197            int startRadius = getResources().getDimensionPixelSize(
1198                    R.dimen.mode_selector_icon_block_width) / 2;
1199
1200            mPeepHoleAnimator = ValueAnimator.ofFloat(0, endRadius);
1201            mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS);
1202            mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE);
1203            mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1204                @Override
1205                public void onAnimationUpdate(ValueAnimator animation) {
1206                    // Modify mask by enlarging the hole
1207                    mRadius = (Float) mPeepHoleAnimator.getAnimatedValue();
1208                    invalidate();
1209                }
1210            });
1211
1212            mPeepHoleAnimator.addListener(new Animator.AnimatorListener() {
1213                @Override
1214                public void onAnimationStart(Animator animation) {
1215
1216                }
1217
1218                @Override
1219                public void onAnimationEnd(Animator animation) {
1220                    if (mEndAction != null) {
1221                        post(mEndAction);
1222                        mEndAction = null;
1223                        post(new Runnable() {
1224                            @Override
1225                            public void run() {
1226                                mPeepHoleAnimator = null;
1227                                mRadius = 0;
1228                                mPeepHoleCenterX = UNSET;
1229                                mPeepHoleCenterY = UNSET;
1230                            }
1231                        });
1232                    } else {
1233                        mPeepHoleAnimator = null;
1234                        mRadius = 0;
1235                        mPeepHoleCenterX = UNSET;
1236                        mPeepHoleCenterY = UNSET;
1237                    }
1238                }
1239
1240                @Override
1241                public void onAnimationCancel(Animator animation) {
1242
1243                }
1244
1245                @Override
1246                public void onAnimationRepeat(Animator animation) {
1247
1248                }
1249            });
1250            mPeepHoleAnimator.start();
1251        }
1252
1253        public void setAnimationEndAction(Runnable runnable) {
1254            mEndAction = runnable;
1255        }
1256
1257        private class BlurTask extends AsyncTask<Bitmap, Integer, Bitmap> {
1258
1259            // Gaussian blur mask size.
1260            private static final int MASK_SIZE = 7;
1261            @Override
1262            protected Bitmap doInBackground(Bitmap... params) {
1263
1264                Bitmap intermediateBitmap = params[0];
1265                int factor = 4;
1266                Bitmap lowResPreview = Bitmap.createScaledBitmap(intermediateBitmap,
1267                        intermediateBitmap.getWidth() / factor,
1268                        intermediateBitmap.getHeight() / factor, true);
1269
1270                int width = lowResPreview.getWidth();
1271                int height = lowResPreview.getHeight();
1272
1273                if (mInputPixels == null || mInputPixels.length < width * height) {
1274                    mInputPixels = new int[width * height];
1275                    mOutputPixels = new int[width * height];
1276                }
1277                lowResPreview.getPixels(mInputPixels, 0, width, 0, 0, width, height);
1278                CameraUtil.blur(mInputPixels, mOutputPixels, width, height, MASK_SIZE);
1279                lowResPreview.setPixels(mOutputPixels, 0, width, 0, 0, width, height);
1280
1281                intermediateBitmap.recycle();
1282                return Bitmap.createScaledBitmap(lowResPreview, width * factor,
1283                        height * factor, true);
1284            }
1285
1286            @Override
1287            protected void onPostExecute(Bitmap bitmap) {
1288                setBlurredBackground(bitmap);
1289            }
1290        };
1291    }
1292}
1293