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