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