StackView.java revision 3042944c6ec68210ba1746540b53789e70d15ef4
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); 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 ViewGroup vg = this; 291 while (vg.getParent() != null && vg.getParent() instanceof ViewGroup) { 292 if (vg == mAncestorContainingAllChildren) break; 293 vg = (ViewGroup) vg.getParent(); 294 vg.setClipChildren(false); 295 vg.setClipToPadding(false); 296 } 297 } 298 } 299 300 private void onLayout() { 301 if (!mFirstLayoutHappened) { 302 mSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight()); 303 updateChildTransforms(); 304 mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * mSlideAmount); 305 mFirstLayoutHappened = true; 306 } 307 } 308 309 @Override 310 public boolean onInterceptTouchEvent(MotionEvent ev) { 311 int action = ev.getAction(); 312 switch(action & MotionEvent.ACTION_MASK) { 313 case MotionEvent.ACTION_DOWN: { 314 if (mActivePointerId == INVALID_POINTER) { 315 mInitialX = ev.getX(); 316 mInitialY = ev.getY(); 317 mActivePointerId = ev.getPointerId(0); 318 } 319 break; 320 } 321 case MotionEvent.ACTION_MOVE: { 322 int pointerIndex = ev.findPointerIndex(mActivePointerId); 323 if (pointerIndex == INVALID_POINTER) { 324 // no data for our primary pointer, this shouldn't happen, log it 325 Log.d(TAG, "Error: No data for our primary pointer."); 326 return false; 327 } 328 float newY = ev.getY(pointerIndex); 329 float deltaY = newY - mInitialY; 330 331 beginGestureIfNeeded(deltaY); 332 break; 333 } 334 case MotionEvent.ACTION_POINTER_UP: { 335 onSecondaryPointerUp(ev); 336 break; 337 } 338 case MotionEvent.ACTION_UP: 339 case MotionEvent.ACTION_CANCEL: { 340 mActivePointerId = INVALID_POINTER; 341 mSwipeGestureType = GESTURE_NONE; 342 } 343 } 344 345 return mSwipeGestureType != GESTURE_NONE; 346 } 347 348 private void beginGestureIfNeeded(float deltaY) { 349 if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) { 350 int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN; 351 cancelLongPress(); 352 requestDisallowInterceptTouchEvent(true); 353 354 int activeIndex; 355 if (mStackMode == ITEMS_SLIDE_UP) { 356 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 357 mNumActiveViews - 1 : mNumActiveViews - 2; 358 } else { 359 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 360 mNumActiveViews - 2 : mNumActiveViews - 1; 361 } 362 363 if (mAdapter == null) return; 364 365 if (mLoopViews) { 366 mStackSlider.setMode(StackSlider.NORMAL_MODE); 367 } else if (mCurrentWindowStartUnbounded + activeIndex == 0) { 368 mStackSlider.setMode(StackSlider.BEGINNING_OF_STACK_MODE); 369 } else if (mCurrentWindowStartUnbounded + activeIndex == mAdapter.getCount()) { 370 activeIndex--; 371 mStackSlider.setMode(StackSlider.END_OF_STACK_MODE); 372 } else { 373 mStackSlider.setMode(StackSlider.NORMAL_MODE); 374 } 375 376 View v = getViewAtRelativeIndex(activeIndex); 377 if (v == null) return; 378 379 mHighlight.setImageBitmap(sHolographicHelper.createOutline(v)); 380 mHighlight.setRotation(v.getRotation()); 381 mHighlight.setTranslationY(v.getTranslationY()); 382 mHighlight.bringToFront(); 383 v.bringToFront(); 384 mStackSlider.setView(v); 385 386 if (swipeGestureType == GESTURE_SLIDE_DOWN) 387 v.setVisibility(VISIBLE); 388 389 // We only register this gesture if we've made it this far without a problem 390 mSwipeGestureType = swipeGestureType; 391 } 392 } 393 394 @Override 395 public boolean onTouchEvent(MotionEvent ev) { 396 int action = ev.getAction(); 397 int pointerIndex = ev.findPointerIndex(mActivePointerId); 398 if (pointerIndex == INVALID_POINTER) { 399 // no data for our primary pointer, this shouldn't happen, log it 400 Log.d(TAG, "Error: No data for our primary pointer."); 401 return false; 402 } 403 404 float newY = ev.getY(pointerIndex); 405 float newX = ev.getX(pointerIndex); 406 float deltaY = newY - mInitialY; 407 float deltaX = newX - mInitialX; 408 if (mVelocityTracker == null) { 409 mVelocityTracker = VelocityTracker.obtain(); 410 } 411 mVelocityTracker.addMovement(ev); 412 413 switch (action & MotionEvent.ACTION_MASK) { 414 case MotionEvent.ACTION_MOVE: { 415 beginGestureIfNeeded(deltaY); 416 417 float rx = deltaX / (mSlideAmount * 1.0f); 418 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 419 float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f; 420 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; 421 mStackSlider.setYProgress(1 - r); 422 mStackSlider.setXProgress(rx); 423 return true; 424 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) { 425 float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f; 426 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; 427 mStackSlider.setYProgress(r); 428 mStackSlider.setXProgress(rx); 429 return true; 430 } 431 break; 432 } 433 case MotionEvent.ACTION_UP: { 434 handlePointerUp(ev); 435 break; 436 } 437 case MotionEvent.ACTION_POINTER_UP: { 438 onSecondaryPointerUp(ev); 439 break; 440 } 441 case MotionEvent.ACTION_CANCEL: { 442 mActivePointerId = INVALID_POINTER; 443 mSwipeGestureType = GESTURE_NONE; 444 break; 445 } 446 } 447 return true; 448 } 449 450 private final Rect touchRect = new Rect(); 451 private void onSecondaryPointerUp(MotionEvent ev) { 452 final int activePointerIndex = ev.getActionIndex(); 453 final int pointerId = ev.getPointerId(activePointerIndex); 454 if (pointerId == mActivePointerId) { 455 456 int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? mNumActiveViews - 1 457 : mNumActiveViews - 2; 458 459 View v = getViewAtRelativeIndex(activeViewIndex); 460 if (v == null) return; 461 462 // Our primary pointer has gone up -- let's see if we can find 463 // another pointer on the view. If so, then we should replace 464 // our primary pointer with this new pointer and adjust things 465 // so that the view doesn't jump 466 for (int index = 0; index < ev.getPointerCount(); index++) { 467 if (index != activePointerIndex) { 468 469 float x = ev.getX(index); 470 float y = ev.getY(index); 471 472 touchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 473 if (touchRect.contains(Math.round(x), Math.round(y))) { 474 float oldX = ev.getX(activePointerIndex); 475 float oldY = ev.getY(activePointerIndex); 476 477 // adjust our frame of reference to avoid a jump 478 mInitialY += (y - oldY); 479 mInitialX += (x - oldX); 480 481 mActivePointerId = ev.getPointerId(index); 482 if (mVelocityTracker != null) { 483 mVelocityTracker.clear(); 484 } 485 // ok, we're good, we found a new pointer which is touching the active view 486 return; 487 } 488 } 489 } 490 // if we made it this far, it means we didn't find a satisfactory new pointer :(, 491 // so end the gesture 492 handlePointerUp(ev); 493 } 494 } 495 496 private void handlePointerUp(MotionEvent ev) { 497 int pointerIndex = ev.findPointerIndex(mActivePointerId); 498 float newY = ev.getY(pointerIndex); 499 int deltaY = (int) (newY - mInitialY); 500 501 if (mVelocityTracker != null) { 502 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 503 mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); 504 } 505 506 if (mVelocityTracker != null) { 507 mVelocityTracker.recycle(); 508 mVelocityTracker = null; 509 } 510 511 if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN 512 && mStackSlider.mMode == StackSlider.NORMAL_MODE) { 513 // Swipe threshold exceeded, swipe down 514 if (mStackMode == ITEMS_SLIDE_UP) { 515 showNext(); 516 } else { 517 showPrevious(); 518 } 519 mHighlight.bringToFront(); 520 } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP 521 && mStackSlider.mMode == StackSlider.NORMAL_MODE) { 522 // Swipe threshold exceeded, swipe up 523 if (mStackMode == ITEMS_SLIDE_UP) { 524 showPrevious(); 525 } else { 526 showNext(); 527 } 528 529 mHighlight.bringToFront(); 530 } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) { 531 // Didn't swipe up far enough, snap back down 532 int duration; 533 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0; 534 if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) { 535 duration = Math.round(mStackSlider.getDurationForNeutralPosition()); 536 } else { 537 duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); 538 } 539 540 StackSlider animationSlider = new StackSlider(mStackSlider); 541 PropertyValuesHolder<Float> snapBackY = 542 new PropertyValuesHolder<Float>("YProgress", finalYProgress); 543 PropertyValuesHolder<Float> snapBackX = 544 new PropertyValuesHolder<Float>("XProgress", 0.0f); 545 ObjectAnimator pa = new ObjectAnimator(duration, animationSlider, 546 snapBackX, snapBackY); 547 pa.setInterpolator(new LinearInterpolator()); 548 pa.start(); 549 } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 550 // Didn't swipe down far enough, snap back up 551 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1; 552 int duration; 553 if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) { 554 duration = Math.round(mStackSlider.getDurationForNeutralPosition()); 555 } else { 556 duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); 557 } 558 559 StackSlider animationSlider = new StackSlider(mStackSlider); 560 PropertyValuesHolder<Float> snapBackY = 561 new PropertyValuesHolder<Float>("YProgress", finalYProgress); 562 PropertyValuesHolder<Float> snapBackX = 563 new PropertyValuesHolder<Float>("XProgress", 0.0f); 564 ObjectAnimator pa = new ObjectAnimator(duration, animationSlider, 565 snapBackX, snapBackY); 566 pa.start(); 567 } 568 569 mActivePointerId = INVALID_POINTER; 570 mSwipeGestureType = GESTURE_NONE; 571 } 572 573 private class StackSlider { 574 View mView; 575 float mYProgress; 576 float mXProgress; 577 578 static final int NORMAL_MODE = 0; 579 static final int BEGINNING_OF_STACK_MODE = 1; 580 static final int END_OF_STACK_MODE = 2; 581 582 int mMode = NORMAL_MODE; 583 584 public StackSlider() { 585 } 586 587 public StackSlider(StackSlider copy) { 588 mView = copy.mView; 589 mYProgress = copy.mYProgress; 590 mXProgress = copy.mXProgress; 591 mMode = copy.mMode; 592 } 593 594 private float cubic(float r) { 595 return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f; 596 } 597 598 private float highlightAlphaInterpolator(float r) { 599 float pivot = 0.4f; 600 if (r < pivot) { 601 return 0.85f * cubic(r / pivot); 602 } else { 603 return 0.85f * cubic(1 - (r - pivot) / (1 - pivot)); 604 } 605 } 606 607 private float viewAlphaInterpolator(float r) { 608 float pivot = 0.3f; 609 if (r > pivot) { 610 return (r - pivot) / (1 - pivot); 611 } else { 612 return 0; 613 } 614 } 615 616 private float rotationInterpolator(float r) { 617 float pivot = 0.2f; 618 if (r < pivot) { 619 return 0; 620 } else { 621 return (r - pivot) / (1 - pivot); 622 } 623 } 624 625 void setView(View v) { 626 mView = v; 627 } 628 629 public void setYProgress(float r) { 630 // enforce r between 0 and 1 631 r = Math.min(1.0f, r); 632 r = Math.max(0, r); 633 634 mYProgress = r; 635 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 636 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 637 638 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1; 639 640 switch (mMode) { 641 case NORMAL_MODE: 642 viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); 643 highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); 644 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 645 646 float alpha = viewAlphaInterpolator(1 - r); 647 648 // We make sure that views which can't be seen (have 0 alpha) are also invisible 649 // so that they don't interfere with click events. 650 if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) { 651 mView.setVisibility(VISIBLE); 652 } else if (alpha == 0 && mView.getAlpha() != 0 653 && mView.getVisibility() == VISIBLE) { 654 mView.setVisibility(INVISIBLE); 655 } 656 657 mView.setAlpha(alpha); 658 mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); 659 mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); 660 break; 661 case BEGINNING_OF_STACK_MODE: 662 r = r * 0.2f; 663 viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount)); 664 highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount)); 665 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 666 break; 667 case END_OF_STACK_MODE: 668 r = (1-r) * 0.2f; 669 viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount)); 670 highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount)); 671 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 672 break; 673 } 674 } 675 676 public void setXProgress(float r) { 677 // enforce r between 0 and 1 678 r = Math.min(2.0f, r); 679 r = Math.max(-2.0f, r); 680 681 mXProgress = r; 682 683 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 684 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 685 686 r *= 0.2f; 687 viewLp.setHorizontalOffset(Math.round(r * mSlideAmount)); 688 highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount)); 689 } 690 691 void setMode(int mode) { 692 mMode = mode; 693 } 694 695 float getDurationForNeutralPosition() { 696 return getDuration(false, 0); 697 } 698 699 float getDurationForOffscreenPosition() { 700 return getDuration(true, 0); 701 } 702 703 float getDurationForNeutralPosition(float velocity) { 704 return getDuration(false, velocity); 705 } 706 707 float getDurationForOffscreenPosition(float velocity) { 708 return getDuration(true, velocity); 709 } 710 711 private float getDuration(boolean invert, float velocity) { 712 if (mView != null) { 713 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 714 715 float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) + 716 Math.pow(viewLp.verticalOffset, 2)); 717 float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) + 718 Math.pow(0.4f * mSlideAmount, 2)); 719 720 if (velocity == 0) { 721 return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION; 722 } else { 723 float duration = invert ? d / Math.abs(velocity) : 724 (maxd - d) / Math.abs(velocity); 725 if (duration < MINIMUM_ANIMATION_DURATION || 726 duration > DEFAULT_ANIMATION_DURATION) { 727 return getDuration(invert, 0); 728 } else { 729 return duration; 730 } 731 } 732 } 733 return 0; 734 } 735 736 public float getYProgress() { 737 return mYProgress; 738 } 739 740 public float getXProgress() { 741 return mXProgress; 742 } 743 } 744 745 @Override 746 public void onRemoteAdapterConnected() { 747 super.onRemoteAdapterConnected(); 748 // On first run, we want to set the stack to the end. 749 if (mAdapter != null && mWhichChild == -1) { 750 mWhichChild = mAdapter.getCount() - 1; 751 } 752 if (mWhichChild >= 0) { 753 setDisplayedChild(mWhichChild); 754 } 755 } 756 757 LayoutParams createOrReuseLayoutParams(View v) { 758 final ViewGroup.LayoutParams currentLp = v.getLayoutParams(); 759 if (currentLp instanceof LayoutParams) { 760 LayoutParams lp = (LayoutParams) currentLp; 761 lp.setHorizontalOffset(0); 762 lp.setVerticalOffset(0); 763 lp.width = 0; 764 lp.width = 0; 765 return lp; 766 } 767 return new LayoutParams(v); 768 } 769 770 @Override 771 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 772 boolean dataChanged = mDataChanged; 773 if (dataChanged) { 774 handleDataChanged(); 775 776 // if the data changes, mWhichChild might be out of the bounds of the adapter 777 // in this case, we reset mWhichChild to the beginning 778 if (mWhichChild >= mAdapter.getCount()) 779 mWhichChild = 0; 780 781 showOnly(mWhichChild, true, true); 782 refreshChildren(); 783 } 784 785 final int childCount = getChildCount(); 786 for (int i = 0; i < childCount; i++) { 787 final View child = getChildAt(i); 788 789 int childRight = mPaddingLeft + child.getMeasuredWidth(); 790 int childBottom = mPaddingTop + child.getMeasuredHeight(); 791 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 792 793 child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset, 794 childRight + lp.horizontalOffset, childBottom + lp.verticalOffset); 795 796 } 797 798 mDataChanged = false; 799 onLayout(); 800 } 801 802 private void measureChildren() { 803 final int count = getChildCount(); 804 final int childWidth = mMeasuredWidth - mPaddingLeft - mPaddingRight; 805 final int childHeight = Math.round(mMeasuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR)) 806 - mPaddingTop - mPaddingBottom; 807 808 for (int i = 0; i < count; i++) { 809 final View child = getChildAt(i); 810 child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), 811 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)); 812 } 813 } 814 815 @Override 816 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 817 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 818 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 819 final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 820 final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 821 822 boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1); 823 824 // We need to deal with the case where our parent hasn't told us how 825 // big we should be. In this case we should 826 float factor = 1/(1 - PERSPECTIVE_SHIFT_FACTOR); 827 if (heightSpecMode == MeasureSpec.UNSPECIFIED) { 828 heightSpecSize = haveChildRefSize ? 829 Math.round(mReferenceChildHeight * (1 + factor)) + 830 mPaddingTop + mPaddingBottom : 0; 831 } else if (heightSpecMode == MeasureSpec.AT_MOST) { 832 heightSpecSize = haveChildRefSize ? Math.min( 833 Math.round(mReferenceChildHeight * (1 + factor)) + mPaddingTop + 834 mPaddingBottom, heightSpecSize) : 0; 835 } 836 837 if (widthSpecMode == MeasureSpec.UNSPECIFIED) { 838 widthSpecSize = haveChildRefSize ? mReferenceChildWidth + mPaddingLeft + 839 mPaddingRight : 0; 840 } else if (heightSpecMode == MeasureSpec.AT_MOST) { 841 widthSpecSize = haveChildRefSize ? Math.min(mReferenceChildWidth + mPaddingLeft + 842 mPaddingRight, widthSpecSize) : 0; 843 } 844 845 setMeasuredDimension(widthSpecSize, heightSpecSize); 846 measureChildren(); 847 } 848 849 class LayoutParams extends ViewGroup.LayoutParams { 850 int horizontalOffset; 851 int verticalOffset; 852 View mView; 853 854 LayoutParams(View view) { 855 super(0, 0); 856 width = 0; 857 height = 0; 858 horizontalOffset = 0; 859 verticalOffset = 0; 860 mView = view; 861 } 862 863 LayoutParams(Context c, AttributeSet attrs) { 864 super(c, attrs); 865 horizontalOffset = 0; 866 verticalOffset = 0; 867 width = 0; 868 height = 0; 869 } 870 871 private Rect parentRect = new Rect(); 872 void invalidateGlobalRegion(View v, Rect r) { 873 View p = v; 874 if (!(v.getParent() != null && v.getParent() instanceof View)) return; 875 876 boolean firstPass = true; 877 parentRect.set(0, 0, 0, 0); 878 int depth = 0; 879 while (p.getParent() != null && p.getParent() instanceof View 880 && !parentRect.contains(r)) { 881 if (!firstPass) { 882 r.offset(p.getLeft() - p.getScrollX(), p.getTop() - p.getScrollY()); 883 depth++; 884 } 885 firstPass = false; 886 p = (View) p.getParent(); 887 parentRect.set(p.getScrollX(), p.getScrollY(), 888 p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY()); 889 890 // TODO: we need to stop early here if we've hit the edge of the screen 891 // so as to prevent us from walking too high in the hierarchy. A lot of this 892 // code might become a lot more straightforward. 893 } 894 895 if (depth > mAncestorHeight) { 896 mAncestorContainingAllChildren = (ViewGroup) p; 897 mAncestorHeight = depth; 898 disableParentalClipping(); 899 } 900 901 p.invalidate(r.left, r.top, r.right, r.bottom); 902 } 903 904 private Rect invalidateRect = new Rect(); 905 private RectF invalidateRectf = new RectF(); 906 // This is public so that ObjectAnimator can access it 907 public void setVerticalOffset(int newVerticalOffset) { 908 int offsetDelta = newVerticalOffset - verticalOffset; 909 verticalOffset = newVerticalOffset; 910 911 if (mView != null) { 912 mView.requestLayout(); 913 int top = Math.min(mView.getTop() + offsetDelta, mView.getTop()); 914 int bottom = Math.max(mView.getBottom() + offsetDelta, mView.getBottom()); 915 916 invalidateRectf.set(mView.getLeft(), top, mView.getRight(), bottom); 917 918 float xoffset = -invalidateRectf.left; 919 float yoffset = -invalidateRectf.top; 920 invalidateRectf.offset(xoffset, yoffset); 921 mView.getMatrix().mapRect(invalidateRectf); 922 invalidateRectf.offset(-xoffset, -yoffset); 923 invalidateRect.set((int) Math.floor(invalidateRectf.left), 924 (int) Math.floor(invalidateRectf.top), 925 (int) Math.ceil(invalidateRectf.right), 926 (int) Math.ceil(invalidateRectf.bottom)); 927 928 invalidateGlobalRegion(mView, invalidateRect); 929 } 930 } 931 932 public void setHorizontalOffset(int newHorizontalOffset) { 933 int offsetDelta = newHorizontalOffset - horizontalOffset; 934 horizontalOffset = newHorizontalOffset; 935 936 if (mView != null) { 937 mView.requestLayout(); 938 int left = Math.min(mView.getLeft() + offsetDelta, mView.getLeft()); 939 int right = Math.max(mView.getRight() + offsetDelta, mView.getRight()); 940 invalidateRectf.set(left, mView.getTop(), right, mView.getBottom()); 941 942 float xoffset = -invalidateRectf.left; 943 float yoffset = -invalidateRectf.top; 944 invalidateRectf.offset(xoffset, yoffset); 945 mView.getMatrix().mapRect(invalidateRectf); 946 invalidateRectf.offset(-xoffset, -yoffset); 947 948 invalidateRect.set((int) Math.floor(invalidateRectf.left), 949 (int) Math.floor(invalidateRectf.top), 950 (int) Math.ceil(invalidateRectf.right), 951 (int) Math.ceil(invalidateRectf.bottom)); 952 953 invalidateGlobalRegion(mView, invalidateRect); 954 } 955 } 956 } 957 958 private static class HolographicHelper { 959 private final Paint mHolographicPaint = new Paint(); 960 private final Paint mErasePaint = new Paint(); 961 private final Paint mBlurPaint = new Paint(); 962 963 HolographicHelper(Context context) { 964 initializePaints(context); 965 } 966 967 void initializePaints(Context context) { 968 final float density = context.getResources().getDisplayMetrics().density; 969 970 mHolographicPaint.setColor(0xff6699ff); 971 mHolographicPaint.setFilterBitmap(true); 972 mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30)); 973 mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); 974 mErasePaint.setFilterBitmap(true); 975 mBlurPaint.setMaskFilter(new BlurMaskFilter(2*density, BlurMaskFilter.Blur.NORMAL)); 976 } 977 978 Bitmap createOutline(View v) { 979 if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) { 980 return null; 981 } 982 983 Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(), 984 Bitmap.Config.ARGB_8888); 985 Canvas canvas = new Canvas(bitmap); 986 987 float rotationX = v.getRotationX(); 988 float rotation = v.getRotation(); 989 float translationY = v.getTranslationY(); 990 v.setRotationX(0); 991 v.setRotation(0); 992 v.setTranslationY(0); 993 v.draw(canvas); 994 v.setRotationX(rotationX); 995 v.setRotation(rotation); 996 v.setTranslationY(translationY); 997 998 drawOutline(canvas, bitmap); 999 return bitmap; 1000 } 1001 1002 final Matrix id = new Matrix(); 1003 void drawOutline(Canvas dest, Bitmap src) { 1004 int[] xy = new int[2]; 1005 Bitmap mask = src.extractAlpha(mBlurPaint, xy); 1006 Canvas maskCanvas = new Canvas(mask); 1007 maskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint); 1008 dest.drawColor(0, PorterDuff.Mode.CLEAR); 1009 dest.setMatrix(id); 1010 dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint); 1011 mask.recycle(); 1012 } 1013 } 1014} 1015