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