ModeListView.java revision e1052c6501f9c1150aeaed99b57744f3afde61ff
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.content.res.Configuration; 26import android.graphics.Canvas; 27import android.graphics.Paint; 28import android.graphics.PorterDuff; 29import android.graphics.PorterDuffXfermode; 30import android.os.SystemClock; 31import android.util.AttributeSet; 32import android.util.Log; 33import android.view.GestureDetector; 34import android.view.LayoutInflater; 35import android.view.MotionEvent; 36import android.widget.LinearLayout; 37import android.widget.ScrollView; 38 39import com.android.camera.util.Gusterpolator; 40import com.android.camera.widget.AnimationEffects; 41import com.android.camera2.R; 42 43import java.util.ArrayList; 44import java.util.LinkedList; 45import java.util.List; 46 47/** 48 * ModeListView class displays all camera modes and settings in the form 49 * of a list. A swipe to the right will bring up this list. Then tapping on 50 * any of the items in the list will take the user to that corresponding mode 51 * with an animation. To dismiss this list, simply swipe left or select a mode. 52 */ 53public class ModeListView extends ScrollView { 54 55 /** Simple struct that defines the look of a mode in the mode switcher. */ 56 private static class Mode { 57 /** Resource ID of the icon for this mode. */ 58 public final int iconResId; 59 /** Resource ID for the text of this mode. */ 60 public final int textResId; 61 /** The ID of the color for this mode. */ 62 public final int colorId; 63 64 public Mode(int iconResId, int textResId, int colorId) { 65 this.iconResId = iconResId; 66 this.textResId = textResId; 67 this.colorId = colorId; 68 } 69 } 70 71 72 private static final String TAG = "ModeListView"; 73 74 // Animation Durations 75 private static final int DEFAULT_DURATION_MS = 200; 76 private static final int FLY_IN_DURATION_MS = 850; 77 private static final int HOLD_DURATION_MS = 0; 78 private static final int FLY_OUT_DURATION_MS = 850; 79 private static final int START_DELAY_MS = 100; 80 private static final int TOTAL_DURATION_MS = FLY_IN_DURATION_MS + HOLD_DURATION_MS 81 + FLY_OUT_DURATION_MS; 82 83 // Different modes in the mode list. Change these to change the order they 84 // appear in the mode switcher. 85 public static final int MODE_PHOTO = 0; 86 public static final int MODE_VIDEO = 1; 87 public static final int MODE_CRAFT = 2; 88 public static final int MODE_WIDEANGLE = 3; 89 public static final int MODE_PHOTOSPHERE = 4; 90 public static final int MODE_TIMELAPSE = 5; 91 public static final int MODE_SETTING = 6; 92 // Special case 93 public static final int MODE_GCAM = 100; 94 private static final int MODE_TOTAL = 7; 95 private static final float ROWS_TO_SHOW_IN_LANDSCAPE = 4.5f; 96 private static final int NO_ITEM_SELECTED = -1; 97 98 // Scrolling states 99 private static final int IDLE = 0; 100 private static final int FULLY_SHOWN = 1; 101 private static final int ACCORDION_ANIMATION = 2; 102 private static final int SCROLLING = 3; 103 private static final int MODE_SELECTED = 4; 104 105 // Scrolling delay between non-focused item and focused item 106 private static final int DELAY_MS = 25; 107 108 private static final Mode[] mModes; 109 static { 110 mModes = new Mode[MODE_TOTAL]; 111 mModes[MODE_PHOTO] = new Mode(R.drawable.ic_camera_normal, 112 R.string.mode_camera, 113 R.color.camera_mode_color); 114 mModes[MODE_VIDEO] = new Mode(R.drawable.ic_video_normal, 115 R.string.mode_video, 116 R.color.video_mode_color); 117 mModes[MODE_PHOTOSPHERE] = new Mode(R.drawable.ic_photo_sphere_normal, 118 R.string.mode_photosphere, 119 R.color.photosphere_mode_color); 120 mModes[MODE_CRAFT] = new Mode(R.drawable.ic_camera_normal, 121 R.string.mode_advanced_camera, 122 R.color.craft_mode_color); 123 mModes[MODE_TIMELAPSE] = new Mode(R.drawable.ic_timelapse_normal, 124 R.string.mode_timelapse, 125 R.color.timelapse_mode_color); 126 mModes[MODE_WIDEANGLE] = new Mode(R.drawable.ic_panorama_normal, 127 R.string.mode_panorama, 128 R.color.panorama_mode_color); 129 mModes[MODE_SETTING] = new Mode(R.drawable.ic_settings_normal, 130 R.string.mode_settings, 131 R.color.settings_mode_color); 132 } 133 134 private final GestureDetector mGestureDetector; 135 private final int mIconBlockWidth; 136 137 private int mListBackgroundColor; 138 private LinearLayout mListView; 139 private int mState = IDLE; 140 private int mTotalModes; 141 private ModeSelectorItem[] mModeSelectorItems; 142 private AnimatorSet mAnimatorSet; 143 private int mFocusItem = NO_ITEM_SELECTED; 144 private AnimationEffects mCurrentEffect; 145 146 // Width and height of this view. They get updated in onLayout() 147 // Unit for width and height are pixels. 148 private int mWidth; 149 private int mHeight; 150 private float mScrollTrendX = 0f; 151 private float mScrollTrendY = 0f; 152 private ModeSwitchListener mListener = null; 153 private int[] mSupportedModes; 154 private final LinkedList<TimeBasedPosition> mPositionHistory 155 = new LinkedList<TimeBasedPosition>(); 156 private long mCurrentTime; 157 158 public interface ModeSwitchListener { 159 public void onModeSelected(int modeIndex); 160 } 161 162 /** 163 * This class aims to help store time and position in pairs. 164 */ 165 private static class TimeBasedPosition { 166 private final float mPosition; 167 private final long mTimeStamp; 168 public TimeBasedPosition(float position, long time) { 169 mPosition = position; 170 mTimeStamp = time; 171 } 172 173 public float getPosition() { 174 return mPosition; 175 } 176 177 public long getTimeStamp() { 178 return mTimeStamp; 179 } 180 } 181 182 /** 183 * This is a highly customized interpolator. The purpose of having this subclass 184 * is to encapsulate intricate animation timing, so that the actual animation 185 * implementation can be re-used with other interpolators to achieve different 186 * animation effects. 187 * 188 * The accordion animation consists of three stages: 189 * 1) Animate into the screen within a pre-specified fly in duration. 190 * 2) Hold in place for a certain amount of time (Optional). 191 * 3) Animate out of the screen within the given time. 192 * 193 * The accordion animator is initialized with 3 parameter: 1) initial position, 194 * 2) how far out the view should be before flying back out, 3) end position. 195 * The interpolation output should be [0f, 0.5f] during animation between 1) 196 * to 2), and [0.5f, 1f] for flying from 2) to 3). 197 */ 198 private final TimeInterpolator mAccordionInterpolator = new TimeInterpolator() { 199 @Override 200 public float getInterpolation(float input) { 201 202 float flyInDuration = (float) FLY_OUT_DURATION_MS / (float) TOTAL_DURATION_MS; 203 float holdDuration = (float) (FLY_OUT_DURATION_MS + HOLD_DURATION_MS) 204 / (float) TOTAL_DURATION_MS; 205 if (input == 0) { 206 return 0; 207 }else if (input < flyInDuration) { 208 // Stage 1, project result to [0f, 0.5f] 209 input /= flyInDuration; 210 float result = Gusterpolator.INSTANCE.getInterpolation(input); 211 return result * 0.5f; 212 } else if (input < holdDuration) { 213 // Stage 2 214 return 0.5f; 215 } else { 216 // Stage 3, project result to [0.5f, 1f] 217 input -= holdDuration; 218 input /= (1 - holdDuration); 219 float result = Gusterpolator.INSTANCE.getInterpolation(input); 220 return 0.5f + result * 0.5f; 221 } 222 } 223 }; 224 225 /** 226 * The listener that is used to notify when gestures occur. 227 * Here we only listen to a subset of gestures. 228 */ 229 private final GestureDetector.OnGestureListener mOnGestureListener 230 = new GestureDetector.SimpleOnGestureListener(){ 231 @Override 232 public boolean onScroll(MotionEvent e1, MotionEvent e2, 233 float distanceX, float distanceY) { 234 235 if (mState == ACCORDION_ANIMATION) { 236 // Scroll happens during accordion animation. 237 if (isRunningAccordionAnimation()) { 238 mAnimatorSet.cancel(); 239 } 240 setVisibility(VISIBLE); 241 } 242 243 if (mState == IDLE) { 244 resetModeSelectors(); 245 setVisibility(VISIBLE); 246 } 247 248 mState = SCROLLING; 249 // Scroll based on the scrolling distance on the currently focused 250 // item. 251 scroll(mFocusItem, distanceX, distanceY); 252 return true; 253 } 254 255 @Override 256 public boolean onSingleTapUp(MotionEvent ev) { 257 if (mState != FULLY_SHOWN) { 258 // Only allows tap to choose mode when the list is fully shown 259 return false; 260 } 261 int index = getFocusItem(ev.getX(), ev.getY()); 262 // Validate the selection 263 if (index != NO_ITEM_SELECTED) { 264 int modeId = getModeIndex(index); 265 mModeSelectorItems[index].highlight(); 266 mState = MODE_SELECTED; 267 PeepholeAnimationEffect effect = new PeepholeAnimationEffect(); 268 effect.setSize(mWidth, mHeight); 269 effect.setAnimationEndAction(new Runnable() { 270 @Override 271 public void run() { 272 setVisibility(INVISIBLE); 273 mCurrentEffect = null; 274 snapBack(false); 275 } 276 }); 277 effect.setAnimationStartingPosition((int) ev.getX(), (int) ev.getY()); 278 mCurrentEffect = effect; 279 280 onModeSelected(modeId); 281 } 282 return true; 283 } 284 }; 285 286 public ModeListView(Context context, AttributeSet attrs) { 287 super(context, attrs); 288 mGestureDetector = new GestureDetector(context, mOnGestureListener); 289 mIconBlockWidth = getResources() 290 .getDimensionPixelSize(R.dimen.mode_selector_icon_block_width); 291 mListBackgroundColor = getResources().getColor(R.color.mode_list_background); 292 } 293 294 /** 295 * Sets the alpha on the list background. This is called whenever the list 296 * is scrolling or animating, so that background can adjust its dimness. 297 * 298 * @param alpha new alpha to be applied on list background color 299 */ 300 private void setBackgroundAlpha(int alpha) { 301 // Make sure alpha is valid. 302 alpha = alpha & 0xFF; 303 // Change alpha on the background color. 304 mListBackgroundColor = mListBackgroundColor & 0xFFFFFF; 305 mListBackgroundColor = mListBackgroundColor | (alpha << 24); 306 // Set new color to list background. 307 mListView.setBackgroundColor(mListBackgroundColor); 308 } 309 310 /** 311 * Initialize mode list with a list of indices of supported modes. 312 * 313 * @param modeIndexList a list of indices of supported modes 314 */ 315 public void init(List<Integer> modeIndexList) { 316 boolean[] modeIsSupported = new boolean[MODE_TOTAL]; 317 // Setting should always be supported 318 modeIsSupported[MODE_SETTING] = true; 319 mTotalModes = 1; 320 321 // Mark the supported modes in a boolean array to preserve the 322 // sequence of the modes 323 for (int i = 0; i < modeIndexList.size(); i++) { 324 int mode = modeIndexList.get(i); 325 if (mode >= MODE_TOTAL) { 326 // This is a mode that we don't display in the mode list, skip. 327 continue; 328 } 329 if (modeIsSupported[mode] == false) { 330 modeIsSupported[mode] = true; 331 mTotalModes++; 332 } 333 } 334 // Put the indices of supported modes into an array preserving their 335 // display order. 336 mSupportedModes = new int[mTotalModes]; 337 int modeCount = 0; 338 for (int i = 0; i < MODE_TOTAL; i++) { 339 if (modeIsSupported[i]) { 340 mSupportedModes[modeCount] = i; 341 modeCount++; 342 } 343 } 344 345 initializeModeSelectorItems(); 346 } 347 348 // TODO: Initialize mode selectors with different sizes based on number of modes supported 349 private void initializeModeSelectorItems() { 350 mModeSelectorItems = new ModeSelectorItem[mTotalModes]; 351 // Inflate the mode selector items and add them to a linear layout 352 LayoutInflater inflater = (LayoutInflater) getContext() 353 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 354 mListView = (LinearLayout) findViewById(R.id.mode_list); 355 for (int i = 0; i < mTotalModes; i++) { 356 ModeSelectorItem selectorItem = 357 (ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null); 358 mListView.addView(selectorItem); 359 // Set alternating background color for each mode selector in the list 360 if (i % 2 == 0) { 361 selectorItem.setDefaultBackgroundColor(getResources() 362 .getColor(R.color.mode_selector_background_light)); 363 } else { 364 selectorItem.setDefaultBackgroundColor(getResources() 365 .getColor(R.color.mode_selector_background_dark)); 366 } 367 int modeId = getModeIndex(i); 368 selectorItem.setIconBackgroundColor(getResources() 369 .getColor(mModes[modeId].colorId)); 370 371 // Set image 372 selectorItem.setImageResource(mModes[modeId].iconResId); 373 374 // Set text 375 CharSequence text = getResources().getText(mModes[modeId].textResId); 376 selectorItem.setText(text); 377 mModeSelectorItems[i] = selectorItem; 378 } 379 380 resetModeSelectors(); 381 } 382 383 /** 384 * Maps between the UI mode selector index to the actual mode id. 385 * 386 * @param modeSelectorIndex the index of the UI item 387 * @return the index of the corresponding camera mode 388 */ 389 private int getModeIndex(int modeSelectorIndex) { 390 if (modeSelectorIndex < mTotalModes && modeSelectorIndex >= 0) { 391 return mSupportedModes[modeSelectorIndex]; 392 } 393 Log.e(TAG, "Invalid mode selector index: " + modeSelectorIndex + ", total modes: " 394 + mTotalModes); 395 return MODE_PHOTO; 396 } 397 398 /** Notify ModeSwitchListener, if any, of the mode change. */ 399 private void onModeSelected(int modeIndex) { 400 if (mListener != null) { 401 mListener.onModeSelected(modeIndex); 402 } 403 } 404 405 /** 406 * Sets a listener that listens to receive mode switch event. 407 * 408 * @param listener a listener that gets notified when mode changes. 409 */ 410 public void setModeSwitchListener(ModeSwitchListener listener) { 411 mListener = listener; 412 } 413 414 @Override 415 public boolean onTouchEvent(MotionEvent ev) { 416 if (mCurrentEffect != null) { 417 return mCurrentEffect.onTouchEvent(ev); 418 } 419 420 super.onTouchEvent(ev); 421 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 422 getParent().requestDisallowInterceptTouchEvent(true); 423 if (mState == FULLY_SHOWN) { 424 mFocusItem = NO_ITEM_SELECTED; 425 setSwipeMode(false); 426 } else { 427 mFocusItem = getFocusItem(ev.getX(), ev.getY()); 428 setSwipeMode(true); 429 } 430 } 431 // Pass all touch events to gesture detector for gesture handling. 432 mGestureDetector.onTouchEvent(ev); 433 if (ev.getActionMasked() == MotionEvent.ACTION_UP || 434 ev.getActionMasked() == MotionEvent.ACTION_CANCEL) { 435 snap(); 436 mFocusItem = NO_ITEM_SELECTED; 437 } 438 return true; 439 } 440 441 /** 442 * Sets the swipe mode to indicate whether this is a swiping in 443 * or out, and therefore we can have different animations. 444 * 445 * @param swipeIn indicates whether the swipe should reveal/hide the list. 446 */ 447 private void setSwipeMode(boolean swipeIn) { 448 for (int i = 0 ; i < mModeSelectorItems.length; i++) { 449 mModeSelectorItems[i].onSwipeModeChanged(swipeIn); 450 } 451 } 452 453 @Override 454 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 455 super.onLayout(changed, left, top, right, bottom); 456 mWidth = right - left; 457 mHeight = bottom - top - getPaddingTop() - getPaddingBottom(); 458 if (mCurrentEffect != null) { 459 mCurrentEffect.setSize(mWidth, mHeight); 460 } 461 } 462 463 /** 464 * Here we calculate the children size based on the orientation, change 465 * their layout parameters if needed before propagating onMeasure call 466 * to the children, so the newly changed params will take effect in this 467 * pass. 468 * 469 * @param widthMeasureSpec Horizontal space requirements as imposed by the 470 * parent 471 * @param heightMeasureSpec Vertical space requirements as imposed by the 472 * parent 473 */ 474 @Override 475 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 476 float height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() 477 - getPaddingBottom(); 478 479 Configuration config = getResources().getConfiguration(); 480 if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { 481 height = height / ROWS_TO_SHOW_IN_LANDSCAPE; 482 setVerticalScrollBarEnabled(true); 483 } else { 484 height = height / mTotalModes; 485 setVerticalScrollBarEnabled(false); 486 } 487 LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(mWidth, 0); 488 lp.width = LayoutParams.MATCH_PARENT; 489 for (int i = 0; i < mTotalModes; i++) { 490 // This is to avoid rounding that would cause the total height of the 491 // list a few pixels off the height of the screen. 492 int itemHeight = (int) (height * (i + 1)) - (int) (height * i); 493 lp.height = itemHeight; 494 mModeSelectorItems[i].setLayoutParams(lp); 495 } 496 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 497 } 498 499 @Override 500 public void draw(Canvas canvas) { 501 if (mCurrentEffect != null) { 502 mCurrentEffect.drawBackground(canvas); 503 super.draw(canvas); 504 mCurrentEffect.drawForeground(canvas); 505 } else { 506 super.draw(canvas); 507 } 508 } 509 510 /** 511 * This starts the accordion animation, unless it's already running, in which 512 * case the start animation call will be ignored. 513 */ 514 public void startAccordionAnimation() { 515 if (mState != IDLE) { 516 return; 517 } 518 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 519 return; 520 } 521 mState = ACCORDION_ANIMATION; 522 resetModeSelectors(); 523 animateListToWidth(START_DELAY_MS, TOTAL_DURATION_MS, mAccordionInterpolator, 524 0, mIconBlockWidth, 0); 525 } 526 527 /** 528 * This starts the accordion animation with a delay. 529 * 530 * @param delay delay in milliseconds before starting animation 531 */ 532 public void startAccordionAnimationWithDelay(int delay) { 533 postDelayed(new Runnable() { 534 @Override 535 public void run() { 536 startAccordionAnimation(); 537 } 538 }, delay); 539 } 540 541 /** 542 * Resets the visible width of all the mode selectors to 0. 543 */ 544 private void resetModeSelectors() { 545 for (int i = 0; i < mModeSelectorItems.length; i++) { 546 mModeSelectorItems[i].setVisibleWidth(0); 547 mModeSelectorItems[i].unHighlight(); 548 } 549 // Visible width has been changed to 0 550 onVisibleWidthChanged(0); 551 } 552 553 private boolean isRunningAccordionAnimation() { 554 return mAnimatorSet != null && mAnimatorSet.isRunning(); 555 } 556 557 /** 558 * Calculate the mode selector item in the list that is at position (x, y). 559 * 560 * @param x horizontal position 561 * @param y vertical position 562 * @return index of the item that is at position (x, y) 563 */ 564 private int getFocusItem(float x, float y) { 565 // Take into account the scrolling offset 566 x += getScrollX(); 567 y += getScrollY(); 568 569 for (int i = 0; i < mModeSelectorItems.length; i++) { 570 if (mModeSelectorItems[i].getTop() <= y && mModeSelectorItems[i].getBottom() >= y) { 571 return i; 572 } 573 } 574 return NO_ITEM_SELECTED; 575 } 576 577 private void scroll(int itemId, float deltaX, float deltaY) { 578 // Scrolling trend on X and Y axis, to track the trend by biasing 579 // towards latest touch events. 580 mScrollTrendX = mScrollTrendX * 0.3f + deltaX * 0.7f; 581 mScrollTrendY = mScrollTrendY * 0.3f + deltaY * 0.7f; 582 583 // TODO: Change how the curve is calculated below when UX finalize their design. 584 mCurrentTime = SystemClock.uptimeMillis(); 585 float longestWidth; 586 if (itemId != NO_ITEM_SELECTED) { 587 longestWidth = mModeSelectorItems[itemId].getVisibleWidth() - deltaX; 588 } else { 589 longestWidth = mModeSelectorItems[0].getVisibleWidth() - deltaX; 590 } 591 insertNewPosition(longestWidth, mCurrentTime); 592 593 for (int i = 0; i < mModeSelectorItems.length; i++) { 594 mModeSelectorItems[i].setVisibleWidth(calculateVisibleWidthForItem(i, 595 (int) longestWidth)); 596 } 597 if (longestWidth <= 0) { 598 reset(); 599 } 600 601 itemId = itemId == NO_ITEM_SELECTED ? 0 : itemId; 602 onVisibleWidthChanged(mModeSelectorItems[itemId].getVisibleWidth()); 603 } 604 605 /** 606 * Calculate the width of a specified item based on its position relative to 607 * the item with longest width. 608 */ 609 private int calculateVisibleWidthForItem(int itemId, int longestWidth) { 610 if (itemId == mFocusItem || mFocusItem == NO_ITEM_SELECTED) { 611 return longestWidth; 612 } 613 614 int delay = Math.abs(itemId - mFocusItem) * DELAY_MS; 615 return (int) getPosition(mCurrentTime - delay); 616 } 617 618 /** 619 * Insert new position and time stamp into the history position list, and 620 * remove stale position items. 621 * 622 * @param position latest position of the focus item 623 * @param time current time in milliseconds 624 */ 625 private void insertNewPosition(float position, long time) { 626 // TODO: Consider re-using stale position objects rather than 627 // always creating new position objects. 628 mPositionHistory.add(new TimeBasedPosition(position, time)); 629 630 // Positions that are from too long ago will not be of any use for 631 // future position interpolation. So we need to remove those positions 632 // from the list. 633 long timeCutoff = time - (mTotalModes - 1) * DELAY_MS; 634 while (mPositionHistory.size() > 0) { 635 // Remove all the position items that are prior to the cutoff time. 636 TimeBasedPosition historyPosition = mPositionHistory.getFirst(); 637 if (historyPosition.getTimeStamp() < timeCutoff) { 638 mPositionHistory.removeFirst(); 639 } else { 640 break; 641 } 642 } 643 } 644 645 /** 646 * Gets the interpolated position at the specified time. This involves going 647 * through the recorded positions until a {@link TimeBasedPosition} is found 648 * such that the position the recorded before the given time, and the 649 * {@link TimeBasedPosition} after that is recorded no earlier than the given 650 * time. These two positions are then interpolated to get the position at the 651 * specified time. 652 */ 653 private float getPosition(long time) { 654 int i; 655 for (i = 0; i < mPositionHistory.size(); i++) { 656 TimeBasedPosition historyPosition = mPositionHistory.get(i); 657 if (historyPosition.getTimeStamp() > time) { 658 // Found the winner. Now interpolate between position i and position i - 1 659 if (i == 0) { 660 return historyPosition.getPosition(); 661 } else { 662 TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1); 663 // Start interpolation 664 float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) / 665 (float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp()); 666 float position = fraction * (historyPosition.getPosition() 667 - prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition(); 668 return position; 669 } 670 } 671 } 672 // It should never get here. 673 Log.e(TAG, "Invalid time input for getPosition(). time: " + time); 674 if (mPositionHistory.size() == 0) { 675 Log.e(TAG, "TimeBasedPosition history size is 0"); 676 } else { 677 Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp() 678 + " , last position recorded at " + mPositionHistory.getLast().getTimeStamp()); 679 } 680 assert (i < mPositionHistory.size()); 681 return i; 682 } 683 684 private void reset() { 685 resetModeSelectors(); 686 mScrollTrendX = 0f; 687 mScrollTrendY = 0f; 688 mCurrentEffect = null; 689 setVisibility(INVISIBLE); 690 } 691 692 /** 693 * When visible width of list is changed, the background of the list needs 694 * to darken/lighten correspondingly. 695 */ 696 private void onVisibleWidthChanged(int focusItemWidth) { 697 // Background alpha should be 0 before the icon block is entirely visible, 698 // and when the longest mode item is entirely shown (across the screen), the 699 // background should be 50% transparent. 700 if (focusItemWidth <= mIconBlockWidth) { 701 setBackgroundAlpha(0); 702 } else { 703 // Alpha should increase linearly when mode item goes beyond the icon block 704 // till it reaches its max width 705 int alpha = 127 * (focusItemWidth - mIconBlockWidth) / (mWidth - mIconBlockWidth); 706 setBackgroundAlpha(alpha); 707 } 708 } 709 710 @Override 711 public void onWindowVisibilityChanged(int visibility) { 712 super.onWindowVisibilityChanged(visibility); 713 if (visibility != VISIBLE) { 714 // Reset mode list if the window is no longer visible. 715 reset(); 716 mState = IDLE; 717 } 718 } 719 720 /** 721 * The list view should either snap back or snap to full screen after a gesture. 722 * This function is called when an up or cancel event is received, and then based 723 * on the current position of the list and the gesture we can decide which way 724 * to snap. 725 */ 726 private void snap() { 727 if (mState == SCROLLING) { 728 int itemId = Math.max(0, mFocusItem); 729 if (mModeSelectorItems[itemId].getVisibleWidth() < mIconBlockWidth) { 730 snapBack(); 731 } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) { 732 snapBack(); 733 } else { 734 snapToFullScreen(); 735 } 736 } 737 } 738 739 /** 740 * Snaps back out of the screen. 741 * 742 * @param withAnimation whether snapping back should be animated 743 */ 744 public void snapBack(boolean withAnimation) { 745 if (withAnimation) { 746 animateListToWidth(0); 747 mState = IDLE; 748 } else { 749 setVisibility(INVISIBLE); 750 resetModeSelectors(); 751 mState = IDLE; 752 } 753 } 754 755 /** 756 * Snaps the mode list back out with animation. 757 */ 758 private void snapBack() { 759 snapBack(true); 760 } 761 762 private void snapToFullScreen() { 763 animateListToWidth(mWidth); 764 mState = FULLY_SHOWN; 765 } 766 767 /** 768 * Overloaded function to provide a simple way to start animation. Animation 769 * will use default duration, and a value of <code>null</code> for interpolator 770 * means linear interpolation will be used. 771 * 772 * @param width a set of values that the animation will animate between over time 773 */ 774 private void animateListToWidth(int... width) { 775 animateListToWidth(0, DEFAULT_DURATION_MS, null, width); 776 } 777 778 /** 779 * Animate the mode list between the given set of visible width. 780 * 781 * @param delay start delay between consecutive mode item 782 * @param duration duration for the animation of each mode item 783 * @param interpolator interpolator to be used by the animation 784 * @param width a set of values that the animation will animate between over time 785 */ 786 private void animateListToWidth(int delay, int duration, 787 TimeInterpolator interpolator, int... width) { 788 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 789 mAnimatorSet.end(); 790 } 791 792 ArrayList<Animator> animators = new ArrayList<Animator>(); 793 int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem; 794 for (int i = 0; i < mTotalModes; i++) { 795 ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i], 796 "visibleWidth", width); 797 animator.setDuration(duration); 798 animator.setStartDelay(i * delay); 799 animators.add(animator); 800 if (i == focusItem) { 801 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 802 @Override 803 public void onAnimationUpdate(ValueAnimator animation) { 804 onVisibleWidthChanged((Integer) animation.getAnimatedValue()); 805 } 806 }); 807 } 808 } 809 810 mAnimatorSet = new AnimatorSet(); 811 mAnimatorSet.playTogether(animators); 812 mAnimatorSet.setInterpolator(interpolator); 813 mAnimatorSet.addListener(new Animator.AnimatorListener() { 814 815 @Override 816 public void onAnimationStart(Animator animation) { 817 setVisibility(VISIBLE); 818 } 819 820 @Override 821 public void onAnimationEnd(Animator animation) { 822 mAnimatorSet = null; 823 if (mState == ACCORDION_ANIMATION || mState == IDLE) { 824 resetModeSelectors(); 825 setVisibility(INVISIBLE); 826 mState = IDLE; 827 } 828 } 829 830 @Override 831 public void onAnimationCancel(Animator animation) { 832 } 833 834 @Override 835 public void onAnimationRepeat(Animator animation) { 836 837 } 838 }); 839 mAnimatorSet.start(); 840 } 841 842 /** 843 * Get the theme color of a specific mode. 844 * 845 * @param modeIndex index of the mode 846 * @return theme color of the mode if input index is valid, otherwise 0 847 */ 848 public static int getModeThemeColor(int modeIndex) { 849 // Photo and gcam has the same theme color 850 if (modeIndex == MODE_GCAM) { 851 return mModes[MODE_PHOTO].colorId; 852 } 853 if (modeIndex < 0 || modeIndex >= MODE_TOTAL) { 854 return 0; 855 } else { 856 return mModes[modeIndex].colorId; 857 } 858 } 859 860 /** 861 * Get the mode icon resource id of a specific mode. 862 * 863 * @param modeIndex index of the mode 864 * @return icon resource id if the index is valid, otherwise 0 865 */ 866 public static int getModeIconResourceId(int modeIndex) { 867 // Photo and gcam has the same mode icon 868 if (modeIndex == MODE_GCAM) { 869 return mModes[MODE_PHOTO].iconResId; 870 } 871 if (modeIndex < 0 || modeIndex >= MODE_TOTAL) { 872 return 0; 873 } else { 874 return mModes[modeIndex].iconResId; 875 } 876 } 877 878 /** 879 * Called when the back key is pressed. 880 * 881 * @return Whether the UI responded to the key event. 882 */ 883 public boolean onBackPressed() { 884 if (mState == FULLY_SHOWN) { 885 snapBack(); 886 return true; 887 } else { 888 return false; 889 } 890 } 891 892 public void startModeSelectionAnimation() { 893 if (mState != MODE_SELECTED || mCurrentEffect == null) { 894 setVisibility(INVISIBLE); 895 snapBack(false); 896 mCurrentEffect = null; 897 } else { 898 mCurrentEffect.startAnimation(); 899 } 900 901 } 902 903 private class PeepholeAnimationEffect extends AnimationEffects { 904 905 private final static int UNSET = -1; 906 private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 650; 907 908 private int mWidth; 909 private int mHeight; 910 911 private int mPeepHoleCenterX = UNSET; 912 private int mPeepHoleCenterY = UNSET; 913 private float mRadius = 0f; 914 private ValueAnimator mPeepHoleAnimator; 915 private Runnable mEndAction; 916 private final Paint mMaskPaint = new Paint(); 917 918 public PeepholeAnimationEffect() { 919 mMaskPaint.setAlpha(0); 920 mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 921 } 922 923 @Override 924 public void setSize(int width, int height) { 925 mWidth = width; 926 mHeight = height; 927 } 928 929 @Override 930 public void drawForeground(Canvas canvas) { 931 // Draw the circle in clear mode 932 if (mPeepHoleAnimator != null) { 933 // Draw a transparent circle using clear mode 934 canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint); 935 } 936 } 937 938 public void setAnimationStartingPosition(int x, int y) { 939 mPeepHoleCenterX = x; 940 mPeepHoleCenterY = y; 941 } 942 943 @Override 944 public void startAnimation() { 945 if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) { 946 return; 947 } 948 if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) { 949 mPeepHoleCenterX = mWidth / 2; 950 mPeepHoleCenterY = mHeight / 2; 951 } 952 953 int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX); 954 int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY); 955 int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge 956 + verticalDistanceToFarEdge * verticalDistanceToFarEdge)); 957 958 mPeepHoleAnimator = ValueAnimator.ofFloat(0, endRadius); 959 mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS); 960 mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE); 961 mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 962 @Override 963 public void onAnimationUpdate(ValueAnimator animation) { 964 // Modify mask by enlarging the hole 965 mRadius = (Float) mPeepHoleAnimator.getAnimatedValue(); 966 invalidate(); 967 } 968 }); 969 970 mPeepHoleAnimator.addListener(new Animator.AnimatorListener() { 971 @Override 972 public void onAnimationStart(Animator animation) { 973 974 } 975 976 @Override 977 public void onAnimationEnd(Animator animation) { 978 if (mEndAction != null) { 979 post(mEndAction); 980 mEndAction = null; 981 post(new Runnable() { 982 @Override 983 public void run() { 984 mPeepHoleAnimator = null; 985 mRadius = 0; 986 mPeepHoleCenterX = UNSET; 987 mPeepHoleCenterY = UNSET; 988 } 989 }); 990 } else { 991 mPeepHoleAnimator = null; 992 mRadius = 0; 993 mPeepHoleCenterX = UNSET; 994 mPeepHoleCenterY = UNSET; 995 } 996 } 997 998 @Override 999 public void onAnimationCancel(Animator animation) { 1000 1001 } 1002 1003 @Override 1004 public void onAnimationRepeat(Animator animation) { 1005 1006 } 1007 }); 1008 mPeepHoleAnimator.start(); 1009 } 1010 1011 public void setAnimationEndAction(Runnable runnable) { 1012 mEndAction = runnable; 1013 } 1014 } 1015} 1016