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