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