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