blob: be45763584c270d7839bee6dacddda7636e7b690 [file] [log] [blame]
Doris Liu1c94b7d2013-11-09 19:13:44 -08001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.camera.ui;
18
19import android.animation.Animator;
20import android.animation.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
Doris Liu70576b62013-11-14 20:30:33 -080023import android.animation.ValueAnimator;
Doris Liu1c94b7d2013-11-09 19:13:44 -080024import android.content.Context;
Doris Liu213a4a02014-02-04 16:57:55 -080025import android.graphics.Bitmap;
Doris Liub6eaa8c2013-12-11 18:39:14 -080026import android.graphics.Canvas;
27import android.graphics.Paint;
28import android.graphics.PorterDuff;
29import android.graphics.PorterDuffXfermode;
Doris Liu213a4a02014-02-04 16:57:55 -080030import android.graphics.RectF;
31import android.os.AsyncTask;
Doris Liucfaf7b02013-12-06 01:08:59 -080032import android.os.SystemClock;
Doris Liu1c94b7d2013-11-09 19:13:44 -080033import android.util.AttributeSet;
Doris Liubd1b8f92014-01-03 17:59:51 -080034import android.util.SparseArray;
Doris Liu1c94b7d2013-11-09 19:13:44 -080035import android.view.GestureDetector;
36import android.view.LayoutInflater;
37import android.view.MotionEvent;
Doris Liu213a4a02014-02-04 16:57:55 -080038import android.view.View;
39import android.widget.FrameLayout;
Doris Liu1c94b7d2013-11-09 19:13:44 -080040import android.widget.LinearLayout;
Doris Liu1c94b7d2013-11-09 19:13:44 -080041
Doris Liu213a4a02014-02-04 16:57:55 -080042import com.android.camera.app.CameraAppUI;
Angus Kong2bca2102014-03-11 16:27:30 -070043import com.android.camera.debug.Log;
Doris Liubd1b8f92014-01-03 17:59:51 -080044import com.android.camera.util.CameraUtil;
Doris Liu1c94b7d2013-11-09 19:13:44 -080045import com.android.camera.util.Gusterpolator;
Doris Liub6eaa8c2013-12-11 18:39:14 -080046import com.android.camera.widget.AnimationEffects;
Doris Liue7e0c022014-02-12 11:14:08 -080047import com.android.camera.widget.SettingsButton;
Doris Liu1c94b7d2013-11-09 19:13:44 -080048import com.android.camera2.R;
49
50import java.util.ArrayList;
Doris Liucfaf7b02013-12-06 01:08:59 -080051import java.util.LinkedList;
Doris Liua20074f2013-12-09 15:19:06 -080052import java.util.List;
Doris Liu1c94b7d2013-11-09 19:13:44 -080053
Doris Liu70576b62013-11-14 20:30:33 -080054/**
55 * ModeListView class displays all camera modes and settings in the form
56 * of a list. A swipe to the right will bring up this list. Then tapping on
57 * any of the items in the list will take the user to that corresponding mode
58 * with an animation. To dismiss this list, simply swipe left or select a mode.
59 */
Doris Liu213a4a02014-02-04 16:57:55 -080060public class ModeListView extends FrameLayout
Doris Liu69cb8402014-03-01 15:05:13 -080061 implements PreviewStatusListener.PreviewAreaChangedListener,
62 ModeSelectorItem.VisibleWidthChangedListener {
Sascha Haeberling601921e2013-12-17 10:01:11 -080063
Angus Kong2bca2102014-03-11 16:27:30 -070064 private static final Log.Tag TAG = new Log.Tag("ModeListView");
Doris Liu70576b62013-11-14 20:30:33 -080065
66 // Animation Durations
67 private static final int DEFAULT_DURATION_MS = 200;
Doris Liub00d6432014-02-26 10:18:47 -080068 private static final int FLY_IN_DURATION_MS = 0;
Doris Liu1c94b7d2013-11-09 19:13:44 -080069 private static final int HOLD_DURATION_MS = 0;
70 private static final int FLY_OUT_DURATION_MS = 850;
71 private static final int START_DELAY_MS = 100;
72 private static final int TOTAL_DURATION_MS = FLY_IN_DURATION_MS + HOLD_DURATION_MS
73 + FLY_OUT_DURATION_MS;
Doris Liub00d6432014-02-26 10:18:47 -080074 private static final int HIDE_SHIMMY_DELAY_MS = 1000;
Doris Liu69cb8402014-03-01 15:05:13 -080075 // Assumption for time since last scroll when no data point for last scroll.
76 private static final int SCROLL_INTERVAL_MS = 50;
77 // Last 20% percent of the drawer opening should be slow to ensure soft landing.
78 private static final float SLOW_ZONE_PERCENTAGE = 0.2f;
Doris Liu1c94b7d2013-11-09 19:13:44 -080079
Doris Liu70576b62013-11-14 20:30:33 -080080 private static final int NO_ITEM_SELECTED = -1;
Doris Liu1c94b7d2013-11-09 19:13:44 -080081
Doris Liucfaf7b02013-12-06 01:08:59 -080082 // Scrolling delay between non-focused item and focused item
Doris Liu213a4a02014-02-04 16:57:55 -080083 private static final int DELAY_MS = 30;
Doris Liu6c5a5372014-01-15 12:26:17 -080084 // If the fling velocity exceeds this threshold, snap to full screen at a constant
85 // speed. Unit: pixel/ms.
86 private static final float VELOCITY_THRESHOLD = 2f;
Doris Liucfaf7b02013-12-06 01:08:59 -080087
Doris Liu213a4a02014-02-04 16:57:55 -080088 /**
89 * A factor to change the UI responsiveness on a scroll.
90 * e.g. A scroll factor of 0.5 means UI will move half as fast as the finger.
91 */
92 private static final float SCROLL_FACTOR = 0.5f;
Seth Raphael5ac2bfb2014-03-10 16:58:17 -070093 // 60% opaque black background.
Seth Raphael3020ba52014-03-10 16:44:07 -070094 private static final int BACKGROUND_TRANSPARENTCY = (int) (0.6f * 255);
Doris Liu213a4a02014-02-04 16:57:55 -080095 private static final int PREVIEW_DOWN_SAMPLE_FACTOR = 4;
96 // Threshold, below which snap back will happen.
97 private static final float SNAP_BACK_THRESHOLD_RATIO = 0.33f;
98
Doris Liu70576b62013-11-14 20:30:33 -080099 private final GestureDetector mGestureDetector;
Doris Liu213a4a02014-02-04 16:57:55 -0800100 private final RectF mPreviewArea = new RectF();
Doris Liue7e0c022014-02-12 11:14:08 -0800101 private final RectF mUncoveredPreviewArea = new RectF();
Doris Liu70576b62013-11-14 20:30:33 -0800102
Alan Newberger26e901e2014-03-25 11:09:29 -0700103 private final CurrentStateManager mCurrentStateManager = new CurrentStateManager();
Doris Liu69cb8402014-03-01 15:05:13 -0800104 private long mLastScrollTime;
Doris Liu70576b62013-11-14 20:30:33 -0800105 private int mListBackgroundColor;
106 private LinearLayout mListView;
Doris Liue7e0c022014-02-12 11:14:08 -0800107 private SettingsButton mSettingsButton;
Doris Liu70576b62013-11-14 20:30:33 -0800108 private int mTotalModes;
109 private ModeSelectorItem[] mModeSelectorItems;
110 private AnimatorSet mAnimatorSet;
Doris Liu1c94b7d2013-11-09 19:13:44 -0800111 private int mFocusItem = NO_ITEM_SELECTED;
Doris Liu2c559c42014-01-08 16:04:38 -0800112 private ModeListOpenListener mModeListOpenListener;
Sascha Haeberling8c1a9222014-02-25 09:38:06 -0800113 private ModeListVisibilityChangedListener mVisibilityChangedListener;
Doris Liu213a4a02014-02-04 16:57:55 -0800114 private CameraAppUI.CameraModuleScreenShotProvider mScreenShotProvider = null;
115 private int[] mInputPixels;
116 private int[] mOutputPixels;
Doris Liua8a24e02014-03-12 13:48:05 -0700117 private float mModeListOpenFactor = 1f;
Doris Liu70576b62013-11-14 20:30:33 -0800118
Doris Liu69cb8402014-03-01 15:05:13 -0800119 private boolean mAdjustPositionWhenUncoveredPreviewAreaChanges = false;
120 private View mChildViewTouched = null;
121 private MotionEvent mLastChildTouchEvent = null;
122 private int mVisibleWidth = 0;
123
Doris Liu70576b62013-11-14 20:30:33 -0800124 // Width and height of this view. They get updated in onLayout()
125 // Unit for width and height are pixels.
Doris Liu1c94b7d2013-11-09 19:13:44 -0800126 private int mWidth;
127 private int mHeight;
128 private float mScrollTrendX = 0f;
129 private float mScrollTrendY = 0f;
Doris Liu2c559c42014-01-08 16:04:38 -0800130 private ModeSwitchListener mModeSwitchListener = null;
Doris Liubd1b8f92014-01-03 17:59:51 -0800131 private ArrayList<Integer> mSupportedModes;
Doris Liucfaf7b02013-12-06 01:08:59 -0800132 private final LinkedList<TimeBasedPosition> mPositionHistory
133 = new LinkedList<TimeBasedPosition>();
134 private long mCurrentTime;
Doris Liu6c5a5372014-01-15 12:26:17 -0800135 private float mVelocityX; // Unit: pixel/ms.
Doris Liu69cb8402014-03-01 15:05:13 -0800136 private long mLastDownTime = 0;
137
Alan Newberger26e901e2014-03-25 11:09:29 -0700138 private class CurrentStateManager {
139 private ModeListState mCurrentState;
140
141 ModeListState getCurrentState() {
142 return mCurrentState;
143 }
144
145 void setCurrentState(ModeListState state) {
146 mCurrentState = state;
147 state.onCurrentState();
148 }
149 }
150
Doris Liu69cb8402014-03-01 15:05:13 -0800151 /**
Doris Liua8a24e02014-03-12 13:48:05 -0700152 * ModeListState defines a set of functions through which the view could manage
153 * or change the states. Sub-classes could selectively override these functions
154 * accordingly to respect the specific requirements for each state. By overriding
155 * these methods, state transition can also be achieved.
Doris Liu69cb8402014-03-01 15:05:13 -0800156 */
Doris Liua8a24e02014-03-12 13:48:05 -0700157 private abstract class ModeListState implements GestureDetector.OnGestureListener {
158 protected AnimationEffects mCurrentAnimationEffects = null;
159
160 /**
Alan Newberger26e901e2014-03-25 11:09:29 -0700161 * Called by the state manager when this state instance becomes the current
162 * mode list state.
163 */
164 public void onCurrentState() {
165 // Do nothing.
166 }
167
168 /**
Doris Liua8a24e02014-03-12 13:48:05 -0700169 * If supported, this should show the mode switcher and starts the accordion
170 * animation with a delay. If the view does not currently have focus, (e.g.
171 * There are popups on top of it.) start the delayed accordion animation
172 * when it gains focus. Otherwise, start the animation with a delay right
173 * away.
174 */
175 public void showSwitcherHint() {
176 // Do nothing.
177 }
178
179 /**
180 * Gets the currently running animation effects for the current state.
181 */
182 public AnimationEffects getCurrentAnimationEffects() {
183 return mCurrentAnimationEffects;
184 }
185
186 /**
187 * Returns true if the touch event should be handled, false otherwise.
188 *
189 * @param ev motion event to be handled
190 * @return true if the event should be handled, false otherwise.
191 */
192 public boolean shouldHandleTouchEvent(MotionEvent ev) {
193 return true;
194 }
195
196 /**
197 * Handles touch event. This will be called if
198 * {@link ModeListState#shouldHandleTouchEvent(android.view.MotionEvent)}
199 * returns {@code true}
200 *
201 * @param ev touch event to be handled
202 * @return always true
203 */
204 public boolean onTouchEvent(MotionEvent ev) {
205 return true;
206 }
207
208 /**
209 * Gets called when the window focus has changed.
210 *
211 * @param hasFocus whether current window has focus
212 */
Doris Liu69cb8402014-03-01 15:05:13 -0800213 public void onWindowFocusChanged(boolean hasFocus) {
214 // Default to do nothing.
215 }
216
217 /**
Doris Liua8a24e02014-03-12 13:48:05 -0700218 * Gets called when back key is pressed.
219 *
220 * @return true if handled, false otherwise.
Doris Liu69cb8402014-03-01 15:05:13 -0800221 */
Doris Liua8a24e02014-03-12 13:48:05 -0700222 public boolean onBackPressed() {
223 return false;
Doris Liu69cb8402014-03-01 15:05:13 -0800224 }
225
226 /**
Erin Dahlgren15691af2014-03-14 14:10:57 -0700227 * Gets called when menu key is pressed.
228 *
229 * @return true if handled, false otherwise.
230 */
231 public boolean onMenuPressed() {
232 return false;
233 }
234
235 /**
Doris Liua8a24e02014-03-12 13:48:05 -0700236 * Gets called when there is a {@link View#setVisibility(int)} call to
237 * change the visibility of the mode drawer. Visibility change does not
238 * always make sense, for example there can be an outside call to make
239 * the mode drawer visible when it is in the fully hidden state. The logic
240 * is that the mode drawer can only be made visible when user swipe it in.
Doris Liu69cb8402014-03-01 15:05:13 -0800241 *
Doris Liua8a24e02014-03-12 13:48:05 -0700242 * @param visibility the proposed visibility change
243 * @return true if the visibility change is valid and therefore should be
244 * handled, false otherwise.
Doris Liu69cb8402014-03-01 15:05:13 -0800245 */
Doris Liua8a24e02014-03-12 13:48:05 -0700246 public boolean shouldHandleVisibilityChange(int visibility) {
247 return true;
248 }
249
250 /**
251 * If supported, this should start blurring the camera preview and
252 * start the mode switch.
253 *
254 * @param selectedItem mode item that has been selected
255 */
256 public void onItemSelected(ModeSelectorItem selectedItem) {
257 // Do nothing.
258 }
259
260 /**
261 * This gets called when mode switch has finished and UI needs to
262 * pinhole into the new mode through animation.
263 */
264 public void startModeSelectionAnimation() {
265 // Do nothing.
266 }
267
268 /**
269 * Hide the mode drawer and switch to fully hidden state.
270 */
271 public void hide() {
272 // Do nothing.
273 }
274
275 /***************GestureListener implementation*****************/
276 @Override
277 public boolean onDown(MotionEvent e) {
278 return false;
279 }
280
281 @Override
282 public void onShowPress(MotionEvent e) {
283 // Do nothing.
284 }
285
286 @Override
287 public boolean onSingleTapUp(MotionEvent e) {
288 return false;
289 }
290
291 @Override
292 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
293 return false;
294 }
295
296 @Override
297 public void onLongPress(MotionEvent e) {
298 // Do nothing.
299 }
300
301 @Override
302 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
303 return false;
304 }
305 }
306
307 /**
308 * Fully hidden state. Transitioning to ScrollingState and ShimmyState are supported
309 * in this state.
310 */
311 private class FullyHiddenState extends ModeListState {
Erin Dahlgren15691af2014-03-14 14:10:57 -0700312 private Animator mAnimator = null;
Doris Liua8a24e02014-03-12 13:48:05 -0700313 private boolean mShouldBeVisible = false;
Erin Dahlgren15691af2014-03-14 14:10:57 -0700314
Doris Liua8a24e02014-03-12 13:48:05 -0700315 public FullyHiddenState() {
316 reset();
317 }
318
319 @Override
320 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
321 mShouldBeVisible = true;
322 // Change visibility, and switch to scrolling state.
323 resetModeSelectors();
Alan Newberger26e901e2014-03-25 11:09:29 -0700324 mCurrentStateManager.setCurrentState(new ScrollingState());
Doris Liua8a24e02014-03-12 13:48:05 -0700325 return true;
326 }
327
328 @Override
329 public void showSwitcherHint() {
330 mShouldBeVisible = true;
Alan Newberger26e901e2014-03-25 11:09:29 -0700331 mCurrentStateManager.setCurrentState(new ShimmyState());
Doris Liua8a24e02014-03-12 13:48:05 -0700332 }
333
334 @Override
335 public boolean shouldHandleTouchEvent(MotionEvent ev) {
336 return true;
337 }
338
339 @Override
340 public boolean onTouchEvent(MotionEvent ev) {
341 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
342 mFocusItem = getFocusItem(ev.getX(), ev.getY());
343 setSwipeMode(true);
344 }
345 return true;
346 }
347
348 @Override
Erin Dahlgren15691af2014-03-14 14:10:57 -0700349 public boolean onMenuPressed() {
350 if (mAnimator != null) {
351 return false;
352 }
353 snapOpenAndShow();
354 return true;
355 }
356
357 @Override
Doris Liua8a24e02014-03-12 13:48:05 -0700358 public boolean shouldHandleVisibilityChange(int visibility) {
Erin Dahlgren15691af2014-03-14 14:10:57 -0700359 if (mAnimator != null) {
360 return false;
361 }
Doris Liua8a24e02014-03-12 13:48:05 -0700362 if (visibility == VISIBLE && !mShouldBeVisible) {
363 return false;
364 }
365 return true;
366 }
Erin Dahlgren15691af2014-03-14 14:10:57 -0700367 /**
368 * Snaps open the mode list and go to the fully shown state.
369 */
370 private void snapOpenAndShow() {
371 mShouldBeVisible = true;
372 setVisibility(VISIBLE);
Doris Liua8a24e02014-03-12 13:48:05 -0700373
Erin Dahlgren15691af2014-03-14 14:10:57 -0700374 mAnimator = snapToFullScreen();
375 if (mAnimator != null) {
376 mAnimator.addListener(new Animator.AnimatorListener() {
377 @Override
378 public void onAnimationStart(Animator animation) {
379
380 }
381
382 @Override
383 public void onAnimationEnd(Animator animation) {
384 mAnimator = null;
Alan Newberger26e901e2014-03-25 11:09:29 -0700385 mCurrentStateManager.setCurrentState(new FullyShownState());
Erin Dahlgren15691af2014-03-14 14:10:57 -0700386 }
387
388 @Override
389 public void onAnimationCancel(Animator animation) {
390
391 }
392
393 @Override
394 public void onAnimationRepeat(Animator animation) {
395
396 }
397 });
398 } else {
Alan Newberger26e901e2014-03-25 11:09:29 -0700399 mCurrentStateManager.setCurrentState(new FullyShownState());
Erin Dahlgren15691af2014-03-14 14:10:57 -0700400 }
401 }
Alan Newberger26e901e2014-03-25 11:09:29 -0700402
403 @Override
404 public void onCurrentState() {
405 announceForAccessibility(
406 getContext().getResources().getString(R.string.accessibility_mode_list_hidden));
407 }
408
Doris Liua8a24e02014-03-12 13:48:05 -0700409 }
410
411 /**
412 * Fully shown state. This state represents when the mode list is entirely shown
413 * on screen without any on-going animation. Transitions from this state could be
414 * to ScrollingState, SelectedState, or FullyHiddenState.
415 */
416 private class FullyShownState extends ModeListState {
417 private Animator mAnimator = null;
418
419 @Override
420 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
421 // Go to scrolling state.
422 if (distanceX > 0) {
423 // Swipe out
424 cancelForwardingTouchEvent();
Alan Newberger26e901e2014-03-25 11:09:29 -0700425 mCurrentStateManager.setCurrentState(new ScrollingState());
Doris Liua8a24e02014-03-12 13:48:05 -0700426 }
427 return true;
428 }
429
430 @Override
431 public boolean shouldHandleTouchEvent(MotionEvent ev) {
432 if (mAnimator != null && mAnimator.isRunning()) {
433 return false;
434 }
435 return true;
436 }
437
438 @Override
439 public boolean onTouchEvent(MotionEvent ev) {
440 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
441 mFocusItem = NO_ITEM_SELECTED;
442 setSwipeMode(false);
443 // If the down event happens inside the mode list, find out which
444 // mode item is being touched and forward all the subsequent touch
445 // events to that mode item for its pressed state and click handling.
446 if (isTouchInsideList(ev)) {
447 mChildViewTouched = mModeSelectorItems[getFocusItem(ev.getX(), ev.getY())];
448 }
449 }
450 forwardTouchEventToChild(ev);
451 return true;
452 }
453
454
455 @Override
456 public boolean onSingleTapUp(MotionEvent ev) {
457 // If the tap is not inside the mode drawer area, snap back.
458 if(!isTouchInsideList(ev)) {
459 snapBackAndHide();
460 return false;
461 }
462 return true;
463 }
464
465 @Override
466 public boolean onBackPressed() {
467 snapBackAndHide();
468 return true;
469 }
470
471 @Override
Erin Dahlgren15691af2014-03-14 14:10:57 -0700472 public boolean onMenuPressed() {
473 snapBackAndHide();
474 return true;
475 }
476
477 @Override
Doris Liua8a24e02014-03-12 13:48:05 -0700478 public void onItemSelected(ModeSelectorItem selectedItem) {
Alan Newberger26e901e2014-03-25 11:09:29 -0700479 mCurrentStateManager.setCurrentState(new SelectedState(selectedItem));
Doris Liua8a24e02014-03-12 13:48:05 -0700480 }
481
482 /**
483 * Snaps back the mode list and go to the fully hidden state.
484 */
485 private void snapBackAndHide() {
486 mAnimator = snapBack(true);
487 if (mAnimator != null) {
488 mAnimator.addListener(new Animator.AnimatorListener() {
489 @Override
490 public void onAnimationStart(Animator animation) {
491
492 }
493
494 @Override
495 public void onAnimationEnd(Animator animation) {
496 mAnimator = null;
Alan Newberger26e901e2014-03-25 11:09:29 -0700497 mCurrentStateManager.setCurrentState(new FullyHiddenState());
Doris Liua8a24e02014-03-12 13:48:05 -0700498 }
499
500 @Override
501 public void onAnimationCancel(Animator animation) {
502
503 }
504
505 @Override
506 public void onAnimationRepeat(Animator animation) {
507
508 }
509 });
510 } else {
Alan Newberger26e901e2014-03-25 11:09:29 -0700511 mCurrentStateManager.setCurrentState(new FullyHiddenState());
Doris Liua8a24e02014-03-12 13:48:05 -0700512 }
513 }
514
515 @Override
516 public void hide() {
517 if (mAnimator != null) {
518 mAnimator.cancel();
519 } else {
Alan Newberger26e901e2014-03-25 11:09:29 -0700520 mCurrentStateManager.setCurrentState(new FullyHiddenState());
Doris Liua8a24e02014-03-12 13:48:05 -0700521 }
522 }
Alan Newberger26e901e2014-03-25 11:09:29 -0700523
524 @Override
525 public void onCurrentState() {
526 announceForAccessibility(
527 getContext().getResources().getString(R.string.accessibility_mode_list_shown));
528 }
529
Doris Liua8a24e02014-03-12 13:48:05 -0700530 }
531
532 /**
533 * Shimmy state handles the specifics for shimmy animation, including
534 * setting up to show mode drawer (without text) and hide it with shimmy animation.
535 *
536 * This state can be interrupted when scrolling or mode selection happened,
537 * in which case the state will transition into ScrollingState, or SelectedState.
538 * Otherwise, after shimmy finishes successfully, a transition to fully hidden
539 * state will happen.
540 */
541 private class ShimmyState extends ModeListState {
542
543 private boolean mStartHidingShimmyWhenWindowGainsFocus = false;
544 private Animator mAnimator = null;
545 private final Runnable mHideShimmy = new Runnable() {
546 @Override
547 public void run() {
548 startHidingShimmy();
549 }
550 };
551
552 public ShimmyState() {
553 setVisibility(VISIBLE);
554 mSettingsButton.setVisibility(INVISIBLE);
555 mModeListOpenFactor = 0f;
556 onModeListOpenRatioUpdate(0);
557 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
558 for (int i = 0; i < mModeSelectorItems.length; i++) {
559 mModeSelectorItems[i].setVisibleWidth(maxVisibleWidth);
560 }
561 if (hasWindowFocus()) {
562 hideShimmyWithDelay();
563 } else {
564 mStartHidingShimmyWhenWindowGainsFocus = true;
565 }
566 }
567
568 @Override
569 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
570 // Scroll happens during accordion animation.
571 cancelAnimation();
572 // Go to scrolling state
Alan Newberger26e901e2014-03-25 11:09:29 -0700573 mCurrentStateManager.setCurrentState(new ScrollingState());
Doris Liua8a24e02014-03-12 13:48:05 -0700574 return true;
575 }
576
577 @Override
578 public boolean shouldHandleTouchEvent(MotionEvent ev) {
579 if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
580 if (isTouchInsideList(ev) &&
581 ev.getX() <= mModeSelectorItems[0].getMaxVisibleWidth()) {
582 mChildViewTouched = mModeSelectorItems[getFocusItem(ev.getX(), ev.getY())];
583 return true;
584 }
585 // If shimmy is on-going, reject the first down event, so that it can be handled
586 // by the view underneath. If a swipe is detected, the same series of touch will
587 // re-enter this function, in which case we will consume the touch events.
588 if (mLastDownTime != ev.getDownTime()) {
589 mLastDownTime = ev.getDownTime();
590 return false;
591 }
592 }
593 return true;
594 }
595
596 @Override
597 public boolean onTouchEvent(MotionEvent ev) {
598 if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
599 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
600 mFocusItem = getFocusItem(ev.getX(), ev.getY());
601 setSwipeMode(true);
602 }
603 }
604 forwardTouchEventToChild(ev);
605 return true;
606 }
607
608 @Override
609 public void onItemSelected(ModeSelectorItem selectedItem) {
610 cancelAnimation();
Alan Newberger26e901e2014-03-25 11:09:29 -0700611 mCurrentStateManager.setCurrentState(new SelectedState(selectedItem));
Doris Liua8a24e02014-03-12 13:48:05 -0700612 }
613
614 private void hideShimmyWithDelay() {
615 postDelayed(mHideShimmy, HIDE_SHIMMY_DELAY_MS);
616 }
617
618 @Override
619 public void onWindowFocusChanged(boolean hasFocus) {
620 if (mStartHidingShimmyWhenWindowGainsFocus && hasFocus) {
621 mStartHidingShimmyWhenWindowGainsFocus = false;
622 hideShimmyWithDelay();
623 }
624 }
625
626 /**
627 * This starts the accordion animation, unless it's already running, in which
628 * case the start animation call will be ignored.
629 */
630 private void startHidingShimmy() {
631 if (mAnimator != null) {
632 return;
633 }
634 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
635 mAnimator = animateListToWidth(START_DELAY_MS * (-1), TOTAL_DURATION_MS,
636 Gusterpolator.INSTANCE, maxVisibleWidth, 0);
637 mAnimator.addListener(new Animator.AnimatorListener() {
638 private boolean mSuccess = true;
639 @Override
640 public void onAnimationStart(Animator animation) {
641 // Do nothing.
642 }
643
644 @Override
645 public void onAnimationEnd(Animator animation) {
646 mAnimator = null;
647 ShimmyState.this.onAnimationEnd(mSuccess);
648 }
649
650 @Override
651 public void onAnimationCancel(Animator animation) {
652 mSuccess = false;
653 }
654
655 @Override
656 public void onAnimationRepeat(Animator animation) {
657 // Do nothing.
658 }
659 });
660 }
661
662 /**
663 * Cancels the pending/on-going animation.
664 */
665 private void cancelAnimation() {
666 removeCallbacks(mHideShimmy);
667 if (mAnimator != null && mAnimator.isRunning()) {
668 mAnimator.cancel();
669 } else {
670 mAnimator = null;
671 onAnimationEnd(false);
672 }
673 }
674
675 /**
676 * Gets called when the animation finishes or gets canceled.
677 *
678 * @param success indicates whether the animation finishes successfully
679 */
680 private void onAnimationEnd(boolean success) {
681 mSettingsButton.setVisibility(VISIBLE);
682 // If successfully finish hiding shimmy, then we should go back to
683 // fully hidden state.
684 if (success) {
685 mModeListOpenFactor = 1;
Alan Newberger26e901e2014-03-25 11:09:29 -0700686 mCurrentStateManager.setCurrentState(new FullyHiddenState());
Doris Liua8a24e02014-03-12 13:48:05 -0700687 return;
688 }
689
690 // If the animation was canceled before it's finished, animate the mode
691 // list open factor from 0 to 1 to ensure a smooth visual transition.
692 final ValueAnimator openFactorAnimator = ValueAnimator.ofFloat(mModeListOpenFactor, 1f);
693 openFactorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
694 @Override
695 public void onAnimationUpdate(ValueAnimator animation) {
696 mModeListOpenFactor = (Float) openFactorAnimator.getAnimatedValue();
697 onVisibleWidthChanged(mVisibleWidth);
698 }
699 });
700 openFactorAnimator.addListener(new Animator.AnimatorListener() {
701 @Override
702 public void onAnimationStart(Animator animation) {
703 // Do nothing.
704 }
705
706 @Override
707 public void onAnimationEnd(Animator animation) {
708 mModeListOpenFactor = 1f;
709 }
710
711 @Override
712 public void onAnimationCancel(Animator animation) {
713 // Do nothing.
714 }
715
716 @Override
717 public void onAnimationRepeat(Animator animation) {
718 // Do nothing.
719 }
720 });
721 openFactorAnimator.start();
722 }
723
724 @Override
725 public void hide() {
726 cancelAnimation();
Alan Newberger26e901e2014-03-25 11:09:29 -0700727 mCurrentStateManager.setCurrentState(new FullyHiddenState());
728 }
729
730 @Override
731 public void onCurrentState() {
732 announceForAccessibility(
733 getContext().getResources().getString(R.string.accessibility_mode_list_shimmy));
Doris Liua8a24e02014-03-12 13:48:05 -0700734 }
735
736 }
737
738 /**
739 * When the mode list is being scrolled, it will be in ScrollingState. From
740 * this state, the mode list could transition to fully hidden, fully open
741 * depending on which direction the scrolling goes.
742 */
743 private class ScrollingState extends ModeListState {
744 private Animator mAnimator = null;
745
746 public ScrollingState() {
747 setVisibility(VISIBLE);
748 }
749
750 @Override
751 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
752 // Scroll based on the scrolling distance on the currently focused
753 // item.
754 scroll(mFocusItem, distanceX * SCROLL_FACTOR,
755 distanceY * SCROLL_FACTOR);
756 return true;
757 }
758
759 @Override
760 public boolean shouldHandleTouchEvent(MotionEvent ev) {
761 // If the snap back/to full screen animation is on going, ignore any
762 // touch.
763 if (mAnimator != null) {
764 return false;
765 }
766 return true;
767 }
768
769 @Override
770 public boolean onTouchEvent(MotionEvent ev) {
771 if (ev.getActionMasked() == MotionEvent.ACTION_UP ||
772 ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
773 final boolean shouldSnapBack = shouldSnapBack();
774 if (shouldSnapBack) {
775 mAnimator = snapBack();
776 } else {
777 mAnimator = snapToFullScreen();
778 }
779 mAnimator.addListener(new Animator.AnimatorListener() {
780 @Override
781 public void onAnimationStart(Animator animation) {
782
783 }
784
785 @Override
786 public void onAnimationEnd(Animator animation) {
787 mAnimator = null;
788 mFocusItem = NO_ITEM_SELECTED;
789 if (shouldSnapBack) {
Alan Newberger26e901e2014-03-25 11:09:29 -0700790 mCurrentStateManager.setCurrentState(new FullyHiddenState());
Doris Liua8a24e02014-03-12 13:48:05 -0700791 } else {
Alan Newberger26e901e2014-03-25 11:09:29 -0700792 mCurrentStateManager.setCurrentState(new FullyShownState());
Doris Liua8a24e02014-03-12 13:48:05 -0700793 }
794 }
795
796 @Override
797 public void onAnimationCancel(Animator animation) {
798
799 }
800
801 @Override
802 public void onAnimationRepeat(Animator animation) {
803
804 }
805 });
806 }
807 return true;
808 }
809 }
810
811 /**
812 * Mode list gets in this state when a mode item has been selected/clicked.
813 * There will be an animation with the blurred preview fading in, a potential
814 * pause to wait for the new mode to be ready, and then the new mode will
815 * be revealed through a pinhole animation. After all the animations finish,
816 * mode list will transition into fully hidden state.
817 */
818 private class SelectedState extends ModeListState {
819
820 public SelectedState(ModeSelectorItem selectedItem) {
821 final int modeId = selectedItem.getModeId();
822 // Un-highlight all the modes.
823 for (int i = 0; i < mModeSelectorItems.length; i++) {
824 mModeSelectorItems[i].setHighlighted(false);
825 mModeSelectorItems[i].setSelected(false);
826 }
827 // Select the focused item.
828 selectedItem.setSelected(true);
829 PeepholeAnimationEffect effect = new PeepholeAnimationEffect();
830 effect.setSize(mWidth, mHeight);
831
832 // Calculate the position of the icon in the selected item, and
833 // start animation from that position.
834 int[] location = new int[2];
835 // Gets icon's center position in relative to the window.
836 selectedItem.getIconCenterLocationInWindow(location);
837 int iconX = location[0];
838 int iconY = location[1];
839 // Gets current view's top left position relative to the window.
840 getLocationInWindow(location);
841 // Calculate icon location relative to this view
842 iconX -= location[0];
843 iconY -= location[1];
844
845 effect.setAnimationStartingPosition(iconX, iconY);
846 if (mScreenShotProvider != null) {
847 effect.setBackground(mScreenShotProvider
848 .getPreviewFrame(PREVIEW_DOWN_SAMPLE_FACTOR), mPreviewArea);
849 effect.setBackgroundOverlay(mScreenShotProvider.getPreviewOverlayAndControls());
850 }
851 mCurrentAnimationEffects = effect;
852 invalidate();
853
854 // Post mode selection runnable to the end of the message queue
855 // so that current UI changes can finish before mode initialization
856 // clogs up UI thread.
857 post(new Runnable() {
858 @Override
859 public void run() {
860 onModeSelected(modeId);
861 }
862 });
863 }
864
865 @Override
866 public boolean shouldHandleTouchEvent(MotionEvent ev) {
867 return false;
868 }
869
870 @Override
871 public void startModeSelectionAnimation() {
872 mCurrentAnimationEffects.startAnimation(new Animator.AnimatorListener() {
873 @Override
874 public void onAnimationStart(Animator animation) {
875
876 }
877
878 @Override
879 public void onAnimationEnd(Animator animation) {
880 mCurrentAnimationEffects = null;
Alan Newberger26e901e2014-03-25 11:09:29 -0700881 mCurrentStateManager.setCurrentState(new FullyHiddenState());
Doris Liua8a24e02014-03-12 13:48:05 -0700882 }
883
884 @Override
885 public void onAnimationCancel(Animator animation) {
886
887 }
888
889 @Override
890 public void onAnimationRepeat(Animator animation) {
891
892 }
893 });
894 }
Doris Liu69cb8402014-03-01 15:05:13 -0800895 }
Doris Liu1c94b7d2013-11-09 19:13:44 -0800896
Doris Liu213a4a02014-02-04 16:57:55 -0800897 @Override
Angus Kong2bacca72014-03-06 12:57:29 -0800898 public void onPreviewAreaChanged(RectF previewArea) {
Doris Liu213a4a02014-02-04 16:57:55 -0800899 mPreviewArea.set(previewArea);
900 }
901
Doris Liue7e0c022014-02-12 11:14:08 -0800902 private final CameraAppUI.UncoveredPreviewAreaSizeChangedListener
903 mUncoveredPreviewAreaSizeChangedListener =
904 new CameraAppUI.UncoveredPreviewAreaSizeChangedListener() {
905
906 @Override
907 public void uncoveredPreviewAreaChanged(RectF uncoveredPreviewArea) {
908 mUncoveredPreviewArea.set(uncoveredPreviewArea);
909 mSettingsButton.uncoveredPreviewAreaChanged(uncoveredPreviewArea);
910 if (mAdjustPositionWhenUncoveredPreviewAreaChanges) {
911 mAdjustPositionWhenUncoveredPreviewAreaChanges = false;
912 centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight());
913 }
914 }
915 };
916
Doris Liu1c94b7d2013-11-09 19:13:44 -0800917 public interface ModeSwitchListener {
918 public void onModeSelected(int modeIndex);
Doris Liu213a4a02014-02-04 16:57:55 -0800919 public int getCurrentModeIndex();
Doris Liu9d264302014-02-11 10:41:40 -0800920 public void onSettingsSelected();
Doris Liu1c94b7d2013-11-09 19:13:44 -0800921 }
922
Doris Liu2c559c42014-01-08 16:04:38 -0800923 public interface ModeListOpenListener {
Doris Liu9d264302014-02-11 10:41:40 -0800924 /**
925 * Mode list will open to full screen after current animation.
926 */
Doris Liu2c559c42014-01-08 16:04:38 -0800927 public void onOpenFullScreen();
Doris Liu9d264302014-02-11 10:41:40 -0800928
929 /**
930 * Updates the listener with the current progress of mode drawer opening.
931 *
932 * @param progress progress of the mode drawer opening, ranging [0f, 1f]
933 * 0 means mode drawer is fully closed, 1 indicates a fully
934 * open mode drawer.
935 */
936 public void onModeListOpenProgress(float progress);
937
938 /**
939 * Gets called when mode list is completely closed.
940 */
941 public void onModeListClosed();
Doris Liu2c559c42014-01-08 16:04:38 -0800942 }
943
Sascha Haeberling8c1a9222014-02-25 09:38:06 -0800944 public static abstract class ModeListVisibilityChangedListener {
Sascha Haeberlingb49ab282014-02-25 11:41:18 -0800945 private Boolean mCurrentVisibility = null;
Sascha Haeberling8c1a9222014-02-25 09:38:06 -0800946
947 /** Whether the mode list is (partially or fully) visible. */
948 public abstract void onVisibilityChanged(boolean visible);
949
950 /**
951 * Internal method to be called by the mode list whenever a visibility
952 * even occurs.
953 * <p>
954 * Do not call {@link #onVisibilityChanged(boolean)} directly, as this
955 * is only called when the visibility has actually changed and not on
956 * each visibility event.
957 *
958 * @param visible whether the mode drawer is currently visible.
959 */
960 private void onVisibilityEvent(boolean visible) {
Sascha Haeberlingb49ab282014-02-25 11:41:18 -0800961 if (mCurrentVisibility == null || mCurrentVisibility != visible) {
962 mCurrentVisibility = visible;
Sascha Haeberling8c1a9222014-02-25 09:38:06 -0800963 onVisibilityChanged(visible);
Sascha Haeberlingb49ab282014-02-25 11:41:18 -0800964 }
Sascha Haeberling8c1a9222014-02-25 09:38:06 -0800965 }
966 }
967
Doris Liu70576b62013-11-14 20:30:33 -0800968 /**
Doris Liucfaf7b02013-12-06 01:08:59 -0800969 * This class aims to help store time and position in pairs.
970 */
971 private static class TimeBasedPosition {
Sascha Haeberling601921e2013-12-17 10:01:11 -0800972 private final float mPosition;
973 private final long mTimeStamp;
Doris Liucfaf7b02013-12-06 01:08:59 -0800974 public TimeBasedPosition(float position, long time) {
975 mPosition = position;
976 mTimeStamp = time;
977 }
978
979 public float getPosition() {
980 return mPosition;
981 }
982
983 public long getTimeStamp() {
984 return mTimeStamp;
985 }
986 }
987
988 /**
Doris Liu70576b62013-11-14 20:30:33 -0800989 * This is a highly customized interpolator. The purpose of having this subclass
990 * is to encapsulate intricate animation timing, so that the actual animation
991 * implementation can be re-used with other interpolators to achieve different
992 * animation effects.
993 *
994 * The accordion animation consists of three stages:
995 * 1) Animate into the screen within a pre-specified fly in duration.
996 * 2) Hold in place for a certain amount of time (Optional).
997 * 3) Animate out of the screen within the given time.
998 *
999 * The accordion animator is initialized with 3 parameter: 1) initial position,
1000 * 2) how far out the view should be before flying back out, 3) end position.
1001 * The interpolation output should be [0f, 0.5f] during animation between 1)
1002 * to 2), and [0.5f, 1f] for flying from 2) to 3).
1003 */
Sascha Haeberling601921e2013-12-17 10:01:11 -08001004 private final TimeInterpolator mAccordionInterpolator = new TimeInterpolator() {
Doris Liu1c94b7d2013-11-09 19:13:44 -08001005 @Override
1006 public float getInterpolation(float input) {
1007
1008 float flyInDuration = (float) FLY_OUT_DURATION_MS / (float) TOTAL_DURATION_MS;
1009 float holdDuration = (float) (FLY_OUT_DURATION_MS + HOLD_DURATION_MS)
1010 / (float) TOTAL_DURATION_MS;
1011 if (input == 0) {
1012 return 0;
Doris Liu69cb8402014-03-01 15:05:13 -08001013 } else if (input < flyInDuration) {
Doris Liu70576b62013-11-14 20:30:33 -08001014 // Stage 1, project result to [0f, 0.5f]
Doris Liu1c94b7d2013-11-09 19:13:44 -08001015 input /= flyInDuration;
1016 float result = Gusterpolator.INSTANCE.getInterpolation(input);
1017 return result * 0.5f;
1018 } else if (input < holdDuration) {
Doris Liu70576b62013-11-14 20:30:33 -08001019 // Stage 2
Doris Liu1c94b7d2013-11-09 19:13:44 -08001020 return 0.5f;
1021 } else {
Doris Liu70576b62013-11-14 20:30:33 -08001022 // Stage 3, project result to [0.5f, 1f]
Doris Liu1c94b7d2013-11-09 19:13:44 -08001023 input -= holdDuration;
1024 input /= (1 - holdDuration);
1025 float result = Gusterpolator.INSTANCE.getInterpolation(input);
1026 return 0.5f + result * 0.5f;
1027 }
1028 }
1029 };
1030
Doris Liu70576b62013-11-14 20:30:33 -08001031 /**
1032 * The listener that is used to notify when gestures occur.
1033 * Here we only listen to a subset of gestures.
1034 */
Sascha Haeberling601921e2013-12-17 10:01:11 -08001035 private final GestureDetector.OnGestureListener mOnGestureListener
Doris Liu1c94b7d2013-11-09 19:13:44 -08001036 = new GestureDetector.SimpleOnGestureListener(){
Doris Liu70576b62013-11-14 20:30:33 -08001037 @Override
Doris Liu1c94b7d2013-11-09 19:13:44 -08001038 public boolean onScroll(MotionEvent e1, MotionEvent e2,
1039 float distanceX, float distanceY) {
Alan Newberger26e901e2014-03-25 11:09:29 -07001040 mCurrentStateManager.getCurrentState().onScroll(e1, e2, distanceX, distanceY);
Doris Liu69cb8402014-03-01 15:05:13 -08001041 mLastScrollTime = System.currentTimeMillis();
Doris Liu1c94b7d2013-11-09 19:13:44 -08001042 return true;
1043 }
1044
1045 @Override
1046 public boolean onSingleTapUp(MotionEvent ev) {
Alan Newberger26e901e2014-03-25 11:09:29 -07001047 mCurrentStateManager.getCurrentState().onSingleTapUp(ev);
Doris Liu1c94b7d2013-11-09 19:13:44 -08001048 return true;
1049 }
Doris Liu6c5a5372014-01-15 12:26:17 -08001050
1051 @Override
1052 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
1053 // Cache velocity in the unit pixel/ms.
Doris Liu213a4a02014-02-04 16:57:55 -08001054 mVelocityX = velocityX / 1000f * SCROLL_FACTOR;
Alan Newberger26e901e2014-03-25 11:09:29 -07001055 mCurrentStateManager.getCurrentState().onFling(e1, e2, velocityX, velocityY);
Doris Liua8a24e02014-03-12 13:48:05 -07001056 return true;
1057 }
1058
1059 @Override
1060 public boolean onDown(MotionEvent ev) {
1061 mVelocityX = 0;
Alan Newberger26e901e2014-03-25 11:09:29 -07001062 mCurrentStateManager.getCurrentState().onDown(ev);
Doris Liu6c5a5372014-01-15 12:26:17 -08001063 return true;
1064 }
Doris Liu1c94b7d2013-11-09 19:13:44 -08001065 };
1066
Doris Liu6ababe92014-02-21 11:12:08 -08001067 /**
1068 * Gets called when a mode item in the mode drawer is clicked.
1069 *
1070 * @param selectedItem the item being clicked
1071 */
1072 private void onItemSelected(ModeSelectorItem selectedItem) {
Alan Newberger26e901e2014-03-25 11:09:29 -07001073 mCurrentStateManager.getCurrentState().onItemSelected(selectedItem);
Doris Liu6ababe92014-02-21 11:12:08 -08001074 }
1075
1076 /**
1077 * Checks whether a touch event is inside of the bounds of the mode list.
1078 *
1079 * @param ev touch event to be checked
1080 * @return whether the touch is inside the bounds of the mode list
1081 */
1082 private boolean isTouchInsideList(MotionEvent ev) {
1083 // Ignore the tap if it happens outside of the mode list linear layout.
1084 float x = ev.getX() - mListView.getX();
1085 float y = ev.getY() - mListView.getY();
1086 if (x < 0 || x > mListView.getWidth() || y < 0 || y > mListView.getHeight()) {
1087 return false;
1088 }
1089 return true;
1090 }
1091
Doris Liu1c94b7d2013-11-09 19:13:44 -08001092 public ModeListView(Context context, AttributeSet attrs) {
1093 super(context, attrs);
1094 mGestureDetector = new GestureDetector(context, mOnGestureListener);
Doris Liu70576b62013-11-14 20:30:33 -08001095 mListBackgroundColor = getResources().getColor(R.color.mode_list_background);
1096 }
1097
Doris Liue7e0c022014-02-12 11:14:08 -08001098 public CameraAppUI.UncoveredPreviewAreaSizeChangedListener
1099 getUncoveredPreviewAreaSizeChangedListener() {
1100 return mUncoveredPreviewAreaSizeChangedListener;
1101 }
1102
Doris Liu70576b62013-11-14 20:30:33 -08001103 /**
1104 * Sets the alpha on the list background. This is called whenever the list
1105 * is scrolling or animating, so that background can adjust its dimness.
1106 *
1107 * @param alpha new alpha to be applied on list background color
1108 */
1109 private void setBackgroundAlpha(int alpha) {
1110 // Make sure alpha is valid.
1111 alpha = alpha & 0xFF;
1112 // Change alpha on the background color.
1113 mListBackgroundColor = mListBackgroundColor & 0xFFFFFF;
1114 mListBackgroundColor = mListBackgroundColor | (alpha << 24);
1115 // Set new color to list background.
Doris Liu213a4a02014-02-04 16:57:55 -08001116 setBackgroundColor(mListBackgroundColor);
Doris Liu1c94b7d2013-11-09 19:13:44 -08001117 }
1118
Doris Liua20074f2013-12-09 15:19:06 -08001119 /**
1120 * Initialize mode list with a list of indices of supported modes.
1121 *
1122 * @param modeIndexList a list of indices of supported modes
1123 */
1124 public void init(List<Integer> modeIndexList) {
Doris Liubd1b8f92014-01-03 17:59:51 -08001125 int[] modeSequence = getResources()
1126 .getIntArray(R.array.camera_modes_in_nav_drawer_if_supported);
1127 int[] visibleModes = getResources()
1128 .getIntArray(R.array.camera_modes_always_visible);
Doris Liu70576b62013-11-14 20:30:33 -08001129
Doris Liua20074f2013-12-09 15:19:06 -08001130 // Mark the supported modes in a boolean array to preserve the
1131 // sequence of the modes
Doris Liubd1b8f92014-01-03 17:59:51 -08001132 SparseArray<Boolean> modeIsSupported = new SparseArray<Boolean>();
Doris Liua20074f2013-12-09 15:19:06 -08001133 for (int i = 0; i < modeIndexList.size(); i++) {
1134 int mode = modeIndexList.get(i);
Doris Liubd1b8f92014-01-03 17:59:51 -08001135 modeIsSupported.put(mode, true);
Doris Liua20074f2013-12-09 15:19:06 -08001136 }
Doris Liubd1b8f92014-01-03 17:59:51 -08001137 for (int i = 0; i < visibleModes.length; i++) {
1138 int mode = visibleModes[i];
1139 modeIsSupported.put(mode, true);
Doris Liua20074f2013-12-09 15:19:06 -08001140 }
1141
Doris Liubd1b8f92014-01-03 17:59:51 -08001142 // Put the indices of supported modes into an array preserving their
1143 // display order.
1144 mSupportedModes = new ArrayList<Integer>();
1145 for (int i = 0; i < modeSequence.length; i++) {
1146 int mode = modeSequence[i];
1147 if (modeIsSupported.get(mode, false)) {
1148 mSupportedModes.add(mode);
1149 }
1150 }
1151 mTotalModes = mSupportedModes.size();
Doris Liua20074f2013-12-09 15:19:06 -08001152 initializeModeSelectorItems();
Doris Liue7e0c022014-02-12 11:14:08 -08001153 mSettingsButton = (SettingsButton) findViewById(R.id.settings_button);
Doris Liu9d264302014-02-11 10:41:40 -08001154 mSettingsButton.setOnClickListener(new OnClickListener() {
1155 @Override
1156 public void onClick(View v) {
Doris Liu58426072014-02-14 15:42:57 -08001157 // Post this callback to make sure current user interaction has
1158 // been reflected in the UI. Specifically, the pressed state gets
1159 // unset after click happens. In order to ensure the pressed state
1160 // gets unset in UI before getting in the low frame rate settings
1161 // activity launch stage, the settings selected callback is posted.
1162 post(new Runnable() {
1163 @Override
1164 public void run() {
1165 mModeSwitchListener.onSettingsSelected();
1166 }
1167 });
Doris Liu9d264302014-02-11 10:41:40 -08001168 }
1169 });
Doris Liu8676e782014-03-10 15:39:01 -07001170 // The mode list is initialized to be all the way closed.
1171 onModeListOpenRatioUpdate(0);
Alan Newberger26e901e2014-03-25 11:09:29 -07001172 if (mCurrentStateManager.getCurrentState() == null) {
1173 mCurrentStateManager.setCurrentState(new FullyHiddenState());
Doris Liua8a24e02014-03-12 13:48:05 -07001174 }
Doris Liua20074f2013-12-09 15:19:06 -08001175 }
1176
Doris Liu213a4a02014-02-04 16:57:55 -08001177 /**
1178 * Sets the screen shot provider for getting a preview frame and a bitmap
1179 * of the controls and overlay.
1180 */
1181 public void setCameraModuleScreenShotProvider(
1182 CameraAppUI.CameraModuleScreenShotProvider provider) {
1183 mScreenShotProvider = provider;
1184 }
1185
Doris Liua20074f2013-12-09 15:19:06 -08001186 private void initializeModeSelectorItems() {
1187 mModeSelectorItems = new ModeSelectorItem[mTotalModes];
Doris Liu70576b62013-11-14 20:30:33 -08001188 // Inflate the mode selector items and add them to a linear layout
Doris Liu1c94b7d2013-11-09 19:13:44 -08001189 LayoutInflater inflater = (LayoutInflater) getContext()
1190 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1191 mListView = (LinearLayout) findViewById(R.id.mode_list);
1192 for (int i = 0; i < mTotalModes; i++) {
Doris Liu6ababe92014-02-21 11:12:08 -08001193 final ModeSelectorItem selectorItem =
Doris Liu1c94b7d2013-11-09 19:13:44 -08001194 (ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null);
1195 mListView.addView(selectorItem);
Doris Liue7e0c022014-02-12 11:14:08 -08001196 // Sets the top padding of the top item to 0.
1197 if (i == 0) {
1198 selectorItem.setPadding(selectorItem.getPaddingLeft(), 0,
1199 selectorItem.getPaddingRight(), selectorItem.getPaddingBottom());
1200 }
1201 // Sets the bottom padding of the bottom item to 0.
1202 if (i == mTotalModes - 1) {
1203 selectorItem.setPadding(selectorItem.getPaddingLeft(), selectorItem.getPaddingTop(),
1204 selectorItem.getPaddingRight(), 0);
1205 }
Doris Liu213a4a02014-02-04 16:57:55 -08001206
Doris Liua20074f2013-12-09 15:19:06 -08001207 int modeId = getModeIndex(i);
Doris Liu213a4a02014-02-04 16:57:55 -08001208 selectorItem.setHighlightColor(getResources()
Doris Liubd1b8f92014-01-03 17:59:51 -08001209 .getColor(CameraUtil.getCameraThemeColorId(modeId, getContext())));
Doris Liu1c94b7d2013-11-09 19:13:44 -08001210
1211 // Set image
Doris Liubd1b8f92014-01-03 17:59:51 -08001212 selectorItem.setImageResource(CameraUtil.getCameraModeIconResId(modeId, getContext()));
Doris Liu70576b62013-11-14 20:30:33 -08001213
Doris Liu1c94b7d2013-11-09 19:13:44 -08001214 // Set text
Doris Liubd1b8f92014-01-03 17:59:51 -08001215 selectorItem.setText(CameraUtil.getCameraModeText(modeId, getContext()));
Spike Sprague79718f62014-01-28 18:48:22 -08001216
1217 // Set content description (for a11y)
1218 selectorItem.setContentDescription(CameraUtil
1219 .getCameraModeContentDescription(modeId, getContext()));
Doris Liu6ababe92014-02-21 11:12:08 -08001220 selectorItem.setModeId(modeId);
1221 selectorItem.setOnClickListener(new OnClickListener() {
1222 @Override
1223 public void onClick(View v) {
1224 onItemSelected(selectorItem);
1225 }
1226 });
Spike Sprague79718f62014-01-28 18:48:22 -08001227
Doris Liu70576b62013-11-14 20:30:33 -08001228 mModeSelectorItems[i] = selectorItem;
Doris Liu1c94b7d2013-11-09 19:13:44 -08001229 }
Doris Liu69cb8402014-03-01 15:05:13 -08001230 // During drawer opening/closing, we change the visible width of the mode
1231 // items in sequence, so we listen to the last item's visible width change
1232 // for a good timing to do corresponding UI adjustments.
1233 mModeSelectorItems[mTotalModes - 1].setVisibleWidthChangedListener(this);
Doris Liu1c94b7d2013-11-09 19:13:44 -08001234 resetModeSelectors();
1235 }
1236
Doris Liua20074f2013-12-09 15:19:06 -08001237 /**
1238 * Maps between the UI mode selector index to the actual mode id.
1239 *
1240 * @param modeSelectorIndex the index of the UI item
1241 * @return the index of the corresponding camera mode
1242 */
1243 private int getModeIndex(int modeSelectorIndex) {
1244 if (modeSelectorIndex < mTotalModes && modeSelectorIndex >= 0) {
Doris Liubd1b8f92014-01-03 17:59:51 -08001245 return mSupportedModes.get(modeSelectorIndex);
Doris Liua20074f2013-12-09 15:19:06 -08001246 }
Angus Kong2bca2102014-03-11 16:27:30 -07001247 Log.e(TAG, "Invalid mode selector index: " + modeSelectorIndex + ", total modes: " +
1248 mTotalModes);
Doris Liubd1b8f92014-01-03 17:59:51 -08001249 return getResources().getInteger(R.integer.camera_mode_photo);
Doris Liua20074f2013-12-09 15:19:06 -08001250 }
1251
Doris Liu70576b62013-11-14 20:30:33 -08001252 /** Notify ModeSwitchListener, if any, of the mode change. */
Doris Liu1c94b7d2013-11-09 19:13:44 -08001253 private void onModeSelected(int modeIndex) {
Doris Liu2c559c42014-01-08 16:04:38 -08001254 if (mModeSwitchListener != null) {
1255 mModeSwitchListener.onModeSelected(modeIndex);
Doris Liu1c94b7d2013-11-09 19:13:44 -08001256 }
Doris Liu1c94b7d2013-11-09 19:13:44 -08001257 }
1258
1259 /**
Doris Liu70576b62013-11-14 20:30:33 -08001260 * Sets a listener that listens to receive mode switch event.
1261 *
1262 * @param listener a listener that gets notified when mode changes.
Doris Liu1c94b7d2013-11-09 19:13:44 -08001263 */
1264 public void setModeSwitchListener(ModeSwitchListener listener) {
Doris Liu2c559c42014-01-08 16:04:38 -08001265 mModeSwitchListener = listener;
1266 }
1267
1268 /**
1269 * Sets a listener that gets notified when the mode list is open full screen.
1270 *
1271 * @param listener a listener that listens to mode list open events
1272 */
1273 public void setModeListOpenListener(ModeListOpenListener listener) {
1274 mModeListOpenListener = listener;
Doris Liu1c94b7d2013-11-09 19:13:44 -08001275 }
1276
Sascha Haeberling8c1a9222014-02-25 09:38:06 -08001277 /**
1278 * Sets or replaces a listener that is called when the visibility of the
1279 * mode list changed.
1280 */
1281 public void setVisibilityChangedListener(ModeListVisibilityChangedListener listener) {
1282 mVisibilityChangedListener = listener;
1283 }
1284
Doris Liu1c94b7d2013-11-09 19:13:44 -08001285 @Override
1286 public boolean onTouchEvent(MotionEvent ev) {
Doris Liua8a24e02014-03-12 13:48:05 -07001287 // Reset touch forward recipient
1288 if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
Doris Liu6ababe92014-02-21 11:12:08 -08001289 mChildViewTouched = null;
Doris Liu1c94b7d2013-11-09 19:13:44 -08001290 }
Doris Liua8a24e02014-03-12 13:48:05 -07001291
Alan Newberger26e901e2014-03-25 11:09:29 -07001292 if (!mCurrentStateManager.getCurrentState().shouldHandleTouchEvent(ev)) {
Doris Liua8a24e02014-03-12 13:48:05 -07001293 return false;
1294 }
1295 super.onTouchEvent(ev);
1296
1297 // Pass all touch events to gesture detector for gesture handling.
1298 mGestureDetector.onTouchEvent(ev);
Alan Newberger26e901e2014-03-25 11:09:29 -07001299 mCurrentStateManager.getCurrentState().onTouchEvent(ev);
Doris Liu1c94b7d2013-11-09 19:13:44 -08001300 return true;
1301 }
1302
1303 /**
Doris Liu6ababe92014-02-21 11:12:08 -08001304 * Forward touch events to a recipient child view. Before feeding the motion
1305 * event into the child view, the event needs to be converted in child view's
1306 * coordinates.
1307 */
1308 private void forwardTouchEventToChild(MotionEvent ev) {
1309 if (mChildViewTouched != null) {
1310 float x = ev.getX() - mListView.getX();
1311 float y = ev.getY() - mListView.getY();
1312 x -= mChildViewTouched.getLeft();
1313 y -= mChildViewTouched.getTop();
1314
1315 mLastChildTouchEvent = MotionEvent.obtain(ev);
1316 mLastChildTouchEvent.setLocation(x, y);
1317 mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
1318 }
1319 }
1320
1321 /**
Doris Liu1c94b7d2013-11-09 19:13:44 -08001322 * Sets the swipe mode to indicate whether this is a swiping in
1323 * or out, and therefore we can have different animations.
Doris Liu70576b62013-11-14 20:30:33 -08001324 *
1325 * @param swipeIn indicates whether the swipe should reveal/hide the list.
Doris Liu1c94b7d2013-11-09 19:13:44 -08001326 */
1327 private void setSwipeMode(boolean swipeIn) {
Doris Liu70576b62013-11-14 20:30:33 -08001328 for (int i = 0 ; i < mModeSelectorItems.length; i++) {
1329 mModeSelectorItems[i].onSwipeModeChanged(swipeIn);
Doris Liu1c94b7d2013-11-09 19:13:44 -08001330 }
1331 }
1332
1333 @Override
1334 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1335 super.onLayout(changed, left, top, right, bottom);
1336 mWidth = right - left;
1337 mHeight = bottom - top - getPaddingTop() - getPaddingBottom();
Alan Newberger26e901e2014-03-25 11:09:29 -07001338 if (mCurrentStateManager.getCurrentState().getCurrentAnimationEffects() != null) {
1339 mCurrentStateManager.getCurrentState().getCurrentAnimationEffects().setSize(
1340 mWidth, mHeight);
Doris Liub6eaa8c2013-12-11 18:39:14 -08001341 }
Doris Liu1c94b7d2013-11-09 19:13:44 -08001342 }
1343
Doris Liu70576b62013-11-14 20:30:33 -08001344 /**
1345 * Here we calculate the children size based on the orientation, change
1346 * their layout parameters if needed before propagating onMeasure call
1347 * to the children, so the newly changed params will take effect in this
1348 * pass.
1349 *
1350 * @param widthMeasureSpec Horizontal space requirements as imposed by the
1351 * parent
1352 * @param heightMeasureSpec Vertical space requirements as imposed by the
1353 * parent
1354 */
Doris Liu1c94b7d2013-11-09 19:13:44 -08001355 @Override
1356 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Doris Liu1c94b7d2013-11-09 19:13:44 -08001357 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Doris Liue7e0c022014-02-12 11:14:08 -08001358 centerModeDrawerInUncoveredPreview(MeasureSpec.getSize(widthMeasureSpec),
1359 MeasureSpec.getSize(heightMeasureSpec));
Doris Liu1c94b7d2013-11-09 19:13:44 -08001360 }
1361
Doris Liub6eaa8c2013-12-11 18:39:14 -08001362 @Override
1363 public void draw(Canvas canvas) {
Alan Newberger26e901e2014-03-25 11:09:29 -07001364 ModeListState currentState = mCurrentStateManager.getCurrentState();
1365 if (currentState.getCurrentAnimationEffects() != null) {
1366 currentState.getCurrentAnimationEffects().drawBackground(canvas);
Doris Liub6eaa8c2013-12-11 18:39:14 -08001367 super.draw(canvas);
Alan Newberger26e901e2014-03-25 11:09:29 -07001368 currentState.getCurrentAnimationEffects().drawForeground(canvas);
Doris Liub6eaa8c2013-12-11 18:39:14 -08001369 } else {
1370 super.draw(canvas);
1371 }
1372 }
1373
Doris Liu70576b62013-11-14 20:30:33 -08001374 /**
Doris Liub00d6432014-02-26 10:18:47 -08001375 * This shows the mode switcher and starts the accordion animation with a delay.
1376 * If the view does not currently have focus, (e.g. There are popups on top of
1377 * it.) start the delayed accordion animation when it gains focus. Otherwise,
1378 * start the animation with a delay right away.
Doris Liub7d3ce12013-12-06 18:11:51 -08001379 */
Doris Liub00d6432014-02-26 10:18:47 -08001380 public void showModeSwitcherHint() {
Alan Newberger26e901e2014-03-25 11:09:29 -07001381 mCurrentStateManager.getCurrentState().showSwitcherHint();
Doris Liub7d3ce12013-12-06 18:11:51 -08001382 }
1383
1384 /**
Doris Liu70576b62013-11-14 20:30:33 -08001385 * Resets the visible width of all the mode selectors to 0.
1386 */
Doris Liu1c94b7d2013-11-09 19:13:44 -08001387 private void resetModeSelectors() {
Doris Liu70576b62013-11-14 20:30:33 -08001388 for (int i = 0; i < mModeSelectorItems.length; i++) {
1389 mModeSelectorItems[i].setVisibleWidth(0);
Doris Liu1c94b7d2013-11-09 19:13:44 -08001390 }
1391 }
1392
1393 private boolean isRunningAccordionAnimation() {
1394 return mAnimatorSet != null && mAnimatorSet.isRunning();
1395 }
1396
Doris Liu70576b62013-11-14 20:30:33 -08001397 /**
1398 * Calculate the mode selector item in the list that is at position (x, y).
Doris Liu213a4a02014-02-04 16:57:55 -08001399 * If the position is above the top item or below the bottom item, return
1400 * the top item or bottom item respectively.
Angus Kong612321f2013-11-18 16:17:43 -08001401 *
Doris Liu70576b62013-11-14 20:30:33 -08001402 * @param x horizontal position
1403 * @param y vertical position
1404 * @return index of the item that is at position (x, y)
1405 */
Doris Liu1c94b7d2013-11-09 19:13:44 -08001406 private int getFocusItem(float x, float y) {
Doris Liu213a4a02014-02-04 16:57:55 -08001407 // Convert coordinates into child view's coordinates.
Doris Liue7e0c022014-02-12 11:14:08 -08001408 x -= mListView.getX();
1409 y -= mListView.getY();
Doris Liu1c94b7d2013-11-09 19:13:44 -08001410
Doris Liu70576b62013-11-14 20:30:33 -08001411 for (int i = 0; i < mModeSelectorItems.length; i++) {
Doris Liu213a4a02014-02-04 16:57:55 -08001412 if (y <= mModeSelectorItems[i].getBottom()) {
Doris Liu1c94b7d2013-11-09 19:13:44 -08001413 return i;
1414 }
1415 }
Doris Liu213a4a02014-02-04 16:57:55 -08001416 return mModeSelectorItems.length - 1;
1417 }
1418
1419 @Override
Doris Liub00d6432014-02-26 10:18:47 -08001420 public void onWindowFocusChanged(boolean hasFocus) {
1421 super.onWindowFocusChanged(hasFocus);
Alan Newberger26e901e2014-03-25 11:09:29 -07001422 mCurrentStateManager.getCurrentState().onWindowFocusChanged(hasFocus);
Doris Liub00d6432014-02-26 10:18:47 -08001423 }
1424
1425 @Override
Doris Liu213a4a02014-02-04 16:57:55 -08001426 public void onVisibilityChanged(View v, int visibility) {
1427 super.onVisibilityChanged(v, visibility);
1428 if (visibility == VISIBLE) {
Doris Liue7e0c022014-02-12 11:14:08 -08001429 centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight());
Doris Liu213a4a02014-02-04 16:57:55 -08001430 // Highlight current module
1431 if (mModeSwitchListener != null) {
1432 int modeId = mModeSwitchListener.getCurrentModeIndex();
1433 int parentMode = CameraUtil.getCameraModeParentModeId(modeId, getContext());
1434 // Find parent mode in the nav drawer.
1435 for (int i = 0; i < mSupportedModes.size(); i++) {
1436 if (mSupportedModes.get(i) == parentMode) {
Doris Liu6ababe92014-02-21 11:12:08 -08001437 mModeSelectorItems[i].setSelected(true);
Doris Liu213a4a02014-02-04 16:57:55 -08001438 }
1439 }
1440 }
Doris Liu9d264302014-02-11 10:41:40 -08001441 } else {
1442 if (mModeSelectorItems != null) {
1443 // When becoming invisible/gone after initializing mode selector items.
1444 for (int i = 0; i < mModeSelectorItems.length; i++) {
1445 mModeSelectorItems[i].setHighlighted(false);
1446 mModeSelectorItems[i].setSelected(false);
1447 }
1448 }
Sascha Haeberling8c1a9222014-02-25 09:38:06 -08001449 if (mModeListOpenListener != null) {
Doris Liu9d264302014-02-11 10:41:40 -08001450 mModeListOpenListener.onModeListClosed();
Doris Liu213a4a02014-02-04 16:57:55 -08001451 }
1452 }
Sascha Haeberling8c1a9222014-02-25 09:38:06 -08001453 if (mVisibilityChangedListener != null) {
Sascha Haeberlingb49ab282014-02-25 11:41:18 -08001454 mVisibilityChangedListener.onVisibilityEvent(getVisibility() == VISIBLE);
Sascha Haeberling8c1a9222014-02-25 09:38:06 -08001455 }
Doris Liu213a4a02014-02-04 16:57:55 -08001456 }
1457
Doris Liua8a24e02014-03-12 13:48:05 -07001458 @Override
1459 public void setVisibility(int visibility) {
Alan Newberger26e901e2014-03-25 11:09:29 -07001460 ModeListState currentState = mCurrentStateManager.getCurrentState();
1461 if (currentState != null && !currentState.shouldHandleVisibilityChange(visibility)) {
Doris Liua8a24e02014-03-12 13:48:05 -07001462 return;
1463 }
1464 super.setVisibility(visibility);
1465 }
1466
Doris Liu213a4a02014-02-04 16:57:55 -08001467 /**
Doris Liue7e0c022014-02-12 11:14:08 -08001468 * Center mode drawer in the portion of camera preview that is not covered by
1469 * bottom bar.
Doris Liu213a4a02014-02-04 16:57:55 -08001470 */
Doris Liue7e0c022014-02-12 11:14:08 -08001471 // TODO: Combine SettingsButton logic into here if UX design does not change
1472 // for another week.
1473 private void centerModeDrawerInUncoveredPreview(int measuredWidth, int measuredHeight) {
Doris Liu213a4a02014-02-04 16:57:55 -08001474
1475 // Assuming the preview is centered in the space aside from bottom bar.
Doris Liue7e0c022014-02-12 11:14:08 -08001476 float previewAreaWidth = mUncoveredPreviewArea.right + mUncoveredPreviewArea.left;
1477 float previewAreaHeight = mUncoveredPreviewArea.top + mUncoveredPreviewArea.bottom;
1478 if (measuredWidth > measuredHeight && previewAreaWidth < previewAreaHeight
1479 || measuredWidth < measuredHeight && previewAreaWidth > previewAreaHeight) {
1480 // Cached preview area is stale, update mode drawer position on next
1481 // layout pass.
1482 mAdjustPositionWhenUncoveredPreviewAreaChanges = true;
Doris Liu213a4a02014-02-04 16:57:55 -08001483 } else {
Doris Liue7e0c022014-02-12 11:14:08 -08001484 // Align left:
1485 mListView.setTranslationX(mUncoveredPreviewArea.left);
1486 // Align center vertical:
1487 mListView.setTranslationY(mUncoveredPreviewArea.centerY()
1488 - mListView.getMeasuredHeight() / 2);
Doris Liu213a4a02014-02-04 16:57:55 -08001489 }
Doris Liu1c94b7d2013-11-09 19:13:44 -08001490 }
1491
1492 private void scroll(int itemId, float deltaX, float deltaY) {
Doris Liu70576b62013-11-14 20:30:33 -08001493 // Scrolling trend on X and Y axis, to track the trend by biasing
1494 // towards latest touch events.
Doris Liu1c94b7d2013-11-09 19:13:44 -08001495 mScrollTrendX = mScrollTrendX * 0.3f + deltaX * 0.7f;
1496 mScrollTrendY = mScrollTrendY * 0.3f + deltaY * 0.7f;
1497
Doris Liu70576b62013-11-14 20:30:33 -08001498 // TODO: Change how the curve is calculated below when UX finalize their design.
Doris Liucfaf7b02013-12-06 01:08:59 -08001499 mCurrentTime = SystemClock.uptimeMillis();
Doris Liu1c94b7d2013-11-09 19:13:44 -08001500 float longestWidth;
Doris Liu70576b62013-11-14 20:30:33 -08001501 if (itemId != NO_ITEM_SELECTED) {
Doris Liu69cb8402014-03-01 15:05:13 -08001502 longestWidth = mModeSelectorItems[itemId].getVisibleWidth();
Doris Liu1c94b7d2013-11-09 19:13:44 -08001503 } else {
Doris Liu69cb8402014-03-01 15:05:13 -08001504 longestWidth = mModeSelectorItems[0].getVisibleWidth();
Doris Liu1c94b7d2013-11-09 19:13:44 -08001505 }
Doris Liu69cb8402014-03-01 15:05:13 -08001506 float newPosition = longestWidth - deltaX;
1507 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
1508 newPosition = Math.min(newPosition, getMaxMovementBasedOnPosition((int) longestWidth,
1509 maxVisibleWidth));
1510 newPosition = Math.max(newPosition, 0);
1511 insertNewPosition(newPosition, mCurrentTime);
Doris Liu1c94b7d2013-11-09 19:13:44 -08001512
Doris Liu70576b62013-11-14 20:30:33 -08001513 for (int i = 0; i < mModeSelectorItems.length; i++) {
Doris Liucfaf7b02013-12-06 01:08:59 -08001514 mModeSelectorItems[i].setVisibleWidth(calculateVisibleWidthForItem(i,
Doris Liu69cb8402014-03-01 15:05:13 -08001515 (int) newPosition));
Doris Liu1c94b7d2013-11-09 19:13:44 -08001516 }
Doris Liu1c94b7d2013-11-09 19:13:44 -08001517 }
1518
Doris Liucfaf7b02013-12-06 01:08:59 -08001519 /**
1520 * Calculate the width of a specified item based on its position relative to
1521 * the item with longest width.
1522 */
1523 private int calculateVisibleWidthForItem(int itemId, int longestWidth) {
1524 if (itemId == mFocusItem || mFocusItem == NO_ITEM_SELECTED) {
1525 return longestWidth;
1526 }
1527
1528 int delay = Math.abs(itemId - mFocusItem) * DELAY_MS;
Doris Liu69cb8402014-03-01 15:05:13 -08001529 return (int) getPosition(mCurrentTime - delay,
1530 mModeSelectorItems[itemId].getVisibleWidth());
Doris Liucfaf7b02013-12-06 01:08:59 -08001531 }
1532
1533 /**
1534 * Insert new position and time stamp into the history position list, and
1535 * remove stale position items.
1536 *
1537 * @param position latest position of the focus item
1538 * @param time current time in milliseconds
1539 */
1540 private void insertNewPosition(float position, long time) {
1541 // TODO: Consider re-using stale position objects rather than
1542 // always creating new position objects.
1543 mPositionHistory.add(new TimeBasedPosition(position, time));
1544
1545 // Positions that are from too long ago will not be of any use for
1546 // future position interpolation. So we need to remove those positions
1547 // from the list.
1548 long timeCutoff = time - (mTotalModes - 1) * DELAY_MS;
1549 while (mPositionHistory.size() > 0) {
1550 // Remove all the position items that are prior to the cutoff time.
1551 TimeBasedPosition historyPosition = mPositionHistory.getFirst();
1552 if (historyPosition.getTimeStamp() < timeCutoff) {
1553 mPositionHistory.removeFirst();
1554 } else {
1555 break;
1556 }
1557 }
1558 }
1559
1560 /**
1561 * Gets the interpolated position at the specified time. This involves going
1562 * through the recorded positions until a {@link TimeBasedPosition} is found
1563 * such that the position the recorded before the given time, and the
1564 * {@link TimeBasedPosition} after that is recorded no earlier than the given
1565 * time. These two positions are then interpolated to get the position at the
1566 * specified time.
1567 */
Doris Liu69cb8402014-03-01 15:05:13 -08001568 private float getPosition(long time, float currentPosition) {
Doris Liucfaf7b02013-12-06 01:08:59 -08001569 int i;
1570 for (i = 0; i < mPositionHistory.size(); i++) {
1571 TimeBasedPosition historyPosition = mPositionHistory.get(i);
1572 if (historyPosition.getTimeStamp() > time) {
1573 // Found the winner. Now interpolate between position i and position i - 1
1574 if (i == 0) {
Doris Liu69cb8402014-03-01 15:05:13 -08001575 // Slowly approaching to the destination if there isn't enough data points
1576 float weight = 0.2f;
1577 return historyPosition.getPosition() * weight + (1f - weight) * currentPosition;
Doris Liucfaf7b02013-12-06 01:08:59 -08001578 } else {
1579 TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1);
1580 // Start interpolation
1581 float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) /
1582 (float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp());
1583 float position = fraction * (historyPosition.getPosition()
1584 - prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition();
1585 return position;
1586 }
1587 }
1588 }
1589 // It should never get here.
1590 Log.e(TAG, "Invalid time input for getPosition(). time: " + time);
1591 if (mPositionHistory.size() == 0) {
1592 Log.e(TAG, "TimeBasedPosition history size is 0");
1593 } else {
1594 Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp()
1595 + " , last position recorded at " + mPositionHistory.getLast().getTimeStamp());
1596 }
1597 assert (i < mPositionHistory.size());
1598 return i;
1599 }
1600
Doris Liu1c94b7d2013-11-09 19:13:44 -08001601 private void reset() {
1602 resetModeSelectors();
1603 mScrollTrendX = 0f;
1604 mScrollTrendY = 0f;
1605 setVisibility(INVISIBLE);
1606 }
1607
1608 /**
Doris Liu70576b62013-11-14 20:30:33 -08001609 * When visible width of list is changed, the background of the list needs
1610 * to darken/lighten correspondingly.
1611 */
Doris Liu69cb8402014-03-01 15:05:13 -08001612 public void onVisibleWidthChanged(int visibleWidth) {
1613 mVisibleWidth = visibleWidth;
Doris Liu69cb8402014-03-01 15:05:13 -08001614
Doris Liu213a4a02014-02-04 16:57:55 -08001615 // When the longest mode item is entirely shown (across the screen), the
Doris Liu70576b62013-11-14 20:30:33 -08001616 // background should be 50% transparent.
Doris Liu213a4a02014-02-04 16:57:55 -08001617 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
Doris Liu69cb8402014-03-01 15:05:13 -08001618 visibleWidth = Math.min(maxVisibleWidth, visibleWidth);
1619 if (visibleWidth != maxVisibleWidth) {
Doris Liu6ababe92014-02-21 11:12:08 -08001620 // No longer full screen.
1621 cancelForwardingTouchEvent();
1622 }
Doris Liu69cb8402014-03-01 15:05:13 -08001623 float openRatio = (float) visibleWidth / maxVisibleWidth;
Doris Liua8a24e02014-03-12 13:48:05 -07001624 onModeListOpenRatioUpdate(openRatio * mModeListOpenFactor);
Doris Liu69cb8402014-03-01 15:05:13 -08001625 }
1626
1627 /**
1628 * Gets called when UI elements such as background and gear icon need to adjust
1629 * their appearance based on the percentage of the mode list opening.
1630 *
1631 * @param openRatio percentage of the mode list opening, ranging [0f, 1f]
1632 */
1633 private void onModeListOpenRatioUpdate(float openRatio) {
1634 for (int i = 0; i < mModeSelectorItems.length; i++) {
1635 mModeSelectorItems[i].setTextAlpha(openRatio);
1636 }
Doris Liu9d264302014-02-11 10:41:40 -08001637 setBackgroundAlpha((int) (BACKGROUND_TRANSPARENTCY * openRatio));
1638 if (mModeListOpenListener != null) {
1639 mModeListOpenListener.onModeListOpenProgress(openRatio);
1640 }
1641 if (mSettingsButton != null) {
1642 mSettingsButton.setAlpha(openRatio);
1643 }
Doris Liu70576b62013-11-14 20:30:33 -08001644 }
1645
Doris Liu6ababe92014-02-21 11:12:08 -08001646 /**
1647 * Cancels the touch event forwarding by sending a cancel event to the recipient
1648 * view and resetting the touch forward recipient to ensure no more events
1649 * can be forwarded in the current series of the touch events.
1650 */
1651 private void cancelForwardingTouchEvent() {
1652 if (mChildViewTouched != null) {
1653 mLastChildTouchEvent.setAction(MotionEvent.ACTION_CANCEL);
1654 mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
1655 mChildViewTouched = null;
1656 }
1657 }
1658
Doris Liub6eaa8c2013-12-11 18:39:14 -08001659 @Override
1660 public void onWindowVisibilityChanged(int visibility) {
1661 super.onWindowVisibilityChanged(visibility);
1662 if (visibility != VISIBLE) {
Alan Newberger26e901e2014-03-25 11:09:29 -07001663 mCurrentStateManager.getCurrentState().hide();
Doris Liub6eaa8c2013-12-11 18:39:14 -08001664 }
1665 }
1666
Doris Liu70576b62013-11-14 20:30:33 -08001667 /**
Erin Dahlgren15691af2014-03-14 14:10:57 -07001668 * Defines how the list view should respond to a menu button pressed
1669 * event.
1670 */
1671 public boolean onMenuPressed() {
Alan Newberger26e901e2014-03-25 11:09:29 -07001672 return mCurrentStateManager.getCurrentState().onMenuPressed();
Erin Dahlgren15691af2014-03-14 14:10:57 -07001673 }
1674
1675 /**
Doris Liu1c94b7d2013-11-09 19:13:44 -08001676 * The list view should either snap back or snap to full screen after a gesture.
1677 * This function is called when an up or cancel event is received, and then based
1678 * on the current position of the list and the gesture we can decide which way
1679 * to snap.
1680 */
1681 private void snap() {
Doris Liua8a24e02014-03-12 13:48:05 -07001682 if (shouldSnapBack()) {
1683 snapBack();
1684 } else {
1685 snapToFullScreen();
1686 }
1687 }
1688
1689 private boolean shouldSnapBack() {
1690 int itemId = Math.max(0, mFocusItem);
1691 if (Math.abs(mVelocityX) > VELOCITY_THRESHOLD) {
1692 // Fling to open / close
1693 return mVelocityX < 0;
1694 } else if (mModeSelectorItems[itemId].getVisibleWidth()
1695 < mModeSelectorItems[itemId].getMaxVisibleWidth() * SNAP_BACK_THRESHOLD_RATIO) {
1696 return true;
1697 } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) {
1698 return true;
1699 } else {
1700 return false;
Doris Liu1c94b7d2013-11-09 19:13:44 -08001701 }
1702 }
1703
Doris Liub6eaa8c2013-12-11 18:39:14 -08001704 /**
1705 * Snaps back out of the screen.
1706 *
1707 * @param withAnimation whether snapping back should be animated
1708 */
Doris Liua8a24e02014-03-12 13:48:05 -07001709 public Animator snapBack(boolean withAnimation) {
Doris Liub6eaa8c2013-12-11 18:39:14 -08001710 if (withAnimation) {
Doris Liu213a4a02014-02-04 16:57:55 -08001711 if (mVelocityX > -VELOCITY_THRESHOLD * SCROLL_FACTOR) {
Doris Liua8a24e02014-03-12 13:48:05 -07001712 return animateListToWidth(0);
Doris Liu213a4a02014-02-04 16:57:55 -08001713 } else {
Doris Liua8a24e02014-03-12 13:48:05 -07001714 return animateListToWidthAtVelocity(mVelocityX, 0);
Doris Liu213a4a02014-02-04 16:57:55 -08001715 }
Doris Liub6eaa8c2013-12-11 18:39:14 -08001716 } else {
1717 setVisibility(INVISIBLE);
1718 resetModeSelectors();
Doris Liua8a24e02014-03-12 13:48:05 -07001719 return null;
Doris Liub6eaa8c2013-12-11 18:39:14 -08001720 }
1721 }
1722
1723 /**
1724 * Snaps the mode list back out with animation.
1725 */
Doris Liua8a24e02014-03-12 13:48:05 -07001726 private Animator snapBack() {
1727 return snapBack(true);
Doris Liu1c94b7d2013-11-09 19:13:44 -08001728 }
1729
Doris Liua8a24e02014-03-12 13:48:05 -07001730 private Animator snapToFullScreen() {
1731 Animator animator;
Doris Liu213a4a02014-02-04 16:57:55 -08001732 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
1733 int fullWidth = mModeSelectorItems[focusItem].getMaxVisibleWidth();
Doris Liu69cb8402014-03-01 15:05:13 -08001734 if (mVelocityX <= VELOCITY_THRESHOLD) {
Doris Liua8a24e02014-03-12 13:48:05 -07001735 animator = animateListToWidth(fullWidth);
Doris Liu6c5a5372014-01-15 12:26:17 -08001736 } else {
1737 // If the fling velocity exceeds this threshold, snap to full screen
1738 // at a constant speed.
Doris Liua8a24e02014-03-12 13:48:05 -07001739 animator = animateListToWidthAtVelocity(VELOCITY_THRESHOLD, fullWidth);
Doris Liu6c5a5372014-01-15 12:26:17 -08001740 }
Doris Liu2c559c42014-01-08 16:04:38 -08001741 if (mModeListOpenListener != null) {
1742 mModeListOpenListener.onOpenFullScreen();
1743 }
Doris Liua8a24e02014-03-12 13:48:05 -07001744 return animator;
Doris Liu1c94b7d2013-11-09 19:13:44 -08001745 }
1746
Doris Liu70576b62013-11-14 20:30:33 -08001747 /**
1748 * Overloaded function to provide a simple way to start animation. Animation
1749 * will use default duration, and a value of <code>null</code> for interpolator
1750 * means linear interpolation will be used.
1751 *
1752 * @param width a set of values that the animation will animate between over time
1753 */
Doris Liua8a24e02014-03-12 13:48:05 -07001754 private Animator animateListToWidth(int... width) {
1755 return animateListToWidth(0, DEFAULT_DURATION_MS, null, width);
Doris Liu1c94b7d2013-11-09 19:13:44 -08001756 }
1757
Doris Liu70576b62013-11-14 20:30:33 -08001758 /**
1759 * Animate the mode list between the given set of visible width.
1760 *
Doris Liub00d6432014-02-26 10:18:47 -08001761 * @param delay start delay between consecutive mode item. If delay < 0, the
1762 * leader in the animation will be the bottom item.
Doris Liu70576b62013-11-14 20:30:33 -08001763 * @param duration duration for the animation of each mode item
1764 * @param interpolator interpolator to be used by the animation
1765 * @param width a set of values that the animation will animate between over time
1766 */
Doris Liu69cb8402014-03-01 15:05:13 -08001767 private Animator animateListToWidth(int delay, int duration,
Doris Liu1c94b7d2013-11-09 19:13:44 -08001768 TimeInterpolator interpolator, int... width) {
1769 if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
1770 mAnimatorSet.end();
1771 }
1772
1773 ArrayList<Animator> animators = new ArrayList<Animator>();
Doris Liub00d6432014-02-26 10:18:47 -08001774 boolean animateModeItemsInOrder = true;
1775 if (delay < 0) {
1776 animateModeItemsInOrder = false;
1777 delay *= -1;
1778 }
Doris Liu70576b62013-11-14 20:30:33 -08001779 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
Doris Liu1c94b7d2013-11-09 19:13:44 -08001780 for (int i = 0; i < mTotalModes; i++) {
Doris Liub00d6432014-02-26 10:18:47 -08001781 ObjectAnimator animator;
1782 if (animateModeItemsInOrder) {
1783 animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
Doris Liu70576b62013-11-14 20:30:33 -08001784 "visibleWidth", width);
Doris Liub00d6432014-02-26 10:18:47 -08001785 } else {
1786 animator = ObjectAnimator.ofInt(mModeSelectorItems[mTotalModes - 1 -i],
1787 "visibleWidth", width);
1788 }
Doris Liu1c94b7d2013-11-09 19:13:44 -08001789 animator.setDuration(duration);
1790 animator.setStartDelay(i * delay);
1791 animators.add(animator);
1792 }
1793
Doris Liu1c94b7d2013-11-09 19:13:44 -08001794 mAnimatorSet = new AnimatorSet();
1795 mAnimatorSet.playTogether(animators);
1796 mAnimatorSet.setInterpolator(interpolator);
Doris Liu6c5a5372014-01-15 12:26:17 -08001797 mAnimatorSet.start();
Doris Liu69cb8402014-03-01 15:05:13 -08001798
1799 return mAnimatorSet;
Doris Liu6c5a5372014-01-15 12:26:17 -08001800 }
Doris Liu1c94b7d2013-11-09 19:13:44 -08001801
Doris Liu6c5a5372014-01-15 12:26:17 -08001802 /**
1803 * Animate the mode list to the given width at a constant velocity.
1804 *
1805 * @param velocity the velocity that animation will be at
1806 * @param width final width of the list
1807 */
Doris Liua8a24e02014-03-12 13:48:05 -07001808 private Animator animateListToWidthAtVelocity(float velocity, int width) {
Doris Liu6c5a5372014-01-15 12:26:17 -08001809 if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
1810 mAnimatorSet.end();
1811 }
1812
1813 ArrayList<Animator> animators = new ArrayList<Animator>();
1814 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
1815 for (int i = 0; i < mTotalModes; i++) {
1816 ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
1817 "visibleWidth", width);
Sascha Haeberling8c1a9222014-02-25 09:38:06 -08001818 int duration = (int) (width / velocity);
Doris Liu6c5a5372014-01-15 12:26:17 -08001819 animator.setDuration(duration);
1820 animators.add(animator);
Doris Liu6c5a5372014-01-15 12:26:17 -08001821 }
Doris Liu1c94b7d2013-11-09 19:13:44 -08001822
Doris Liu6c5a5372014-01-15 12:26:17 -08001823 mAnimatorSet = new AnimatorSet();
1824 mAnimatorSet.playTogether(animators);
1825 mAnimatorSet.setInterpolator(null);
Doris Liu1c94b7d2013-11-09 19:13:44 -08001826 mAnimatorSet.start();
Doris Liua8a24e02014-03-12 13:48:05 -07001827
1828 return mAnimatorSet;
Doris Liu1c94b7d2013-11-09 19:13:44 -08001829 }
Doris Liuf55f3c42013-11-20 00:24:46 -08001830
1831 /**
Doris Liub9b72402013-12-19 19:07:58 -08001832 * Called when the back key is pressed.
1833 *
1834 * @return Whether the UI responded to the key event.
1835 */
1836 public boolean onBackPressed() {
Alan Newberger26e901e2014-03-25 11:09:29 -07001837 return mCurrentStateManager.getCurrentState().onBackPressed();
Doris Liub9b72402013-12-19 19:07:58 -08001838 }
1839
Doris Liub6eaa8c2013-12-11 18:39:14 -08001840 public void startModeSelectionAnimation() {
Alan Newberger26e901e2014-03-25 11:09:29 -07001841 mCurrentStateManager.getCurrentState().startModeSelectionAnimation();
Doris Liub6eaa8c2013-12-11 18:39:14 -08001842 }
1843
Doris Liu69cb8402014-03-01 15:05:13 -08001844 public float getMaxMovementBasedOnPosition(int lastVisibleWidth, int maxWidth) {
1845 int timeElapsed = (int) (System.currentTimeMillis() - mLastScrollTime);
1846 if (timeElapsed > SCROLL_INTERVAL_MS) {
1847 timeElapsed = SCROLL_INTERVAL_MS;
1848 }
1849 float position;
1850 int slowZone = (int) (maxWidth * SLOW_ZONE_PERCENTAGE);
1851 if (lastVisibleWidth < (maxWidth - slowZone)) {
1852 position = VELOCITY_THRESHOLD * (float) timeElapsed + lastVisibleWidth;
1853 } else {
1854 float percentageIntoSlowZone = (lastVisibleWidth - (maxWidth - slowZone)) / slowZone;
1855 float velocity = (1 - percentageIntoSlowZone) * VELOCITY_THRESHOLD;
1856 position = velocity * (float) timeElapsed + lastVisibleWidth;
1857 }
1858 position = Math.min(maxWidth, position);
1859 return position;
1860 }
1861
Doris Liua8a24e02014-03-12 13:48:05 -07001862 private class PeepholeAnimationEffect extends AnimationEffects {
Doris Liub6eaa8c2013-12-11 18:39:14 -08001863
1864 private final static int UNSET = -1;
Doris Liu213a4a02014-02-04 16:57:55 -08001865 private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 300;
1866
1867 private final Paint mMaskPaint = new Paint();
1868 private final Paint mBackgroundPaint = new Paint();
1869 private final RectF mBackgroundDrawArea = new RectF();
Doris Liub6eaa8c2013-12-11 18:39:14 -08001870
1871 private int mWidth;
1872 private int mHeight;
Doris Liub6eaa8c2013-12-11 18:39:14 -08001873 private int mPeepHoleCenterX = UNSET;
1874 private int mPeepHoleCenterY = UNSET;
1875 private float mRadius = 0f;
1876 private ValueAnimator mPeepHoleAnimator;
Doris Liu213a4a02014-02-04 16:57:55 -08001877 private Bitmap mBackground;
1878 private Bitmap mBlurredBackground;
1879 private Bitmap mBackgroundOverlay;
Doris Liub6eaa8c2013-12-11 18:39:14 -08001880
1881 public PeepholeAnimationEffect() {
1882 mMaskPaint.setAlpha(0);
1883 mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
1884 }
1885
Sascha Haeberling601921e2013-12-17 10:01:11 -08001886 @Override
Doris Liub6eaa8c2013-12-11 18:39:14 -08001887 public void setSize(int width, int height) {
1888 mWidth = width;
1889 mHeight = height;
1890 }
1891
Sascha Haeberling601921e2013-12-17 10:01:11 -08001892 @Override
Doris Liu69cb8402014-03-01 15:05:13 -08001893 public boolean onTouchEvent(MotionEvent event) {
1894 return true;
1895 }
1896
1897 @Override
Doris Liub6eaa8c2013-12-11 18:39:14 -08001898 public void drawForeground(Canvas canvas) {
1899 // Draw the circle in clear mode
1900 if (mPeepHoleAnimator != null) {
1901 // Draw a transparent circle using clear mode
1902 canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint);
1903 }
1904 }
1905
1906 public void setAnimationStartingPosition(int x, int y) {
1907 mPeepHoleCenterX = x;
1908 mPeepHoleCenterY = y;
1909 }
1910
Doris Liu213a4a02014-02-04 16:57:55 -08001911 /**
1912 * Sets the bitmap to be drawn in the background and the drawArea to draw
1913 * the bitmap. In the meantime, start processing the image in a background
1914 * thread to get a blurred background image.
1915 *
1916 * @param background image to be drawn in the background
1917 * @param drawArea area to draw the background image
1918 */
1919 public void setBackground(Bitmap background, RectF drawArea) {
1920 mBackground = background;
1921 mBackgroundDrawArea.set(drawArea);
1922 new BlurTask().execute(Bitmap.createScaledBitmap(background, background.getWidth(),
1923 background.getHeight(), true));
1924 }
1925
1926 /**
1927 * Sets the overlay image to be drawn on top of the background.
1928 */
1929 public void setBackgroundOverlay(Bitmap overlay) {
1930 mBackgroundOverlay = overlay;
1931 }
1932
1933 /**
1934 * This gets called when a blurred image of the background is generated.
1935 * Start an animation to fade in the blur.
1936 *
1937 * @param blur blurred image of the background.
1938 */
1939 public void setBlurredBackground(Bitmap blur) {
1940 mBlurredBackground = blur;
1941 // Start fade in.
1942 ObjectAnimator alpha = ObjectAnimator.ofInt(mBackgroundPaint, "alpha", 80, 255);
1943 alpha.setDuration(250);
1944 alpha.setInterpolator(Gusterpolator.INSTANCE);
1945 alpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1946 @Override
1947 public void onAnimationUpdate(ValueAnimator animation) {
1948 invalidate();
1949 }
1950 });
1951 alpha.start();
1952 invalidate();
1953 }
1954
1955 @Override
1956 public void drawBackground(Canvas canvas) {
1957 if (mBackground != null && mBackgroundOverlay != null) {
1958 canvas.drawARGB(255, 0, 0, 0);
1959 canvas.drawBitmap(mBackground, null, mBackgroundDrawArea, null);
1960 if (mBlurredBackground != null) {
1961 canvas.drawBitmap(mBlurredBackground, null, mBackgroundDrawArea, mBackgroundPaint);
1962 }
1963 canvas.drawBitmap(mBackgroundOverlay, 0, 0, null);
1964 }
1965 }
1966
Sascha Haeberling601921e2013-12-17 10:01:11 -08001967 @Override
Doris Liua8a24e02014-03-12 13:48:05 -07001968 public void startAnimation(Animator.AnimatorListener listener) {
Doris Liub6eaa8c2013-12-11 18:39:14 -08001969 if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) {
1970 return;
1971 }
1972 if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) {
1973 mPeepHoleCenterX = mWidth / 2;
1974 mPeepHoleCenterY = mHeight / 2;
1975 }
1976
1977 int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX);
1978 int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY);
1979 int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge
1980 + verticalDistanceToFarEdge * verticalDistanceToFarEdge));
Doris Liu213a4a02014-02-04 16:57:55 -08001981 int startRadius = getResources().getDimensionPixelSize(
1982 R.dimen.mode_selector_icon_block_width) / 2;
Doris Liub6eaa8c2013-12-11 18:39:14 -08001983
1984 mPeepHoleAnimator = ValueAnimator.ofFloat(0, endRadius);
1985 mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS);
1986 mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE);
1987 mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1988 @Override
1989 public void onAnimationUpdate(ValueAnimator animation) {
1990 // Modify mask by enlarging the hole
1991 mRadius = (Float) mPeepHoleAnimator.getAnimatedValue();
1992 invalidate();
1993 }
1994 });
1995
Doris Liua8a24e02014-03-12 13:48:05 -07001996 if (listener != null) {
1997 mPeepHoleAnimator.addListener(listener);
1998 }
Doris Liub6eaa8c2013-12-11 18:39:14 -08001999 mPeepHoleAnimator.start();
2000 }
2001
Doris Liu69cb8402014-03-01 15:05:13 -08002002 @Override
2003 public void endAnimation() {
Doris Liub6eaa8c2013-12-11 18:39:14 -08002004 }
Doris Liu213a4a02014-02-04 16:57:55 -08002005
2006 private class BlurTask extends AsyncTask<Bitmap, Integer, Bitmap> {
2007
2008 // Gaussian blur mask size.
2009 private static final int MASK_SIZE = 7;
2010 @Override
2011 protected Bitmap doInBackground(Bitmap... params) {
2012
2013 Bitmap intermediateBitmap = params[0];
2014 int factor = 4;
2015 Bitmap lowResPreview = Bitmap.createScaledBitmap(intermediateBitmap,
2016 intermediateBitmap.getWidth() / factor,
2017 intermediateBitmap.getHeight() / factor, true);
2018
2019 int width = lowResPreview.getWidth();
2020 int height = lowResPreview.getHeight();
2021
2022 if (mInputPixels == null || mInputPixels.length < width * height) {
2023 mInputPixels = new int[width * height];
2024 mOutputPixels = new int[width * height];
2025 }
2026 lowResPreview.getPixels(mInputPixels, 0, width, 0, 0, width, height);
2027 CameraUtil.blur(mInputPixels, mOutputPixels, width, height, MASK_SIZE);
2028 lowResPreview.setPixels(mOutputPixels, 0, width, 0, 0, width, height);
2029
2030 intermediateBitmap.recycle();
2031 return Bitmap.createScaledBitmap(lowResPreview, width * factor,
2032 height * factor, true);
2033 }
2034
2035 @Override
2036 protected void onPostExecute(Bitmap bitmap) {
2037 setBlurredBackground(bitmap);
2038 }
2039 };
Doris Liub6eaa8c2013-12-11 18:39:14 -08002040 }
Doris Liu69cb8402014-03-01 15:05:13 -08002041
Doris Liu1c94b7d2013-11-09 19:13:44 -08002042}