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