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