StackView.java revision 2794eb3b02e2404d453d3ad22a8a85a138130a07
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 fadeIn = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 1.0f); 179 fadeIn.setDuration(DEFAULT_ANIMATION_DURATION); 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 slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f); 190 PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 191 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 192 slideInX, slideInY); 193 pa.setDuration(duration); 194 pa.setInterpolator(new LinearInterpolator()); 195 pa.start(); 196 } else if (fromIndex == mNumActiveViews - 2 && toIndex == mNumActiveViews - 1) { 197 // Slide item out 198 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 199 200 int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity)); 201 202 StackSlider animationSlider = new StackSlider(mStackSlider); 203 PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f); 204 PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 205 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 206 slideOutX, slideOutY); 207 pa.setDuration(duration); 208 pa.setInterpolator(new LinearInterpolator()); 209 pa.start(); 210 } else if (fromIndex == -1 && toIndex == mNumActiveViews - 1) { 211 // Make sure this view that is "waiting in the wings" is invisible 212 view.setAlpha(0.0f); 213 view.setVisibility(INVISIBLE); 214 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 215 lp.setVerticalOffset(-mSlideAmount); 216 } else if (toIndex == -1) { 217 // Fade item out 218 ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 0.0f); 219 fadeOut.setDuration(DEFAULT_ANIMATION_DURATION); 220 fadeOut.start(); 221 } 222 223 // Implement the faked perspective 224 if (toIndex != -1) { 225 transformViewAtIndex(toIndex, view); 226 } 227 } 228 229 private void transformViewAtIndex(int index, View view) { 230 float maxPerpectiveShift = mMeasuredHeight * PERSPECTIVE_SHIFT_FACTOR; 231 232 if (index == mNumActiveViews -1) index--; 233 234 float r = (index * 1.0f) / (mNumActiveViews - 2); 235 236 float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r); 237 PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", scale); 238 PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", scale); 239 240 r = (float) Math.pow(r, 2); 241 242 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1; 243 float perspectiveTranslation = -stackDirection * r * maxPerpectiveShift; 244 float scaleShiftCorrection = stackDirection * (1 - scale) * 245 (mMeasuredHeight * (1 - PERSPECTIVE_SHIFT_FACTOR) / 2.0f); 246 float transY = perspectiveTranslation + scaleShiftCorrection; 247 248 PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY); 249 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(view, scaleX, scaleY, translationY); 250 pa.setDuration(100); 251 pa.start(); 252 } 253 254 private void updateChildTransforms() { 255 for (int i = 0; i < mNumActiveViews - 1; i++) { 256 View v = getViewAtRelativeIndex(i); 257 if (v != null) { 258 transformViewAtIndex(i, v); 259 } 260 } 261 } 262 263 @Override 264 FrameLayout getFrameForChild() { 265 FrameLayout fl = new FrameLayout(mContext); 266 fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding); 267 return fl; 268 } 269 270 /** 271 * Apply any necessary tranforms for the child that is being added. 272 */ 273 void applyTransformForChildAtIndex(View child, int relativeIndex) { 274 } 275 276 @Override 277 protected void dispatchDraw(Canvas canvas) { 278 super.dispatchDraw(canvas); 279 } 280 281 // TODO: right now, this code walks up the hierarchy as far as needed and disables clipping 282 // so that the stack's children can draw outside of the stack's bounds. This is fine within 283 // the context of widgets in the launcher, but is destructive in general, as the clipping 284 // values are not being reset. For this to be a full framework level widget, we will need 285 // framework level support for drawing outside of a parent's bounds. 286 private void disableParentalClipping() { 287 if (mAncestorContainingAllChildren != null) { 288 ViewGroup vg = this; 289 while (vg.getParent() != null && vg.getParent() instanceof ViewGroup) { 290 if (vg == mAncestorContainingAllChildren) break; 291 vg = (ViewGroup) vg.getParent(); 292 vg.setClipChildren(false); 293 vg.setClipToPadding(false); 294 } 295 } 296 } 297 298 private void onLayout() { 299 if (!mFirstLayoutHappened) { 300 mSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight()); 301 updateChildTransforms(); 302 mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * mSlideAmount); 303 mFirstLayoutHappened = true; 304 } 305 } 306 307 @Override 308 public boolean onInterceptTouchEvent(MotionEvent ev) { 309 int action = ev.getAction(); 310 switch(action & MotionEvent.ACTION_MASK) { 311 case MotionEvent.ACTION_DOWN: { 312 if (mActivePointerId == INVALID_POINTER) { 313 mInitialX = ev.getX(); 314 mInitialY = ev.getY(); 315 mActivePointerId = ev.getPointerId(0); 316 } 317 break; 318 } 319 case MotionEvent.ACTION_MOVE: { 320 int pointerIndex = ev.findPointerIndex(mActivePointerId); 321 if (pointerIndex == INVALID_POINTER) { 322 // no data for our primary pointer, this shouldn't happen, log it 323 Log.d(TAG, "Error: No data for our primary pointer."); 324 return false; 325 } 326 float newY = ev.getY(pointerIndex); 327 float deltaY = newY - mInitialY; 328 329 beginGestureIfNeeded(deltaY); 330 break; 331 } 332 case MotionEvent.ACTION_POINTER_UP: { 333 onSecondaryPointerUp(ev); 334 break; 335 } 336 case MotionEvent.ACTION_UP: 337 case MotionEvent.ACTION_CANCEL: { 338 mActivePointerId = INVALID_POINTER; 339 mSwipeGestureType = GESTURE_NONE; 340 } 341 } 342 343 return mSwipeGestureType != GESTURE_NONE; 344 } 345 346 private void beginGestureIfNeeded(float deltaY) { 347 if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) { 348 int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN; 349 cancelLongPress(); 350 requestDisallowInterceptTouchEvent(true); 351 352 int activeIndex; 353 if (mStackMode == ITEMS_SLIDE_UP) { 354 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 355 mNumActiveViews - 1 : mNumActiveViews - 2; 356 } else { 357 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 358 mNumActiveViews - 2 : mNumActiveViews - 1; 359 } 360 361 if (mAdapter == null) return; 362 363 if (mLoopViews) { 364 mStackSlider.setMode(StackSlider.NORMAL_MODE); 365 } else if (mCurrentWindowStartUnbounded + activeIndex == 0) { 366 mStackSlider.setMode(StackSlider.BEGINNING_OF_STACK_MODE); 367 } else if (mCurrentWindowStartUnbounded + activeIndex == mAdapter.getCount()) { 368 activeIndex--; 369 mStackSlider.setMode(StackSlider.END_OF_STACK_MODE); 370 } else { 371 mStackSlider.setMode(StackSlider.NORMAL_MODE); 372 } 373 374 View v = getViewAtRelativeIndex(activeIndex); 375 if (v == null) return; 376 377 mHighlight.setImageBitmap(sHolographicHelper.createOutline(v)); 378 mHighlight.setRotation(v.getRotation()); 379 mHighlight.setTranslationY(v.getTranslationY()); 380 mHighlight.bringToFront(); 381 v.bringToFront(); 382 mStackSlider.setView(v); 383 384 if (swipeGestureType == GESTURE_SLIDE_DOWN) 385 v.setVisibility(VISIBLE); 386 387 // We only register this gesture if we've made it this far without a problem 388 mSwipeGestureType = swipeGestureType; 389 } 390 } 391 392 @Override 393 public boolean onTouchEvent(MotionEvent ev) { 394 int action = ev.getAction(); 395 int pointerIndex = ev.findPointerIndex(mActivePointerId); 396 if (pointerIndex == INVALID_POINTER) { 397 // no data for our primary pointer, this shouldn't happen, log it 398 Log.d(TAG, "Error: No data for our primary pointer."); 399 return false; 400 } 401 402 float newY = ev.getY(pointerIndex); 403 float newX = ev.getX(pointerIndex); 404 float deltaY = newY - mInitialY; 405 float deltaX = newX - mInitialX; 406 if (mVelocityTracker == null) { 407 mVelocityTracker = VelocityTracker.obtain(); 408 } 409 mVelocityTracker.addMovement(ev); 410 411 switch (action & MotionEvent.ACTION_MASK) { 412 case MotionEvent.ACTION_MOVE: { 413 beginGestureIfNeeded(deltaY); 414 415 float rx = deltaX / (mSlideAmount * 1.0f); 416 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 417 float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f; 418 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; 419 mStackSlider.setYProgress(1 - r); 420 mStackSlider.setXProgress(rx); 421 return true; 422 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) { 423 float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f; 424 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; 425 mStackSlider.setYProgress(r); 426 mStackSlider.setXProgress(rx); 427 return true; 428 } 429 break; 430 } 431 case MotionEvent.ACTION_UP: { 432 handlePointerUp(ev); 433 break; 434 } 435 case MotionEvent.ACTION_POINTER_UP: { 436 onSecondaryPointerUp(ev); 437 break; 438 } 439 case MotionEvent.ACTION_CANCEL: { 440 mActivePointerId = INVALID_POINTER; 441 mSwipeGestureType = GESTURE_NONE; 442 break; 443 } 444 } 445 return true; 446 } 447 448 private final Rect touchRect = new Rect(); 449 private void onSecondaryPointerUp(MotionEvent ev) { 450 final int activePointerIndex = ev.getActionIndex(); 451 final int pointerId = ev.getPointerId(activePointerIndex); 452 if (pointerId == mActivePointerId) { 453 454 int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? mNumActiveViews - 1 455 : mNumActiveViews - 2; 456 457 View v = getViewAtRelativeIndex(activeViewIndex); 458 if (v == null) return; 459 460 // Our primary pointer has gone up -- let's see if we can find 461 // another pointer on the view. If so, then we should replace 462 // our primary pointer with this new pointer and adjust things 463 // so that the view doesn't jump 464 for (int index = 0; index < ev.getPointerCount(); index++) { 465 if (index != activePointerIndex) { 466 467 float x = ev.getX(index); 468 float y = ev.getY(index); 469 470 touchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 471 if (touchRect.contains(Math.round(x), Math.round(y))) { 472 float oldX = ev.getX(activePointerIndex); 473 float oldY = ev.getY(activePointerIndex); 474 475 // adjust our frame of reference to avoid a jump 476 mInitialY += (y - oldY); 477 mInitialX += (x - oldX); 478 479 mActivePointerId = ev.getPointerId(index); 480 if (mVelocityTracker != null) { 481 mVelocityTracker.clear(); 482 } 483 // ok, we're good, we found a new pointer which is touching the active view 484 return; 485 } 486 } 487 } 488 // if we made it this far, it means we didn't find a satisfactory new pointer :(, 489 // so end the gesture 490 handlePointerUp(ev); 491 } 492 } 493 494 private void handlePointerUp(MotionEvent ev) { 495 int pointerIndex = ev.findPointerIndex(mActivePointerId); 496 float newY = ev.getY(pointerIndex); 497 int deltaY = (int) (newY - mInitialY); 498 499 if (mVelocityTracker != null) { 500 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 501 mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); 502 } 503 504 if (mVelocityTracker != null) { 505 mVelocityTracker.recycle(); 506 mVelocityTracker = null; 507 } 508 509 if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN 510 && mStackSlider.mMode == StackSlider.NORMAL_MODE) { 511 // Swipe threshold exceeded, swipe down 512 if (mStackMode == ITEMS_SLIDE_UP) { 513 showNext(); 514 } else { 515 showPrevious(); 516 } 517 mHighlight.bringToFront(); 518 } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP 519 && mStackSlider.mMode == StackSlider.NORMAL_MODE) { 520 // Swipe threshold exceeded, swipe up 521 if (mStackMode == ITEMS_SLIDE_UP) { 522 showPrevious(); 523 } else { 524 showNext(); 525 } 526 527 mHighlight.bringToFront(); 528 } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) { 529 // Didn't swipe up far enough, snap back down 530 int duration; 531 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0; 532 if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) { 533 duration = Math.round(mStackSlider.getDurationForNeutralPosition()); 534 } else { 535 duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); 536 } 537 538 StackSlider animationSlider = new StackSlider(mStackSlider); 539 PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress); 540 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 541 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 542 snapBackX, snapBackY); 543 pa.setDuration(duration); 544 pa.setInterpolator(new LinearInterpolator()); 545 pa.start(); 546 } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 547 // Didn't swipe down far enough, snap back up 548 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1; 549 int duration; 550 if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) { 551 duration = Math.round(mStackSlider.getDurationForNeutralPosition()); 552 } else { 553 duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); 554 } 555 556 StackSlider animationSlider = new StackSlider(mStackSlider); 557 PropertyValuesHolder snapBackY = 558 PropertyValuesHolder.ofFloat("YProgress",finalYProgress); 559 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 560 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 561 snapBackX, snapBackY); 562 pa.setDuration(duration); 563 pa.start(); 564 } 565 566 mActivePointerId = INVALID_POINTER; 567 mSwipeGestureType = GESTURE_NONE; 568 } 569 570 private class StackSlider { 571 View mView; 572 float mYProgress; 573 float mXProgress; 574 575 static final int NORMAL_MODE = 0; 576 static final int BEGINNING_OF_STACK_MODE = 1; 577 static final int END_OF_STACK_MODE = 2; 578 579 int mMode = NORMAL_MODE; 580 581 public StackSlider() { 582 } 583 584 public StackSlider(StackSlider copy) { 585 mView = copy.mView; 586 mYProgress = copy.mYProgress; 587 mXProgress = copy.mXProgress; 588 mMode = copy.mMode; 589 } 590 591 private float cubic(float r) { 592 return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f; 593 } 594 595 private float highlightAlphaInterpolator(float r) { 596 float pivot = 0.4f; 597 if (r < pivot) { 598 return 0.85f * cubic(r / pivot); 599 } else { 600 return 0.85f * cubic(1 - (r - pivot) / (1 - pivot)); 601 } 602 } 603 604 private float viewAlphaInterpolator(float r) { 605 float pivot = 0.3f; 606 if (r > pivot) { 607 return (r - pivot) / (1 - pivot); 608 } else { 609 return 0; 610 } 611 } 612 613 private float rotationInterpolator(float r) { 614 float pivot = 0.2f; 615 if (r < pivot) { 616 return 0; 617 } else { 618 return (r - pivot) / (1 - pivot); 619 } 620 } 621 622 void setView(View v) { 623 mView = v; 624 } 625 626 public void setYProgress(float r) { 627 // enforce r between 0 and 1 628 r = Math.min(1.0f, r); 629 r = Math.max(0, r); 630 631 mYProgress = r; 632 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 633 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 634 635 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1; 636 637 switch (mMode) { 638 case NORMAL_MODE: 639 viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); 640 highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); 641 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 642 643 float alpha = viewAlphaInterpolator(1 - r); 644 645 // We make sure that views which can't be seen (have 0 alpha) are also invisible 646 // so that they don't interfere with click events. 647 if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) { 648 mView.setVisibility(VISIBLE); 649 } else if (alpha == 0 && mView.getAlpha() != 0 650 && mView.getVisibility() == VISIBLE) { 651 mView.setVisibility(INVISIBLE); 652 } 653 654 mView.setAlpha(alpha); 655 mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); 656 mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); 657 break; 658 case BEGINNING_OF_STACK_MODE: 659 r = r * 0.2f; 660 viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount)); 661 highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount)); 662 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 663 break; 664 case END_OF_STACK_MODE: 665 r = (1-r) * 0.2f; 666 viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount)); 667 highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount)); 668 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 669 break; 670 } 671 } 672 673 public void setXProgress(float r) { 674 // enforce r between 0 and 1 675 r = Math.min(2.0f, r); 676 r = Math.max(-2.0f, r); 677 678 mXProgress = r; 679 680 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 681 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 682 683 r *= 0.2f; 684 viewLp.setHorizontalOffset(Math.round(r * mSlideAmount)); 685 highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount)); 686 } 687 688 void setMode(int mode) { 689 mMode = mode; 690 } 691 692 float getDurationForNeutralPosition() { 693 return getDuration(false, 0); 694 } 695 696 float getDurationForOffscreenPosition() { 697 return getDuration(true, 0); 698 } 699 700 float getDurationForNeutralPosition(float velocity) { 701 return getDuration(false, velocity); 702 } 703 704 float getDurationForOffscreenPosition(float velocity) { 705 return getDuration(true, velocity); 706 } 707 708 private float getDuration(boolean invert, float velocity) { 709 if (mView != null) { 710 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 711 712 float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) + 713 Math.pow(viewLp.verticalOffset, 2)); 714 float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) + 715 Math.pow(0.4f * mSlideAmount, 2)); 716 717 if (velocity == 0) { 718 return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION; 719 } else { 720 float duration = invert ? d / Math.abs(velocity) : 721 (maxd - d) / Math.abs(velocity); 722 if (duration < MINIMUM_ANIMATION_DURATION || 723 duration > DEFAULT_ANIMATION_DURATION) { 724 return getDuration(invert, 0); 725 } else { 726 return duration; 727 } 728 } 729 } 730 return 0; 731 } 732 733 public float getYProgress() { 734 return mYProgress; 735 } 736 737 public float getXProgress() { 738 return mXProgress; 739 } 740 } 741 742 @Override 743 public void onRemoteAdapterConnected() { 744 super.onRemoteAdapterConnected(); 745 // On first run, we want to set the stack to the end. 746 if (mAdapter != null && mWhichChild == -1) { 747 mWhichChild = mAdapter.getCount() - 1; 748 } 749 if (mWhichChild >= 0) { 750 setDisplayedChild(mWhichChild); 751 } 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