OSDN Git Service

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