1 /* Copyright (C) 2010 The Android Open Source Project 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package android.widget; 17 18 import java.lang.ref.WeakReference; 19 20 import android.animation.ObjectAnimator; 21 import android.animation.PropertyValuesHolder; 22 import android.content.Context; 23 import android.content.res.TypedArray; 24 import android.graphics.Bitmap; 25 import android.graphics.BlurMaskFilter; 26 import android.graphics.Canvas; 27 import android.graphics.Matrix; 28 import android.graphics.Paint; 29 import android.graphics.PorterDuff; 30 import android.graphics.PorterDuffXfermode; 31 import android.graphics.Rect; 32 import android.graphics.RectF; 33 import android.graphics.Region; 34 import android.graphics.TableMaskFilter; 35 import android.os.Bundle; 36 import android.util.AttributeSet; 37 import android.util.Log; 38 import android.view.InputDevice; 39 import android.view.MotionEvent; 40 import android.view.VelocityTracker; 41 import android.view.View; 42 import android.view.ViewConfiguration; 43 import android.view.ViewGroup; 44 import android.view.accessibility.AccessibilityEvent; 45 import android.view.accessibility.AccessibilityNodeInfo; 46 import android.view.animation.LinearInterpolator; 47 import android.widget.RemoteViews.RemoteView; 48 49 @RemoteView 50 /** 51 * A view that displays its children in a stack and allows users to discretely swipe 52 * through the children. 53 */ 54 public class StackView extends AdapterViewAnimator { 55 private final String TAG = "StackView"; 56 57 /** 58 * Default animation parameters 59 */ 60 private static final int DEFAULT_ANIMATION_DURATION = 400; 61 private static final int MINIMUM_ANIMATION_DURATION = 50; 62 private static final int STACK_RELAYOUT_DURATION = 100; 63 64 /** 65 * Parameters effecting the perspective visuals 66 */ 67 private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f; 68 private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f; 69 70 private float mPerspectiveShiftX; 71 private float mPerspectiveShiftY; 72 private float mNewPerspectiveShiftX; 73 private float mNewPerspectiveShiftY; 74 75 @SuppressWarnings({"FieldCanBeLocal"}) 76 private static final float PERSPECTIVE_SCALE_FACTOR = 0f; 77 78 /** 79 * Represent the two possible stack modes, one where items slide up, and the other 80 * where items slide down. The perspective is also inverted between these two modes. 81 */ 82 private static final int ITEMS_SLIDE_UP = 0; 83 private static final int ITEMS_SLIDE_DOWN = 1; 84 85 /** 86 * These specify the different gesture states 87 */ 88 private static final int GESTURE_NONE = 0; 89 private static final int GESTURE_SLIDE_UP = 1; 90 private static final int GESTURE_SLIDE_DOWN = 2; 91 92 /** 93 * Specifies how far you need to swipe (up or down) before it 94 * will be consider a completed gesture when you lift your finger 95 */ 96 private static final float SWIPE_THRESHOLD_RATIO = 0.2f; 97 98 /** 99 * Specifies the total distance, relative to the size of the stack, 100 * that views will be slid, either up or down 101 */ 102 private static final float SLIDE_UP_RATIO = 0.7f; 103 104 /** 105 * Sentinel value for no current active pointer. 106 * Used by {@link #mActivePointerId}. 107 */ 108 private static final int INVALID_POINTER = -1; 109 110 /** 111 * Number of active views in the stack. One fewer view is actually visible, as one is hidden. 112 */ 113 private static final int NUM_ACTIVE_VIEWS = 5; 114 115 private static final int FRAME_PADDING = 4; 116 117 private final Rect mTouchRect = new Rect(); 118 119 private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000; 120 121 private static final long MIN_TIME_BETWEEN_SCROLLS = 100; 122 123 /** 124 * These variables are all related to the current state of touch interaction 125 * with the stack 126 */ 127 private float mInitialY; 128 private float mInitialX; 129 private int mActivePointerId; 130 private int mYVelocity = 0; 131 private int mSwipeGestureType = GESTURE_NONE; 132 private int mSlideAmount; 133 private int mSwipeThreshold; 134 private int mTouchSlop; 135 private int mMaximumVelocity; 136 private VelocityTracker mVelocityTracker; 137 private boolean mTransitionIsSetup = false; 138 private int mResOutColor; 139 private int mClickColor; 140 141 private static HolographicHelper sHolographicHelper; 142 private ImageView mHighlight; 143 private ImageView mClickFeedback; 144 private boolean mClickFeedbackIsValid = false; 145 private StackSlider mStackSlider; 146 private boolean mFirstLayoutHappened = false; 147 private long mLastInteractionTime = 0; 148 private long mLastScrollTime; 149 private int mStackMode; 150 private int mFramePadding; 151 private final Rect stackInvalidateRect = new Rect(); 152 153 /** 154 * {@inheritDoc} 155 */ 156 public StackView(Context context) { 157 this(context, null); 158 } 159 160 /** 161 * {@inheritDoc} 162 */ 163 public StackView(Context context, AttributeSet attrs) { 164 this(context, attrs, com.android.internal.R.attr.stackViewStyle); 165 } 166 167 /** 168 * {@inheritDoc} 169 */ 170 public StackView(Context context, AttributeSet attrs, int defStyleAttr) { 171 super(context, attrs, defStyleAttr); 172 TypedArray a = context.obtainStyledAttributes(attrs, 173 com.android.internal.R.styleable.StackView, defStyleAttr, 0); 174 175 mResOutColor = a.getColor( 176 com.android.internal.R.styleable.StackView_resOutColor, 0); 177 mClickColor = a.getColor( 178 com.android.internal.R.styleable.StackView_clickColor, 0); 179 180 a.recycle(); 181 initStackView(); 182 } 183 184 private void initStackView() { 185 configureViewAnimator(NUM_ACTIVE_VIEWS, 1); 186 setStaticTransformationsEnabled(true); 187 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 188 mTouchSlop = configuration.getScaledTouchSlop(); 189 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 190 mActivePointerId = INVALID_POINTER; 191 192 mHighlight = new ImageView(getContext()); 193 mHighlight.setLayoutParams(new LayoutParams(mHighlight)); 194 addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight)); 195 196 mClickFeedback = new ImageView(getContext()); 197 mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback)); 198 addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback)); 199 mClickFeedback.setVisibility(INVISIBLE); 200 201 mStackSlider = new StackSlider(); 202 203 if (sHolographicHelper == null) { 204 sHolographicHelper = new HolographicHelper(mContext); 205 } 206 setClipChildren(false); 207 setClipToPadding(false); 208 209 // This sets the form of the StackView, which is currently to have the perspective-shifted 210 // views above the active view, and have items slide down when sliding out. The opposite is 211 // available by using ITEMS_SLIDE_UP. 212 mStackMode = ITEMS_SLIDE_DOWN; 213 214 // This is a flag to indicate the the stack is loading for the first time 215 mWhichChild = -1; 216 217 // Adjust the frame padding based on the density, since the highlight changes based 218 // on the density 219 final float density = mContext.getResources().getDisplayMetrics().density; 220 mFramePadding = (int) Math.ceil(density * FRAME_PADDING); 221 } 222 223 /** 224 * Animate the views between different relative indexes within the {@link AdapterViewAnimator} 225 */ 226 void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) { 227 if (!animate) { 228 ((StackFrame) view).cancelSliderAnimator(); 229 view.setRotationX(0f); 230 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 231 lp.setVerticalOffset(0); 232 lp.setHorizontalOffset(0); 233 } 234 235 if (fromIndex == -1 && toIndex == getNumActiveViews() -1) { 236 transformViewAtIndex(toIndex, view, false); 237 view.setVisibility(VISIBLE); 238 view.setAlpha(1.0f); 239 } else if (fromIndex == 0 && toIndex == 1) { 240 // Slide item in 241 ((StackFrame) view).cancelSliderAnimator(); 242 view.setVisibility(VISIBLE); 243 244 int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity)); 245 StackSlider animationSlider = new StackSlider(mStackSlider); 246 animationSlider.setView(view); 247 248 if (animate) { 249 PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f); 250 PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 251 ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 252 slideInX, slideInY); 253 slideIn.setDuration(duration); 254 slideIn.setInterpolator(new LinearInterpolator()); 255 ((StackFrame) view).setSliderAnimator(slideIn); 256 slideIn.start(); 257 } else { 258 animationSlider.setYProgress(0f); 259 animationSlider.setXProgress(0f); 260 } 261 } else if (fromIndex == 1 && toIndex == 0) { 262 // Slide item out 263 ((StackFrame) view).cancelSliderAnimator(); 264 int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity)); 265 266 StackSlider animationSlider = new StackSlider(mStackSlider); 267 animationSlider.setView(view); 268 if (animate) { 269 PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f); 270 PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 271 ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 272 slideOutX, slideOutY); 273 slideOut.setDuration(duration); 274 slideOut.setInterpolator(new LinearInterpolator()); 275 ((StackFrame) view).setSliderAnimator(slideOut); 276 slideOut.start(); 277 } else { 278 animationSlider.setYProgress(1.0f); 279 animationSlider.setXProgress(0f); 280 } 281 } else if (toIndex == 0) { 282 // Make sure this view that is "waiting in the wings" is invisible 283 view.setAlpha(0.0f); 284 view.setVisibility(INVISIBLE); 285 } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) { 286 view.setVisibility(VISIBLE); 287 view.setAlpha(1.0f); 288 view.setRotationX(0f); 289 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 290 lp.setVerticalOffset(0); 291 lp.setHorizontalOffset(0); 292 } else if (fromIndex == -1) { 293 view.setAlpha(1.0f); 294 view.setVisibility(VISIBLE); 295 } else if (toIndex == -1) { 296 if (animate) { 297 postDelayed(new Runnable() { 298 public void run() { 299 view.setAlpha(0); 300 } 301 }, STACK_RELAYOUT_DURATION); 302 } else { 303 view.setAlpha(0f); 304 } 305 } 306 307 // Implement the faked perspective 308 if (toIndex != -1) { 309 transformViewAtIndex(toIndex, view, animate); 310 } 311 } 312 313 private void transformViewAtIndex(int index, final View view, boolean animate) { 314 final float maxPerspectiveShiftY = mPerspectiveShiftY; 315 final float maxPerspectiveShiftX = mPerspectiveShiftX; 316 317 if (mStackMode == ITEMS_SLIDE_DOWN) { 318 index = mMaxNumActiveViews - index - 1; 319 if (index == mMaxNumActiveViews - 1) index--; 320 } else { 321 index--; 322 if (index < 0) index++; 323 } 324 325 float r = (index * 1.0f) / (mMaxNumActiveViews - 2); 326 327 final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r); 328 329 float perspectiveTranslationY = r * maxPerspectiveShiftY; 330 float scaleShiftCorrectionY = (scale - 1) * 331 (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f); 332 final float transY = perspectiveTranslationY + scaleShiftCorrectionY; 333 334 float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX; 335 float scaleShiftCorrectionX = (1 - scale) * 336 (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f); 337 final float transX = perspectiveTranslationX + scaleShiftCorrectionX; 338 339 // If this view is currently being animated for a certain position, we need to cancel 340 // this animation so as not to interfere with the new transformation. 341 if (view instanceof StackFrame) { 342 ((StackFrame) view).cancelTransformAnimator(); 343 } 344 345 if (animate) { 346 PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX); 347 PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY); 348 PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale); 349 PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale); 350 351 ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY, 352 translationY, translationX); 353 oa.setDuration(STACK_RELAYOUT_DURATION); 354 if (view instanceof StackFrame) { 355 ((StackFrame) view).setTransformAnimator(oa); 356 } 357 oa.start(); 358 } else { 359 view.setTranslationX(transX); 360 view.setTranslationY(transY); 361 view.setScaleX(scale); 362 view.setScaleY(scale); 363 } 364 } 365 366 private void setupStackSlider(View v, int mode) { 367 mStackSlider.setMode(mode); 368 if (v != null) { 369 mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor)); 370 mHighlight.setRotation(v.getRotation()); 371 mHighlight.setTranslationY(v.getTranslationY()); 372 mHighlight.setTranslationX(v.getTranslationX()); 373 mHighlight.bringToFront(); 374 v.bringToFront(); 375 mStackSlider.setView(v); 376 377 v.setVisibility(VISIBLE); 378 } 379 } 380 381 /** 382 * {@inheritDoc} 383 */ 384 @Override 385 @android.view.RemotableViewMethod 386 public void showNext() { 387 if (mSwipeGestureType != GESTURE_NONE) return; 388 if (!mTransitionIsSetup) { 389 View v = getViewAtRelativeIndex(1); 390 if (v != null) { 391 setupStackSlider(v, StackSlider.NORMAL_MODE); 392 mStackSlider.setYProgress(0); 393 mStackSlider.setXProgress(0); 394 } 395 } 396 super.showNext(); 397 } 398 399 /** 400 * {@inheritDoc} 401 */ 402 @Override 403 @android.view.RemotableViewMethod 404 public void showPrevious() { 405 if (mSwipeGestureType != GESTURE_NONE) return; 406 if (!mTransitionIsSetup) { 407 View v = getViewAtRelativeIndex(0); 408 if (v != null) { 409 setupStackSlider(v, StackSlider.NORMAL_MODE); 410 mStackSlider.setYProgress(1); 411 mStackSlider.setXProgress(0); 412 } 413 } 414 super.showPrevious(); 415 } 416 417 @Override 418 void showOnly(int childIndex, boolean animate) { 419 super.showOnly(childIndex, animate); 420 421 // Here we need to make sure that the z-order of the children is correct 422 for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) { 423 int index = modulo(i, getWindowSize()); 424 ViewAndMetaData vm = mViewsMap.get(index); 425 if (vm != null) { 426 View v = mViewsMap.get(index).view; 427 if (v != null) v.bringToFront(); 428 } 429 } 430 if (mHighlight != null) { 431 mHighlight.bringToFront(); 432 } 433 mTransitionIsSetup = false; 434 mClickFeedbackIsValid = false; 435 } 436 437 void updateClickFeedback() { 438 if (!mClickFeedbackIsValid) { 439 View v = getViewAtRelativeIndex(1); 440 if (v != null) { 441 mClickFeedback.setImageBitmap( 442 sHolographicHelper.createClickOutline(v, mClickColor)); 443 mClickFeedback.setTranslationX(v.getTranslationX()); 444 mClickFeedback.setTranslationY(v.getTranslationY()); 445 } 446 mClickFeedbackIsValid = true; 447 } 448 } 449 450 @Override 451 void showTapFeedback(View v) { 452 updateClickFeedback(); 453 mClickFeedback.setVisibility(VISIBLE); 454 mClickFeedback.bringToFront(); 455 invalidate(); 456 } 457 458 @Override 459 void hideTapFeedback(View v) { 460 mClickFeedback.setVisibility(INVISIBLE); 461 invalidate(); 462 } 463 464 private void updateChildTransforms() { 465 for (int i = 0; i < getNumActiveViews(); i++) { 466 View v = getViewAtRelativeIndex(i); 467 if (v != null) { 468 transformViewAtIndex(i, v, false); 469 } 470 } 471 } 472 473 private static class StackFrame extends FrameLayout { 474 WeakReference<ObjectAnimator> transformAnimator; 475 WeakReference<ObjectAnimator> sliderAnimator; 476 477 public StackFrame(Context context) { 478 super(context); 479 } 480 481 void setTransformAnimator(ObjectAnimator oa) { 482 transformAnimator = new WeakReference<ObjectAnimator>(oa); 483 } 484 485 void setSliderAnimator(ObjectAnimator oa) { 486 sliderAnimator = new WeakReference<ObjectAnimator>(oa); 487 } 488 489 boolean cancelTransformAnimator() { 490 if (transformAnimator != null) { 491 ObjectAnimator oa = transformAnimator.get(); 492 if (oa != null) { 493 oa.cancel(); 494 return true; 495 } 496 } 497 return false; 498 } 499 500 boolean cancelSliderAnimator() { 501 if (sliderAnimator != null) { 502 ObjectAnimator oa = sliderAnimator.get(); 503 if (oa != null) { 504 oa.cancel(); 505 return true; 506 } 507 } 508 return false; 509 } 510 } 511 512 @Override 513 FrameLayout getFrameForChild() { 514 StackFrame fl = new StackFrame(mContext); 515 fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding); 516 return fl; 517 } 518 519 /** 520 * Apply any necessary tranforms for the child that is being added. 521 */ 522 void applyTransformForChildAtIndex(View child, int relativeIndex) { 523 } 524 525 @Override 526 protected void dispatchDraw(Canvas canvas) { 527 boolean expandClipRegion = false; 528 529 canvas.getClipBounds(stackInvalidateRect); 530 final int childCount = getChildCount(); 531 for (int i = 0; i < childCount; i++) { 532 final View child = getChildAt(i); 533 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 534 if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) || 535 child.getAlpha() == 0f || child.getVisibility() != VISIBLE) { 536 lp.resetInvalidateRect(); 537 } 538 Rect childInvalidateRect = lp.getInvalidateRect(); 539 if (!childInvalidateRect.isEmpty()) { 540 expandClipRegion = true; 541 stackInvalidateRect.union(childInvalidateRect); 542 } 543 } 544 545 // We only expand the clip bounds if necessary. 546 if (expandClipRegion) { 547 canvas.save(Canvas.CLIP_SAVE_FLAG); 548 canvas.clipRect(stackInvalidateRect, Region.Op.UNION); 549 super.dispatchDraw(canvas); 550 canvas.restore(); 551 } else { 552 super.dispatchDraw(canvas); 553 } 554 } 555 556 private void onLayout() { 557 if (!mFirstLayoutHappened) { 558 mFirstLayoutHappened = true; 559 updateChildTransforms(); 560 } 561 562 final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight()); 563 if (mSlideAmount != newSlideAmount) { 564 mSlideAmount = newSlideAmount; 565 mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount); 566 } 567 568 if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 || 569 Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) { 570 571 mPerspectiveShiftY = mNewPerspectiveShiftY; 572 mPerspectiveShiftX = mNewPerspectiveShiftX; 573 updateChildTransforms(); 574 } 575 } 576 577 @Override 578 public boolean onGenericMotionEvent(MotionEvent event) { 579 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { 580 switch (event.getAction()) { 581 case MotionEvent.ACTION_SCROLL: { 582 final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 583 if (vscroll < 0) { 584 pacedScroll(false); 585 return true; 586 } else if (vscroll > 0) { 587 pacedScroll(true); 588 return true; 589 } 590 } 591 } 592 } 593 return super.onGenericMotionEvent(event); 594 } 595 596 // This ensures that the frequency of stack flips caused by scrolls is capped 597 private void pacedScroll(boolean up) { 598 long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime; 599 if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) { 600 if (up) { 601 showPrevious(); 602 } else { 603 showNext(); 604 } 605 mLastScrollTime = System.currentTimeMillis(); 606 } 607 } 608 609 /** 610 * {@inheritDoc} 611 */ 612 @Override 613 public boolean onInterceptTouchEvent(MotionEvent ev) { 614 int action = ev.getAction(); 615 switch(action & MotionEvent.ACTION_MASK) { 616 case MotionEvent.ACTION_DOWN: { 617 if (mActivePointerId == INVALID_POINTER) { 618 mInitialX = ev.getX(); 619 mInitialY = ev.getY(); 620 mActivePointerId = ev.getPointerId(0); 621 } 622 break; 623 } 624 case MotionEvent.ACTION_MOVE: { 625 int pointerIndex = ev.findPointerIndex(mActivePointerId); 626 if (pointerIndex == INVALID_POINTER) { 627 // no data for our primary pointer, this shouldn't happen, log it 628 Log.d(TAG, "Error: No data for our primary pointer."); 629 return false; 630 } 631 float newY = ev.getY(pointerIndex); 632 float deltaY = newY - mInitialY; 633 634 beginGestureIfNeeded(deltaY); 635 break; 636 } 637 case MotionEvent.ACTION_POINTER_UP: { 638 onSecondaryPointerUp(ev); 639 break; 640 } 641 case MotionEvent.ACTION_UP: 642 case MotionEvent.ACTION_CANCEL: { 643 mActivePointerId = INVALID_POINTER; 644 mSwipeGestureType = GESTURE_NONE; 645 } 646 } 647 648 return mSwipeGestureType != GESTURE_NONE; 649 } 650 651 private void beginGestureIfNeeded(float deltaY) { 652 if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) { 653 final int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN; 654 cancelLongPress(); 655 requestDisallowInterceptTouchEvent(true); 656 657 if (mAdapter == null) return; 658 final int adapterCount = getCount(); 659 660 int activeIndex; 661 if (mStackMode == ITEMS_SLIDE_UP) { 662 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1; 663 } else { 664 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0; 665 } 666 667 boolean endOfStack = mLoopViews && adapterCount == 1 && 668 ((mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_UP) || 669 (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_DOWN)); 670 boolean beginningOfStack = mLoopViews && adapterCount == 1 && 671 ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_UP) || 672 (mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_DOWN)); 673 674 int stackMode; 675 if (mLoopViews && !beginningOfStack && !endOfStack) { 676 stackMode = StackSlider.NORMAL_MODE; 677 } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) { 678 activeIndex++; 679 stackMode = StackSlider.BEGINNING_OF_STACK_MODE; 680 } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) { 681 stackMode = StackSlider.END_OF_STACK_MODE; 682 } else { 683 stackMode = StackSlider.NORMAL_MODE; 684 } 685 686 mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE; 687 688 View v = getViewAtRelativeIndex(activeIndex); 689 if (v == null) return; 690 691 setupStackSlider(v, stackMode); 692 693 // We only register this gesture if we've made it this far without a problem 694 mSwipeGestureType = swipeGestureType; 695 cancelHandleClick(); 696 } 697 } 698 699 /** 700 * {@inheritDoc} 701 */ 702 @Override 703 public boolean onTouchEvent(MotionEvent ev) { 704 super.onTouchEvent(ev); 705 706 int action = ev.getAction(); 707 int pointerIndex = ev.findPointerIndex(mActivePointerId); 708 if (pointerIndex == INVALID_POINTER) { 709 // no data for our primary pointer, this shouldn't happen, log it 710 Log.d(TAG, "Error: No data for our primary pointer."); 711 return false; 712 } 713 714 float newY = ev.getY(pointerIndex); 715 float newX = ev.getX(pointerIndex); 716 float deltaY = newY - mInitialY; 717 float deltaX = newX - mInitialX; 718 if (mVelocityTracker == null) { 719 mVelocityTracker = VelocityTracker.obtain(); 720 } 721 mVelocityTracker.addMovement(ev); 722 723 switch (action & MotionEvent.ACTION_MASK) { 724 case MotionEvent.ACTION_MOVE: { 725 beginGestureIfNeeded(deltaY); 726 727 float rx = deltaX / (mSlideAmount * 1.0f); 728 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 729 float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f; 730 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; 731 mStackSlider.setYProgress(1 - r); 732 mStackSlider.setXProgress(rx); 733 return true; 734 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) { 735 float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f; 736 if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; 737 mStackSlider.setYProgress(r); 738 mStackSlider.setXProgress(rx); 739 return true; 740 } 741 break; 742 } 743 case MotionEvent.ACTION_UP: { 744 handlePointerUp(ev); 745 break; 746 } 747 case MotionEvent.ACTION_POINTER_UP: { 748 onSecondaryPointerUp(ev); 749 break; 750 } 751 case MotionEvent.ACTION_CANCEL: { 752 mActivePointerId = INVALID_POINTER; 753 mSwipeGestureType = GESTURE_NONE; 754 break; 755 } 756 } 757 return true; 758 } 759 760 private void onSecondaryPointerUp(MotionEvent ev) { 761 final int activePointerIndex = ev.getActionIndex(); 762 final int pointerId = ev.getPointerId(activePointerIndex); 763 if (pointerId == mActivePointerId) { 764 765 int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1; 766 767 View v = getViewAtRelativeIndex(activeViewIndex); 768 if (v == null) return; 769 770 // Our primary pointer has gone up -- let's see if we can find 771 // another pointer on the view. If so, then we should replace 772 // our primary pointer with this new pointer and adjust things 773 // so that the view doesn't jump 774 for (int index = 0; index < ev.getPointerCount(); index++) { 775 if (index != activePointerIndex) { 776 777 float x = ev.getX(index); 778 float y = ev.getY(index); 779 780 mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 781 if (mTouchRect.contains(Math.round(x), Math.round(y))) { 782 float oldX = ev.getX(activePointerIndex); 783 float oldY = ev.getY(activePointerIndex); 784 785 // adjust our frame of reference to avoid a jump 786 mInitialY += (y - oldY); 787 mInitialX += (x - oldX); 788 789 mActivePointerId = ev.getPointerId(index); 790 if (mVelocityTracker != null) { 791 mVelocityTracker.clear(); 792 } 793 // ok, we're good, we found a new pointer which is touching the active view 794 return; 795 } 796 } 797 } 798 // if we made it this far, it means we didn't find a satisfactory new pointer :(, 799 // so end the gesture 800 handlePointerUp(ev); 801 } 802 } 803 804 private void handlePointerUp(MotionEvent ev) { 805 int pointerIndex = ev.findPointerIndex(mActivePointerId); 806 float newY = ev.getY(pointerIndex); 807 int deltaY = (int) (newY - mInitialY); 808 mLastInteractionTime = System.currentTimeMillis(); 809 810 if (mVelocityTracker != null) { 811 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 812 mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); 813 } 814 815 if (mVelocityTracker != null) { 816 mVelocityTracker.recycle(); 817 mVelocityTracker = null; 818 } 819 820 if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN 821 && mStackSlider.mMode == StackSlider.NORMAL_MODE) { 822 // We reset the gesture variable, because otherwise we will ignore showPrevious() / 823 // showNext(); 824 mSwipeGestureType = GESTURE_NONE; 825 826 // Swipe threshold exceeded, swipe down 827 if (mStackMode == ITEMS_SLIDE_UP) { 828 showPrevious(); 829 } else { 830 showNext(); 831 } 832 mHighlight.bringToFront(); 833 } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP 834 && mStackSlider.mMode == StackSlider.NORMAL_MODE) { 835 // We reset the gesture variable, because otherwise we will ignore showPrevious() / 836 // showNext(); 837 mSwipeGestureType = GESTURE_NONE; 838 839 // Swipe threshold exceeded, swipe up 840 if (mStackMode == ITEMS_SLIDE_UP) { 841 showNext(); 842 } else { 843 showPrevious(); 844 } 845 846 mHighlight.bringToFront(); 847 } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) { 848 // Didn't swipe up far enough, snap back down 849 int duration; 850 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0; 851 if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) { 852 duration = Math.round(mStackSlider.getDurationForNeutralPosition()); 853 } else { 854 duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); 855 } 856 857 StackSlider animationSlider = new StackSlider(mStackSlider); 858 PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress); 859 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 860 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 861 snapBackX, snapBackY); 862 pa.setDuration(duration); 863 pa.setInterpolator(new LinearInterpolator()); 864 pa.start(); 865 } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 866 // Didn't swipe down far enough, snap back up 867 float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1; 868 int duration; 869 if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) { 870 duration = Math.round(mStackSlider.getDurationForNeutralPosition()); 871 } else { 872 duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); 873 } 874 875 StackSlider animationSlider = new StackSlider(mStackSlider); 876 PropertyValuesHolder snapBackY = 877 PropertyValuesHolder.ofFloat("YProgress",finalYProgress); 878 PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); 879 ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, 880 snapBackX, snapBackY); 881 pa.setDuration(duration); 882 pa.start(); 883 } 884 885 mActivePointerId = INVALID_POINTER; 886 mSwipeGestureType = GESTURE_NONE; 887 } 888 889 private class StackSlider { 890 View mView; 891 float mYProgress; 892 float mXProgress; 893 894 static final int NORMAL_MODE = 0; 895 static final int BEGINNING_OF_STACK_MODE = 1; 896 static final int END_OF_STACK_MODE = 2; 897 898 int mMode = NORMAL_MODE; 899 900 public StackSlider() { 901 } 902 903 public StackSlider(StackSlider copy) { 904 mView = copy.mView; 905 mYProgress = copy.mYProgress; 906 mXProgress = copy.mXProgress; 907 mMode = copy.mMode; 908 } 909 910 private float cubic(float r) { 911 return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f; 912 } 913 914 private float highlightAlphaInterpolator(float r) { 915 float pivot = 0.4f; 916 if (r < pivot) { 917 return 0.85f * cubic(r / pivot); 918 } else { 919 return 0.85f * cubic(1 - (r - pivot) / (1 - pivot)); 920 } 921 } 922 923 private float viewAlphaInterpolator(float r) { 924 float pivot = 0.3f; 925 if (r > pivot) { 926 return (r - pivot) / (1 - pivot); 927 } else { 928 return 0; 929 } 930 } 931 932 private float rotationInterpolator(float r) { 933 float pivot = 0.2f; 934 if (r < pivot) { 935 return 0; 936 } else { 937 return (r - pivot) / (1 - pivot); 938 } 939 } 940 941 void setView(View v) { 942 mView = v; 943 } 944 945 public void setYProgress(float r) { 946 // enforce r between 0 and 1 947 r = Math.min(1.0f, r); 948 r = Math.max(0, r); 949 950 mYProgress = r; 951 if (mView == null) return; 952 953 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 954 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 955 956 int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1; 957 958 // We need to prevent any clipping issues which may arise by setting a layer type. 959 // This doesn't come for free however, so we only want to enable it when required. 960 if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) { 961 if (mView.getLayerType() == LAYER_TYPE_NONE) { 962 mView.setLayerType(LAYER_TYPE_HARDWARE, null); 963 } 964 } else { 965 if (mView.getLayerType() != LAYER_TYPE_NONE) { 966 mView.setLayerType(LAYER_TYPE_NONE, null); 967 } 968 } 969 970 switch (mMode) { 971 case NORMAL_MODE: 972 viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); 973 highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); 974 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 975 976 float alpha = viewAlphaInterpolator(1 - r); 977 978 // We make sure that views which can't be seen (have 0 alpha) are also invisible 979 // so that they don't interfere with click events. 980 if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) { 981 mView.setVisibility(VISIBLE); 982 } else if (alpha == 0 && mView.getAlpha() != 0 983 && mView.getVisibility() == VISIBLE) { 984 mView.setVisibility(INVISIBLE); 985 } 986 987 mView.setAlpha(alpha); 988 mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); 989 mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); 990 break; 991 case END_OF_STACK_MODE: 992 r = r * 0.2f; 993 viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount)); 994 highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount)); 995 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 996 break; 997 case BEGINNING_OF_STACK_MODE: 998 r = (1-r) * 0.2f; 999 viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount)); 1000 highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount)); 1001 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 1002 break; 1003 } 1004 } 1005 1006 public void setXProgress(float r) { 1007 // enforce r between 0 and 1 1008 r = Math.min(2.0f, r); 1009 r = Math.max(-2.0f, r); 1010 1011 mXProgress = r; 1012 1013 if (mView == null) return; 1014 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 1015 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 1016 1017 r *= 0.2f; 1018 viewLp.setHorizontalOffset(Math.round(r * mSlideAmount)); 1019 highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount)); 1020 } 1021 1022 void setMode(int mode) { 1023 mMode = mode; 1024 } 1025 1026 float getDurationForNeutralPosition() { 1027 return getDuration(false, 0); 1028 } 1029 1030 float getDurationForOffscreenPosition() { 1031 return getDuration(true, 0); 1032 } 1033 1034 float getDurationForNeutralPosition(float velocity) { 1035 return getDuration(false, velocity); 1036 } 1037 1038 float getDurationForOffscreenPosition(float velocity) { 1039 return getDuration(true, velocity); 1040 } 1041 1042 private float getDuration(boolean invert, float velocity) { 1043 if (mView != null) { 1044 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 1045 1046 float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) + 1047 Math.pow(viewLp.verticalOffset, 2)); 1048 float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) + 1049 Math.pow(0.4f * mSlideAmount, 2)); 1050 1051 if (velocity == 0) { 1052 return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION; 1053 } else { 1054 float duration = invert ? d / Math.abs(velocity) : 1055 (maxd - d) / Math.abs(velocity); 1056 if (duration < MINIMUM_ANIMATION_DURATION || 1057 duration > DEFAULT_ANIMATION_DURATION) { 1058 return getDuration(invert, 0); 1059 } else { 1060 return duration; 1061 } 1062 } 1063 } 1064 return 0; 1065 } 1066 1067 // Used for animations 1068 @SuppressWarnings({"UnusedDeclaration"}) 1069 public float getYProgress() { 1070 return mYProgress; 1071 } 1072 1073 // Used for animations 1074 @SuppressWarnings({"UnusedDeclaration"}) 1075 public float getXProgress() { 1076 return mXProgress; 1077 } 1078 } 1079 1080 LayoutParams createOrReuseLayoutParams(View v) { 1081 final ViewGroup.LayoutParams currentLp = v.getLayoutParams(); 1082 if (currentLp instanceof LayoutParams) { 1083 LayoutParams lp = (LayoutParams) currentLp; 1084 lp.setHorizontalOffset(0); 1085 lp.setVerticalOffset(0); 1086 lp.width = 0; 1087 lp.width = 0; 1088 return lp; 1089 } 1090 return new LayoutParams(v); 1091 } 1092 1093 @Override 1094 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1095 checkForAndHandleDataChanged(); 1096 1097 final int childCount = getChildCount(); 1098 for (int i = 0; i < childCount; i++) { 1099 final View child = getChildAt(i); 1100 1101 int childRight = mPaddingLeft + child.getMeasuredWidth(); 1102 int childBottom = mPaddingTop + child.getMeasuredHeight(); 1103 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1104 1105 child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset, 1106 childRight + lp.horizontalOffset, childBottom + lp.verticalOffset); 1107 1108 } 1109 onLayout(); 1110 } 1111 1112 @Override 1113 public void advance() { 1114 long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime; 1115 1116 if (mAdapter == null) return; 1117 final int adapterCount = getCount(); 1118 if (adapterCount == 1 && mLoopViews) return; 1119 1120 if (mSwipeGestureType == GESTURE_NONE && 1121 timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) { 1122 showNext(); 1123 } 1124 } 1125 1126 private void measureChildren() { 1127 final int count = getChildCount(); 1128 1129 final int measuredWidth = getMeasuredWidth(); 1130 final int measuredHeight = getMeasuredHeight(); 1131 1132 final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X)) 1133 - mPaddingLeft - mPaddingRight; 1134 final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y)) 1135 - mPaddingTop - mPaddingBottom; 1136 1137 int maxWidth = 0; 1138 int maxHeight = 0; 1139 1140 for (int i = 0; i < count; i++) { 1141 final View child = getChildAt(i); 1142 child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST), 1143 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST)); 1144 1145 if (child != mHighlight && child != mClickFeedback) { 1146 final int childMeasuredWidth = child.getMeasuredWidth(); 1147 final int childMeasuredHeight = child.getMeasuredHeight(); 1148 if (childMeasuredWidth > maxWidth) { 1149 maxWidth = childMeasuredWidth; 1150 } 1151 if (childMeasuredHeight > maxHeight) { 1152 maxHeight = childMeasuredHeight; 1153 } 1154 } 1155 } 1156 1157 mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth; 1158 mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight; 1159 1160 // If we have extra space, we try and spread the items out 1161 if (maxWidth > 0 && count > 0 && maxWidth < childWidth) { 1162 mNewPerspectiveShiftX = measuredWidth - maxWidth; 1163 } 1164 1165 if (maxHeight > 0 && count > 0 && maxHeight < childHeight) { 1166 mNewPerspectiveShiftY = measuredHeight - maxHeight; 1167 } 1168 } 1169 1170 @Override 1171 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1172 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 1173 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 1174 final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 1175 final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 1176 1177 boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1); 1178 1179 // We need to deal with the case where our parent hasn't told us how 1180 // big we should be. In this case we should 1181 float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y); 1182 if (heightSpecMode == MeasureSpec.UNSPECIFIED) { 1183 heightSpecSize = haveChildRefSize ? 1184 Math.round(mReferenceChildHeight * (1 + factorY)) + 1185 mPaddingTop + mPaddingBottom : 0; 1186 } else if (heightSpecMode == MeasureSpec.AT_MOST) { 1187 if (haveChildRefSize) { 1188 int height = Math.round(mReferenceChildHeight * (1 + factorY)) 1189 + mPaddingTop + mPaddingBottom; 1190 if (height <= heightSpecSize) { 1191 heightSpecSize = height; 1192 } else { 1193 heightSpecSize |= MEASURED_STATE_TOO_SMALL; 1194 1195 } 1196 } else { 1197 heightSpecSize = 0; 1198 } 1199 } 1200 1201 float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X); 1202 if (widthSpecMode == MeasureSpec.UNSPECIFIED) { 1203 widthSpecSize = haveChildRefSize ? 1204 Math.round(mReferenceChildWidth * (1 + factorX)) + 1205 mPaddingLeft + mPaddingRight : 0; 1206 } else if (heightSpecMode == MeasureSpec.AT_MOST) { 1207 if (haveChildRefSize) { 1208 int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight; 1209 if (width <= widthSpecSize) { 1210 widthSpecSize = width; 1211 } else { 1212 widthSpecSize |= MEASURED_STATE_TOO_SMALL; 1213 } 1214 } else { 1215 widthSpecSize = 0; 1216 } 1217 } 1218 setMeasuredDimension(widthSpecSize, heightSpecSize); 1219 measureChildren(); 1220 } 1221 1222 @Override 1223 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1224 super.onInitializeAccessibilityEvent(event); 1225 event.setClassName(StackView.class.getName()); 1226 } 1227 1228 @Override 1229 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1230 super.onInitializeAccessibilityNodeInfo(info); 1231 info.setClassName(StackView.class.getName()); 1232 info.setScrollable(getChildCount() > 1); 1233 if (isEnabled()) { 1234 if (getDisplayedChild() < getChildCount() - 1) { 1235 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 1236 } 1237 if (getDisplayedChild() > 0) { 1238 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 1239 } 1240 } 1241 } 1242 1243 @Override 1244 public boolean performAccessibilityAction(int action, Bundle arguments) { 1245 if (super.performAccessibilityAction(action, arguments)) { 1246 return true; 1247 } 1248 if (!isEnabled()) { 1249 return false; 1250 } 1251 switch (action) { 1252 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 1253 if (getDisplayedChild() < getChildCount() - 1) { 1254 showNext(); 1255 return true; 1256 } 1257 } return false; 1258 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 1259 if (getDisplayedChild() > 0) { 1260 showPrevious(); 1261 return true; 1262 } 1263 } return false; 1264 } 1265 return false; 1266 } 1267 1268 class LayoutParams extends ViewGroup.LayoutParams { 1269 int horizontalOffset; 1270 int verticalOffset; 1271 View mView; 1272 private final Rect parentRect = new Rect(); 1273 private final Rect invalidateRect = new Rect(); 1274 private final RectF invalidateRectf = new RectF(); 1275 private final Rect globalInvalidateRect = new Rect(); 1276 1277 LayoutParams(View view) { 1278 super(0, 0); 1279 width = 0; 1280 height = 0; 1281 horizontalOffset = 0; 1282 verticalOffset = 0; 1283 mView = view; 1284 } 1285 1286 LayoutParams(Context c, AttributeSet attrs) { 1287 super(c, attrs); 1288 horizontalOffset = 0; 1289 verticalOffset = 0; 1290 width = 0; 1291 height = 0; 1292 } 1293 1294 void invalidateGlobalRegion(View v, Rect r) { 1295 // We need to make a new rect here, so as not to modify the one passed 1296 globalInvalidateRect.set(r); 1297 globalInvalidateRect.union(0, 0, getWidth(), getHeight()); 1298 View p = v; 1299 if (!(v.getParent() != null && v.getParent() instanceof View)) return; 1300 1301 boolean firstPass = true; 1302 parentRect.set(0, 0, 0, 0); 1303 while (p.getParent() != null && p.getParent() instanceof View 1304 && !parentRect.contains(globalInvalidateRect)) { 1305 if (!firstPass) { 1306 globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop() 1307 - p.getScrollY()); 1308 } 1309 firstPass = false; 1310 p = (View) p.getParent(); 1311 parentRect.set(p.getScrollX(), p.getScrollY(), 1312 p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY()); 1313 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top, 1314 globalInvalidateRect.right, globalInvalidateRect.bottom); 1315 } 1316 1317 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top, 1318 globalInvalidateRect.right, globalInvalidateRect.bottom); 1319 } 1320 1321 Rect getInvalidateRect() { 1322 return invalidateRect; 1323 } 1324 1325 void resetInvalidateRect() { 1326 invalidateRect.set(0, 0, 0, 0); 1327 } 1328 1329 // This is public so that ObjectAnimator can access it 1330 public void setVerticalOffset(int newVerticalOffset) { 1331 setOffsets(horizontalOffset, newVerticalOffset); 1332 } 1333 1334 public void setHorizontalOffset(int newHorizontalOffset) { 1335 setOffsets(newHorizontalOffset, verticalOffset); 1336 } 1337 1338 public void setOffsets(int newHorizontalOffset, int newVerticalOffset) { 1339 int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset; 1340 horizontalOffset = newHorizontalOffset; 1341 int verticalOffsetDelta = newVerticalOffset - verticalOffset; 1342 verticalOffset = newVerticalOffset; 1343 1344 if (mView != null) { 1345 mView.requestLayout(); 1346 int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft()); 1347 int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight()); 1348 int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop()); 1349 int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom()); 1350 1351 invalidateRectf.set(left, top, right, bottom); 1352 1353 float xoffset = -invalidateRectf.left; 1354 float yoffset = -invalidateRectf.top; 1355 invalidateRectf.offset(xoffset, yoffset); 1356 mView.getMatrix().mapRect(invalidateRectf); 1357 invalidateRectf.offset(-xoffset, -yoffset); 1358 1359 invalidateRect.set((int) Math.floor(invalidateRectf.left), 1360 (int) Math.floor(invalidateRectf.top), 1361 (int) Math.ceil(invalidateRectf.right), 1362 (int) Math.ceil(invalidateRectf.bottom)); 1363 1364 invalidateGlobalRegion(mView, invalidateRect); 1365 } 1366 } 1367 } 1368 1369 private static class HolographicHelper { 1370 private final Paint mHolographicPaint = new Paint(); 1371 private final Paint mErasePaint = new Paint(); 1372 private final Paint mBlurPaint = new Paint(); 1373 private static final int RES_OUT = 0; 1374 private static final int CLICK_FEEDBACK = 1; 1375 private float mDensity; 1376 private BlurMaskFilter mSmallBlurMaskFilter; 1377 private BlurMaskFilter mLargeBlurMaskFilter; 1378 private final Canvas mCanvas = new Canvas(); 1379 private final Canvas mMaskCanvas = new Canvas(); 1380 private final int[] mTmpXY = new int[2]; 1381 private final Matrix mIdentityMatrix = new Matrix(); 1382 1383 HolographicHelper(Context context) { 1384 mDensity = context.getResources().getDisplayMetrics().density; 1385 1386 mHolographicPaint.setFilterBitmap(true); 1387 mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30)); 1388 mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); 1389 mErasePaint.setFilterBitmap(true); 1390 1391 mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL); 1392 mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL); 1393 } 1394 1395 Bitmap createClickOutline(View v, int color) { 1396 return createOutline(v, CLICK_FEEDBACK, color); 1397 } 1398 1399 Bitmap createResOutline(View v, int color) { 1400 return createOutline(v, RES_OUT, color); 1401 } 1402 1403 Bitmap createOutline(View v, int type, int color) { 1404 mHolographicPaint.setColor(color); 1405 if (type == RES_OUT) { 1406 mBlurPaint.setMaskFilter(mSmallBlurMaskFilter); 1407 } else if (type == CLICK_FEEDBACK) { 1408 mBlurPaint.setMaskFilter(mLargeBlurMaskFilter); 1409 } 1410 1411 if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) { 1412 return null; 1413 } 1414 1415 Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(), 1416 v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888); 1417 mCanvas.setBitmap(bitmap); 1418 1419 float rotationX = v.getRotationX(); 1420 float rotation = v.getRotation(); 1421 float translationY = v.getTranslationY(); 1422 float translationX = v.getTranslationX(); 1423 v.setRotationX(0); 1424 v.setRotation(0); 1425 v.setTranslationY(0); 1426 v.setTranslationX(0); 1427 v.draw(mCanvas); 1428 v.setRotationX(rotationX); 1429 v.setRotation(rotation); 1430 v.setTranslationY(translationY); 1431 v.setTranslationX(translationX); 1432 1433 drawOutline(mCanvas, bitmap); 1434 mCanvas.setBitmap(null); 1435 return bitmap; 1436 } 1437 1438 void drawOutline(Canvas dest, Bitmap src) { 1439 final int[] xy = mTmpXY; 1440 Bitmap mask = src.extractAlpha(mBlurPaint, xy); 1441 mMaskCanvas.setBitmap(mask); 1442 mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint); 1443 dest.drawColor(0, PorterDuff.Mode.CLEAR); 1444 dest.setMatrix(mIdentityMatrix); 1445 dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint); 1446 mMaskCanvas.setBitmap(null); 1447 mask.recycle(); 1448 } 1449 } 1450 } 1451