ModeListView.java revision d2710487e724660cbb890ebf5eb887b4a93281c1
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((int) newPosition); 1585 } 1586 } 1587 1588 /** 1589 * Insert new position and time stamp into the history position list, and 1590 * remove stale position items. 1591 * 1592 * @param position latest position of the focus item 1593 * @param time current time in milliseconds 1594 */ 1595 private void insertNewPosition(float position, long time) { 1596 // TODO: Consider re-using stale position objects rather than 1597 // always creating new position objects. 1598 mPositionHistory.add(new TimeBasedPosition(position, time)); 1599 1600 // Positions that are from too long ago will not be of any use for 1601 // future position interpolation. So we need to remove those positions 1602 // from the list. 1603 long timeCutoff = time - (mTotalModes - 1) * DELAY_MS; 1604 while (mPositionHistory.size() > 0) { 1605 // Remove all the position items that are prior to the cutoff time. 1606 TimeBasedPosition historyPosition = mPositionHistory.getFirst(); 1607 if (historyPosition.getTimeStamp() < timeCutoff) { 1608 mPositionHistory.removeFirst(); 1609 } else { 1610 break; 1611 } 1612 } 1613 } 1614 1615 /** 1616 * Gets the interpolated position at the specified time. This involves going 1617 * through the recorded positions until a {@link TimeBasedPosition} is found 1618 * such that the position the recorded before the given time, and the 1619 * {@link TimeBasedPosition} after that is recorded no earlier than the given 1620 * time. These two positions are then interpolated to get the position at the 1621 * specified time. 1622 */ 1623 private float getPosition(long time, float currentPosition) { 1624 int i; 1625 for (i = 0; i < mPositionHistory.size(); i++) { 1626 TimeBasedPosition historyPosition = mPositionHistory.get(i); 1627 if (historyPosition.getTimeStamp() > time) { 1628 // Found the winner. Now interpolate between position i and position i - 1 1629 if (i == 0) { 1630 // Slowly approaching to the destination if there isn't enough data points 1631 float weight = 0.2f; 1632 return historyPosition.getPosition() * weight + (1f - weight) * currentPosition; 1633 } else { 1634 TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1); 1635 // Start interpolation 1636 float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) / 1637 (float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp()); 1638 float position = fraction * (historyPosition.getPosition() 1639 - prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition(); 1640 return position; 1641 } 1642 } 1643 } 1644 // It should never get here. 1645 Log.e(TAG, "Invalid time input for getPosition(). time: " + time); 1646 if (mPositionHistory.size() == 0) { 1647 Log.e(TAG, "TimeBasedPosition history size is 0"); 1648 } else { 1649 Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp() 1650 + " , last position recorded at " + mPositionHistory.getLast().getTimeStamp()); 1651 } 1652 assert (i < mPositionHistory.size()); 1653 return i; 1654 } 1655 1656 private void reset() { 1657 resetModeSelectors(); 1658 mScrollTrendX = 0f; 1659 mScrollTrendY = 0f; 1660 setVisibility(INVISIBLE); 1661 } 1662 1663 /** 1664 * When visible width of list is changed, the background of the list needs 1665 * to darken/lighten correspondingly. 1666 */ 1667 @Override 1668 public void onVisibleWidthChanged(int visibleWidth) { 1669 mVisibleWidth = visibleWidth; 1670 1671 // When the longest mode item is entirely shown (across the screen), the 1672 // background should be 50% transparent. 1673 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth(); 1674 visibleWidth = Math.min(maxVisibleWidth, visibleWidth); 1675 if (visibleWidth != maxVisibleWidth) { 1676 // No longer full screen. 1677 cancelForwardingTouchEvent(); 1678 } 1679 float openRatio = (float) visibleWidth / maxVisibleWidth; 1680 onModeListOpenRatioUpdate(openRatio * mModeListOpenFactor); 1681 } 1682 1683 /** 1684 * Gets called when UI elements such as background and gear icon need to adjust 1685 * their appearance based on the percentage of the mode list opening. 1686 * 1687 * @param openRatio percentage of the mode list opening, ranging [0f, 1f] 1688 */ 1689 private void onModeListOpenRatioUpdate(float openRatio) { 1690 for (int i = 0; i < mModeSelectorItems.length; i++) { 1691 mModeSelectorItems[i].setTextAlpha(openRatio); 1692 } 1693 setBackgroundAlpha((int) (BACKGROUND_TRANSPARENTCY * openRatio)); 1694 if (mModeListOpenListener != null) { 1695 mModeListOpenListener.onModeListOpenProgress(openRatio); 1696 } 1697 if (mSettingsButton != null) { 1698 mSettingsButton.setAlpha(openRatio); 1699 } 1700 } 1701 1702 /** 1703 * Cancels the touch event forwarding by sending a cancel event to the recipient 1704 * view and resetting the touch forward recipient to ensure no more events 1705 * can be forwarded in the current series of the touch events. 1706 */ 1707 private void cancelForwardingTouchEvent() { 1708 if (mChildViewTouched != null) { 1709 mLastChildTouchEvent.setAction(MotionEvent.ACTION_CANCEL); 1710 mChildViewTouched.onTouchEvent(mLastChildTouchEvent); 1711 mChildViewTouched = null; 1712 } 1713 } 1714 1715 @Override 1716 public void onWindowVisibilityChanged(int visibility) { 1717 super.onWindowVisibilityChanged(visibility); 1718 if (visibility != VISIBLE) { 1719 mCurrentStateManager.getCurrentState().hide(); 1720 } 1721 } 1722 1723 /** 1724 * Defines how the list view should respond to a menu button pressed 1725 * event. 1726 */ 1727 public boolean onMenuPressed() { 1728 return mCurrentStateManager.getCurrentState().onMenuPressed(); 1729 } 1730 1731 /** 1732 * The list view should either snap back or snap to full screen after a gesture. 1733 * This function is called when an up or cancel event is received, and then based 1734 * on the current position of the list and the gesture we can decide which way 1735 * to snap. 1736 */ 1737 private void snap() { 1738 if (shouldSnapBack()) { 1739 snapBack(); 1740 } else { 1741 snapToFullScreen(); 1742 } 1743 } 1744 1745 private boolean shouldSnapBack() { 1746 int itemId = Math.max(0, mFocusItem); 1747 if (Math.abs(mVelocityX) > VELOCITY_THRESHOLD) { 1748 // Fling to open / close 1749 return mVelocityX < 0; 1750 } else if (mModeSelectorItems[itemId].getVisibleWidth() 1751 < mModeSelectorItems[itemId].getMaxVisibleWidth() * SNAP_BACK_THRESHOLD_RATIO) { 1752 return true; 1753 } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) { 1754 return true; 1755 } else { 1756 return false; 1757 } 1758 } 1759 1760 /** 1761 * Snaps back out of the screen. 1762 * 1763 * @param withAnimation whether snapping back should be animated 1764 */ 1765 public Animator snapBack(boolean withAnimation) { 1766 if (withAnimation) { 1767 if (mVelocityX > -VELOCITY_THRESHOLD * SCROLL_FACTOR) { 1768 return animateListToWidth(0); 1769 } else { 1770 return animateListToWidthAtVelocity(mVelocityX, 0); 1771 } 1772 } else { 1773 setVisibility(INVISIBLE); 1774 resetModeSelectors(); 1775 return null; 1776 } 1777 } 1778 1779 /** 1780 * Snaps the mode list back out with animation. 1781 */ 1782 private Animator snapBack() { 1783 return snapBack(true); 1784 } 1785 1786 private Animator snapToFullScreen() { 1787 Animator animator; 1788 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem; 1789 int fullWidth = mModeSelectorItems[focusItem].getMaxVisibleWidth(); 1790 if (mVelocityX <= VELOCITY_THRESHOLD) { 1791 animator = animateListToWidth(fullWidth); 1792 } else { 1793 // If the fling velocity exceeds this threshold, snap to full screen 1794 // at a constant speed. 1795 animator = animateListToWidthAtVelocity(VELOCITY_THRESHOLD, fullWidth); 1796 } 1797 if (mModeListOpenListener != null) { 1798 mModeListOpenListener.onOpenFullScreen(); 1799 } 1800 return animator; 1801 } 1802 1803 /** 1804 * Overloaded function to provide a simple way to start animation. Animation 1805 * will use default duration, and a value of <code>null</code> for interpolator 1806 * means linear interpolation will be used. 1807 * 1808 * @param width a set of values that the animation will animate between over time 1809 */ 1810 private Animator animateListToWidth(int... width) { 1811 return animateListToWidth(0, DEFAULT_DURATION_MS, null, width); 1812 } 1813 1814 /** 1815 * Animate the mode list between the given set of visible width. 1816 * 1817 * @param delay start delay between consecutive mode item. If delay < 0, the 1818 * leader in the animation will be the bottom item. 1819 * @param duration duration for the animation of each mode item 1820 * @param interpolator interpolator to be used by the animation 1821 * @param width a set of values that the animation will animate between over time 1822 */ 1823 private Animator animateListToWidth(int delay, int duration, 1824 TimeInterpolator interpolator, int... width) { 1825 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 1826 mAnimatorSet.end(); 1827 } 1828 1829 ArrayList<Animator> animators = new ArrayList<Animator>(); 1830 boolean animateModeItemsInOrder = true; 1831 if (delay < 0) { 1832 animateModeItemsInOrder = false; 1833 delay *= -1; 1834 } 1835 for (int i = 0; i < mTotalModes; i++) { 1836 ObjectAnimator animator; 1837 if (animateModeItemsInOrder) { 1838 animator = ObjectAnimator.ofInt(mModeSelectorItems[i], 1839 "visibleWidth", width); 1840 } else { 1841 animator = ObjectAnimator.ofInt(mModeSelectorItems[mTotalModes - 1 -i], 1842 "visibleWidth", width); 1843 } 1844 animator.setDuration(duration); 1845 animators.add(animator); 1846 } 1847 1848 mAnimatorSet = new AnimatorSet(); 1849 mAnimatorSet.playTogether(animators); 1850 mAnimatorSet.setInterpolator(interpolator); 1851 mAnimatorSet.start(); 1852 1853 return mAnimatorSet; 1854 } 1855 1856 /** 1857 * Animate the mode list to the given width at a constant velocity. 1858 * 1859 * @param velocity the velocity that animation will be at 1860 * @param width final width of the list 1861 */ 1862 private Animator animateListToWidthAtVelocity(float velocity, int width) { 1863 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 1864 mAnimatorSet.end(); 1865 } 1866 1867 ArrayList<Animator> animators = new ArrayList<Animator>(); 1868 for (int i = 0; i < mTotalModes; i++) { 1869 ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i], 1870 "visibleWidth", width); 1871 int duration = (int) (width / velocity); 1872 animator.setDuration(duration); 1873 animators.add(animator); 1874 } 1875 1876 mAnimatorSet = new AnimatorSet(); 1877 mAnimatorSet.playTogether(animators); 1878 mAnimatorSet.setInterpolator(null); 1879 mAnimatorSet.start(); 1880 1881 return mAnimatorSet; 1882 } 1883 1884 /** 1885 * Called when the back key is pressed. 1886 * 1887 * @return Whether the UI responded to the key event. 1888 */ 1889 public boolean onBackPressed() { 1890 return mCurrentStateManager.getCurrentState().onBackPressed(); 1891 } 1892 1893 public void startModeSelectionAnimation() { 1894 mCurrentStateManager.getCurrentState().startModeSelectionAnimation(); 1895 } 1896 1897 public float getMaxMovementBasedOnPosition(int lastVisibleWidth, int maxWidth) { 1898 int timeElapsed = (int) (System.currentTimeMillis() - mLastScrollTime); 1899 if (timeElapsed > SCROLL_INTERVAL_MS) { 1900 timeElapsed = SCROLL_INTERVAL_MS; 1901 } 1902 float position; 1903 int slowZone = (int) (maxWidth * SLOW_ZONE_PERCENTAGE); 1904 if (lastVisibleWidth < (maxWidth - slowZone)) { 1905 position = VELOCITY_THRESHOLD * timeElapsed + lastVisibleWidth; 1906 } else { 1907 float percentageIntoSlowZone = (lastVisibleWidth - (maxWidth - slowZone)) / slowZone; 1908 float velocity = (1 - percentageIntoSlowZone) * VELOCITY_THRESHOLD; 1909 position = velocity * timeElapsed + lastVisibleWidth; 1910 } 1911 position = Math.min(maxWidth, position); 1912 return position; 1913 } 1914 1915 private class PeepholeAnimationEffect extends AnimationEffects { 1916 1917 private final static int UNSET = -1; 1918 private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 500; 1919 1920 private final Paint mMaskPaint = new Paint(); 1921 private final RectF mBackgroundDrawArea = new RectF(); 1922 1923 private int mPeepHoleCenterX = UNSET; 1924 private int mPeepHoleCenterY = UNSET; 1925 private float mRadius = 0f; 1926 private ValueAnimator mPeepHoleAnimator; 1927 private ValueAnimator mFadeOutAlphaAnimator; 1928 private ValueAnimator mRevealAlphaAnimator; 1929 private Bitmap mBackground; 1930 private Bitmap mBackgroundOverlay; 1931 1932 private Paint mCirclePaint = new Paint(); 1933 private Paint mCoverPaint = new Paint(); 1934 1935 private TouchCircleDrawable mCircleDrawable; 1936 1937 public PeepholeAnimationEffect() { 1938 mMaskPaint.setAlpha(0); 1939 mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 1940 1941 mCirclePaint.setColor(0); 1942 mCirclePaint.setAlpha(0); 1943 1944 mCoverPaint.setColor(0); 1945 mCoverPaint.setAlpha(0); 1946 1947 setupAnimators(); 1948 } 1949 1950 private void setupAnimators() { 1951 mFadeOutAlphaAnimator = ValueAnimator.ofInt(0, 255); 1952 mFadeOutAlphaAnimator.setDuration(100); 1953 mFadeOutAlphaAnimator.setInterpolator(Gusterpolator.INSTANCE); 1954 mFadeOutAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1955 @Override 1956 public void onAnimationUpdate(ValueAnimator animation) { 1957 mCoverPaint.setAlpha((Integer) animation.getAnimatedValue()); 1958 invalidate(); 1959 } 1960 }); 1961 mFadeOutAlphaAnimator.addListener(new AnimatorListenerAdapter() { 1962 @Override 1963 public void onAnimationStart(Animator animation) { 1964 // Sets a HW layer on the view for the animation. 1965 setLayerType(LAYER_TYPE_HARDWARE, null); 1966 } 1967 1968 @Override 1969 public void onAnimationEnd(Animator animation) { 1970 // Sets the layer type back to NONE as a workaround for b/12594617. 1971 setLayerType(LAYER_TYPE_NONE, null); 1972 } 1973 }); 1974 1975 ///////////////// 1976 1977 mRevealAlphaAnimator = ValueAnimator.ofInt(255, 0); 1978 mRevealAlphaAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS); 1979 mRevealAlphaAnimator.setInterpolator(Gusterpolator.INSTANCE); 1980 mRevealAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1981 @Override 1982 public void onAnimationUpdate(ValueAnimator animation) { 1983 int alpha = (Integer) animation.getAnimatedValue(); 1984 mCirclePaint.setAlpha(alpha); 1985 mCoverPaint.setAlpha(alpha); 1986 } 1987 }); 1988 mRevealAlphaAnimator.addListener(new AnimatorListenerAdapter() { 1989 @Override 1990 public void onAnimationStart(Animator animation) { 1991 // Sets a HW layer on the view for the animation. 1992 setLayerType(LAYER_TYPE_HARDWARE, null); 1993 } 1994 1995 @Override 1996 public void onAnimationEnd(Animator animation) { 1997 // Sets the layer type back to NONE as a workaround for b/12594617. 1998 setLayerType(LAYER_TYPE_NONE, null); 1999 } 2000 }); 2001 2002 //////////////// 2003 2004 int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX); 2005 int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY); 2006 int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge 2007 + verticalDistanceToFarEdge * verticalDistanceToFarEdge)); 2008 int startRadius = getResources().getDimensionPixelSize( 2009 R.dimen.mode_selector_icon_block_width) / 2; 2010 2011 mPeepHoleAnimator = ValueAnimator.ofFloat(startRadius, endRadius); 2012 mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS); 2013 mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE); 2014 mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 2015 @Override 2016 public void onAnimationUpdate(ValueAnimator animation) { 2017 // Modify mask by enlarging the hole 2018 mRadius = (Float) mPeepHoleAnimator.getAnimatedValue(); 2019 invalidate(); 2020 } 2021 }); 2022 mPeepHoleAnimator.addListener(new AnimatorListenerAdapter() { 2023 @Override 2024 public void onAnimationStart(Animator animation) { 2025 // Sets a HW layer on the view for the animation. 2026 setLayerType(LAYER_TYPE_HARDWARE, null); 2027 } 2028 2029 @Override 2030 public void onAnimationEnd(Animator animation) { 2031 // Sets the layer type back to NONE as a workaround for b/12594617. 2032 setLayerType(LAYER_TYPE_NONE, null); 2033 } 2034 }); 2035 2036 //////////////// 2037 int size = getContext().getResources() 2038 .getDimensionPixelSize(R.dimen.mode_selector_icon_block_width); 2039 mCircleDrawable = new TouchCircleDrawable(getContext().getResources()); 2040 mCircleDrawable.setSize(size, size); 2041 mCircleDrawable.setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 2042 @Override 2043 public void onAnimationUpdate(ValueAnimator animation) { 2044 invalidate(); 2045 } 2046 }); 2047 } 2048 2049 @Override 2050 public void setSize(int width, int height) { 2051 mWidth = width; 2052 mHeight = height; 2053 } 2054 2055 @Override 2056 public boolean onTouchEvent(MotionEvent event) { 2057 return true; 2058 } 2059 2060 @Override 2061 public void drawForeground(Canvas canvas) { 2062 // Draw the circle in clear mode 2063 if (mPeepHoleAnimator != null) { 2064 // Draw a transparent circle using clear mode 2065 canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint); 2066 canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mCirclePaint); 2067 } 2068 } 2069 2070 public void setAnimationStartingPosition(int x, int y) { 2071 mPeepHoleCenterX = x; 2072 mPeepHoleCenterY = y; 2073 } 2074 2075 public void setModeSpecificColor(int color) { 2076 mCirclePaint.setColor(color & 0x00ffffff); 2077 } 2078 2079 /** 2080 * Sets the bitmap to be drawn in the background and the drawArea to draw 2081 * the bitmap. 2082 * 2083 * @param background image to be drawn in the background 2084 * @param drawArea area to draw the background image 2085 */ 2086 public void setBackground(Bitmap background, RectF drawArea) { 2087 mBackground = background; 2088 mBackgroundDrawArea.set(drawArea); 2089 } 2090 2091 /** 2092 * Sets the overlay image to be drawn on top of the background. 2093 */ 2094 public void setBackgroundOverlay(Bitmap overlay) { 2095 mBackgroundOverlay = overlay; 2096 } 2097 2098 @Override 2099 public void drawBackground(Canvas canvas) { 2100 if (mBackground != null && mBackgroundOverlay != null) { 2101 canvas.drawBitmap(mBackground, null, mBackgroundDrawArea, null); 2102 canvas.drawPaint(mCoverPaint); 2103 canvas.drawBitmap(mBackgroundOverlay, 0, 0, null); 2104 2105 if (mCircleDrawable != null) { 2106 mCircleDrawable.draw(canvas); 2107 } 2108 } 2109 } 2110 2111 @Override 2112 public boolean shouldDrawSuper() { 2113 // No need to draw super when mBackgroundOverlay is being drawn, as 2114 // background overlay already contains what's drawn in super. 2115 return (mBackground == null || mBackgroundOverlay == null); 2116 } 2117 2118 public void startFadeoutAnimation(Animator.AnimatorListener listener, 2119 final ModeSelectorItem selectedItem, 2120 int x, int y, final int modeId) { 2121 mCoverPaint.setColor(0); 2122 mCoverPaint.setAlpha(0); 2123 2124 mCircleDrawable.setIconDrawable( 2125 selectedItem.getIcon().getIconDrawableClone(), 2126 selectedItem.getIcon().getIconDrawableSize()); 2127 mCircleDrawable.setCenter(new Point(x, y)); 2128 mCircleDrawable.setColor(selectedItem.getHighlightColor()); 2129 mCircleDrawable.setAnimatorListener(new AnimatorListenerAdapter() { 2130 @Override 2131 public void onAnimationEnd(Animator animation) { 2132 // Post mode selection runnable to the end of the message queue 2133 // so that current UI changes can finish before mode initialization 2134 // clogs up UI thread. 2135 post(new Runnable() { 2136 @Override 2137 public void run() { 2138 // Select the focused item. 2139 selectedItem.setSelected(true); 2140 onModeSelected(modeId); 2141 } 2142 }); 2143 } 2144 }); 2145 2146 // add fade out animator to a set, so we can freely add 2147 // the listener without having to worry about listener dupes 2148 AnimatorSet s = new AnimatorSet(); 2149 s.play(mFadeOutAlphaAnimator); 2150 if (listener != null) { 2151 s.addListener(listener); 2152 } 2153 mCircleDrawable.animate(); 2154 s.start(); 2155 } 2156 2157 @Override 2158 public void startAnimation(Animator.AnimatorListener listener) { 2159 if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) { 2160 return; 2161 } 2162 if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) { 2163 mPeepHoleCenterX = mWidth / 2; 2164 mPeepHoleCenterY = mHeight / 2; 2165 } 2166 2167 mCirclePaint.setAlpha(255); 2168 mCoverPaint.setAlpha(255); 2169 2170 // add peephole and reveal animators to a set, so we can 2171 // freely add the listener without having to worry about 2172 // listener dupes 2173 AnimatorSet s = new AnimatorSet(); 2174 s.play(mPeepHoleAnimator).with(mRevealAlphaAnimator); 2175 if (listener != null) { 2176 s.addListener(listener); 2177 } 2178 s.start(); 2179 } 2180 2181 @Override 2182 public void endAnimation() { 2183 } 2184 2185 @Override 2186 public boolean cancelAnimation() { 2187 if (mPeepHoleAnimator == null || !mPeepHoleAnimator.isRunning()) { 2188 return false; 2189 } else { 2190 mPeepHoleAnimator.cancel(); 2191 return true; 2192 } 2193 } 2194 } 2195} 2196