ModeListView.java revision 5e5734b558013e23f21902ad96d0dc2949610b90
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.AnimatorSet; 21import android.animation.ObjectAnimator; 22import android.animation.TimeInterpolator; 23import android.animation.ValueAnimator; 24import android.content.Context; 25import android.graphics.Bitmap; 26import android.graphics.Canvas; 27import android.graphics.Paint; 28import android.graphics.PorterDuff; 29import android.graphics.PorterDuffXfermode; 30import android.graphics.RectF; 31import android.os.AsyncTask; 32import android.os.SystemClock; 33import android.util.AttributeSet; 34import android.util.Log; 35import android.util.SparseArray; 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.app.CameraAppUI; 44import com.android.camera.util.CameraUtil; 45import com.android.camera.util.Gusterpolator; 46import com.android.camera.widget.AnimationEffects; 47import com.android.camera.widget.SettingsButton; 48import com.android.camera2.R; 49 50import java.util.ArrayList; 51import java.util.LinkedList; 52import java.util.List; 53 54/** 55 * ModeListView class displays all camera modes and settings in the form 56 * of a list. A swipe to the right will bring up this list. Then tapping on 57 * any of the items in the list will take the user to that corresponding mode 58 * with an animation. To dismiss this list, simply swipe left or select a mode. 59 */ 60public class ModeListView extends FrameLayout 61 implements PreviewStatusListener.PreviewAreaChangedListener, 62 ModeSelectorItem.VisibleWidthChangedListener { 63 64 private static final String TAG = "ModeListView"; 65 66 // Animation Durations 67 private static final int DEFAULT_DURATION_MS = 200; 68 private static final int FLY_IN_DURATION_MS = 0; 69 private static final int HOLD_DURATION_MS = 0; 70 private static final int FLY_OUT_DURATION_MS = 850; 71 private static final int START_DELAY_MS = 100; 72 private static final int TOTAL_DURATION_MS = FLY_IN_DURATION_MS + HOLD_DURATION_MS 73 + FLY_OUT_DURATION_MS; 74 private static final int HIDE_SHIMMY_DELAY_MS = 1000; 75 // Assumption for time since last scroll when no data point for last scroll. 76 private static final int SCROLL_INTERVAL_MS = 50; 77 // Last 20% percent of the drawer opening should be slow to ensure soft landing. 78 private static final float SLOW_ZONE_PERCENTAGE = 0.2f; 79 80 private static final float ROWS_TO_SHOW_IN_LANDSCAPE = 4.5f; 81 private static final int NO_ITEM_SELECTED = -1; 82 83 // Scrolling states 84 private static final int FULLY_HIDDEN = 0; 85 private static final int FULLY_SHOWN = 1; 86 private static final int ACCORDION_ANIMATION = 2; 87 private static final int SCROLLING = 3; 88 private static final int MODE_SELECTED = 4; 89 90 // Scrolling delay between non-focused item and focused item 91 private static final int DELAY_MS = 30; 92 // If the fling velocity exceeds this threshold, snap to full screen at a constant 93 // speed. Unit: pixel/ms. 94 private static final float VELOCITY_THRESHOLD = 2f; 95 96 /** 97 * A factor to change the UI responsiveness on a scroll. 98 * e.g. A scroll factor of 0.5 means UI will move half as fast as the finger. 99 */ 100 private static final float SCROLL_FACTOR = 0.5f; 101 // 30% transparent black background. 102 private static final int BACKGROUND_TRANSPARENTCY = (int) (0.3f * 255); 103 private static final int PREVIEW_DOWN_SAMPLE_FACTOR = 4; 104 // Threshold, below which snap back will happen. 105 private static final float SNAP_BACK_THRESHOLD_RATIO = 0.33f; 106 107 private final GestureDetector mGestureDetector; 108 private final RectF mPreviewArea = new RectF(); 109 private final RectF mUncoveredPreviewArea = new RectF(); 110 111 private long mLastScrollTime; 112 private int mListBackgroundColor; 113 private LinearLayout mListView; 114 private SettingsButton mSettingsButton; 115 private int mState = FULLY_HIDDEN; 116 private int mTotalModes; 117 private ModeSelectorItem[] mModeSelectorItems; 118 private AnimatorSet mAnimatorSet; 119 private int mFocusItem = NO_ITEM_SELECTED; 120 private ModeListAnimationEffects mCurrentEffect = null; 121 private ModeListOpenListener mModeListOpenListener; 122 private ModeListVisibilityChangedListener mVisibilityChangedListener; 123 private CameraAppUI.CameraModuleScreenShotProvider mScreenShotProvider = null; 124 private int[] mInputPixels; 125 private int[] mOutputPixels; 126 127 private boolean mAdjustPositionWhenUncoveredPreviewAreaChanges = false; 128 private View mChildViewTouched = null; 129 private MotionEvent mLastChildTouchEvent = null; 130 private int mVisibleWidth = 0; 131 132 // Width and height of this view. They get updated in onLayout() 133 // Unit for width and height are pixels. 134 private int mWidth; 135 private int mHeight; 136 private float mScrollTrendX = 0f; 137 private float mScrollTrendY = 0f; 138 private ModeSwitchListener mModeSwitchListener = null; 139 private ArrayList<Integer> mSupportedModes; 140 private final LinkedList<TimeBasedPosition> mPositionHistory 141 = new LinkedList<TimeBasedPosition>(); 142 private long mCurrentTime; 143 private float mVelocityX; // Unit: pixel/ms. 144 private final Animator.AnimatorListener mModeListAnimatorListener = 145 new Animator.AnimatorListener() { 146 private boolean mCancelled = false; 147 148 @Override 149 public void onAnimationStart(Animator animation) { 150 mCancelled = false; 151 setVisibility(VISIBLE); 152 } 153 154 @Override 155 public void onAnimationEnd(Animator animation) { 156 mAnimatorSet = null; 157 if (mCancelled) { 158 return; 159 } 160 if (mState == ACCORDION_ANIMATION || mState == FULLY_HIDDEN) { 161 resetModeSelectors(); 162 setVisibility(INVISIBLE); 163 mState = FULLY_HIDDEN; 164 } 165 } 166 167 @Override 168 public void onAnimationCancel(Animator animation) { 169 mCancelled = true; 170 } 171 172 @Override 173 public void onAnimationRepeat(Animator animation) { 174 175 } 176 }; 177 private long mLastDownTime = 0; 178 179 /** 180 * Abstract class for animation effects that are specific for mode list. 181 */ 182 private abstract class ModeListAnimationEffects extends AnimationEffects { 183 public void onWindowFocusChanged(boolean hasFocus) { 184 // Default to do nothing. 185 } 186 187 /** 188 * Specifies how the UI elements should respond when mode list opens. 189 * Range: [0f, 1f]. 0f means no change in the UI elements other than 190 * mode drawer itself (i.e. No background dimming, etc). 1f means the 191 * change in the surrounding UI elements should stay in sync with the 192 * mode drawer opening. 193 */ 194 public float getModeListOpenFactor() { 195 return 1f; 196 } 197 198 /** 199 * Sets the action (i.e. a runnable to run) at the end of the animation 200 * effects. 201 * 202 * @param runnable the action for the end of animation effects. 203 */ 204 public abstract void setAnimationEndAction(Runnable runnable); 205 } 206 207 @Override 208 public void onPreviewAreaChanged(RectF previewArea) { 209 mPreviewArea.set(previewArea); 210 } 211 212 private final CameraAppUI.UncoveredPreviewAreaSizeChangedListener 213 mUncoveredPreviewAreaSizeChangedListener = 214 new CameraAppUI.UncoveredPreviewAreaSizeChangedListener() { 215 216 @Override 217 public void uncoveredPreviewAreaChanged(RectF uncoveredPreviewArea) { 218 mUncoveredPreviewArea.set(uncoveredPreviewArea); 219 mSettingsButton.uncoveredPreviewAreaChanged(uncoveredPreviewArea); 220 if (mAdjustPositionWhenUncoveredPreviewAreaChanges) { 221 mAdjustPositionWhenUncoveredPreviewAreaChanges = false; 222 centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight()); 223 } 224 } 225 }; 226 227 public interface ModeSwitchListener { 228 public void onModeSelected(int modeIndex); 229 public int getCurrentModeIndex(); 230 public void onSettingsSelected(); 231 } 232 233 public interface ModeListOpenListener { 234 /** 235 * Mode list will open to full screen after current animation. 236 */ 237 public void onOpenFullScreen(); 238 239 /** 240 * Updates the listener with the current progress of mode drawer opening. 241 * 242 * @param progress progress of the mode drawer opening, ranging [0f, 1f] 243 * 0 means mode drawer is fully closed, 1 indicates a fully 244 * open mode drawer. 245 */ 246 public void onModeListOpenProgress(float progress); 247 248 /** 249 * Gets called when mode list is completely closed. 250 */ 251 public void onModeListClosed(); 252 } 253 254 public static abstract class ModeListVisibilityChangedListener { 255 private Boolean mCurrentVisibility = null; 256 257 /** Whether the mode list is (partially or fully) visible. */ 258 public abstract void onVisibilityChanged(boolean visible); 259 260 /** 261 * Internal method to be called by the mode list whenever a visibility 262 * even occurs. 263 * <p> 264 * Do not call {@link #onVisibilityChanged(boolean)} directly, as this 265 * is only called when the visibility has actually changed and not on 266 * each visibility event. 267 * 268 * @param visible whether the mode drawer is currently visible. 269 */ 270 private void onVisibilityEvent(boolean visible) { 271 if (mCurrentVisibility == null || mCurrentVisibility != visible) { 272 mCurrentVisibility = visible; 273 onVisibilityChanged(visible); 274 } 275 } 276 } 277 278 /** 279 * This class aims to help store time and position in pairs. 280 */ 281 private static class TimeBasedPosition { 282 private final float mPosition; 283 private final long mTimeStamp; 284 public TimeBasedPosition(float position, long time) { 285 mPosition = position; 286 mTimeStamp = time; 287 } 288 289 public float getPosition() { 290 return mPosition; 291 } 292 293 public long getTimeStamp() { 294 return mTimeStamp; 295 } 296 } 297 298 /** 299 * This is a highly customized interpolator. The purpose of having this subclass 300 * is to encapsulate intricate animation timing, so that the actual animation 301 * implementation can be re-used with other interpolators to achieve different 302 * animation effects. 303 * 304 * The accordion animation consists of three stages: 305 * 1) Animate into the screen within a pre-specified fly in duration. 306 * 2) Hold in place for a certain amount of time (Optional). 307 * 3) Animate out of the screen within the given time. 308 * 309 * The accordion animator is initialized with 3 parameter: 1) initial position, 310 * 2) how far out the view should be before flying back out, 3) end position. 311 * The interpolation output should be [0f, 0.5f] during animation between 1) 312 * to 2), and [0.5f, 1f] for flying from 2) to 3). 313 */ 314 private final TimeInterpolator mAccordionInterpolator = new TimeInterpolator() { 315 @Override 316 public float getInterpolation(float input) { 317 318 float flyInDuration = (float) FLY_OUT_DURATION_MS / (float) TOTAL_DURATION_MS; 319 float holdDuration = (float) (FLY_OUT_DURATION_MS + HOLD_DURATION_MS) 320 / (float) TOTAL_DURATION_MS; 321 if (input == 0) { 322 return 0; 323 } else if (input < flyInDuration) { 324 // Stage 1, project result to [0f, 0.5f] 325 input /= flyInDuration; 326 float result = Gusterpolator.INSTANCE.getInterpolation(input); 327 return result * 0.5f; 328 } else if (input < holdDuration) { 329 // Stage 2 330 return 0.5f; 331 } else { 332 // Stage 3, project result to [0.5f, 1f] 333 input -= holdDuration; 334 input /= (1 - holdDuration); 335 float result = Gusterpolator.INSTANCE.getInterpolation(input); 336 return 0.5f + result * 0.5f; 337 } 338 } 339 }; 340 341 /** 342 * The listener that is used to notify when gestures occur. 343 * Here we only listen to a subset of gestures. 344 */ 345 private final GestureDetector.OnGestureListener mOnGestureListener 346 = new GestureDetector.SimpleOnGestureListener(){ 347 @Override 348 public boolean onScroll(MotionEvent e1, MotionEvent e2, 349 float distanceX, float distanceY) { 350 351 if (mState == ACCORDION_ANIMATION) { 352 if (mCurrentEffect != null) { 353 // Scroll happens during accordion animation. 354 mCurrentEffect.cancelAnimation(); 355 } 356 } else if (mState == FULLY_HIDDEN) { 357 resetModeSelectors(); 358 setVisibility(VISIBLE); 359 } 360 mState = SCROLLING; 361 // Scroll based on the scrolling distance on the currently focused 362 // item. 363 scroll(mFocusItem, distanceX * SCROLL_FACTOR, distanceY * SCROLL_FACTOR); 364 mLastScrollTime = System.currentTimeMillis(); 365 return true; 366 } 367 368 @Override 369 public boolean onSingleTapUp(MotionEvent ev) { 370 if (mState != FULLY_SHOWN) { 371 // Only allows tap to choose mode when the list is fully shown 372 return false; 373 } 374 375 // If the tap is not inside the mode drawer area, snap back. 376 if(!isTouchInsideList(ev)) { 377 snapBack(true); 378 return false; 379 } 380 return true; 381 } 382 383 @Override 384 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 385 // Cache velocity in the unit pixel/ms. 386 mVelocityX = velocityX / 1000f * SCROLL_FACTOR; 387 return true; 388 } 389 }; 390 391 /** 392 * Gets called when a mode item in the mode drawer is clicked. 393 * 394 * @param selectedItem the item being clicked 395 */ 396 private void onItemSelected(ModeSelectorItem selectedItem) { 397 398 final int modeId = selectedItem.getModeId(); 399 // Un-highlight all the modes. 400 for (int i = 0; i < mModeSelectorItems.length; i++) { 401 mModeSelectorItems[i].setHighlighted(false); 402 mModeSelectorItems[i].setSelected(false); 403 } 404 // Select the focused item. 405 selectedItem.setSelected(true); 406 mState = MODE_SELECTED; 407 PeepholeAnimationEffect effect = new PeepholeAnimationEffect(); 408 effect.setSize(mWidth, mHeight); 409 effect.setAnimationEndAction(new Runnable() { 410 @Override 411 public void run() { 412 setVisibility(INVISIBLE); 413 snapBack(false); 414 } 415 }); 416 417 // Calculate the position of the icon in the selected item, and 418 // start animation from that position. 419 int[] location = new int[2]; 420 // Gets icon's center position in relative to the window. 421 selectedItem.getIconCenterLocationInWindow(location); 422 int iconX = location[0]; 423 int iconY = location[1]; 424 // Gets current view's top left position relative to the window. 425 getLocationInWindow(location); 426 // Calculate icon location relative to this view 427 iconX -= location[0]; 428 iconY -= location[1]; 429 430 effect.setAnimationStartingPosition(iconX, iconY); 431 if (mScreenShotProvider != null) { 432 effect.setBackground(mScreenShotProvider 433 .getPreviewFrame(PREVIEW_DOWN_SAMPLE_FACTOR), mPreviewArea); 434 effect.setBackgroundOverlay(mScreenShotProvider.getPreviewOverlayAndControls()); 435 } 436 mCurrentEffect = effect; 437 invalidate(); 438 439 // Post mode selection runnable to the end of the message queue 440 // so that current UI changes can finish before mode initialization 441 // clogs up UI thread. 442 post(new Runnable() { 443 @Override 444 public void run() { 445 onModeSelected(modeId); 446 } 447 }); 448 } 449 450 /** 451 * Checks whether a touch event is inside of the bounds of the mode list. 452 * 453 * @param ev touch event to be checked 454 * @return whether the touch is inside the bounds of the mode list 455 */ 456 private boolean isTouchInsideList(MotionEvent ev) { 457 // Ignore the tap if it happens outside of the mode list linear layout. 458 float x = ev.getX() - mListView.getX(); 459 float y = ev.getY() - mListView.getY(); 460 if (x < 0 || x > mListView.getWidth() || y < 0 || y > mListView.getHeight()) { 461 return false; 462 } 463 return true; 464 } 465 466 public ModeListView(Context context, AttributeSet attrs) { 467 super(context, attrs); 468 mGestureDetector = new GestureDetector(context, mOnGestureListener); 469 mListBackgroundColor = getResources().getColor(R.color.mode_list_background); 470 } 471 472 public CameraAppUI.UncoveredPreviewAreaSizeChangedListener 473 getUncoveredPreviewAreaSizeChangedListener() { 474 return mUncoveredPreviewAreaSizeChangedListener; 475 } 476 477 /** 478 * Sets the alpha on the list background. This is called whenever the list 479 * is scrolling or animating, so that background can adjust its dimness. 480 * 481 * @param alpha new alpha to be applied on list background color 482 */ 483 private void setBackgroundAlpha(int alpha) { 484 // Make sure alpha is valid. 485 alpha = alpha & 0xFF; 486 // Change alpha on the background color. 487 mListBackgroundColor = mListBackgroundColor & 0xFFFFFF; 488 mListBackgroundColor = mListBackgroundColor | (alpha << 24); 489 // Set new color to list background. 490 setBackgroundColor(mListBackgroundColor); 491 } 492 493 /** 494 * Initialize mode list with a list of indices of supported modes. 495 * 496 * @param modeIndexList a list of indices of supported modes 497 */ 498 public void init(List<Integer> modeIndexList) { 499 int[] modeSequence = getResources() 500 .getIntArray(R.array.camera_modes_in_nav_drawer_if_supported); 501 int[] visibleModes = getResources() 502 .getIntArray(R.array.camera_modes_always_visible); 503 504 // Mark the supported modes in a boolean array to preserve the 505 // sequence of the modes 506 SparseArray<Boolean> modeIsSupported = new SparseArray<Boolean>(); 507 for (int i = 0; i < modeIndexList.size(); i++) { 508 int mode = modeIndexList.get(i); 509 modeIsSupported.put(mode, true); 510 } 511 for (int i = 0; i < visibleModes.length; i++) { 512 int mode = visibleModes[i]; 513 modeIsSupported.put(mode, true); 514 } 515 516 // Put the indices of supported modes into an array preserving their 517 // display order. 518 mSupportedModes = new ArrayList<Integer>(); 519 for (int i = 0; i < modeSequence.length; i++) { 520 int mode = modeSequence[i]; 521 if (modeIsSupported.get(mode, false)) { 522 mSupportedModes.add(mode); 523 } 524 } 525 mTotalModes = mSupportedModes.size(); 526 initializeModeSelectorItems(); 527 mSettingsButton = (SettingsButton) findViewById(R.id.settings_button); 528 mSettingsButton.setOnClickListener(new OnClickListener() { 529 @Override 530 public void onClick(View v) { 531 // Post this callback to make sure current user interaction has 532 // been reflected in the UI. Specifically, the pressed state gets 533 // unset after click happens. In order to ensure the pressed state 534 // gets unset in UI before getting in the low frame rate settings 535 // activity launch stage, the settings selected callback is posted. 536 post(new Runnable() { 537 @Override 538 public void run() { 539 mModeSwitchListener.onSettingsSelected(); 540 } 541 }); 542 } 543 }); 544 // The mode list is initialized to be all the way closed. 545 onModeListOpenRatioUpdate(0); 546 } 547 548 /** 549 * Sets the screen shot provider for getting a preview frame and a bitmap 550 * of the controls and overlay. 551 */ 552 public void setCameraModuleScreenShotProvider( 553 CameraAppUI.CameraModuleScreenShotProvider provider) { 554 mScreenShotProvider = provider; 555 } 556 557 private void initializeModeSelectorItems() { 558 mModeSelectorItems = new ModeSelectorItem[mTotalModes]; 559 // Inflate the mode selector items and add them to a linear layout 560 LayoutInflater inflater = (LayoutInflater) getContext() 561 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 562 mListView = (LinearLayout) findViewById(R.id.mode_list); 563 for (int i = 0; i < mTotalModes; i++) { 564 final ModeSelectorItem selectorItem = 565 (ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null); 566 mListView.addView(selectorItem); 567 // Sets the top padding of the top item to 0. 568 if (i == 0) { 569 selectorItem.setPadding(selectorItem.getPaddingLeft(), 0, 570 selectorItem.getPaddingRight(), selectorItem.getPaddingBottom()); 571 } 572 // Sets the bottom padding of the bottom item to 0. 573 if (i == mTotalModes - 1) { 574 selectorItem.setPadding(selectorItem.getPaddingLeft(), selectorItem.getPaddingTop(), 575 selectorItem.getPaddingRight(), 0); 576 } 577 578 int modeId = getModeIndex(i); 579 selectorItem.setHighlightColor(getResources() 580 .getColor(CameraUtil.getCameraThemeColorId(modeId, getContext()))); 581 582 // Set image 583 selectorItem.setImageResource(CameraUtil.getCameraModeIconResId(modeId, getContext())); 584 585 // Set text 586 selectorItem.setText(CameraUtil.getCameraModeText(modeId, getContext())); 587 588 // Set content description (for a11y) 589 selectorItem.setContentDescription(CameraUtil 590 .getCameraModeContentDescription(modeId, getContext())); 591 selectorItem.setModeId(modeId); 592 selectorItem.setOnClickListener(new OnClickListener() { 593 @Override 594 public void onClick(View v) { 595 onItemSelected(selectorItem); 596 } 597 }); 598 599 mModeSelectorItems[i] = selectorItem; 600 } 601 // During drawer opening/closing, we change the visible width of the mode 602 // items in sequence, so we listen to the last item's visible width change 603 // for a good timing to do corresponding UI adjustments. 604 mModeSelectorItems[mTotalModes - 1].setVisibleWidthChangedListener(this); 605 resetModeSelectors(); 606 } 607 608 /** 609 * Maps between the UI mode selector index to the actual mode id. 610 * 611 * @param modeSelectorIndex the index of the UI item 612 * @return the index of the corresponding camera mode 613 */ 614 private int getModeIndex(int modeSelectorIndex) { 615 if (modeSelectorIndex < mTotalModes && modeSelectorIndex >= 0) { 616 return mSupportedModes.get(modeSelectorIndex); 617 } 618 Log.e(TAG, "Invalid mode selector index: " + modeSelectorIndex + ", total modes: " 619 + mTotalModes); 620 return getResources().getInteger(R.integer.camera_mode_photo); 621 } 622 623 /** Notify ModeSwitchListener, if any, of the mode change. */ 624 private void onModeSelected(int modeIndex) { 625 if (mModeSwitchListener != null) { 626 mModeSwitchListener.onModeSelected(modeIndex); 627 } 628 } 629 630 /** 631 * Sets a listener that listens to receive mode switch event. 632 * 633 * @param listener a listener that gets notified when mode changes. 634 */ 635 public void setModeSwitchListener(ModeSwitchListener listener) { 636 mModeSwitchListener = listener; 637 } 638 639 /** 640 * Sets a listener that gets notified when the mode list is open full screen. 641 * 642 * @param listener a listener that listens to mode list open events 643 */ 644 public void setModeListOpenListener(ModeListOpenListener listener) { 645 mModeListOpenListener = listener; 646 } 647 648 /** 649 * Sets or replaces a listener that is called when the visibility of the 650 * mode list changed. 651 */ 652 public void setVisibilityChangedListener(ModeListVisibilityChangedListener listener) { 653 mVisibilityChangedListener = listener; 654 } 655 656 @Override 657 public boolean onTouchEvent(MotionEvent ev) { 658 if (mCurrentEffect != null && mCurrentEffect.onTouchEvent(ev)) { 659 return true; 660 } 661 662 if (mState == ACCORDION_ANIMATION && MotionEvent.ACTION_DOWN == ev.getActionMasked()) { 663 // If shimmy is on-going, reject the first down event, so that it can be handled 664 // by the view underneath. If a swipe is detected, the same series of touch will 665 // re-enter this function, in which case we will consume the touch events. 666 if (mLastDownTime != ev.getDownTime()) { 667 mLastDownTime = ev.getDownTime(); 668 return false; 669 } 670 } 671 super.onTouchEvent(ev); 672 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 673 mVelocityX = 0; 674 if (mState == FULLY_SHOWN) { 675 mFocusItem = NO_ITEM_SELECTED; 676 setSwipeMode(false); 677 // If the down event happens inside the mode list, find out which 678 // mode item is being touched and forward all the subsequent touch 679 // events to that mode item for its pressed state and click handling. 680 if (isTouchInsideList(ev)) { 681 mChildViewTouched = mModeSelectorItems[getFocusItem(ev.getX(), ev.getY())]; 682 } 683 684 } else { 685 mFocusItem = getFocusItem(ev.getX(), ev.getY()); 686 setSwipeMode(true); 687 } 688 } else if (mState == ACCORDION_ANIMATION) { 689 // This is a swipe during accordion animation 690 mFocusItem = getFocusItem(ev.getX(), ev.getY()); 691 setSwipeMode(true); 692 693 } 694 forwardTouchEventToChild(ev); 695 // Pass all touch events to gesture detector for gesture handling. 696 mGestureDetector.onTouchEvent(ev); 697 if (ev.getActionMasked() == MotionEvent.ACTION_UP || 698 ev.getActionMasked() == MotionEvent.ACTION_CANCEL) { 699 snap(); 700 mFocusItem = NO_ITEM_SELECTED; 701 // Reset the touch forward recipient at the end of a touch event series, 702 // i.e. when an up or a cancel event is received. 703 mChildViewTouched = null; 704 } 705 return true; 706 } 707 708 /** 709 * Forward touch events to a recipient child view. Before feeding the motion 710 * event into the child view, the event needs to be converted in child view's 711 * coordinates. 712 */ 713 private void forwardTouchEventToChild(MotionEvent ev) { 714 if (mChildViewTouched != null) { 715 float x = ev.getX() - mListView.getX(); 716 float y = ev.getY() - mListView.getY(); 717 x -= mChildViewTouched.getLeft(); 718 y -= mChildViewTouched.getTop(); 719 720 mLastChildTouchEvent = MotionEvent.obtain(ev); 721 mLastChildTouchEvent.setLocation(x, y); 722 mChildViewTouched.onTouchEvent(mLastChildTouchEvent); 723 } 724 } 725 726 /** 727 * Sets the swipe mode to indicate whether this is a swiping in 728 * or out, and therefore we can have different animations. 729 * 730 * @param swipeIn indicates whether the swipe should reveal/hide the list. 731 */ 732 private void setSwipeMode(boolean swipeIn) { 733 for (int i = 0 ; i < mModeSelectorItems.length; i++) { 734 mModeSelectorItems[i].onSwipeModeChanged(swipeIn); 735 } 736 } 737 738 @Override 739 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 740 super.onLayout(changed, left, top, right, bottom); 741 mWidth = right - left; 742 mHeight = bottom - top - getPaddingTop() - getPaddingBottom(); 743 if (mCurrentEffect != null) { 744 mCurrentEffect.setSize(mWidth, mHeight); 745 } 746 } 747 748 /** 749 * Here we calculate the children size based on the orientation, change 750 * their layout parameters if needed before propagating onMeasure call 751 * to the children, so the newly changed params will take effect in this 752 * pass. 753 * 754 * @param widthMeasureSpec Horizontal space requirements as imposed by the 755 * parent 756 * @param heightMeasureSpec Vertical space requirements as imposed by the 757 * parent 758 */ 759 @Override 760 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 761 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 762 centerModeDrawerInUncoveredPreview(MeasureSpec.getSize(widthMeasureSpec), 763 MeasureSpec.getSize(heightMeasureSpec)); 764 } 765 766 @Override 767 public void draw(Canvas canvas) { 768 if (mCurrentEffect != null) { 769 mCurrentEffect.drawBackground(canvas); 770 super.draw(canvas); 771 mCurrentEffect.drawForeground(canvas); 772 } else { 773 super.draw(canvas); 774 } 775 } 776 777 /** 778 * This shows the mode switcher and starts the accordion animation with a delay. 779 * If the view does not currently have focus, (e.g. There are popups on top of 780 * it.) start the delayed accordion animation when it gains focus. Otherwise, 781 * start the animation with a delay right away. 782 */ 783 public void showModeSwitcherHint() { 784 if (mState != FULLY_HIDDEN) { 785 return; 786 } 787 mState = ACCORDION_ANIMATION; 788 mCurrentEffect = new ShimmyAnimationEffects(); 789 mCurrentEffect.startAnimation(); 790 } 791 792 /** 793 * Resets the visible width of all the mode selectors to 0. 794 */ 795 private void resetModeSelectors() { 796 for (int i = 0; i < mModeSelectorItems.length; i++) { 797 mModeSelectorItems[i].setVisibleWidth(0); 798 } 799 } 800 801 private boolean isRunningAccordionAnimation() { 802 return mAnimatorSet != null && mAnimatorSet.isRunning(); 803 } 804 805 /** 806 * Calculate the mode selector item in the list that is at position (x, y). 807 * If the position is above the top item or below the bottom item, return 808 * the top item or bottom item respectively. 809 * 810 * @param x horizontal position 811 * @param y vertical position 812 * @return index of the item that is at position (x, y) 813 */ 814 private int getFocusItem(float x, float y) { 815 // Convert coordinates into child view's coordinates. 816 x -= mListView.getX(); 817 y -= mListView.getY(); 818 819 for (int i = 0; i < mModeSelectorItems.length; i++) { 820 if (y <= mModeSelectorItems[i].getBottom()) { 821 return i; 822 } 823 } 824 return mModeSelectorItems.length - 1; 825 } 826 827 @Override 828 public void onWindowFocusChanged(boolean hasFocus) { 829 super.onWindowFocusChanged(hasFocus); 830 if (mCurrentEffect != null) { 831 mCurrentEffect.onWindowFocusChanged(hasFocus); 832 } 833 } 834 835 @Override 836 public void onVisibilityChanged(View v, int visibility) { 837 super.onVisibilityChanged(v, visibility); 838 if (visibility == VISIBLE) { 839 centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight()); 840 // Highlight current module 841 if (mModeSwitchListener != null) { 842 int modeId = mModeSwitchListener.getCurrentModeIndex(); 843 int parentMode = CameraUtil.getCameraModeParentModeId(modeId, getContext()); 844 // Find parent mode in the nav drawer. 845 for (int i = 0; i < mSupportedModes.size(); i++) { 846 if (mSupportedModes.get(i) == parentMode) { 847 mModeSelectorItems[i].setSelected(true); 848 } 849 } 850 } 851 } else { 852 if (mModeSelectorItems != null) { 853 // When becoming invisible/gone after initializing mode selector items. 854 for (int i = 0; i < mModeSelectorItems.length; i++) { 855 mModeSelectorItems[i].setHighlighted(false); 856 mModeSelectorItems[i].setSelected(false); 857 } 858 } 859 if (mModeListOpenListener != null) { 860 mModeListOpenListener.onModeListClosed(); 861 } 862 } 863 if (mVisibilityChangedListener != null) { 864 mVisibilityChangedListener.onVisibilityEvent(getVisibility() == VISIBLE); 865 } 866 } 867 868 /** 869 * Center mode drawer in the portion of camera preview that is not covered by 870 * bottom bar. 871 */ 872 // TODO: Combine SettingsButton logic into here if UX design does not change 873 // for another week. 874 private void centerModeDrawerInUncoveredPreview(int measuredWidth, int measuredHeight) { 875 876 // Assuming the preview is centered in the space aside from bottom bar. 877 float previewAreaWidth = mUncoveredPreviewArea.right + mUncoveredPreviewArea.left; 878 float previewAreaHeight = mUncoveredPreviewArea.top + mUncoveredPreviewArea.bottom; 879 if (measuredWidth > measuredHeight && previewAreaWidth < previewAreaHeight 880 || measuredWidth < measuredHeight && previewAreaWidth > previewAreaHeight) { 881 // Cached preview area is stale, update mode drawer position on next 882 // layout pass. 883 mAdjustPositionWhenUncoveredPreviewAreaChanges = true; 884 } else { 885 // Align left: 886 mListView.setTranslationX(mUncoveredPreviewArea.left); 887 // Align center vertical: 888 mListView.setTranslationY(mUncoveredPreviewArea.centerY() 889 - mListView.getMeasuredHeight() / 2); 890 } 891 } 892 893 private void scroll(int itemId, float deltaX, float deltaY) { 894 // Scrolling trend on X and Y axis, to track the trend by biasing 895 // towards latest touch events. 896 mScrollTrendX = mScrollTrendX * 0.3f + deltaX * 0.7f; 897 mScrollTrendY = mScrollTrendY * 0.3f + deltaY * 0.7f; 898 899 // TODO: Change how the curve is calculated below when UX finalize their design. 900 mCurrentTime = SystemClock.uptimeMillis(); 901 float longestWidth; 902 if (itemId != NO_ITEM_SELECTED) { 903 longestWidth = mModeSelectorItems[itemId].getVisibleWidth(); 904 } else { 905 longestWidth = mModeSelectorItems[0].getVisibleWidth(); 906 } 907 float newPosition = longestWidth - deltaX; 908 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth(); 909 newPosition = Math.min(newPosition, getMaxMovementBasedOnPosition((int) longestWidth, 910 maxVisibleWidth)); 911 newPosition = Math.max(newPosition, 0); 912 insertNewPosition(newPosition, mCurrentTime); 913 914 for (int i = 0; i < mModeSelectorItems.length; i++) { 915 mModeSelectorItems[i].setVisibleWidth(calculateVisibleWidthForItem(i, 916 (int) newPosition)); 917 } 918 } 919 920 /** 921 * Calculate the width of a specified item based on its position relative to 922 * the item with longest width. 923 */ 924 private int calculateVisibleWidthForItem(int itemId, int longestWidth) { 925 if (itemId == mFocusItem || mFocusItem == NO_ITEM_SELECTED) { 926 return longestWidth; 927 } 928 929 int delay = Math.abs(itemId - mFocusItem) * DELAY_MS; 930 return (int) getPosition(mCurrentTime - delay, 931 mModeSelectorItems[itemId].getVisibleWidth()); 932 } 933 934 /** 935 * Insert new position and time stamp into the history position list, and 936 * remove stale position items. 937 * 938 * @param position latest position of the focus item 939 * @param time current time in milliseconds 940 */ 941 private void insertNewPosition(float position, long time) { 942 // TODO: Consider re-using stale position objects rather than 943 // always creating new position objects. 944 mPositionHistory.add(new TimeBasedPosition(position, time)); 945 946 // Positions that are from too long ago will not be of any use for 947 // future position interpolation. So we need to remove those positions 948 // from the list. 949 long timeCutoff = time - (mTotalModes - 1) * DELAY_MS; 950 while (mPositionHistory.size() > 0) { 951 // Remove all the position items that are prior to the cutoff time. 952 TimeBasedPosition historyPosition = mPositionHistory.getFirst(); 953 if (historyPosition.getTimeStamp() < timeCutoff) { 954 mPositionHistory.removeFirst(); 955 } else { 956 break; 957 } 958 } 959 } 960 961 /** 962 * Gets the interpolated position at the specified time. This involves going 963 * through the recorded positions until a {@link TimeBasedPosition} is found 964 * such that the position the recorded before the given time, and the 965 * {@link TimeBasedPosition} after that is recorded no earlier than the given 966 * time. These two positions are then interpolated to get the position at the 967 * specified time. 968 */ 969 private float getPosition(long time, float currentPosition) { 970 int i; 971 for (i = 0; i < mPositionHistory.size(); i++) { 972 TimeBasedPosition historyPosition = mPositionHistory.get(i); 973 if (historyPosition.getTimeStamp() > time) { 974 // Found the winner. Now interpolate between position i and position i - 1 975 if (i == 0) { 976 // Slowly approaching to the destination if there isn't enough data points 977 float weight = 0.2f; 978 return historyPosition.getPosition() * weight + (1f - weight) * currentPosition; 979 } else { 980 TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1); 981 // Start interpolation 982 float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) / 983 (float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp()); 984 float position = fraction * (historyPosition.getPosition() 985 - prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition(); 986 return position; 987 } 988 } 989 } 990 // It should never get here. 991 Log.e(TAG, "Invalid time input for getPosition(). time: " + time); 992 if (mPositionHistory.size() == 0) { 993 Log.e(TAG, "TimeBasedPosition history size is 0"); 994 } else { 995 Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp() 996 + " , last position recorded at " + mPositionHistory.getLast().getTimeStamp()); 997 } 998 assert (i < mPositionHistory.size()); 999 return i; 1000 } 1001 1002 private void reset() { 1003 resetModeSelectors(); 1004 mScrollTrendX = 0f; 1005 mScrollTrendY = 0f; 1006 mCurrentEffect = null; 1007 setVisibility(INVISIBLE); 1008 } 1009 1010 /** 1011 * When visible width of list is changed, the background of the list needs 1012 * to darken/lighten correspondingly. 1013 */ 1014 public void onVisibleWidthChanged(int visibleWidth) { 1015 mVisibleWidth = visibleWidth; 1016 float factor = 1f; 1017 if (mCurrentEffect != null) { 1018 factor = mCurrentEffect.getModeListOpenFactor(); 1019 } 1020 1021 // When the longest mode item is entirely shown (across the screen), the 1022 // background should be 50% transparent. 1023 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth(); 1024 visibleWidth = Math.min(maxVisibleWidth, visibleWidth); 1025 if (visibleWidth != maxVisibleWidth) { 1026 // No longer full screen. 1027 cancelForwardingTouchEvent(); 1028 } 1029 float openRatio = (float) visibleWidth / maxVisibleWidth; 1030 onModeListOpenRatioUpdate(openRatio * factor); 1031 } 1032 1033 /** 1034 * Gets called when UI elements such as background and gear icon need to adjust 1035 * their appearance based on the percentage of the mode list opening. 1036 * 1037 * @param openRatio percentage of the mode list opening, ranging [0f, 1f] 1038 */ 1039 private void onModeListOpenRatioUpdate(float openRatio) { 1040 for (int i = 0; i < mModeSelectorItems.length; i++) { 1041 mModeSelectorItems[i].setTextAlpha(openRatio); 1042 } 1043 setBackgroundAlpha((int) (BACKGROUND_TRANSPARENTCY * openRatio)); 1044 if (mModeListOpenListener != null) { 1045 mModeListOpenListener.onModeListOpenProgress(openRatio); 1046 } 1047 if (mSettingsButton != null) { 1048 mSettingsButton.setAlpha(openRatio); 1049 } 1050 } 1051 1052 /** 1053 * Cancels the touch event forwarding by sending a cancel event to the recipient 1054 * view and resetting the touch forward recipient to ensure no more events 1055 * can be forwarded in the current series of the touch events. 1056 */ 1057 private void cancelForwardingTouchEvent() { 1058 if (mChildViewTouched != null) { 1059 mLastChildTouchEvent.setAction(MotionEvent.ACTION_CANCEL); 1060 mChildViewTouched.onTouchEvent(mLastChildTouchEvent); 1061 mChildViewTouched = null; 1062 } 1063 } 1064 1065 @Override 1066 public void onWindowVisibilityChanged(int visibility) { 1067 super.onWindowVisibilityChanged(visibility); 1068 if (visibility != VISIBLE) { 1069 // Reset mode list if the window is no longer visible. 1070 reset(); 1071 mState = FULLY_HIDDEN; 1072 } 1073 } 1074 1075 /** 1076 * The list view should either snap back or snap to full screen after a gesture. 1077 * This function is called when an up or cancel event is received, and then based 1078 * on the current position of the list and the gesture we can decide which way 1079 * to snap. 1080 */ 1081 private void snap() { 1082 if (mState == SCROLLING) { 1083 int itemId = Math.max(0, mFocusItem); 1084 if (mModeSelectorItems[itemId].getVisibleWidth() 1085 < mModeSelectorItems[itemId].getMaxVisibleWidth() * SNAP_BACK_THRESHOLD_RATIO) { 1086 snapBack(); 1087 } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) { 1088 snapBack(); 1089 } else { 1090 snapToFullScreen(); 1091 } 1092 } 1093 } 1094 1095 /** 1096 * Snaps back out of the screen. 1097 * 1098 * @param withAnimation whether snapping back should be animated 1099 */ 1100 public void snapBack(boolean withAnimation) { 1101 if (withAnimation) { 1102 if (mVelocityX > -VELOCITY_THRESHOLD * SCROLL_FACTOR) { 1103 animateListToWidth(0); 1104 } else { 1105 animateListToWidthAtVelocity(mVelocityX, 0); 1106 } 1107 mState = FULLY_HIDDEN; 1108 } else { 1109 setVisibility(INVISIBLE); 1110 resetModeSelectors(); 1111 mState = FULLY_HIDDEN; 1112 } 1113 } 1114 1115 /** 1116 * Snaps the mode list back out with animation. 1117 */ 1118 private void snapBack() { 1119 snapBack(true); 1120 } 1121 1122 private void snapToFullScreen() { 1123 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem; 1124 int fullWidth = mModeSelectorItems[focusItem].getMaxVisibleWidth(); 1125 if (mVelocityX <= VELOCITY_THRESHOLD) { 1126 animateListToWidth(fullWidth); 1127 } else { 1128 // If the fling velocity exceeds this threshold, snap to full screen 1129 // at a constant speed. 1130 animateListToWidthAtVelocity(VELOCITY_THRESHOLD, fullWidth); 1131 } 1132 mState = FULLY_SHOWN; 1133 if (mModeListOpenListener != null) { 1134 mModeListOpenListener.onOpenFullScreen(); 1135 } 1136 } 1137 1138 /** 1139 * Overloaded function to provide a simple way to start animation. Animation 1140 * will use default duration, and a value of <code>null</code> for interpolator 1141 * means linear interpolation will be used. 1142 * 1143 * @param width a set of values that the animation will animate between over time 1144 */ 1145 private void animateListToWidth(int... width) { 1146 animateListToWidth(0, DEFAULT_DURATION_MS, null, width); 1147 } 1148 1149 /** 1150 * Animate the mode list between the given set of visible width. 1151 * 1152 * @param delay start delay between consecutive mode item. If delay < 0, the 1153 * leader in the animation will be the bottom item. 1154 * @param duration duration for the animation of each mode item 1155 * @param interpolator interpolator to be used by the animation 1156 * @param width a set of values that the animation will animate between over time 1157 */ 1158 private Animator animateListToWidth(int delay, int duration, 1159 TimeInterpolator interpolator, int... width) { 1160 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 1161 mAnimatorSet.end(); 1162 } 1163 1164 ArrayList<Animator> animators = new ArrayList<Animator>(); 1165 boolean animateModeItemsInOrder = true; 1166 if (delay < 0) { 1167 animateModeItemsInOrder = false; 1168 delay *= -1; 1169 } 1170 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem; 1171 for (int i = 0; i < mTotalModes; i++) { 1172 ObjectAnimator animator; 1173 if (animateModeItemsInOrder) { 1174 animator = ObjectAnimator.ofInt(mModeSelectorItems[i], 1175 "visibleWidth", width); 1176 } else { 1177 animator = ObjectAnimator.ofInt(mModeSelectorItems[mTotalModes - 1 -i], 1178 "visibleWidth", width); 1179 } 1180 animator.setDuration(duration); 1181 animator.setStartDelay(i * delay); 1182 animators.add(animator); 1183 } 1184 1185 mAnimatorSet = new AnimatorSet(); 1186 mAnimatorSet.playTogether(animators); 1187 mAnimatorSet.setInterpolator(interpolator); 1188 mAnimatorSet.addListener(mModeListAnimatorListener); 1189 mAnimatorSet.start(); 1190 1191 return mAnimatorSet; 1192 } 1193 1194 /** 1195 * Animate the mode list to the given width at a constant velocity. 1196 * 1197 * @param velocity the velocity that animation will be at 1198 * @param width final width of the list 1199 */ 1200 private void animateListToWidthAtVelocity(float velocity, int width) { 1201 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 1202 mAnimatorSet.end(); 1203 } 1204 1205 ArrayList<Animator> animators = new ArrayList<Animator>(); 1206 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem; 1207 for (int i = 0; i < mTotalModes; i++) { 1208 ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i], 1209 "visibleWidth", width); 1210 int duration = (int) (width / velocity); 1211 animator.setDuration(duration); 1212 animators.add(animator); 1213 } 1214 1215 mAnimatorSet = new AnimatorSet(); 1216 mAnimatorSet.playTogether(animators); 1217 mAnimatorSet.setInterpolator(null); 1218 mAnimatorSet.addListener(mModeListAnimatorListener); 1219 mAnimatorSet.start(); 1220 } 1221 1222 /** 1223 * Called when the back key is pressed. 1224 * 1225 * @return Whether the UI responded to the key event. 1226 */ 1227 public boolean onBackPressed() { 1228 if (mState == FULLY_SHOWN) { 1229 snapBack(); 1230 return true; 1231 } else { 1232 return false; 1233 } 1234 } 1235 1236 public void startModeSelectionAnimation() { 1237 if (mState != MODE_SELECTED || mCurrentEffect == null) { 1238 setVisibility(INVISIBLE); 1239 snapBack(false); 1240 mCurrentEffect = null; 1241 } else { 1242 mCurrentEffect.startAnimation(); 1243 } 1244 1245 } 1246 1247 public float getMaxMovementBasedOnPosition(int lastVisibleWidth, int maxWidth) { 1248 int timeElapsed = (int) (System.currentTimeMillis() - mLastScrollTime); 1249 if (timeElapsed > SCROLL_INTERVAL_MS) { 1250 timeElapsed = SCROLL_INTERVAL_MS; 1251 } 1252 float position; 1253 int slowZone = (int) (maxWidth * SLOW_ZONE_PERCENTAGE); 1254 if (lastVisibleWidth < (maxWidth - slowZone)) { 1255 position = VELOCITY_THRESHOLD * (float) timeElapsed + lastVisibleWidth; 1256 } else { 1257 float percentageIntoSlowZone = (lastVisibleWidth - (maxWidth - slowZone)) / slowZone; 1258 float velocity = (1 - percentageIntoSlowZone) * VELOCITY_THRESHOLD; 1259 position = velocity * (float) timeElapsed + lastVisibleWidth; 1260 } 1261 position = Math.min(maxWidth, position); 1262 return position; 1263 } 1264 1265 private class PeepholeAnimationEffect extends ModeListAnimationEffects { 1266 1267 private final static int UNSET = -1; 1268 private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 300; 1269 1270 private final Paint mMaskPaint = new Paint(); 1271 private final Paint mBackgroundPaint = new Paint(); 1272 private final RectF mBackgroundDrawArea = new RectF(); 1273 1274 private int mWidth; 1275 private int mHeight; 1276 private int mPeepHoleCenterX = UNSET; 1277 private int mPeepHoleCenterY = UNSET; 1278 private float mRadius = 0f; 1279 private ValueAnimator mPeepHoleAnimator; 1280 private Runnable mEndAction; 1281 private Bitmap mBackground; 1282 private Bitmap mBlurredBackground; 1283 private Bitmap mBackgroundOverlay; 1284 1285 public PeepholeAnimationEffect() { 1286 mMaskPaint.setAlpha(0); 1287 mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 1288 } 1289 1290 @Override 1291 public void setSize(int width, int height) { 1292 mWidth = width; 1293 mHeight = height; 1294 } 1295 1296 @Override 1297 public boolean onTouchEvent(MotionEvent event) { 1298 return true; 1299 } 1300 1301 @Override 1302 public void drawForeground(Canvas canvas) { 1303 // Draw the circle in clear mode 1304 if (mPeepHoleAnimator != null) { 1305 // Draw a transparent circle using clear mode 1306 canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint); 1307 } 1308 } 1309 1310 public void setAnimationStartingPosition(int x, int y) { 1311 mPeepHoleCenterX = x; 1312 mPeepHoleCenterY = y; 1313 } 1314 1315 /** 1316 * Sets the bitmap to be drawn in the background and the drawArea to draw 1317 * the bitmap. In the meantime, start processing the image in a background 1318 * thread to get a blurred background image. 1319 * 1320 * @param background image to be drawn in the background 1321 * @param drawArea area to draw the background image 1322 */ 1323 public void setBackground(Bitmap background, RectF drawArea) { 1324 mBackground = background; 1325 mBackgroundDrawArea.set(drawArea); 1326 new BlurTask().execute(Bitmap.createScaledBitmap(background, background.getWidth(), 1327 background.getHeight(), true)); 1328 } 1329 1330 /** 1331 * Sets the overlay image to be drawn on top of the background. 1332 */ 1333 public void setBackgroundOverlay(Bitmap overlay) { 1334 mBackgroundOverlay = overlay; 1335 } 1336 1337 /** 1338 * This gets called when a blurred image of the background is generated. 1339 * Start an animation to fade in the blur. 1340 * 1341 * @param blur blurred image of the background. 1342 */ 1343 public void setBlurredBackground(Bitmap blur) { 1344 mBlurredBackground = blur; 1345 // Start fade in. 1346 ObjectAnimator alpha = ObjectAnimator.ofInt(mBackgroundPaint, "alpha", 80, 255); 1347 alpha.setDuration(250); 1348 alpha.setInterpolator(Gusterpolator.INSTANCE); 1349 alpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1350 @Override 1351 public void onAnimationUpdate(ValueAnimator animation) { 1352 invalidate(); 1353 } 1354 }); 1355 alpha.start(); 1356 invalidate(); 1357 } 1358 1359 @Override 1360 public void drawBackground(Canvas canvas) { 1361 if (mBackground != null && mBackgroundOverlay != null) { 1362 canvas.drawARGB(255, 0, 0, 0); 1363 canvas.drawBitmap(mBackground, null, mBackgroundDrawArea, null); 1364 if (mBlurredBackground != null) { 1365 canvas.drawBitmap(mBlurredBackground, null, mBackgroundDrawArea, mBackgroundPaint); 1366 } 1367 canvas.drawBitmap(mBackgroundOverlay, 0, 0, null); 1368 } 1369 } 1370 1371 @Override 1372 public void startAnimation() { 1373 if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) { 1374 return; 1375 } 1376 if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) { 1377 mPeepHoleCenterX = mWidth / 2; 1378 mPeepHoleCenterY = mHeight / 2; 1379 } 1380 1381 int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX); 1382 int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY); 1383 int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge 1384 + verticalDistanceToFarEdge * verticalDistanceToFarEdge)); 1385 int startRadius = getResources().getDimensionPixelSize( 1386 R.dimen.mode_selector_icon_block_width) / 2; 1387 1388 mPeepHoleAnimator = ValueAnimator.ofFloat(0, endRadius); 1389 mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS); 1390 mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE); 1391 mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1392 @Override 1393 public void onAnimationUpdate(ValueAnimator animation) { 1394 // Modify mask by enlarging the hole 1395 mRadius = (Float) mPeepHoleAnimator.getAnimatedValue(); 1396 invalidate(); 1397 } 1398 }); 1399 1400 mPeepHoleAnimator.addListener(new Animator.AnimatorListener() { 1401 @Override 1402 public void onAnimationStart(Animator animation) { 1403 1404 } 1405 1406 @Override 1407 public void onAnimationEnd(Animator animation) { 1408 endAnimation(); 1409 } 1410 1411 @Override 1412 public void onAnimationCancel(Animator animation) { 1413 1414 } 1415 1416 @Override 1417 public void onAnimationRepeat(Animator animation) { 1418 1419 } 1420 }); 1421 mPeepHoleAnimator.start(); 1422 } 1423 1424 @Override 1425 public void endAnimation() { 1426 if (mEndAction != null) { 1427 post(mEndAction); 1428 mEndAction = null; 1429 post(new Runnable() { 1430 @Override 1431 public void run() { 1432 mPeepHoleAnimator = null; 1433 mRadius = 0; 1434 mPeepHoleCenterX = UNSET; 1435 mPeepHoleCenterY = UNSET; 1436 mCurrentEffect = null; 1437 } 1438 }); 1439 } else { 1440 mPeepHoleAnimator = null; 1441 mRadius = 0; 1442 mPeepHoleCenterX = UNSET; 1443 mPeepHoleCenterY = UNSET; 1444 mCurrentEffect = null; 1445 } 1446 } 1447 1448 @Override 1449 public void setAnimationEndAction(Runnable runnable) { 1450 mEndAction = runnable; 1451 } 1452 1453 private class BlurTask extends AsyncTask<Bitmap, Integer, Bitmap> { 1454 1455 // Gaussian blur mask size. 1456 private static final int MASK_SIZE = 7; 1457 @Override 1458 protected Bitmap doInBackground(Bitmap... params) { 1459 1460 Bitmap intermediateBitmap = params[0]; 1461 int factor = 4; 1462 Bitmap lowResPreview = Bitmap.createScaledBitmap(intermediateBitmap, 1463 intermediateBitmap.getWidth() / factor, 1464 intermediateBitmap.getHeight() / factor, true); 1465 1466 int width = lowResPreview.getWidth(); 1467 int height = lowResPreview.getHeight(); 1468 1469 if (mInputPixels == null || mInputPixels.length < width * height) { 1470 mInputPixels = new int[width * height]; 1471 mOutputPixels = new int[width * height]; 1472 } 1473 lowResPreview.getPixels(mInputPixels, 0, width, 0, 0, width, height); 1474 CameraUtil.blur(mInputPixels, mOutputPixels, width, height, MASK_SIZE); 1475 lowResPreview.setPixels(mOutputPixels, 0, width, 0, 0, width, height); 1476 1477 intermediateBitmap.recycle(); 1478 return Bitmap.createScaledBitmap(lowResPreview, width * factor, 1479 height * factor, true); 1480 } 1481 1482 @Override 1483 protected void onPostExecute(Bitmap bitmap) { 1484 setBlurredBackground(bitmap); 1485 } 1486 }; 1487 } 1488 1489 /** 1490 * Shimmy animation effects handles the specifics for shimmy animation, including 1491 * setting up to show mode drawer (without text) and hide it with shimmy animation. 1492 */ 1493 private class ShimmyAnimationEffects extends ModeListAnimationEffects { 1494 private boolean mStartHidingShimmyWhenWindowGainsFocus = false; 1495 private Animator mAnimator = null; 1496 private float mModeListOpenFactor = 0f; 1497 private final Runnable mHideShimmy = new Runnable() { 1498 @Override 1499 public void run() { 1500 startHidingShimmy(); 1501 } 1502 }; 1503 private Runnable mEndAction = null; 1504 1505 @Override 1506 public void setSize(int width, int height) { 1507 // Do nothing. 1508 } 1509 1510 @Override 1511 public void drawForeground(Canvas canvas) { 1512 // Do nothing. 1513 } 1514 1515 @Override 1516 public void startAnimation() { 1517 setVisibility(VISIBLE); 1518 mSettingsButton.setVisibility(INVISIBLE); 1519 onModeListOpenRatioUpdate(0); 1520 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth(); 1521 for (int i = 0; i < mModeSelectorItems.length; i++) { 1522 mModeSelectorItems[i].setVisibleWidth(maxVisibleWidth); 1523 } 1524 if (hasWindowFocus()) { 1525 hideShimmyWithDelay(); 1526 } else { 1527 mStartHidingShimmyWhenWindowGainsFocus = true; 1528 } 1529 } 1530 1531 private void hideShimmyWithDelay() { 1532 postDelayed(mHideShimmy, HIDE_SHIMMY_DELAY_MS); 1533 } 1534 1535 @Override 1536 public void onWindowFocusChanged(boolean hasFocus) { 1537 if (mStartHidingShimmyWhenWindowGainsFocus && hasFocus) { 1538 mStartHidingShimmyWhenWindowGainsFocus = false; 1539 hideShimmyWithDelay(); 1540 } 1541 } 1542 1543 /** 1544 * This starts the accordion animation, unless it's already running, in which 1545 * case the start animation call will be ignored. 1546 */ 1547 private void startHidingShimmy() { 1548 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth(); 1549 mAnimator = animateListToWidth(START_DELAY_MS * (-1), TOTAL_DURATION_MS, 1550 Gusterpolator.INSTANCE, maxVisibleWidth, 0); 1551 mAnimator.addListener(new Animator.AnimatorListener() { 1552 private boolean mCanceled = false; 1553 @Override 1554 public void onAnimationStart(Animator animation) { 1555 // Do nothing. 1556 } 1557 1558 @Override 1559 public void onAnimationEnd(Animator animation) { 1560 endAnimation(); 1561 } 1562 1563 @Override 1564 public void onAnimationCancel(Animator animation) { 1565 mCanceled = true; 1566 } 1567 1568 @Override 1569 public void onAnimationRepeat(Animator animation) { 1570 // Do nothing. 1571 } 1572 }); 1573 } 1574 1575 @Override 1576 public boolean cancelAnimation() { 1577 removeCallbacks(mHideShimmy); 1578 if (mAnimator != null && mAnimator.isRunning()) { 1579 mAnimator.cancel(); 1580 } 1581 endAnimation(); 1582 return true; 1583 } 1584 1585 @Override 1586 public void endAnimation() { 1587 mAnimator = null; 1588 mSettingsButton.setVisibility(VISIBLE); 1589 if (mEndAction != null) { 1590 post(mEndAction); 1591 } 1592 final ValueAnimator openFactorAnimator = ValueAnimator.ofFloat(mModeListOpenFactor, 1f); 1593 openFactorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1594 @Override 1595 public void onAnimationUpdate(ValueAnimator animation) { 1596 mModeListOpenFactor = (Float) openFactorAnimator.getAnimatedValue(); 1597 onVisibleWidthChanged(mVisibleWidth); 1598 } 1599 }); 1600 openFactorAnimator.addListener(new Animator.AnimatorListener() { 1601 @Override 1602 public void onAnimationStart(Animator animation) { 1603 // Do nothing. 1604 } 1605 1606 @Override 1607 public void onAnimationEnd(Animator animation) { 1608 mModeListOpenFactor = 1f; 1609 mCurrentEffect = null; 1610 } 1611 1612 @Override 1613 public void onAnimationCancel(Animator animation) { 1614 // Do nothing. 1615 } 1616 1617 @Override 1618 public void onAnimationRepeat(Animator animation) { 1619 // Do nothing. 1620 } 1621 }); 1622 openFactorAnimator.start(); 1623 } 1624 1625 @Override 1626 public float getModeListOpenFactor() { 1627 return mModeListOpenFactor; 1628 } 1629 1630 @Override 1631 public void setAnimationEndAction(Runnable runnable) { 1632 mEndAction = runnable; 1633 } 1634 } 1635} 1636