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