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