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