ModeListView.java revision e1052c6501f9c1150aeaed99b57744f3afde61ff
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.content.res.Configuration;
26import android.graphics.Canvas;
27import android.graphics.Paint;
28import android.graphics.PorterDuff;
29import android.graphics.PorterDuffXfermode;
30import android.os.SystemClock;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.view.GestureDetector;
34import android.view.LayoutInflater;
35import android.view.MotionEvent;
36import android.widget.LinearLayout;
37import android.widget.ScrollView;
38
39import com.android.camera.util.Gusterpolator;
40import com.android.camera.widget.AnimationEffects;
41import com.android.camera2.R;
42
43import java.util.ArrayList;
44import java.util.LinkedList;
45import java.util.List;
46
47/**
48 * ModeListView class displays all camera modes and settings in the form
49 * of a list. A swipe to the right will bring up this list. Then tapping on
50 * any of the items in the list will take the user to that corresponding mode
51 * with an animation. To dismiss this list, simply swipe left or select a mode.
52 */
53public class ModeListView extends ScrollView {
54
55    /** Simple struct that defines the look of a mode in the mode switcher. */
56    private static class Mode {
57        /** Resource ID of the icon for this mode. */
58        public final int iconResId;
59        /** Resource ID for the text of this mode. */
60        public final int textResId;
61        /** The ID of the color for this mode. */
62        public final int colorId;
63
64        public Mode(int iconResId, int textResId, int colorId) {
65            this.iconResId = iconResId;
66            this.textResId = textResId;
67            this.colorId = colorId;
68        }
69    }
70
71
72    private static final String TAG = "ModeListView";
73
74    // Animation Durations
75    private static final int DEFAULT_DURATION_MS = 200;
76    private static final int FLY_IN_DURATION_MS = 850;
77    private static final int HOLD_DURATION_MS = 0;
78    private static final int FLY_OUT_DURATION_MS = 850;
79    private static final int START_DELAY_MS = 100;
80    private static final int TOTAL_DURATION_MS = FLY_IN_DURATION_MS + HOLD_DURATION_MS
81            + FLY_OUT_DURATION_MS;
82
83    // Different modes in the mode list. Change these to change the order they
84    // appear in the mode switcher.
85    public static final int MODE_PHOTO = 0;
86    public static final int MODE_VIDEO = 1;
87    public static final int MODE_CRAFT = 2;
88    public static final int MODE_WIDEANGLE = 3;
89    public static final int MODE_PHOTOSPHERE = 4;
90    public static final int MODE_TIMELAPSE = 5;
91    public static final int MODE_SETTING = 6;
92    // Special case
93    public static final int MODE_GCAM = 100;
94    private static final int MODE_TOTAL = 7;
95    private static final float ROWS_TO_SHOW_IN_LANDSCAPE = 4.5f;
96    private static final int NO_ITEM_SELECTED = -1;
97
98    // Scrolling states
99    private static final int IDLE = 0;
100    private static final int FULLY_SHOWN = 1;
101    private static final int ACCORDION_ANIMATION = 2;
102    private static final int SCROLLING = 3;
103    private static final int MODE_SELECTED = 4;
104
105    // Scrolling delay between non-focused item and focused item
106    private static final int DELAY_MS = 25;
107
108    private static final Mode[] mModes;
109    static {
110        mModes = new Mode[MODE_TOTAL];
111        mModes[MODE_PHOTO] = new Mode(R.drawable.ic_camera_normal,
112                R.string.mode_camera,
113                R.color.camera_mode_color);
114        mModes[MODE_VIDEO] = new Mode(R.drawable.ic_video_normal,
115                R.string.mode_video,
116                R.color.video_mode_color);
117        mModes[MODE_PHOTOSPHERE] = new Mode(R.drawable.ic_photo_sphere_normal,
118                R.string.mode_photosphere,
119                R.color.photosphere_mode_color);
120        mModes[MODE_CRAFT] = new Mode(R.drawable.ic_camera_normal,
121                R.string.mode_advanced_camera,
122                R.color.craft_mode_color);
123        mModes[MODE_TIMELAPSE] = new Mode(R.drawable.ic_timelapse_normal,
124                R.string.mode_timelapse,
125                R.color.timelapse_mode_color);
126        mModes[MODE_WIDEANGLE] = new Mode(R.drawable.ic_panorama_normal,
127                R.string.mode_panorama,
128                R.color.panorama_mode_color);
129        mModes[MODE_SETTING] = new Mode(R.drawable.ic_settings_normal,
130                R.string.mode_settings,
131                R.color.settings_mode_color);
132    }
133
134    private final GestureDetector mGestureDetector;
135    private final int mIconBlockWidth;
136
137    private int mListBackgroundColor;
138    private LinearLayout mListView;
139    private int mState = IDLE;
140    private int mTotalModes;
141    private ModeSelectorItem[] mModeSelectorItems;
142    private AnimatorSet mAnimatorSet;
143    private int mFocusItem = NO_ITEM_SELECTED;
144    private AnimationEffects mCurrentEffect;
145
146    // Width and height of this view. They get updated in onLayout()
147    // Unit for width and height are pixels.
148    private int mWidth;
149    private int mHeight;
150    private float mScrollTrendX = 0f;
151    private float mScrollTrendY = 0f;
152    private ModeSwitchListener mListener = null;
153    private int[] mSupportedModes;
154    private final LinkedList<TimeBasedPosition> mPositionHistory
155            = new LinkedList<TimeBasedPosition>();
156    private long mCurrentTime;
157
158    public interface ModeSwitchListener {
159        public void onModeSelected(int modeIndex);
160    }
161
162    /**
163     * This class aims to help store time and position in pairs.
164     */
165    private static class TimeBasedPosition {
166        private final float mPosition;
167        private final long mTimeStamp;
168        public TimeBasedPosition(float position, long time) {
169            mPosition = position;
170            mTimeStamp = time;
171        }
172
173        public float getPosition() {
174            return mPosition;
175        }
176
177        public long getTimeStamp() {
178            return mTimeStamp;
179        }
180    }
181
182    /**
183     * This is a highly customized interpolator. The purpose of having this subclass
184     * is to encapsulate intricate animation timing, so that the actual animation
185     * implementation can be re-used with other interpolators to achieve different
186     * animation effects.
187     *
188     * The accordion animation consists of three stages:
189     * 1) Animate into the screen within a pre-specified fly in duration.
190     * 2) Hold in place for a certain amount of time (Optional).
191     * 3) Animate out of the screen within the given time.
192     *
193     * The accordion animator is initialized with 3 parameter: 1) initial position,
194     * 2) how far out the view should be before flying back out,  3) end position.
195     * The interpolation output should be [0f, 0.5f] during animation between 1)
196     * to 2), and [0.5f, 1f] for flying from 2) to 3).
197     */
198    private final TimeInterpolator mAccordionInterpolator = new TimeInterpolator() {
199        @Override
200        public float getInterpolation(float input) {
201
202            float flyInDuration = (float) FLY_OUT_DURATION_MS / (float) TOTAL_DURATION_MS;
203            float holdDuration = (float) (FLY_OUT_DURATION_MS + HOLD_DURATION_MS)
204                    / (float) TOTAL_DURATION_MS;
205            if (input == 0) {
206                return 0;
207            }else if (input < flyInDuration) {
208                // Stage 1, project result to [0f, 0.5f]
209                input /= flyInDuration;
210                float result = Gusterpolator.INSTANCE.getInterpolation(input);
211                return result * 0.5f;
212            } else if (input < holdDuration) {
213                // Stage 2
214                return 0.5f;
215            } else {
216                // Stage 3, project result to [0.5f, 1f]
217                input -= holdDuration;
218                input /= (1 - holdDuration);
219                float result = Gusterpolator.INSTANCE.getInterpolation(input);
220                return 0.5f + result * 0.5f;
221            }
222        }
223    };
224
225    /**
226     * The listener that is used to notify when gestures occur.
227     * Here we only listen to a subset of gestures.
228     */
229    private final GestureDetector.OnGestureListener mOnGestureListener
230            = new GestureDetector.SimpleOnGestureListener(){
231        @Override
232        public boolean onScroll(MotionEvent e1, MotionEvent e2,
233                                float distanceX, float distanceY) {
234
235            if (mState == ACCORDION_ANIMATION) {
236                // Scroll happens during accordion animation.
237                if (isRunningAccordionAnimation()) {
238                    mAnimatorSet.cancel();
239                }
240                setVisibility(VISIBLE);
241            }
242
243            if (mState == IDLE) {
244                resetModeSelectors();
245                setVisibility(VISIBLE);
246            }
247
248            mState = SCROLLING;
249            // Scroll based on the scrolling distance on the currently focused
250            // item.
251            scroll(mFocusItem, distanceX, distanceY);
252            return true;
253        }
254
255        @Override
256        public boolean onSingleTapUp(MotionEvent ev) {
257            if (mState != FULLY_SHOWN) {
258                // Only allows tap to choose mode when the list is fully shown
259                return false;
260            }
261            int index = getFocusItem(ev.getX(), ev.getY());
262            // Validate the selection
263            if (index != NO_ITEM_SELECTED) {
264                int modeId = getModeIndex(index);
265                mModeSelectorItems[index].highlight();
266                mState = MODE_SELECTED;
267                PeepholeAnimationEffect effect = new PeepholeAnimationEffect();
268                effect.setSize(mWidth, mHeight);
269                effect.setAnimationEndAction(new Runnable() {
270                    @Override
271                    public void run() {
272                        setVisibility(INVISIBLE);
273                        mCurrentEffect = null;
274                        snapBack(false);
275                    }
276                });
277                effect.setAnimationStartingPosition((int) ev.getX(), (int) ev.getY());
278                mCurrentEffect = effect;
279
280                onModeSelected(modeId);
281            }
282            return true;
283        }
284    };
285
286    public ModeListView(Context context, AttributeSet attrs) {
287        super(context, attrs);
288        mGestureDetector = new GestureDetector(context, mOnGestureListener);
289        mIconBlockWidth = getResources()
290                .getDimensionPixelSize(R.dimen.mode_selector_icon_block_width);
291        mListBackgroundColor = getResources().getColor(R.color.mode_list_background);
292    }
293
294    /**
295     * Sets the alpha on the list background. This is called whenever the list
296     * is scrolling or animating, so that background can adjust its dimness.
297     *
298     * @param alpha new alpha to be applied on list background color
299     */
300    private void setBackgroundAlpha(int alpha) {
301        // Make sure alpha is valid.
302        alpha = alpha & 0xFF;
303        // Change alpha on the background color.
304        mListBackgroundColor = mListBackgroundColor & 0xFFFFFF;
305        mListBackgroundColor = mListBackgroundColor | (alpha << 24);
306        // Set new color to list background.
307        mListView.setBackgroundColor(mListBackgroundColor);
308    }
309
310    /**
311     * Initialize mode list with a list of indices of supported modes.
312     *
313     * @param modeIndexList a list of indices of supported modes
314     */
315    public void init(List<Integer> modeIndexList) {
316        boolean[] modeIsSupported = new boolean[MODE_TOTAL];
317        // Setting should always be supported
318        modeIsSupported[MODE_SETTING] = true;
319        mTotalModes = 1;
320
321        // Mark the supported modes in a boolean array to preserve the
322        // sequence of the modes
323        for (int i = 0; i < modeIndexList.size(); i++) {
324            int mode = modeIndexList.get(i);
325            if (mode >= MODE_TOTAL) {
326                // This is a mode that we don't display in the mode list, skip.
327                continue;
328            }
329            if (modeIsSupported[mode] == false) {
330                modeIsSupported[mode] = true;
331                mTotalModes++;
332            }
333        }
334        // Put the indices of supported modes into an array preserving their
335        // display order.
336        mSupportedModes = new int[mTotalModes];
337        int modeCount = 0;
338        for (int i = 0; i < MODE_TOTAL; i++) {
339            if (modeIsSupported[i]) {
340                mSupportedModes[modeCount] = i;
341                modeCount++;
342            }
343        }
344
345        initializeModeSelectorItems();
346    }
347
348    // TODO: Initialize mode selectors with different sizes based on number of modes supported
349    private void initializeModeSelectorItems() {
350        mModeSelectorItems = new ModeSelectorItem[mTotalModes];
351        // Inflate the mode selector items and add them to a linear layout
352        LayoutInflater inflater = (LayoutInflater) getContext()
353                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
354        mListView = (LinearLayout) findViewById(R.id.mode_list);
355        for (int i = 0; i < mTotalModes; i++) {
356            ModeSelectorItem selectorItem =
357                    (ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null);
358            mListView.addView(selectorItem);
359            // Set alternating background color for each mode selector in the list
360            if (i % 2 == 0) {
361                selectorItem.setDefaultBackgroundColor(getResources()
362                        .getColor(R.color.mode_selector_background_light));
363            } else {
364                selectorItem.setDefaultBackgroundColor(getResources()
365                        .getColor(R.color.mode_selector_background_dark));
366            }
367            int modeId = getModeIndex(i);
368            selectorItem.setIconBackgroundColor(getResources()
369                    .getColor(mModes[modeId].colorId));
370
371            // Set image
372            selectorItem.setImageResource(mModes[modeId].iconResId);
373
374            // Set text
375            CharSequence text = getResources().getText(mModes[modeId].textResId);
376            selectorItem.setText(text);
377            mModeSelectorItems[i] = selectorItem;
378        }
379
380        resetModeSelectors();
381    }
382
383    /**
384     * Maps between the UI mode selector index to the actual mode id.
385     *
386     * @param modeSelectorIndex the index of the UI item
387     * @return the index of the corresponding camera mode
388     */
389    private int getModeIndex(int modeSelectorIndex) {
390        if (modeSelectorIndex < mTotalModes && modeSelectorIndex >= 0) {
391            return mSupportedModes[modeSelectorIndex];
392        }
393        Log.e(TAG, "Invalid mode selector index: " + modeSelectorIndex + ", total modes: "
394                + mTotalModes);
395        return MODE_PHOTO;
396    }
397
398    /** Notify ModeSwitchListener, if any, of the mode change. */
399    private void onModeSelected(int modeIndex) {
400        if (mListener != null) {
401            mListener.onModeSelected(modeIndex);
402        }
403    }
404
405    /**
406     * Sets a listener that listens to receive mode switch event.
407     *
408     * @param listener a listener that gets notified when mode changes.
409     */
410    public void setModeSwitchListener(ModeSwitchListener listener) {
411        mListener = listener;
412    }
413
414    @Override
415    public boolean onTouchEvent(MotionEvent ev) {
416        if (mCurrentEffect != null) {
417            return mCurrentEffect.onTouchEvent(ev);
418        }
419
420        super.onTouchEvent(ev);
421        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
422            getParent().requestDisallowInterceptTouchEvent(true);
423            if (mState == FULLY_SHOWN) {
424                mFocusItem = NO_ITEM_SELECTED;
425                setSwipeMode(false);
426            } else {
427                mFocusItem = getFocusItem(ev.getX(), ev.getY());
428                setSwipeMode(true);
429            }
430        }
431        // Pass all touch events to gesture detector for gesture handling.
432        mGestureDetector.onTouchEvent(ev);
433        if (ev.getActionMasked() == MotionEvent.ACTION_UP ||
434                ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
435            snap();
436            mFocusItem = NO_ITEM_SELECTED;
437        }
438        return true;
439    }
440
441    /**
442     * Sets the swipe mode to indicate whether this is a swiping in
443     * or out, and therefore we can have different animations.
444     *
445     * @param swipeIn indicates whether the swipe should reveal/hide the list.
446     */
447    private void setSwipeMode(boolean swipeIn) {
448        for (int i = 0 ; i < mModeSelectorItems.length; i++) {
449            mModeSelectorItems[i].onSwipeModeChanged(swipeIn);
450        }
451    }
452
453    @Override
454    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
455        super.onLayout(changed, left, top, right, bottom);
456        mWidth = right - left;
457        mHeight = bottom - top - getPaddingTop() - getPaddingBottom();
458        if (mCurrentEffect != null) {
459            mCurrentEffect.setSize(mWidth, mHeight);
460        }
461    }
462
463    /**
464     * Here we calculate the children size based on the orientation, change
465     * their layout parameters if needed before propagating onMeasure call
466     * to the children, so the newly changed params will take effect in this
467     * pass.
468     *
469     * @param widthMeasureSpec Horizontal space requirements as imposed by the
470     *        parent
471     * @param heightMeasureSpec Vertical space requirements as imposed by the
472     *        parent
473     */
474    @Override
475    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
476        float height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop()
477                - getPaddingBottom();
478
479        Configuration config = getResources().getConfiguration();
480        if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
481            height = height / ROWS_TO_SHOW_IN_LANDSCAPE;
482            setVerticalScrollBarEnabled(true);
483        } else {
484            height = height / mTotalModes;
485            setVerticalScrollBarEnabled(false);
486        }
487        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(mWidth, 0);
488        lp.width = LayoutParams.MATCH_PARENT;
489        for (int i = 0; i < mTotalModes; i++) {
490            // This is to avoid rounding that would cause the total height of the
491            // list a few pixels off the height of the screen.
492            int itemHeight = (int) (height * (i + 1)) - (int) (height * i);
493            lp.height = itemHeight;
494            mModeSelectorItems[i].setLayoutParams(lp);
495        }
496        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
497    }
498
499    @Override
500    public void draw(Canvas canvas) {
501        if (mCurrentEffect != null) {
502            mCurrentEffect.drawBackground(canvas);
503            super.draw(canvas);
504            mCurrentEffect.drawForeground(canvas);
505        } else {
506            super.draw(canvas);
507        }
508    }
509
510    /**
511     * This starts the accordion animation, unless it's already running, in which
512     * case the start animation call will be ignored.
513     */
514    public void startAccordionAnimation() {
515        if (mState != IDLE) {
516            return;
517        }
518        if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
519            return;
520        }
521        mState = ACCORDION_ANIMATION;
522        resetModeSelectors();
523        animateListToWidth(START_DELAY_MS, TOTAL_DURATION_MS, mAccordionInterpolator,
524                0, mIconBlockWidth, 0);
525    }
526
527    /**
528     * This starts the accordion animation with a delay.
529     *
530     * @param delay delay in milliseconds before starting animation
531     */
532    public void startAccordionAnimationWithDelay(int delay) {
533        postDelayed(new Runnable() {
534            @Override
535            public void run() {
536                startAccordionAnimation();
537            }
538        }, delay);
539    }
540
541    /**
542     * Resets the visible width of all the mode selectors to 0.
543     */
544    private void resetModeSelectors() {
545        for (int i = 0; i < mModeSelectorItems.length; i++) {
546            mModeSelectorItems[i].setVisibleWidth(0);
547            mModeSelectorItems[i].unHighlight();
548        }
549        // Visible width has been changed to 0
550        onVisibleWidthChanged(0);
551    }
552
553    private boolean isRunningAccordionAnimation() {
554        return mAnimatorSet != null && mAnimatorSet.isRunning();
555    }
556
557    /**
558     * Calculate the mode selector item in the list that is at position (x, y).
559     *
560     * @param x horizontal position
561     * @param y vertical position
562     * @return index of the item that is at position (x, y)
563     */
564    private int getFocusItem(float x, float y) {
565        // Take into account the scrolling offset
566        x += getScrollX();
567        y += getScrollY();
568
569        for (int i = 0; i < mModeSelectorItems.length; i++) {
570            if (mModeSelectorItems[i].getTop() <= y && mModeSelectorItems[i].getBottom() >= y) {
571                return i;
572            }
573        }
574        return NO_ITEM_SELECTED;
575    }
576
577    private void scroll(int itemId, float deltaX, float deltaY) {
578        // Scrolling trend on X and Y axis, to track the trend by biasing
579        // towards latest touch events.
580        mScrollTrendX = mScrollTrendX * 0.3f + deltaX * 0.7f;
581        mScrollTrendY = mScrollTrendY * 0.3f + deltaY * 0.7f;
582
583        // TODO: Change how the curve is calculated below when UX finalize their design.
584        mCurrentTime = SystemClock.uptimeMillis();
585        float longestWidth;
586        if (itemId != NO_ITEM_SELECTED) {
587            longestWidth = mModeSelectorItems[itemId].getVisibleWidth() - deltaX;
588        } else {
589            longestWidth = mModeSelectorItems[0].getVisibleWidth() - deltaX;
590        }
591        insertNewPosition(longestWidth, mCurrentTime);
592
593        for (int i = 0; i < mModeSelectorItems.length; i++) {
594            mModeSelectorItems[i].setVisibleWidth(calculateVisibleWidthForItem(i,
595                    (int) longestWidth));
596        }
597        if (longestWidth <= 0) {
598            reset();
599        }
600
601        itemId = itemId == NO_ITEM_SELECTED ? 0 : itemId;
602        onVisibleWidthChanged(mModeSelectorItems[itemId].getVisibleWidth());
603    }
604
605    /**
606     * Calculate the width of a specified item based on its position relative to
607     * the item with longest width.
608     */
609    private int calculateVisibleWidthForItem(int itemId, int longestWidth) {
610        if (itemId == mFocusItem || mFocusItem == NO_ITEM_SELECTED) {
611            return longestWidth;
612        }
613
614        int delay = Math.abs(itemId - mFocusItem) * DELAY_MS;
615        return (int) getPosition(mCurrentTime - delay);
616    }
617
618    /**
619     * Insert new position and time stamp into the history position list, and
620     * remove stale position items.
621     *
622     * @param position latest position of the focus item
623     * @param time  current time in milliseconds
624     */
625    private void insertNewPosition(float position, long time) {
626        // TODO: Consider re-using stale position objects rather than
627        // always creating new position objects.
628        mPositionHistory.add(new TimeBasedPosition(position, time));
629
630        // Positions that are from too long ago will not be of any use for
631        // future position interpolation. So we need to remove those positions
632        // from the list.
633        long timeCutoff = time - (mTotalModes - 1) * DELAY_MS;
634        while (mPositionHistory.size() > 0) {
635            // Remove all the position items that are prior to the cutoff time.
636            TimeBasedPosition historyPosition = mPositionHistory.getFirst();
637            if (historyPosition.getTimeStamp() < timeCutoff) {
638                mPositionHistory.removeFirst();
639            } else {
640                break;
641            }
642        }
643    }
644
645    /**
646     * Gets the interpolated position at the specified time. This involves going
647     * through the recorded positions until a {@link TimeBasedPosition} is found
648     * such that the position the recorded before the given time, and the
649     * {@link TimeBasedPosition} after that is recorded no earlier than the given
650     * time. These two positions are then interpolated to get the position at the
651     * specified time.
652     */
653    private float getPosition(long time) {
654        int i;
655        for (i = 0; i < mPositionHistory.size(); i++) {
656            TimeBasedPosition historyPosition = mPositionHistory.get(i);
657            if (historyPosition.getTimeStamp() > time) {
658                // Found the winner. Now interpolate between position i and position i - 1
659                if (i == 0) {
660                    return historyPosition.getPosition();
661                } else {
662                    TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1);
663                    // Start interpolation
664                    float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) /
665                            (float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp());
666                    float position = fraction * (historyPosition.getPosition()
667                            - prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition();
668                    return position;
669                }
670            }
671        }
672        // It should never get here.
673        Log.e(TAG, "Invalid time input for getPosition(). time: " + time);
674        if (mPositionHistory.size() == 0) {
675            Log.e(TAG, "TimeBasedPosition history size is 0");
676        } else {
677            Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp()
678            + " , last position recorded at " + mPositionHistory.getLast().getTimeStamp());
679        }
680        assert (i < mPositionHistory.size());
681        return i;
682    }
683
684    private void reset() {
685        resetModeSelectors();
686        mScrollTrendX = 0f;
687        mScrollTrendY = 0f;
688        mCurrentEffect = null;
689        setVisibility(INVISIBLE);
690    }
691
692    /**
693     * When visible width of list is changed, the background of the list needs
694     * to darken/lighten correspondingly.
695     */
696    private void onVisibleWidthChanged(int focusItemWidth) {
697        // Background alpha should be 0 before the icon block is entirely visible,
698        // and when the longest mode item is entirely shown (across the screen), the
699        // background should be 50% transparent.
700        if (focusItemWidth <= mIconBlockWidth) {
701            setBackgroundAlpha(0);
702        } else {
703            // Alpha should increase linearly when mode item goes beyond the icon block
704            // till it reaches its max width
705            int alpha = 127 * (focusItemWidth - mIconBlockWidth) / (mWidth - mIconBlockWidth);
706            setBackgroundAlpha(alpha);
707        }
708    }
709
710    @Override
711    public void onWindowVisibilityChanged(int visibility) {
712        super.onWindowVisibilityChanged(visibility);
713        if (visibility != VISIBLE) {
714            // Reset mode list if the window is no longer visible.
715            reset();
716            mState = IDLE;
717        }
718    }
719
720    /**
721     * The list view should either snap back or snap to full screen after a gesture.
722     * This function is called when an up or cancel event is received, and then based
723     * on the current position of the list and the gesture we can decide which way
724     * to snap.
725     */
726    private void snap() {
727        if (mState == SCROLLING) {
728            int itemId = Math.max(0, mFocusItem);
729            if (mModeSelectorItems[itemId].getVisibleWidth() < mIconBlockWidth) {
730                snapBack();
731            } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) {
732                snapBack();
733            } else {
734                snapToFullScreen();
735            }
736        }
737    }
738
739    /**
740     * Snaps back out of the screen.
741     *
742     * @param withAnimation whether snapping back should be animated
743     */
744    public void snapBack(boolean withAnimation) {
745        if (withAnimation) {
746            animateListToWidth(0);
747            mState = IDLE;
748        } else {
749            setVisibility(INVISIBLE);
750            resetModeSelectors();
751            mState = IDLE;
752        }
753    }
754
755    /**
756     * Snaps the mode list back out with animation.
757     */
758    private void snapBack() {
759        snapBack(true);
760    }
761
762    private void snapToFullScreen() {
763        animateListToWidth(mWidth);
764        mState = FULLY_SHOWN;
765    }
766
767    /**
768     * Overloaded function to provide a simple way to start animation. Animation
769     * will use default duration, and a value of <code>null</code> for interpolator
770     * means linear interpolation will be used.
771     *
772     * @param width a set of values that the animation will animate between over time
773     */
774    private void animateListToWidth(int... width) {
775        animateListToWidth(0, DEFAULT_DURATION_MS, null, width);
776    }
777
778    /**
779     * Animate the mode list between the given set of visible width.
780     *
781     * @param delay start delay between consecutive mode item
782     * @param duration duration for the animation of each mode item
783     * @param interpolator interpolator to be used by the animation
784     * @param width a set of values that the animation will animate between over time
785     */
786    private void animateListToWidth(int delay, int duration,
787                                    TimeInterpolator interpolator, int... width) {
788        if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
789            mAnimatorSet.end();
790        }
791
792        ArrayList<Animator> animators = new ArrayList<Animator>();
793        int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
794        for (int i = 0; i < mTotalModes; i++) {
795            ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
796                    "visibleWidth", width);
797            animator.setDuration(duration);
798            animator.setStartDelay(i * delay);
799            animators.add(animator);
800            if (i == focusItem) {
801                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
802                    @Override
803                    public void onAnimationUpdate(ValueAnimator animation) {
804                        onVisibleWidthChanged((Integer) animation.getAnimatedValue());
805                    }
806                });
807            }
808        }
809
810        mAnimatorSet = new AnimatorSet();
811        mAnimatorSet.playTogether(animators);
812        mAnimatorSet.setInterpolator(interpolator);
813        mAnimatorSet.addListener(new Animator.AnimatorListener() {
814
815            @Override
816            public void onAnimationStart(Animator animation) {
817                setVisibility(VISIBLE);
818            }
819
820            @Override
821            public void onAnimationEnd(Animator animation) {
822                mAnimatorSet = null;
823                if (mState == ACCORDION_ANIMATION || mState == IDLE) {
824                    resetModeSelectors();
825                    setVisibility(INVISIBLE);
826                    mState = IDLE;
827                }
828            }
829
830            @Override
831            public void onAnimationCancel(Animator animation) {
832            }
833
834            @Override
835            public void onAnimationRepeat(Animator animation) {
836
837            }
838        });
839        mAnimatorSet.start();
840    }
841
842    /**
843     * Get the theme color of a specific mode.
844     *
845     * @param modeIndex index of the mode
846     * @return theme color of the mode if input index is valid, otherwise 0
847     */
848    public static int getModeThemeColor(int modeIndex) {
849        // Photo and gcam has the same theme color
850        if (modeIndex == MODE_GCAM) {
851            return mModes[MODE_PHOTO].colorId;
852        }
853        if (modeIndex < 0 || modeIndex >= MODE_TOTAL) {
854            return 0;
855        } else {
856            return mModes[modeIndex].colorId;
857        }
858    }
859
860    /**
861     * Get the mode icon resource id of a specific mode.
862     *
863     * @param modeIndex index of the mode
864     * @return icon resource id if the index is valid, otherwise 0
865     */
866    public static int getModeIconResourceId(int modeIndex) {
867        // Photo and gcam has the same mode icon
868        if (modeIndex == MODE_GCAM) {
869            return mModes[MODE_PHOTO].iconResId;
870        }
871        if (modeIndex < 0 || modeIndex >= MODE_TOTAL) {
872            return 0;
873        } else {
874            return mModes[modeIndex].iconResId;
875        }
876    }
877
878    /**
879     * Called when the back key is pressed.
880     *
881     * @return Whether the UI responded to the key event.
882     */
883    public boolean onBackPressed() {
884        if (mState == FULLY_SHOWN) {
885            snapBack();
886            return true;
887        } else {
888            return false;
889        }
890    }
891
892    public void startModeSelectionAnimation() {
893        if (mState != MODE_SELECTED || mCurrentEffect == null) {
894            setVisibility(INVISIBLE);
895            snapBack(false);
896            mCurrentEffect = null;
897        } else {
898            mCurrentEffect.startAnimation();
899        }
900
901    }
902
903    private class PeepholeAnimationEffect extends AnimationEffects {
904
905        private final static int UNSET = -1;
906        private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 650;
907
908        private int mWidth;
909        private int mHeight;
910
911        private int mPeepHoleCenterX = UNSET;
912        private int mPeepHoleCenterY = UNSET;
913        private float mRadius = 0f;
914        private ValueAnimator mPeepHoleAnimator;
915        private Runnable mEndAction;
916        private final Paint mMaskPaint = new Paint();
917
918        public PeepholeAnimationEffect() {
919            mMaskPaint.setAlpha(0);
920            mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
921        }
922
923        @Override
924        public void setSize(int width, int height) {
925            mWidth = width;
926            mHeight = height;
927        }
928
929        @Override
930        public void drawForeground(Canvas canvas) {
931            // Draw the circle in clear mode
932            if (mPeepHoleAnimator != null) {
933                // Draw a transparent circle using clear mode
934                canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint);
935            }
936        }
937
938        public void setAnimationStartingPosition(int x, int y) {
939            mPeepHoleCenterX = x;
940            mPeepHoleCenterY = y;
941        }
942
943        @Override
944        public void startAnimation() {
945            if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) {
946                return;
947            }
948            if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) {
949                mPeepHoleCenterX = mWidth / 2;
950                mPeepHoleCenterY = mHeight / 2;
951            }
952
953            int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX);
954            int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY);
955            int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge
956                    + verticalDistanceToFarEdge * verticalDistanceToFarEdge));
957
958            mPeepHoleAnimator = ValueAnimator.ofFloat(0, endRadius);
959            mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS);
960            mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE);
961            mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
962                @Override
963                public void onAnimationUpdate(ValueAnimator animation) {
964                    // Modify mask by enlarging the hole
965                    mRadius = (Float) mPeepHoleAnimator.getAnimatedValue();
966                    invalidate();
967                }
968            });
969
970            mPeepHoleAnimator.addListener(new Animator.AnimatorListener() {
971                @Override
972                public void onAnimationStart(Animator animation) {
973
974                }
975
976                @Override
977                public void onAnimationEnd(Animator animation) {
978                    if (mEndAction != null) {
979                        post(mEndAction);
980                        mEndAction = null;
981                        post(new Runnable() {
982                            @Override
983                            public void run() {
984                                mPeepHoleAnimator = null;
985                                mRadius = 0;
986                                mPeepHoleCenterX = UNSET;
987                                mPeepHoleCenterY = UNSET;
988                            }
989                        });
990                    } else {
991                        mPeepHoleAnimator = null;
992                        mRadius = 0;
993                        mPeepHoleCenterX = UNSET;
994                        mPeepHoleCenterY = UNSET;
995                    }
996                }
997
998                @Override
999                public void onAnimationCancel(Animator animation) {
1000
1001                }
1002
1003                @Override
1004                public void onAnimationRepeat(Animator animation) {
1005
1006                }
1007            });
1008            mPeepHoleAnimator.start();
1009        }
1010
1011        public void setAnimationEndAction(Runnable runnable) {
1012            mEndAction = runnable;
1013        }
1014    }
1015}
1016