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