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