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