StackView.java revision a32edd4b4c894f4fb3d9fd7e9d5b80321df79e20
1/* 2 * Copyright (C) 2010 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 android.widget; 18 19import android.animation.ObjectAnimator; 20import android.animation.PropertyValuesHolder; 21import android.content.Context; 22import android.graphics.Bitmap; 23import android.graphics.BlurMaskFilter; 24import android.graphics.Canvas; 25import android.graphics.Matrix; 26import android.graphics.Paint; 27import android.graphics.PorterDuff; 28import android.graphics.PorterDuffXfermode; 29import android.graphics.Rect; 30import android.graphics.RectF; 31import android.graphics.Region; 32import android.graphics.TableMaskFilter; 33import android.util.AttributeSet; 34import android.util.Log; 35import android.view.MotionEvent; 36import android.view.VelocityTracker; 37import android.view.View; 38import android.view.ViewConfiguration; 39import android.view.ViewGroup; 40import android.view.animation.LinearInterpolator; 41import android.widget.RemoteViews.RemoteView; 42 43@RemoteView 44/** 45 * A view that displays its children in a stack and allows users to discretely swipe 46 * through the children. 47 */ 48public class StackView extends AdapterViewAnimator { 49 private final String TAG = "StackView"; 50 51 /** 52 * Default animation parameters 53 */ 54 private static final int DEFAULT_ANIMATION_DURATION = 400; 55 private static final int MINIMUM_ANIMATION_DURATION = 50; 56 57 /** 58 * Parameters effecting the perspective visuals 59 */ 60 private static float PERSPECTIVE_SHIFT_FACTOR = 0.12f; 61 @SuppressWarnings({"FieldCanBeLocal"}) 62 private static float PERSPECTIVE_SCALE_FACTOR = 0.35f; 63 64 /** 65 * Represent the two possible stack modes, one where items slide up, and the other 66 * where items slide down. The perspective is also inverted between these two modes. 67 */ 68 private static final int ITEMS_SLIDE_UP = 0; 69 private static final int ITEMS_SLIDE_DOWN = 1; 70 71 /** 72 * These specify the different gesture states 73 */ 74 private static final int GESTURE_NONE = 0; 75 private static final int GESTURE_SLIDE_UP = 1; 76 private static final int GESTURE_SLIDE_DOWN = 2; 77 78 /** 79 * Specifies how far you need to swipe (up or down) before it 80 * will be consider a completed gesture when you lift your finger 81 */ 82 private static final float SWIPE_THRESHOLD_RATIO = 0.2f; 83 84 /** 85 * Specifies the total distance, relative to the size of the stack, 86 * that views will be slid, either up or down 87 */ 88 private static final float SLIDE_UP_RATIO = 0.7f; 89 90 /** 91 * Sentinel value for no current active pointer. 92 * Used by {@link #mActivePointerId}. 93 */ 94 private static final int INVALID_POINTER = -1; 95 96 /** 97 * Number of active views in the stack. One fewer view is actually visible, as one is hidden. 98 */ 99 private static final int NUM_ACTIVE_VIEWS = 5; 100 101 private static final int FRAME_PADDING = 4; 102 103 /** 104 * These variables are all related to the current state of touch interaction 105 * with the stack 106 */ 107 private float mInitialY; 108 private float mInitialX; 109 private int mActivePointerId; 110 private int mYVelocity = 0; 111 private int mSwipeGestureType = GESTURE_NONE; 112 private int mSlideAmount; 113 private int mSwipeThreshold; 114 private int mTouchSlop; 115 private int mMaximumVelocity; 116 private VelocityTracker mVelocityTracker; 117 private boolean mTransitionIsSetup = false; 118 119 private static HolographicHelper sHolographicHelper; 120 private ImageView mHighlight; 121 private StackSlider mStackSlider; 122 private boolean mFirstLayoutHappened = false; 123 private int mStackMode; 124 private int mFramePadding; 125 private final Rect invalidateRect = new Rect(); 126 127 public StackView(Context context) { 128 super(context); 129 initStackView(); 130 } 131 132 public StackView(Context context, AttributeSet attrs) { 133 super(context, attrs); 134 initStackView(); 135 } 136 137 private void initStackView() { 138 configureViewAnimator(NUM_ACTIVE_VIEWS, 1); 139 setStaticTransformationsEnabled(true); 140 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 141 mTouchSlop = configuration.getScaledTouchSlop(); 142 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 143 mActivePointerId = INVALID_POINTER; 144 145 mHighlight = new ImageView(getContext()); 146 mHighlight.setLayoutParams(new LayoutParams(mHighlight)); 147 addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight)); 148 mStackSlider = new StackSlider(); 149 150 if (sHolographicHelper == null) { 151 sHolographicHelper = new HolographicHelper(mContext); 152 } 153 setClipChildren(false); 154 setClipToPadding(false); 155 156 // This sets the form of the StackView, which is currently to have the perspective-shifted 157 // views above the active view, and have items slide down when sliding out. The opposite is 158 // available by using ITEMS_SLIDE_UP. 159 mStackMode = ITEMS_SLIDE_DOWN; 160 161 // This is a flag to indicate the the stack is loading for the first time 162 mWhichChild = -1; 163 164 // Adjust the frame padding based on the density, since the highlight changes based 165 // on the density 166 final float density = mContext.getResources().getDisplayMetrics().density; 167 mFramePadding = (int) Math.ceil(density * FRAME_PADDING); 168 } 169 170 /** 171 * Animate the views between different relative indexes within the {@link AdapterViewAnimator} 172 */ 173 void animateViewForTransition(int fromIndex, int toIndex, View view) { 174 if (fromIndex == -1 && toIndex != 0) { 175 // Fade item in 176 if (view.getAlpha() == 1) { 177 view.setAlpha(0); 178 } 179 view.setVisibility(VISIBLE); 180 181 ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 1.0f); 182 fadeIn.setDuration(DEFAULT_ANIMATION_DURATION); 183 fadeIn.start(); 184 } else if (fromIndex == 0 && toIndex == 1) { 185 // Slide item in 186 view.setVisibility(VISIBLE); 187 188 int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity)); 189 190 StackSlider animationSlider = new StackSlider(mStackSlider); 191 PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f); 192 PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 193 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 194 slideInX, slideInY); 195 pa.setDuration(duration); 196 pa.setInterpolator(new LinearInterpolator()); 197 pa.start(); 198 } else if (fromIndex == 1 && toIndex == 0) { 199 // Slide item out 200 int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity)); 201 202 StackSlider animationSlider = new StackSlider(mStackSlider); 203 PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f); 204 PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 205 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 206 slideOutX, slideOutY); 207 pa.setDuration(duration); 208 pa.setInterpolator(new LinearInterpolator()); 209 pa.start(); 210 } else if (fromIndex == -1 && toIndex == 0) { 211 // Make sure this view that is "waiting in the wings" is invisible 212 view.setAlpha(0.0f); 213 view.setVisibility(INVISIBLE); 214 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 215 lp.setVerticalOffset(-mSlideAmount); 216 } else if (toIndex == -1) { 217 // Fade item out 218 ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 0.0f); 219 fadeOut.setDuration(DEFAULT_ANIMATION_DURATION); 220 fadeOut.start(); 221 } 222 223 // Implement the faked perspective 224 if (toIndex != -1) { 225 transformViewAtIndex(toIndex, view); 226 } 227 } 228 229 private void setupStackSlider(View v, int mode) { 230 mStackSlider.setMode(mode); 231 if (v != null) { 232 mHighlight.setImageBitmap(sHolographicHelper.createOutline(v)); 233 mHighlight.setRotation(v.getRotation()); 234 mHighlight.setTranslationY(v.getTranslationY()); 235 mHighlight.bringToFront(); 236 v.bringToFront(); 237 mStackSlider.setView(v); 238 239 v.setVisibility(VISIBLE); 240 } 241 } 242 243 @Override 244 @android.view.RemotableViewMethod 245 public void showNext() { 246 if (!mTransitionIsSetup) { 247 View v = getViewAtRelativeIndex(1); 248 if (v != null) { 249 setupStackSlider(v, StackSlider.NORMAL_MODE); 250 mStackSlider.setYProgress(0); 251 mStackSlider.setXProgress(0); 252 } 253 } 254 super.showNext(); 255 } 256 257 @Override 258 @android.view.RemotableViewMethod 259 public void showPrevious() { 260 if (!mTransitionIsSetup) { 261 View v = getViewAtRelativeIndex(0); 262 if (v != null) { 263 setupStackSlider(v, StackSlider.NORMAL_MODE); 264 mStackSlider.setYProgress(1); 265 mStackSlider.setXProgress(0); 266 } 267 } 268 super.showPrevious(); 269 } 270 271 private void transformViewAtIndex(int index, View view) { 272 float maxPerpectiveShift = mMeasuredHeight * PERSPECTIVE_SHIFT_FACTOR; 273 274 index = mMaxNumActiveViews - index - 1; 275 if (index == mMaxNumActiveViews - 1) index--; 276 277 float r = (index * 1.0f) / (mMaxNumActiveViews - 2); 278 279 float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r); 280 PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", scale); 281 PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", scale); 282 283 r = (float) Math.pow(r, 2); 284 285 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1; 286 float perspectiveTranslation = -stackDirection * r * maxPerpectiveShift; 287 float scaleShiftCorrection = stackDirection * (1 - scale) * 288 (mMeasuredHeight * (1 - PERSPECTIVE_SHIFT_FACTOR) / 2.0f); 289 float transY = perspectiveTranslation + scaleShiftCorrection; 290 291 PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY); 292 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(view, scaleX, scaleY, translationY); 293 pa.setDuration(100); 294 pa.start(); 295 } 296 297 @Override 298 void showOnly(int childIndex, boolean animate, boolean onLayout) { 299 super.showOnly(childIndex, animate, onLayout); 300 301 // Here we need to make sure that the z-order of the children is correct 302 for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) { 303 int index = modulo(i, getWindowSize()); 304 View v = mViewsMap.get(index).view; 305 if (v != null) v.bringToFront(); 306 } 307 mTransitionIsSetup = false; 308 } 309 310 private void updateChildTransforms() { 311 for (int i = 0; i < getNumActiveViews(); i++) { 312 View v = getViewAtRelativeIndex(i); 313 if (v != null) { 314 transformViewAtIndex(i, v); 315 } 316 } 317 } 318 319 @Override 320 FrameLayout getFrameForChild() { 321 FrameLayout fl = new FrameLayout(mContext); 322 fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding); 323 return fl; 324 } 325 326 /** 327 * Apply any necessary tranforms for the child that is being added. 328 */ 329 void applyTransformForChildAtIndex(View child, int relativeIndex) { 330 } 331 332 @Override 333 protected void dispatchDraw(Canvas canvas) { 334 canvas.getClipBounds(invalidateRect); 335 final int childCount = getChildCount(); 336 for (int i = 0; i < childCount; i++) { 337 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 338 invalidateRect.union(lp.getInvalidateRect()); 339 lp.resetInvalidateRect(); 340 } 341 342 canvas.save(Canvas.CLIP_SAVE_FLAG); 343 canvas.clipRect(invalidateRect, Region.Op.UNION); 344 super.dispatchDraw(canvas); 345 canvas.restore(); 346 } 347 348 private void onLayout() { 349 if (!mFirstLayoutHappened) { 350 mSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight()); 351 updateChildTransforms(); 352 mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * mSlideAmount); 353 mFirstLayoutHappened = true; 354 } 355 } 356 357 @Override 358 public boolean onInterceptTouchEvent(MotionEvent ev) { 359 int action = ev.getAction(); 360 switch(action & MotionEvent.ACTION_MASK) { 361 case MotionEvent.ACTION_DOWN: { 362 if (mActivePointerId == INVALID_POINTER) { 363 mInitialX = ev.getX(); 364 mInitialY = ev.getY(); 365 mActivePointerId = ev.getPointerId(0); 366 } 367 break; 368 } 369 case MotionEvent.ACTION_MOVE: { 370 int pointerIndex = ev.findPointerIndex(mActivePointerId); 371 if (pointerIndex == INVALID_POINTER) { 372 // no data for our primary pointer, this shouldn't happen, log it 373 Log.d(TAG, "Error: No data for our primary pointer."); 374 return false; 375 } 376 float newY = ev.getY(pointerIndex); 377 float deltaY = newY - mInitialY; 378 379 beginGestureIfNeeded(deltaY); 380 break; 381 } 382 case MotionEvent.ACTION_POINTER_UP: { 383 onSecondaryPointerUp(ev); 384 break; 385 } 386 case MotionEvent.ACTION_UP: 387 case MotionEvent.ACTION_CANCEL: { 388 mActivePointerId = INVALID_POINTER; 389 mSwipeGestureType = GESTURE_NONE; 390 } 391 } 392 393 return mSwipeGestureType != GESTURE_NONE; 394 } 395 396 private void beginGestureIfNeeded(float deltaY) { 397 if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) { 398 int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN; 399 cancelLongPress(); 400 requestDisallowInterceptTouchEvent(true); 401 402 if (mAdapter == null) return; 403 404 int activeIndex; 405 if (mStackMode == ITEMS_SLIDE_UP) { 406 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1; 407 } else { 408 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0; 409 } 410 411 int stackMode; 412 if (mLoopViews) { 413 stackMode = StackSlider.NORMAL_MODE; 414 } else if (mCurrentWindowStartUnbounded + activeIndex == -1) { 415 activeIndex++; 416 stackMode = StackSlider.BEGINNING_OF_STACK_MODE; 417 } else if (mCurrentWindowStartUnbounded + activeIndex == mAdapter.getCount() - 1) { 418 stackMode = StackSlider.END_OF_STACK_MODE; 419 } else { 420 stackMode = StackSlider.NORMAL_MODE; 421 } 422 423 mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE; 424 425 View v = getViewAtRelativeIndex(activeIndex); 426 if (v == null) return; 427 428 setupStackSlider(v, stackMode); 429 430 // We only register this gesture if we've made it this far without a problem 431 mSwipeGestureType = swipeGestureType; 432 cancelHandleClick(); 433 } 434 } 435 436 @Override 437 public boolean onTouchEvent(MotionEvent ev) { 438 super.onTouchEvent(ev); 439 440 int action = ev.getAction(); 441 int pointerIndex = ev.findPointerIndex(mActivePointerId); 442 if (pointerIndex == INVALID_POINTER) { 443 // no data for our primary pointer, this shouldn't happen, log it 444 Log.d(TAG, "Error: No data for our primary pointer."); 445 return false; 446 } 447 448 float newY = ev.getY(pointerIndex); 449 float newX = ev.getX(pointerIndex); 450 float deltaY = newY - mInitialY; 451 float deltaX = newX - mInitialX; 452 if (mVelocityTracker == null) { 453 mVelocityTracker = VelocityTracker.obtain(); 454 } 455 mVelocityTracker.addMovement(ev); 456 457 switch (action & MotionEvent.ACTION_MASK) { 458 case MotionEvent.ACTION_MOVE: { 459 beginGestureIfNeeded(deltaY); 460 461 float rx = deltaX / (mSlideAmount * 1.0f); 462 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 463 float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f; 464 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; 465 mStackSlider.setYProgress(1 - r); 466 mStackSlider.setXProgress(rx); 467 return true; 468 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) { 469 float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f; 470 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; 471 mStackSlider.setYProgress(r); 472 mStackSlider.setXProgress(rx); 473 return true; 474 } 475 break; 476 } 477 case MotionEvent.ACTION_UP: { 478 handlePointerUp(ev); 479 break; 480 } 481 case MotionEvent.ACTION_POINTER_UP: { 482 onSecondaryPointerUp(ev); 483 break; 484 } 485 case MotionEvent.ACTION_CANCEL: { 486 mActivePointerId = INVALID_POINTER; 487 mSwipeGestureType = GESTURE_NONE; 488 break; 489 } 490 } 491 return true; 492 } 493 494 private final Rect touchRect = new Rect(); 495 private void onSecondaryPointerUp(MotionEvent ev) { 496 final int activePointerIndex = ev.getActionIndex(); 497 final int pointerId = ev.getPointerId(activePointerIndex); 498 if (pointerId == mActivePointerId) { 499 500 int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1; 501 502 View v = getViewAtRelativeIndex(activeViewIndex); 503 if (v == null) return; 504 505 // Our primary pointer has gone up -- let's see if we can find 506 // another pointer on the view. If so, then we should replace 507 // our primary pointer with this new pointer and adjust things 508 // so that the view doesn't jump 509 for (int index = 0; index < ev.getPointerCount(); index++) { 510 if (index != activePointerIndex) { 511 512 float x = ev.getX(index); 513 float y = ev.getY(index); 514 515 touchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 516 if (touchRect.contains(Math.round(x), Math.round(y))) { 517 float oldX = ev.getX(activePointerIndex); 518 float oldY = ev.getY(activePointerIndex); 519 520 // adjust our frame of reference to avoid a jump 521 mInitialY += (y - oldY); 522 mInitialX += (x - oldX); 523 524 mActivePointerId = ev.getPointerId(index); 525 if (mVelocityTracker != null) { 526 mVelocityTracker.clear(); 527 } 528 // ok, we're good, we found a new pointer which is touching the active view 529 return; 530 } 531 } 532 } 533 // if we made it this far, it means we didn't find a satisfactory new pointer :(, 534 // so end the gesture 535 handlePointerUp(ev); 536 } 537 } 538 539 private void handlePointerUp(MotionEvent ev) { 540 int pointerIndex = ev.findPointerIndex(mActivePointerId); 541 float newY = ev.getY(pointerIndex); 542 int deltaY = (int) (newY - mInitialY); 543 544 if (mVelocityTracker != null) { 545 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 546 mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); 547 } 548 549 if (mVelocityTracker != null) { 550 mVelocityTracker.recycle(); 551 mVelocityTracker = null; 552 } 553 554 if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN 555 && mStackSlider.mMode == StackSlider.NORMAL_MODE) { 556 // Swipe threshold exceeded, swipe down 557 if (mStackMode == ITEMS_SLIDE_UP) { 558 showPrevious(); 559 } else { 560 showNext(); 561 } 562 mHighlight.bringToFront(); 563 } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP 564 && mStackSlider.mMode == StackSlider.NORMAL_MODE) { 565 // Swipe threshold exceeded, swipe up 566 if (mStackMode == ITEMS_SLIDE_UP) { 567 showNext(); 568 } else { 569 showPrevious(); 570 } 571 572 mHighlight.bringToFront(); 573 } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) { 574 // Didn't swipe up far enough, snap back down 575 int duration; 576 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0; 577 if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) { 578 duration = Math.round(mStackSlider.getDurationForNeutralPosition()); 579 } else { 580 duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); 581 } 582 583 StackSlider animationSlider = new StackSlider(mStackSlider); 584 PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress); 585 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 586 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 587 snapBackX, snapBackY); 588 pa.setDuration(duration); 589 pa.setInterpolator(new LinearInterpolator()); 590 pa.start(); 591 } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 592 // Didn't swipe down far enough, snap back up 593 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1; 594 int duration; 595 if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) { 596 duration = Math.round(mStackSlider.getDurationForNeutralPosition()); 597 } else { 598 duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); 599 } 600 601 StackSlider animationSlider = new StackSlider(mStackSlider); 602 PropertyValuesHolder snapBackY = 603 PropertyValuesHolder.ofFloat("YProgress",finalYProgress); 604 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 605 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 606 snapBackX, snapBackY); 607 pa.setDuration(duration); 608 pa.start(); 609 } 610 611 mActivePointerId = INVALID_POINTER; 612 mSwipeGestureType = GESTURE_NONE; 613 } 614 615 private class StackSlider { 616 View mView; 617 float mYProgress; 618 float mXProgress; 619 620 static final int NORMAL_MODE = 0; 621 static final int BEGINNING_OF_STACK_MODE = 1; 622 static final int END_OF_STACK_MODE = 2; 623 624 int mMode = NORMAL_MODE; 625 626 public StackSlider() { 627 } 628 629 public StackSlider(StackSlider copy) { 630 mView = copy.mView; 631 mYProgress = copy.mYProgress; 632 mXProgress = copy.mXProgress; 633 mMode = copy.mMode; 634 } 635 636 private float cubic(float r) { 637 return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f; 638 } 639 640 private float highlightAlphaInterpolator(float r) { 641 float pivot = 0.4f; 642 if (r < pivot) { 643 return 0.85f * cubic(r / pivot); 644 } else { 645 return 0.85f * cubic(1 - (r - pivot) / (1 - pivot)); 646 } 647 } 648 649 private float viewAlphaInterpolator(float r) { 650 float pivot = 0.3f; 651 if (r > pivot) { 652 return (r - pivot) / (1 - pivot); 653 } else { 654 return 0; 655 } 656 } 657 658 private float rotationInterpolator(float r) { 659 float pivot = 0.2f; 660 if (r < pivot) { 661 return 0; 662 } else { 663 return (r - pivot) / (1 - pivot); 664 } 665 } 666 667 void setView(View v) { 668 mView = v; 669 } 670 671 public void setYProgress(float r) { 672 // enforce r between 0 and 1 673 r = Math.min(1.0f, r); 674 r = Math.max(0, r); 675 676 mYProgress = r; 677 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 678 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 679 680 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1; 681 682 switch (mMode) { 683 case NORMAL_MODE: 684 viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); 685 highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); 686 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 687 688 float alpha = viewAlphaInterpolator(1 - r); 689 690 // We make sure that views which can't be seen (have 0 alpha) are also invisible 691 // so that they don't interfere with click events. 692 if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) { 693 mView.setVisibility(VISIBLE); 694 } else if (alpha == 0 && mView.getAlpha() != 0 695 && mView.getVisibility() == VISIBLE) { 696 mView.setVisibility(INVISIBLE); 697 } 698 699 mView.setAlpha(alpha); 700 mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); 701 mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); 702 break; 703 case END_OF_STACK_MODE: 704 r = r * 0.2f; 705 viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount)); 706 highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount)); 707 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 708 break; 709 case BEGINNING_OF_STACK_MODE: 710 r = (1-r) * 0.2f; 711 viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount)); 712 highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount)); 713 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 714 break; 715 } 716 } 717 718 public void setXProgress(float r) { 719 // enforce r between 0 and 1 720 r = Math.min(2.0f, r); 721 r = Math.max(-2.0f, r); 722 723 mXProgress = r; 724 725 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 726 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 727 728 r *= 0.2f; 729 viewLp.setHorizontalOffset(Math.round(r * mSlideAmount)); 730 highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount)); 731 } 732 733 void setMode(int mode) { 734 mMode = mode; 735 } 736 737 float getDurationForNeutralPosition() { 738 return getDuration(false, 0); 739 } 740 741 float getDurationForOffscreenPosition() { 742 return getDuration(true, 0); 743 } 744 745 float getDurationForNeutralPosition(float velocity) { 746 return getDuration(false, velocity); 747 } 748 749 float getDurationForOffscreenPosition(float velocity) { 750 return getDuration(true, velocity); 751 } 752 753 private float getDuration(boolean invert, float velocity) { 754 if (mView != null) { 755 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 756 757 float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) + 758 Math.pow(viewLp.verticalOffset, 2)); 759 float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) + 760 Math.pow(0.4f * mSlideAmount, 2)); 761 762 if (velocity == 0) { 763 return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION; 764 } else { 765 float duration = invert ? d / Math.abs(velocity) : 766 (maxd - d) / Math.abs(velocity); 767 if (duration < MINIMUM_ANIMATION_DURATION || 768 duration > DEFAULT_ANIMATION_DURATION) { 769 return getDuration(invert, 0); 770 } else { 771 return duration; 772 } 773 } 774 } 775 return 0; 776 } 777 778 // Used for animations 779 @SuppressWarnings({"UnusedDeclaration"}) 780 public float getYProgress() { 781 return mYProgress; 782 } 783 784 // Used for animations 785 @SuppressWarnings({"UnusedDeclaration"}) 786 public float getXProgress() { 787 return mXProgress; 788 } 789 } 790 791 @Override 792 public void onRemoteAdapterConnected() { 793 super.onRemoteAdapterConnected(); 794 // On first run, we want to set the stack to the end. 795 if (mWhichChild == -1) { 796 mWhichChild = 0; 797 } 798 setDisplayedChild(mWhichChild); 799 } 800 801 LayoutParams createOrReuseLayoutParams(View v) { 802 final ViewGroup.LayoutParams currentLp = v.getLayoutParams(); 803 if (currentLp instanceof LayoutParams) { 804 LayoutParams lp = (LayoutParams) currentLp; 805 lp.setHorizontalOffset(0); 806 lp.setVerticalOffset(0); 807 lp.width = 0; 808 lp.width = 0; 809 return lp; 810 } 811 return new LayoutParams(v); 812 } 813 814 @Override 815 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 816 boolean dataChanged = mDataChanged; 817 if (dataChanged) { 818 handleDataChanged(); 819 820 // if the data changes, mWhichChild might be out of the bounds of the adapter 821 // in this case, we reset mWhichChild to the beginning 822 if (mWhichChild >= mAdapter.getCount()) 823 mWhichChild = 0; 824 825 showOnly(mWhichChild, true, true); 826 refreshChildren(); 827 } 828 829 final int childCount = getChildCount(); 830 for (int i = 0; i < childCount; i++) { 831 final View child = getChildAt(i); 832 833 int childRight = mPaddingLeft + child.getMeasuredWidth(); 834 int childBottom = mPaddingTop + child.getMeasuredHeight(); 835 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 836 837 child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset, 838 childRight + lp.horizontalOffset, childBottom + lp.verticalOffset); 839 840 } 841 842 mDataChanged = false; 843 onLayout(); 844 } 845 846 private void measureChildren() { 847 final int count = getChildCount(); 848 final int childWidth = mMeasuredWidth - mPaddingLeft - mPaddingRight; 849 final int childHeight = Math.round(mMeasuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR)) 850 - mPaddingTop - mPaddingBottom; 851 852 for (int i = 0; i < count; i++) { 853 final View child = getChildAt(i); 854 child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), 855 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)); 856 } 857 } 858 859 @Override 860 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 861 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 862 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 863 final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 864 final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 865 866 boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1); 867 868 // We need to deal with the case where our parent hasn't told us how 869 // big we should be. In this case we should 870 float factor = 1/(1 - PERSPECTIVE_SHIFT_FACTOR); 871 if (heightSpecMode == MeasureSpec.UNSPECIFIED) { 872 heightSpecSize = haveChildRefSize ? 873 Math.round(mReferenceChildHeight * (1 + factor)) + 874 mPaddingTop + mPaddingBottom : 0; 875 } else if (heightSpecMode == MeasureSpec.AT_MOST) { 876 heightSpecSize = haveChildRefSize ? Math.min( 877 Math.round(mReferenceChildHeight * (1 + factor)) + mPaddingTop + 878 mPaddingBottom, heightSpecSize) : 0; 879 } 880 881 if (widthSpecMode == MeasureSpec.UNSPECIFIED) { 882 widthSpecSize = haveChildRefSize ? mReferenceChildWidth + mPaddingLeft + 883 mPaddingRight : 0; 884 } else if (heightSpecMode == MeasureSpec.AT_MOST) { 885 widthSpecSize = haveChildRefSize ? Math.min(mReferenceChildWidth + mPaddingLeft + 886 mPaddingRight, widthSpecSize) : 0; 887 } 888 889 setMeasuredDimension(widthSpecSize, heightSpecSize); 890 measureChildren(); 891 } 892 893 class LayoutParams extends ViewGroup.LayoutParams { 894 int horizontalOffset; 895 int verticalOffset; 896 View mView; 897 int left, top, right, bottom; 898 private final Rect parentRect = new Rect(); 899 private final Rect invalidateRect = new Rect(); 900 private final RectF invalidateRectf = new RectF(); 901 private final Rect globalInvalidateRect = new Rect(); 902 903 LayoutParams(View view) { 904 super(0, 0); 905 width = 0; 906 height = 0; 907 horizontalOffset = 0; 908 verticalOffset = 0; 909 mView = view; 910 } 911 912 LayoutParams(Context c, AttributeSet attrs) { 913 super(c, attrs); 914 horizontalOffset = 0; 915 verticalOffset = 0; 916 width = 0; 917 height = 0; 918 } 919 920 void invalidateGlobalRegion(View v, Rect r) { 921 // We need to make a new rect here, so as not to modify the one passed 922 globalInvalidateRect.set(r); 923 View p = v; 924 if (!(v.getParent() != null && v.getParent() instanceof View)) return; 925 926 boolean firstPass = true; 927 parentRect.set(0, 0, 0, 0); 928 int depth = 0; 929 while (p.getParent() != null && p.getParent() instanceof View 930 && !parentRect.contains(globalInvalidateRect)) { 931 if (!firstPass) { 932 globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop() 933 - p.getScrollY()); 934 depth++; 935 } 936 firstPass = false; 937 p = (View) p.getParent(); 938 parentRect.set(p.getScrollX(), p.getScrollY(), 939 p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY()); 940 941 } 942 943 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top, 944 globalInvalidateRect.right, globalInvalidateRect.bottom); 945 } 946 947 Rect getInvalidateRect() { 948 return invalidateRect; 949 } 950 951 void resetInvalidateRect() { 952 invalidateRect.set(0, 0, 0, 0); 953 } 954 955 // This is public so that ObjectAnimator can access it 956 public void setVerticalOffset(int newVerticalOffset) { 957 int offsetDelta = newVerticalOffset - verticalOffset; 958 verticalOffset = newVerticalOffset; 959 960 if (mView != null) { 961 mView.requestLayout(); 962 int top = Math.min(mView.getTop() + offsetDelta, mView.getTop()); 963 int bottom = Math.max(mView.getBottom() + offsetDelta, mView.getBottom()); 964 965 invalidateRectf.set(mView.getLeft(), top, mView.getRight(), bottom); 966 967 float xoffset = -invalidateRectf.left; 968 float yoffset = -invalidateRectf.top; 969 invalidateRectf.offset(xoffset, yoffset); 970 mView.getMatrix().mapRect(invalidateRectf); 971 invalidateRectf.offset(-xoffset, -yoffset); 972 invalidateRect.union((int) Math.floor(invalidateRectf.left), 973 (int) Math.floor(invalidateRectf.top), 974 (int) Math.ceil(invalidateRectf.right), 975 (int) Math.ceil(invalidateRectf.bottom)); 976 977 invalidateGlobalRegion(mView, invalidateRect); 978 } 979 } 980 981 public void setHorizontalOffset(int newHorizontalOffset) { 982 int offsetDelta = newHorizontalOffset - horizontalOffset; 983 horizontalOffset = newHorizontalOffset; 984 985 if (mView != null) { 986 mView.requestLayout(); 987 int left = Math.min(mView.getLeft() + offsetDelta, mView.getLeft()); 988 int right = Math.max(mView.getRight() + offsetDelta, mView.getRight()); 989 invalidateRectf.set(left, mView.getTop(), right, mView.getBottom()); 990 991 float xoffset = -invalidateRectf.left; 992 float yoffset = -invalidateRectf.top; 993 invalidateRectf.offset(xoffset, yoffset); 994 mView.getMatrix().mapRect(invalidateRectf); 995 invalidateRectf.offset(-xoffset, -yoffset); 996 997 invalidateRect.union((int) Math.floor(invalidateRectf.left), 998 (int) Math.floor(invalidateRectf.top), 999 (int) Math.ceil(invalidateRectf.right), 1000 (int) Math.ceil(invalidateRectf.bottom)); 1001 1002 invalidateGlobalRegion(mView, invalidateRect); 1003 } 1004 } 1005 } 1006 1007 private static class HolographicHelper { 1008 private final Paint mHolographicPaint = new Paint(); 1009 private final Paint mErasePaint = new Paint(); 1010 private final Paint mBlurPaint = new Paint(); 1011 1012 HolographicHelper(Context context) { 1013 initializePaints(context); 1014 } 1015 1016 void initializePaints(Context context) { 1017 final float density = context.getResources().getDisplayMetrics().density; 1018 1019 mHolographicPaint.setColor(0xff6699ff); 1020 mHolographicPaint.setFilterBitmap(true); 1021 mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30)); 1022 mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); 1023 mErasePaint.setFilterBitmap(true); 1024 mBlurPaint.setMaskFilter(new BlurMaskFilter(2*density, BlurMaskFilter.Blur.NORMAL)); 1025 } 1026 1027 Bitmap createOutline(View v) { 1028 if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) { 1029 return null; 1030 } 1031 1032 Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(), 1033 Bitmap.Config.ARGB_8888); 1034 Canvas canvas = new Canvas(bitmap); 1035 1036 float rotationX = v.getRotationX(); 1037 float rotation = v.getRotation(); 1038 float translationY = v.getTranslationY(); 1039 v.setRotationX(0); 1040 v.setRotation(0); 1041 v.setTranslationY(0); 1042 v.draw(canvas); 1043 v.setRotationX(rotationX); 1044 v.setRotation(rotation); 1045 v.setTranslationY(translationY); 1046 1047 drawOutline(canvas, bitmap); 1048 return bitmap; 1049 } 1050 1051 final Matrix id = new Matrix(); 1052 void drawOutline(Canvas dest, Bitmap src) { 1053 int[] xy = new int[2]; 1054 Bitmap mask = src.extractAlpha(mBlurPaint, xy); 1055 Canvas maskCanvas = new Canvas(mask); 1056 maskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint); 1057 dest.drawColor(0, PorterDuff.Mode.CLEAR); 1058 dest.setMatrix(id); 1059 dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint); 1060 mask.recycle(); 1061 } 1062 } 1063} 1064