ModeListView.java revision 948de99e58b72e815224d4e12761863dfceffae0
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.PreviewAreaSizeChangedListener { 62 63 private static final String TAG = "ModeListView"; 64 65 // Animation Durations 66 private static final int DEFAULT_DURATION_MS = 200; 67 private static final int FLY_IN_DURATION_MS = 850; 68 private static final int HOLD_DURATION_MS = 0; 69 private static final int FLY_OUT_DURATION_MS = 850; 70 private static final int START_DELAY_MS = 100; 71 private static final int TOTAL_DURATION_MS = FLY_IN_DURATION_MS + HOLD_DURATION_MS 72 + FLY_OUT_DURATION_MS; 73 74 private static final float ROWS_TO_SHOW_IN_LANDSCAPE = 4.5f; 75 private static final int NO_ITEM_SELECTED = -1; 76 77 // Scrolling states 78 private static final int IDLE = 0; 79 private static final int FULLY_SHOWN = 1; 80 private static final int ACCORDION_ANIMATION = 2; 81 private static final int SCROLLING = 3; 82 private static final int MODE_SELECTED = 4; 83 84 // Scrolling delay between non-focused item and focused item 85 private static final int DELAY_MS = 30; 86 // If the fling velocity exceeds this threshold, snap to full screen at a constant 87 // speed. Unit: pixel/ms. 88 private static final float VELOCITY_THRESHOLD = 2f; 89 90 /** 91 * A factor to change the UI responsiveness on a scroll. 92 * e.g. A scroll factor of 0.5 means UI will move half as fast as the finger. 93 */ 94 private static final float SCROLL_FACTOR = 0.5f; 95 // 30% transparent black background. 96 private static final int BACKGROUND_TRANSPARENTCY = (int) (0.3f * 255); 97 private static final int PREVIEW_DOWN_SAMPLE_FACTOR = 4; 98 // Threshold, below which snap back will happen. 99 private static final float SNAP_BACK_THRESHOLD_RATIO = 0.33f; 100 101 private final GestureDetector mGestureDetector; 102 private final int mIconBlockWidth; 103 private final RectF mPreviewArea = new RectF(); 104 private final RectF mUncoveredPreviewArea = new RectF(); 105 106 private int mListBackgroundColor; 107 private LinearLayout mListView; 108 private SettingsButton mSettingsButton; 109 private int mState = IDLE; 110 private int mTotalModes; 111 private ModeSelectorItem[] mModeSelectorItems; 112 private AnimatorSet mAnimatorSet; 113 private int mFocusItem = NO_ITEM_SELECTED; 114 private AnimationEffects mCurrentEffect; 115 private ModeListOpenListener mModeListOpenListener; 116 private CameraAppUI.CameraModuleScreenShotProvider mScreenShotProvider = null; 117 private int[] mInputPixels; 118 private int[] mOutputPixels; 119 120 // Width and height of this view. They get updated in onLayout() 121 // Unit for width and height are pixels. 122 private int mWidth; 123 private int mHeight; 124 private float mScrollTrendX = 0f; 125 private float mScrollTrendY = 0f; 126 private ModeSwitchListener mModeSwitchListener = null; 127 private ArrayList<Integer> mSupportedModes; 128 private final LinkedList<TimeBasedPosition> mPositionHistory 129 = new LinkedList<TimeBasedPosition>(); 130 private long mCurrentTime; 131 private float mVelocityX; // Unit: pixel/ms. 132 private final Animator.AnimatorListener mModeListAnimatorListener = 133 new Animator.AnimatorListener() { 134 135 @Override 136 public void onAnimationStart(Animator animation) { 137 setVisibility(VISIBLE); 138 } 139 140 @Override 141 public void onAnimationEnd(Animator animation) { 142 mAnimatorSet = null; 143 if (mState == ACCORDION_ANIMATION || mState == IDLE) { 144 resetModeSelectors(); 145 setVisibility(INVISIBLE); 146 mState = IDLE; 147 } 148 } 149 150 @Override 151 public void onAnimationCancel(Animator animation) { 152 } 153 154 @Override 155 public void onAnimationRepeat(Animator animation) { 156 157 } 158 }; 159 private boolean mAdjustPositionWhenUncoveredPreviewAreaChanges = false; 160 161 @Override 162 public void onPreviewAreaSizeChanged(RectF previewArea) { 163 mPreviewArea.set(previewArea); 164 } 165 166 private final CameraAppUI.UncoveredPreviewAreaSizeChangedListener 167 mUncoveredPreviewAreaSizeChangedListener = 168 new CameraAppUI.UncoveredPreviewAreaSizeChangedListener() { 169 170 @Override 171 public void uncoveredPreviewAreaChanged(RectF uncoveredPreviewArea) { 172 mUncoveredPreviewArea.set(uncoveredPreviewArea); 173 mSettingsButton.uncoveredPreviewAreaChanged(uncoveredPreviewArea); 174 if (mAdjustPositionWhenUncoveredPreviewAreaChanges) { 175 mAdjustPositionWhenUncoveredPreviewAreaChanges = false; 176 centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight()); 177 } 178 } 179 }; 180 181 public interface ModeSwitchListener { 182 public void onModeSelected(int modeIndex); 183 public int getCurrentModeIndex(); 184 public void onSettingsSelected(); 185 } 186 187 public interface ModeListOpenListener { 188 /** 189 * Mode list will open to full screen after current animation. 190 */ 191 public void onOpenFullScreen(); 192 193 /** 194 * Updates the listener with the current progress of mode drawer opening. 195 * 196 * @param progress progress of the mode drawer opening, ranging [0f, 1f] 197 * 0 means mode drawer is fully closed, 1 indicates a fully 198 * open mode drawer. 199 */ 200 public void onModeListOpenProgress(float progress); 201 202 /** 203 * Gets called when mode list is completely closed. 204 */ 205 public void onModeListClosed(); 206 } 207 208 /** 209 * This class aims to help store time and position in pairs. 210 */ 211 private static class TimeBasedPosition { 212 private final float mPosition; 213 private final long mTimeStamp; 214 public TimeBasedPosition(float position, long time) { 215 mPosition = position; 216 mTimeStamp = time; 217 } 218 219 public float getPosition() { 220 return mPosition; 221 } 222 223 public long getTimeStamp() { 224 return mTimeStamp; 225 } 226 } 227 228 /** 229 * This is a highly customized interpolator. The purpose of having this subclass 230 * is to encapsulate intricate animation timing, so that the actual animation 231 * implementation can be re-used with other interpolators to achieve different 232 * animation effects. 233 * 234 * The accordion animation consists of three stages: 235 * 1) Animate into the screen within a pre-specified fly in duration. 236 * 2) Hold in place for a certain amount of time (Optional). 237 * 3) Animate out of the screen within the given time. 238 * 239 * The accordion animator is initialized with 3 parameter: 1) initial position, 240 * 2) how far out the view should be before flying back out, 3) end position. 241 * The interpolation output should be [0f, 0.5f] during animation between 1) 242 * to 2), and [0.5f, 1f] for flying from 2) to 3). 243 */ 244 private final TimeInterpolator mAccordionInterpolator = new TimeInterpolator() { 245 @Override 246 public float getInterpolation(float input) { 247 248 float flyInDuration = (float) FLY_OUT_DURATION_MS / (float) TOTAL_DURATION_MS; 249 float holdDuration = (float) (FLY_OUT_DURATION_MS + HOLD_DURATION_MS) 250 / (float) TOTAL_DURATION_MS; 251 if (input == 0) { 252 return 0; 253 }else if (input < flyInDuration) { 254 // Stage 1, project result to [0f, 0.5f] 255 input /= flyInDuration; 256 float result = Gusterpolator.INSTANCE.getInterpolation(input); 257 return result * 0.5f; 258 } else if (input < holdDuration) { 259 // Stage 2 260 return 0.5f; 261 } else { 262 // Stage 3, project result to [0.5f, 1f] 263 input -= holdDuration; 264 input /= (1 - holdDuration); 265 float result = Gusterpolator.INSTANCE.getInterpolation(input); 266 return 0.5f + result * 0.5f; 267 } 268 } 269 }; 270 271 /** 272 * The listener that is used to notify when gestures occur. 273 * Here we only listen to a subset of gestures. 274 */ 275 private final GestureDetector.OnGestureListener mOnGestureListener 276 = new GestureDetector.SimpleOnGestureListener(){ 277 @Override 278 public boolean onScroll(MotionEvent e1, MotionEvent e2, 279 float distanceX, float distanceY) { 280 281 if (mState == ACCORDION_ANIMATION) { 282 // Scroll happens during accordion animation. 283 if (isRunningAccordionAnimation()) { 284 mAnimatorSet.cancel(); 285 } 286 setVisibility(VISIBLE); 287 } 288 289 if (mState == IDLE) { 290 resetModeSelectors(); 291 setVisibility(VISIBLE); 292 } 293 294 mState = SCROLLING; 295 // Scroll based on the scrolling distance on the currently focused 296 // item. 297 scroll(mFocusItem, distanceX * SCROLL_FACTOR, distanceY * SCROLL_FACTOR); 298 return true; 299 } 300 301 @Override 302 public boolean onSingleTapUp(MotionEvent ev) { 303 if (mState != FULLY_SHOWN) { 304 // Only allows tap to choose mode when the list is fully shown 305 return false; 306 } 307 308 // Ignore the tap if it happens outside of the mode list linear layout. 309 float x = ev.getX() - mListView.getX(); 310 float y = ev.getY() - mListView.getY(); 311 if (x < 0 || x > mListView.getWidth() || y < 0 || y > mListView.getHeight()) { 312 snapBack(true); 313 return false; 314 } 315 316 int index = getFocusItem(ev.getX(), ev.getY()); 317 // Validate the selection 318 if (index != NO_ITEM_SELECTED) { 319 final int modeId = getModeIndex(index); 320 // Un-highlight all the modes. 321 for (int i = 0; i < mModeSelectorItems.length; i++) { 322 mModeSelectorItems[i].setHighlighted(false); 323 } 324 // Select the focused item. 325 mModeSelectorItems[index].setSelected(true); 326 mState = MODE_SELECTED; 327 PeepholeAnimationEffect effect = new PeepholeAnimationEffect(); 328 effect.setSize(mWidth, mHeight); 329 effect.setAnimationEndAction(new Runnable() { 330 @Override 331 public void run() { 332 setVisibility(INVISIBLE); 333 mCurrentEffect = null; 334 snapBack(false); 335 } 336 }); 337 338 // Calculate the position of the icon in the selected item, and 339 // start animation from that position. 340 int[] location = new int[2]; 341 // Gets icon's center position in relative to the window. 342 mModeSelectorItems[index].getIconCenterLocationInWindow(location); 343 int iconX = location[0]; 344 int iconY = location[1]; 345 // Gets current view's top left position relative to the window. 346 getLocationInWindow(location); 347 // Calculate icon location relative to this view 348 iconX -= location[0]; 349 iconY -= location[1]; 350 351 effect.setAnimationStartingPosition(iconX, iconY); 352 if (mScreenShotProvider != null) { 353 effect.setBackground(mScreenShotProvider 354 .getPreviewFrame(PREVIEW_DOWN_SAMPLE_FACTOR), mPreviewArea); 355 effect.setBackgroundOverlay(mScreenShotProvider.getPreviewOverlayAndControls()); 356 } 357 mCurrentEffect = effect; 358 invalidate(); 359 360 // Post mode selection runnable to the end of the message queue 361 // so that current UI changes can finish before mode initialization 362 // clogs up UI thread. 363 post(new Runnable() { 364 @Override 365 public void run() { 366 onModeSelected(modeId); 367 } 368 }); 369 } 370 return true; 371 } 372 373 @Override 374 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 375 // Cache velocity in the unit pixel/ms. 376 mVelocityX = velocityX / 1000f * SCROLL_FACTOR; 377 return true; 378 } 379 }; 380 381 public ModeListView(Context context, AttributeSet attrs) { 382 super(context, attrs); 383 mGestureDetector = new GestureDetector(context, mOnGestureListener); 384 mIconBlockWidth = getResources() 385 .getDimensionPixelSize(R.dimen.mode_selector_icon_block_width); 386 mListBackgroundColor = getResources().getColor(R.color.mode_list_background); 387 } 388 389 public CameraAppUI.UncoveredPreviewAreaSizeChangedListener 390 getUncoveredPreviewAreaSizeChangedListener() { 391 return mUncoveredPreviewAreaSizeChangedListener; 392 } 393 394 /** 395 * Sets the alpha on the list background. This is called whenever the list 396 * is scrolling or animating, so that background can adjust its dimness. 397 * 398 * @param alpha new alpha to be applied on list background color 399 */ 400 private void setBackgroundAlpha(int alpha) { 401 // Make sure alpha is valid. 402 alpha = alpha & 0xFF; 403 // Change alpha on the background color. 404 mListBackgroundColor = mListBackgroundColor & 0xFFFFFF; 405 mListBackgroundColor = mListBackgroundColor | (alpha << 24); 406 // Set new color to list background. 407 setBackgroundColor(mListBackgroundColor); 408 } 409 410 /** 411 * Initialize mode list with a list of indices of supported modes. 412 * 413 * @param modeIndexList a list of indices of supported modes 414 */ 415 public void init(List<Integer> modeIndexList) { 416 int[] modeSequence = getResources() 417 .getIntArray(R.array.camera_modes_in_nav_drawer_if_supported); 418 int[] visibleModes = getResources() 419 .getIntArray(R.array.camera_modes_always_visible); 420 421 // Mark the supported modes in a boolean array to preserve the 422 // sequence of the modes 423 SparseArray<Boolean> modeIsSupported = new SparseArray<Boolean>(); 424 for (int i = 0; i < modeIndexList.size(); i++) { 425 int mode = modeIndexList.get(i); 426 modeIsSupported.put(mode, true); 427 } 428 for (int i = 0; i < visibleModes.length; i++) { 429 int mode = visibleModes[i]; 430 modeIsSupported.put(mode, true); 431 } 432 433 // Put the indices of supported modes into an array preserving their 434 // display order. 435 mSupportedModes = new ArrayList<Integer>(); 436 for (int i = 0; i < modeSequence.length; i++) { 437 int mode = modeSequence[i]; 438 if (modeIsSupported.get(mode, false)) { 439 mSupportedModes.add(mode); 440 } 441 } 442 mTotalModes = mSupportedModes.size(); 443 initializeModeSelectorItems(); 444 mSettingsButton = (SettingsButton) findViewById(R.id.settings_button); 445 mSettingsButton.setOnClickListener(new OnClickListener() { 446 @Override 447 public void onClick(View v) { 448 // Post this callback to make sure current user interaction has 449 // been reflected in the UI. Specifically, the pressed state gets 450 // unset after click happens. In order to ensure the pressed state 451 // gets unset in UI before getting in the low frame rate settings 452 // activity launch stage, the settings selected callback is posted. 453 post(new Runnable() { 454 @Override 455 public void run() { 456 mModeSwitchListener.onSettingsSelected(); 457 } 458 }); 459 } 460 }); 461 } 462 463 /** 464 * Sets the screen shot provider for getting a preview frame and a bitmap 465 * of the controls and overlay. 466 */ 467 public void setCameraModuleScreenShotProvider( 468 CameraAppUI.CameraModuleScreenShotProvider provider) { 469 mScreenShotProvider = provider; 470 } 471 472 private void initializeModeSelectorItems() { 473 mModeSelectorItems = new ModeSelectorItem[mTotalModes]; 474 // Inflate the mode selector items and add them to a linear layout 475 LayoutInflater inflater = (LayoutInflater) getContext() 476 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 477 mListView = (LinearLayout) findViewById(R.id.mode_list); 478 for (int i = 0; i < mTotalModes; i++) { 479 ModeSelectorItem selectorItem = 480 (ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null); 481 mListView.addView(selectorItem); 482 // Sets the top padding of the top item to 0. 483 if (i == 0) { 484 selectorItem.setPadding(selectorItem.getPaddingLeft(), 0, 485 selectorItem.getPaddingRight(), selectorItem.getPaddingBottom()); 486 } 487 // Sets the bottom padding of the bottom item to 0. 488 if (i == mTotalModes - 1) { 489 selectorItem.setPadding(selectorItem.getPaddingLeft(), selectorItem.getPaddingTop(), 490 selectorItem.getPaddingRight(), 0); 491 } 492 493 int modeId = getModeIndex(i); 494 selectorItem.setHighlightColor(getResources() 495 .getColor(CameraUtil.getCameraThemeColorId(modeId, getContext()))); 496 497 // Set image 498 selectorItem.setImageResource(CameraUtil.getCameraModeIconResId(modeId, getContext())); 499 500 // Set text 501 selectorItem.setText(CameraUtil.getCameraModeText(modeId, getContext())); 502 503 // Set content description (for a11y) 504 selectorItem.setContentDescription(CameraUtil 505 .getCameraModeContentDescription(modeId, getContext())); 506 507 mModeSelectorItems[i] = selectorItem; 508 } 509 510 resetModeSelectors(); 511 } 512 513 /** 514 * Maps between the UI mode selector index to the actual mode id. 515 * 516 * @param modeSelectorIndex the index of the UI item 517 * @return the index of the corresponding camera mode 518 */ 519 private int getModeIndex(int modeSelectorIndex) { 520 if (modeSelectorIndex < mTotalModes && modeSelectorIndex >= 0) { 521 return mSupportedModes.get(modeSelectorIndex); 522 } 523 Log.e(TAG, "Invalid mode selector index: " + modeSelectorIndex + ", total modes: " 524 + mTotalModes); 525 return getResources().getInteger(R.integer.camera_mode_photo); 526 } 527 528 /** Notify ModeSwitchListener, if any, of the mode change. */ 529 private void onModeSelected(int modeIndex) { 530 if (mModeSwitchListener != null) { 531 mModeSwitchListener.onModeSelected(modeIndex); 532 } 533 } 534 535 /** 536 * Sets a listener that listens to receive mode switch event. 537 * 538 * @param listener a listener that gets notified when mode changes. 539 */ 540 public void setModeSwitchListener(ModeSwitchListener listener) { 541 mModeSwitchListener = listener; 542 } 543 544 /** 545 * Sets a listener that gets notified when the mode list is open full screen. 546 * 547 * @param listener a listener that listens to mode list open events 548 */ 549 public void setModeListOpenListener(ModeListOpenListener listener) { 550 mModeListOpenListener = listener; 551 } 552 553 @Override 554 public boolean onTouchEvent(MotionEvent ev) { 555 if (mCurrentEffect != null) { 556 return mCurrentEffect.onTouchEvent(ev); 557 } 558 559 super.onTouchEvent(ev); 560 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 561 mVelocityX = 0; 562 if (mState == ACCORDION_ANIMATION) { 563 // Let taps go through to take a capture during the accordion 564 return false; 565 } 566 getParent().requestDisallowInterceptTouchEvent(true); 567 if (mState == FULLY_SHOWN) { 568 mFocusItem = NO_ITEM_SELECTED; 569 setSwipeMode(false); 570 } else { 571 mFocusItem = getFocusItem(ev.getX(), ev.getY()); 572 setSwipeMode(true); 573 } 574 } else if (mState == ACCORDION_ANIMATION) { 575 // This is a swipe during accordion animation 576 mFocusItem = getFocusItem(ev.getX(), ev.getY()); 577 setSwipeMode(true); 578 579 } 580 // Pass all touch events to gesture detector for gesture handling. 581 mGestureDetector.onTouchEvent(ev); 582 if (ev.getActionMasked() == MotionEvent.ACTION_UP || 583 ev.getActionMasked() == MotionEvent.ACTION_CANCEL) { 584 snap(); 585 mFocusItem = NO_ITEM_SELECTED; 586 } 587 return true; 588 } 589 590 /** 591 * Sets the swipe mode to indicate whether this is a swiping in 592 * or out, and therefore we can have different animations. 593 * 594 * @param swipeIn indicates whether the swipe should reveal/hide the list. 595 */ 596 private void setSwipeMode(boolean swipeIn) { 597 for (int i = 0 ; i < mModeSelectorItems.length; i++) { 598 mModeSelectorItems[i].onSwipeModeChanged(swipeIn); 599 } 600 } 601 602 @Override 603 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 604 super.onLayout(changed, left, top, right, bottom); 605 mWidth = right - left; 606 mHeight = bottom - top - getPaddingTop() - getPaddingBottom(); 607 if (mCurrentEffect != null) { 608 mCurrentEffect.setSize(mWidth, mHeight); 609 } 610 } 611 612 /** 613 * Here we calculate the children size based on the orientation, change 614 * their layout parameters if needed before propagating onMeasure call 615 * to the children, so the newly changed params will take effect in this 616 * pass. 617 * 618 * @param widthMeasureSpec Horizontal space requirements as imposed by the 619 * parent 620 * @param heightMeasureSpec Vertical space requirements as imposed by the 621 * parent 622 */ 623 @Override 624 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 625 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 626 centerModeDrawerInUncoveredPreview(MeasureSpec.getSize(widthMeasureSpec), 627 MeasureSpec.getSize(heightMeasureSpec)); 628 } 629 630 @Override 631 public void draw(Canvas canvas) { 632 if (mCurrentEffect != null) { 633 mCurrentEffect.drawBackground(canvas); 634 super.draw(canvas); 635 mCurrentEffect.drawForeground(canvas); 636 } else { 637 super.draw(canvas); 638 } 639 } 640 641 /** 642 * This starts the accordion animation, unless it's already running, in which 643 * case the start animation call will be ignored. 644 */ 645 public void startAccordionAnimation() { 646 if (mState != IDLE) { 647 return; 648 } 649 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 650 return; 651 } 652 mState = ACCORDION_ANIMATION; 653 resetModeSelectors(); 654 animateListToWidth(START_DELAY_MS, TOTAL_DURATION_MS, mAccordionInterpolator, 655 0, mIconBlockWidth, 0); 656 } 657 658 /** 659 * This starts the accordion animation with a delay. 660 * 661 * @param delay delay in milliseconds before starting animation 662 */ 663 public void startAccordionAnimationWithDelay(int delay) { 664 postDelayed(new Runnable() { 665 @Override 666 public void run() { 667 startAccordionAnimation(); 668 } 669 }, delay); 670 } 671 672 /** 673 * Resets the visible width of all the mode selectors to 0. 674 */ 675 private void resetModeSelectors() { 676 for (int i = 0; i < mModeSelectorItems.length; i++) { 677 mModeSelectorItems[i].setVisibleWidth(0); 678 } 679 // Visible width has been changed to 0 680 onVisibleWidthChanged(0); 681 } 682 683 private boolean isRunningAccordionAnimation() { 684 return mAnimatorSet != null && mAnimatorSet.isRunning(); 685 } 686 687 /** 688 * Calculate the mode selector item in the list that is at position (x, y). 689 * If the position is above the top item or below the bottom item, return 690 * the top item or bottom item respectively. 691 * 692 * @param x horizontal position 693 * @param y vertical position 694 * @return index of the item that is at position (x, y) 695 */ 696 private int getFocusItem(float x, float y) { 697 // Convert coordinates into child view's coordinates. 698 x -= mListView.getX(); 699 y -= mListView.getY(); 700 701 for (int i = 0; i < mModeSelectorItems.length; i++) { 702 if (y <= mModeSelectorItems[i].getBottom()) { 703 return i; 704 } 705 } 706 return mModeSelectorItems.length - 1; 707 } 708 709 @Override 710 public void onVisibilityChanged(View v, int visibility) { 711 super.onVisibilityChanged(v, visibility); 712 if (visibility == VISIBLE) { 713 centerModeDrawerInUncoveredPreview(getMeasuredWidth(), getMeasuredHeight()); 714 // Highlight current module 715 if (mModeSwitchListener != null) { 716 int modeId = mModeSwitchListener.getCurrentModeIndex(); 717 int parentMode = CameraUtil.getCameraModeParentModeId(modeId, getContext()); 718 // Find parent mode in the nav drawer. 719 for (int i = 0; i < mSupportedModes.size(); i++) { 720 if (mSupportedModes.get(i) == parentMode) { 721 mModeSelectorItems[i].setHighlighted(true); 722 } 723 } 724 } 725 } else { 726 if (mModeSelectorItems != null) { 727 // When becoming invisible/gone after initializing mode selector items. 728 for (int i = 0; i < mModeSelectorItems.length; i++) { 729 mModeSelectorItems[i].setHighlighted(false); 730 mModeSelectorItems[i].setSelected(false); 731 } 732 } 733 if (mModeSwitchListener != null) { 734 mModeListOpenListener.onModeListClosed(); 735 } 736 } 737 } 738 739 /** 740 * Center mode drawer in the portion of camera preview that is not covered by 741 * bottom bar. 742 */ 743 // TODO: Combine SettingsButton logic into here if UX design does not change 744 // for another week. 745 private void centerModeDrawerInUncoveredPreview(int measuredWidth, int measuredHeight) { 746 747 // Assuming the preview is centered in the space aside from bottom bar. 748 float previewAreaWidth = mUncoveredPreviewArea.right + mUncoveredPreviewArea.left; 749 float previewAreaHeight = mUncoveredPreviewArea.top + mUncoveredPreviewArea.bottom; 750 if (measuredWidth > measuredHeight && previewAreaWidth < previewAreaHeight 751 || measuredWidth < measuredHeight && previewAreaWidth > previewAreaHeight) { 752 // Cached preview area is stale, update mode drawer position on next 753 // layout pass. 754 mAdjustPositionWhenUncoveredPreviewAreaChanges = true; 755 } else { 756 // Align left: 757 mListView.setTranslationX(mUncoveredPreviewArea.left); 758 // Align center vertical: 759 mListView.setTranslationY(mUncoveredPreviewArea.centerY() 760 - mListView.getMeasuredHeight() / 2); 761 } 762 } 763 764 private void scroll(int itemId, float deltaX, float deltaY) { 765 // Scrolling trend on X and Y axis, to track the trend by biasing 766 // towards latest touch events. 767 mScrollTrendX = mScrollTrendX * 0.3f + deltaX * 0.7f; 768 mScrollTrendY = mScrollTrendY * 0.3f + deltaY * 0.7f; 769 770 // TODO: Change how the curve is calculated below when UX finalize their design. 771 mCurrentTime = SystemClock.uptimeMillis(); 772 float longestWidth; 773 if (itemId != NO_ITEM_SELECTED) { 774 longestWidth = mModeSelectorItems[itemId].getVisibleWidth() - deltaX; 775 } else { 776 longestWidth = mModeSelectorItems[0].getVisibleWidth() - deltaX; 777 } 778 insertNewPosition(longestWidth, mCurrentTime); 779 780 for (int i = 0; i < mModeSelectorItems.length; i++) { 781 mModeSelectorItems[i].setVisibleWidth(calculateVisibleWidthForItem(i, 782 (int) longestWidth)); 783 } 784 if (longestWidth <= 0) { 785 reset(); 786 } 787 788 itemId = itemId == NO_ITEM_SELECTED ? 0 : itemId; 789 onVisibleWidthChanged(mModeSelectorItems[itemId].getVisibleWidth()); 790 } 791 792 /** 793 * Calculate the width of a specified item based on its position relative to 794 * the item with longest width. 795 */ 796 private int calculateVisibleWidthForItem(int itemId, int longestWidth) { 797 if (itemId == mFocusItem || mFocusItem == NO_ITEM_SELECTED) { 798 return longestWidth; 799 } 800 801 int delay = Math.abs(itemId - mFocusItem) * DELAY_MS; 802 return (int) getPosition(mCurrentTime - delay); 803 } 804 805 /** 806 * Insert new position and time stamp into the history position list, and 807 * remove stale position items. 808 * 809 * @param position latest position of the focus item 810 * @param time current time in milliseconds 811 */ 812 private void insertNewPosition(float position, long time) { 813 // TODO: Consider re-using stale position objects rather than 814 // always creating new position objects. 815 mPositionHistory.add(new TimeBasedPosition(position, time)); 816 817 // Positions that are from too long ago will not be of any use for 818 // future position interpolation. So we need to remove those positions 819 // from the list. 820 long timeCutoff = time - (mTotalModes - 1) * DELAY_MS; 821 while (mPositionHistory.size() > 0) { 822 // Remove all the position items that are prior to the cutoff time. 823 TimeBasedPosition historyPosition = mPositionHistory.getFirst(); 824 if (historyPosition.getTimeStamp() < timeCutoff) { 825 mPositionHistory.removeFirst(); 826 } else { 827 break; 828 } 829 } 830 } 831 832 /** 833 * Gets the interpolated position at the specified time. This involves going 834 * through the recorded positions until a {@link TimeBasedPosition} is found 835 * such that the position the recorded before the given time, and the 836 * {@link TimeBasedPosition} after that is recorded no earlier than the given 837 * time. These two positions are then interpolated to get the position at the 838 * specified time. 839 */ 840 private float getPosition(long time) { 841 int i; 842 for (i = 0; i < mPositionHistory.size(); i++) { 843 TimeBasedPosition historyPosition = mPositionHistory.get(i); 844 if (historyPosition.getTimeStamp() > time) { 845 // Found the winner. Now interpolate between position i and position i - 1 846 if (i == 0) { 847 return historyPosition.getPosition(); 848 } else { 849 TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1); 850 // Start interpolation 851 float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) / 852 (float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp()); 853 float position = fraction * (historyPosition.getPosition() 854 - prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition(); 855 return position; 856 } 857 } 858 } 859 // It should never get here. 860 Log.e(TAG, "Invalid time input for getPosition(). time: " + time); 861 if (mPositionHistory.size() == 0) { 862 Log.e(TAG, "TimeBasedPosition history size is 0"); 863 } else { 864 Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp() 865 + " , last position recorded at " + mPositionHistory.getLast().getTimeStamp()); 866 } 867 assert (i < mPositionHistory.size()); 868 return i; 869 } 870 871 private void reset() { 872 resetModeSelectors(); 873 mScrollTrendX = 0f; 874 mScrollTrendY = 0f; 875 mCurrentEffect = null; 876 setVisibility(INVISIBLE); 877 } 878 879 /** 880 * When visible width of list is changed, the background of the list needs 881 * to darken/lighten correspondingly. 882 */ 883 private void onVisibleWidthChanged(int focusItemWidth) { 884 // When the longest mode item is entirely shown (across the screen), the 885 // background should be 50% transparent. 886 int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth(); 887 focusItemWidth = Math.min(maxVisibleWidth, focusItemWidth); 888 float openRatio = (float) focusItemWidth / maxVisibleWidth; 889 setBackgroundAlpha((int) (BACKGROUND_TRANSPARENTCY * openRatio)); 890 if (mModeListOpenListener != null) { 891 mModeListOpenListener.onModeListOpenProgress(openRatio); 892 } 893 if (mSettingsButton != null) { 894 mSettingsButton.setAlpha(openRatio); 895 } 896 } 897 898 @Override 899 public void onWindowVisibilityChanged(int visibility) { 900 super.onWindowVisibilityChanged(visibility); 901 if (visibility != VISIBLE) { 902 // Reset mode list if the window is no longer visible. 903 reset(); 904 mState = IDLE; 905 } 906 } 907 908 /** 909 * The list view should either snap back or snap to full screen after a gesture. 910 * This function is called when an up or cancel event is received, and then based 911 * on the current position of the list and the gesture we can decide which way 912 * to snap. 913 */ 914 private void snap() { 915 if (mState == SCROLLING) { 916 int itemId = Math.max(0, mFocusItem); 917 if (mModeSelectorItems[itemId].getVisibleWidth() 918 < mModeSelectorItems[itemId].getMaxVisibleWidth() * SNAP_BACK_THRESHOLD_RATIO) { 919 snapBack(); 920 } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) { 921 snapBack(); 922 } else { 923 snapToFullScreen(); 924 } 925 } 926 } 927 928 /** 929 * Snaps back out of the screen. 930 * 931 * @param withAnimation whether snapping back should be animated 932 */ 933 public void snapBack(boolean withAnimation) { 934 if (withAnimation) { 935 if (mVelocityX > -VELOCITY_THRESHOLD * SCROLL_FACTOR) { 936 animateListToWidth(0); 937 } else { 938 animateListToWidthAtVelocity(mVelocityX, 0); 939 } 940 mState = IDLE; 941 } else { 942 setVisibility(INVISIBLE); 943 resetModeSelectors(); 944 mState = IDLE; 945 } 946 } 947 948 /** 949 * Snaps the mode list back out with animation. 950 */ 951 private void snapBack() { 952 snapBack(true); 953 } 954 955 private void snapToFullScreen() { 956 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem; 957 int fullWidth = mModeSelectorItems[focusItem].getMaxVisibleWidth(); 958 if (mVelocityX <= VELOCITY_THRESHOLD * SCROLL_FACTOR) { 959 animateListToWidth(fullWidth); 960 } else { 961 // If the fling velocity exceeds this threshold, snap to full screen 962 // at a constant speed. 963 animateListToWidthAtVelocity(mVelocityX, fullWidth); 964 } 965 mState = FULLY_SHOWN; 966 if (mModeListOpenListener != null) { 967 mModeListOpenListener.onOpenFullScreen(); 968 } 969 } 970 971 /** 972 * Overloaded function to provide a simple way to start animation. Animation 973 * will use default duration, and a value of <code>null</code> for interpolator 974 * means linear interpolation will be used. 975 * 976 * @param width a set of values that the animation will animate between over time 977 */ 978 private void animateListToWidth(int... width) { 979 animateListToWidth(0, DEFAULT_DURATION_MS, null, width); 980 } 981 982 /** 983 * Animate the mode list between the given set of visible width. 984 * 985 * @param delay start delay between consecutive mode item 986 * @param duration duration for the animation of each mode item 987 * @param interpolator interpolator to be used by the animation 988 * @param width a set of values that the animation will animate between over time 989 */ 990 private void animateListToWidth(int delay, int duration, 991 TimeInterpolator interpolator, int... width) { 992 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 993 mAnimatorSet.end(); 994 } 995 996 ArrayList<Animator> animators = new ArrayList<Animator>(); 997 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem; 998 for (int i = 0; i < mTotalModes; i++) { 999 ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i], 1000 "visibleWidth", width); 1001 animator.setDuration(duration); 1002 animator.setStartDelay(i * delay); 1003 animators.add(animator); 1004 if (i == focusItem) { 1005 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1006 @Override 1007 public void onAnimationUpdate(ValueAnimator animation) { 1008 onVisibleWidthChanged((Integer) animation.getAnimatedValue()); 1009 } 1010 }); 1011 } 1012 } 1013 1014 mAnimatorSet = new AnimatorSet(); 1015 mAnimatorSet.playTogether(animators); 1016 mAnimatorSet.setInterpolator(interpolator); 1017 mAnimatorSet.addListener(mModeListAnimatorListener); 1018 mAnimatorSet.start(); 1019 } 1020 1021 /** 1022 * Animate the mode list to the given width at a constant velocity. 1023 * 1024 * @param velocity the velocity that animation will be at 1025 * @param width final width of the list 1026 */ 1027 private void animateListToWidthAtVelocity(float velocity, int width) { 1028 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 1029 mAnimatorSet.end(); 1030 } 1031 1032 ArrayList<Animator> animators = new ArrayList<Animator>(); 1033 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem; 1034 for (int i = 0; i < mTotalModes; i++) { 1035 ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i], 1036 "visibleWidth", width); 1037 int duration = (int) ((float) width / velocity); 1038 animator.setDuration(duration); 1039 animators.add(animator); 1040 if (i == focusItem) { 1041 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1042 @Override 1043 public void onAnimationUpdate(ValueAnimator animation) { 1044 onVisibleWidthChanged((Integer) animation.getAnimatedValue()); 1045 } 1046 }); 1047 } 1048 } 1049 1050 mAnimatorSet = new AnimatorSet(); 1051 mAnimatorSet.playTogether(animators); 1052 mAnimatorSet.setInterpolator(null); 1053 mAnimatorSet.addListener(mModeListAnimatorListener); 1054 mAnimatorSet.start(); 1055 } 1056 1057 /** 1058 * Called when the back key is pressed. 1059 * 1060 * @return Whether the UI responded to the key event. 1061 */ 1062 public boolean onBackPressed() { 1063 if (mState == FULLY_SHOWN) { 1064 snapBack(); 1065 return true; 1066 } else { 1067 return false; 1068 } 1069 } 1070 1071 public void startModeSelectionAnimation() { 1072 if (mState != MODE_SELECTED || mCurrentEffect == null) { 1073 setVisibility(INVISIBLE); 1074 snapBack(false); 1075 mCurrentEffect = null; 1076 } else { 1077 mCurrentEffect.startAnimation(); 1078 } 1079 1080 } 1081 1082 private class PeepholeAnimationEffect extends AnimationEffects { 1083 1084 private final static int UNSET = -1; 1085 private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 300; 1086 1087 private final Paint mMaskPaint = new Paint(); 1088 private final Paint mBackgroundPaint = new Paint(); 1089 private final RectF mBackgroundDrawArea = new RectF(); 1090 1091 private int mWidth; 1092 private int mHeight; 1093 private int mPeepHoleCenterX = UNSET; 1094 private int mPeepHoleCenterY = UNSET; 1095 private float mRadius = 0f; 1096 private ValueAnimator mPeepHoleAnimator; 1097 private Runnable mEndAction; 1098 private Bitmap mBackground; 1099 private Bitmap mBlurredBackground; 1100 private Bitmap mBackgroundOverlay; 1101 1102 public PeepholeAnimationEffect() { 1103 mMaskPaint.setAlpha(0); 1104 mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 1105 } 1106 1107 @Override 1108 public void setSize(int width, int height) { 1109 mWidth = width; 1110 mHeight = height; 1111 } 1112 1113 @Override 1114 public void drawForeground(Canvas canvas) { 1115 // Draw the circle in clear mode 1116 if (mPeepHoleAnimator != null) { 1117 // Draw a transparent circle using clear mode 1118 canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint); 1119 } 1120 } 1121 1122 public void setAnimationStartingPosition(int x, int y) { 1123 mPeepHoleCenterX = x; 1124 mPeepHoleCenterY = y; 1125 } 1126 1127 /** 1128 * Sets the bitmap to be drawn in the background and the drawArea to draw 1129 * the bitmap. In the meantime, start processing the image in a background 1130 * thread to get a blurred background image. 1131 * 1132 * @param background image to be drawn in the background 1133 * @param drawArea area to draw the background image 1134 */ 1135 public void setBackground(Bitmap background, RectF drawArea) { 1136 mBackground = background; 1137 mBackgroundDrawArea.set(drawArea); 1138 new BlurTask().execute(Bitmap.createScaledBitmap(background, background.getWidth(), 1139 background.getHeight(), true)); 1140 } 1141 1142 /** 1143 * Sets the overlay image to be drawn on top of the background. 1144 */ 1145 public void setBackgroundOverlay(Bitmap overlay) { 1146 mBackgroundOverlay = overlay; 1147 } 1148 1149 /** 1150 * This gets called when a blurred image of the background is generated. 1151 * Start an animation to fade in the blur. 1152 * 1153 * @param blur blurred image of the background. 1154 */ 1155 public void setBlurredBackground(Bitmap blur) { 1156 mBlurredBackground = blur; 1157 // Start fade in. 1158 ObjectAnimator alpha = ObjectAnimator.ofInt(mBackgroundPaint, "alpha", 80, 255); 1159 alpha.setDuration(250); 1160 alpha.setInterpolator(Gusterpolator.INSTANCE); 1161 alpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1162 @Override 1163 public void onAnimationUpdate(ValueAnimator animation) { 1164 invalidate(); 1165 } 1166 }); 1167 alpha.start(); 1168 invalidate(); 1169 } 1170 1171 @Override 1172 public void drawBackground(Canvas canvas) { 1173 if (mBackground != null && mBackgroundOverlay != null) { 1174 canvas.drawARGB(255, 0, 0, 0); 1175 canvas.drawBitmap(mBackground, null, mBackgroundDrawArea, null); 1176 if (mBlurredBackground != null) { 1177 canvas.drawBitmap(mBlurredBackground, null, mBackgroundDrawArea, mBackgroundPaint); 1178 } 1179 canvas.drawBitmap(mBackgroundOverlay, 0, 0, null); 1180 } 1181 } 1182 1183 @Override 1184 public void startAnimation() { 1185 if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) { 1186 return; 1187 } 1188 if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) { 1189 mPeepHoleCenterX = mWidth / 2; 1190 mPeepHoleCenterY = mHeight / 2; 1191 } 1192 1193 int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX); 1194 int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY); 1195 int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge 1196 + verticalDistanceToFarEdge * verticalDistanceToFarEdge)); 1197 int startRadius = getResources().getDimensionPixelSize( 1198 R.dimen.mode_selector_icon_block_width) / 2; 1199 1200 mPeepHoleAnimator = ValueAnimator.ofFloat(0, endRadius); 1201 mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS); 1202 mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE); 1203 mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1204 @Override 1205 public void onAnimationUpdate(ValueAnimator animation) { 1206 // Modify mask by enlarging the hole 1207 mRadius = (Float) mPeepHoleAnimator.getAnimatedValue(); 1208 invalidate(); 1209 } 1210 }); 1211 1212 mPeepHoleAnimator.addListener(new Animator.AnimatorListener() { 1213 @Override 1214 public void onAnimationStart(Animator animation) { 1215 1216 } 1217 1218 @Override 1219 public void onAnimationEnd(Animator animation) { 1220 if (mEndAction != null) { 1221 post(mEndAction); 1222 mEndAction = null; 1223 post(new Runnable() { 1224 @Override 1225 public void run() { 1226 mPeepHoleAnimator = null; 1227 mRadius = 0; 1228 mPeepHoleCenterX = UNSET; 1229 mPeepHoleCenterY = UNSET; 1230 } 1231 }); 1232 } else { 1233 mPeepHoleAnimator = null; 1234 mRadius = 0; 1235 mPeepHoleCenterX = UNSET; 1236 mPeepHoleCenterY = UNSET; 1237 } 1238 } 1239 1240 @Override 1241 public void onAnimationCancel(Animator animation) { 1242 1243 } 1244 1245 @Override 1246 public void onAnimationRepeat(Animator animation) { 1247 1248 } 1249 }); 1250 mPeepHoleAnimator.start(); 1251 } 1252 1253 public void setAnimationEndAction(Runnable runnable) { 1254 mEndAction = runnable; 1255 } 1256 1257 private class BlurTask extends AsyncTask<Bitmap, Integer, Bitmap> { 1258 1259 // Gaussian blur mask size. 1260 private static final int MASK_SIZE = 7; 1261 @Override 1262 protected Bitmap doInBackground(Bitmap... params) { 1263 1264 Bitmap intermediateBitmap = params[0]; 1265 int factor = 4; 1266 Bitmap lowResPreview = Bitmap.createScaledBitmap(intermediateBitmap, 1267 intermediateBitmap.getWidth() / factor, 1268 intermediateBitmap.getHeight() / factor, true); 1269 1270 int width = lowResPreview.getWidth(); 1271 int height = lowResPreview.getHeight(); 1272 1273 if (mInputPixels == null || mInputPixels.length < width * height) { 1274 mInputPixels = new int[width * height]; 1275 mOutputPixels = new int[width * height]; 1276 } 1277 lowResPreview.getPixels(mInputPixels, 0, width, 0, 0, width, height); 1278 CameraUtil.blur(mInputPixels, mOutputPixels, width, height, MASK_SIZE); 1279 lowResPreview.setPixels(mOutputPixels, 0, width, 0, 0, width, height); 1280 1281 intermediateBitmap.recycle(); 1282 return Bitmap.createScaledBitmap(lowResPreview, width * factor, 1283 height * factor, true); 1284 } 1285 1286 @Override 1287 protected void onPostExecute(Bitmap bitmap) { 1288 setBlurredBackground(bitmap); 1289 } 1290 }; 1291 } 1292} 1293