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