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 java.lang.ref.WeakReference; 19 20 import android.animation.ObjectAnimator; 21 import android.animation.PropertyValuesHolder; 22 import android.content.Context; 23 import android.content.res.TypedArray; 24 import android.graphics.Bitmap; 25 import android.graphics.BlurMaskFilter; 26 import android.graphics.Canvas; 27 import android.graphics.Matrix; 28 import android.graphics.Paint; 29 import android.graphics.PorterDuff; 30 import android.graphics.PorterDuffXfermode; 31 import android.graphics.Rect; 32 import android.graphics.RectF; 33 import android.graphics.Region; 34 import android.graphics.TableMaskFilter; 35 import android.os.Bundle; 36 import android.util.AttributeSet; 37 import android.util.Log; 38 import android.view.InputDevice; 39 import android.view.MotionEvent; 40 import android.view.VelocityTracker; 41 import android.view.View; 42 import android.view.ViewConfiguration; 43 import android.view.ViewGroup; 44 import android.view.accessibility.AccessibilityEvent; 45 import android.view.accessibility.AccessibilityNodeInfo; 46 import android.view.animation.LinearInterpolator; 47 import android.widget.RemoteViews.RemoteView; 48 49 @RemoteView 50 /** 51 * A view that displays its children in a stack and allows users to discretely swipe 52 * through the children. 53 */ 54 public class StackView extends AdapterViewAnimator { 55 private final String TAG = "StackView"; 56 57 /** 58 * Default animation parameters 59 */ 60 private static final int DEFAULT_ANIMATION_DURATION = 400; 61 private static final int MINIMUM_ANIMATION_DURATION = 50; 62 private static final int STACK_RELAYOUT_DURATION = 100; 63 64 /** 65 * Parameters effecting the perspective visuals 66 */ 67 private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f; 68 private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f; 69 70 private float mPerspectiveShiftX; 71 private float mPerspectiveShiftY; 72 private float mNewPerspectiveShiftX; 73 private float mNewPerspectiveShiftY; 74 75 @SuppressWarnings({"FieldCanBeLocal"}) 76 private static final float PERSPECTIVE_SCALE_FACTOR = 0f; 77 78 /** 79 * Represent the two possible stack modes, one where items slide up, and the other 80 * where items slide down. The perspective is also inverted between these two modes. 81 */ 82 private static final int ITEMS_SLIDE_UP = 0; 83 private static final int ITEMS_SLIDE_DOWN = 1; 84 85 /** 86 * These specify the different gesture states 87 */ 88 private static final int GESTURE_NONE = 0; 89 private static final int GESTURE_SLIDE_UP = 1; 90 private static final int GESTURE_SLIDE_DOWN = 2; 91 92 /** 93 * Specifies how far you need to swipe (up or down) before it 94 * will be consider a completed gesture when you lift your finger 95 */ 96 private static final float SWIPE_THRESHOLD_RATIO = 0.2f; 97 98 /** 99 * Specifies the total distance, relative to the size of the stack, 100 * that views will be slid, either up or down 101 */ 102 private static final float SLIDE_UP_RATIO = 0.7f; 103 104 /** 105 * Sentinel value for no current active pointer. 106 * Used by {@link #mActivePointerId}. 107 */ 108 private static final int INVALID_POINTER = -1; 109 110 /** 111 * Number of active views in the stack. One fewer view is actually visible, as one is hidden. 112 */ 113 private static final int NUM_ACTIVE_VIEWS = 5; 114 115 private static final int FRAME_PADDING = 4; 116 117 private final Rect mTouchRect = new Rect(); 118 119 private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000; 120 121 private static final long MIN_TIME_BETWEEN_SCROLLS = 100; 122 123 /** 124 * These variables are all related to the current state of touch interaction 125 * with the stack 126 */ 127 private float mInitialY; 128 private float mInitialX; 129 private int mActivePointerId; 130 private int mYVelocity = 0; 131 private int mSwipeGestureType = GESTURE_NONE; 132 private int mSlideAmount; 133 private int mSwipeThreshold; 134 private int mTouchSlop; 135 private int mMaximumVelocity; 136 private VelocityTracker mVelocityTracker; 137 private boolean mTransitionIsSetup = false; 138 private int mResOutColor; 139 private int mClickColor; 140 141 private static HolographicHelper sHolographicHelper; 142 private ImageView mHighlight; 143 private ImageView mClickFeedback; 144 private boolean mClickFeedbackIsValid = false; 145 private StackSlider mStackSlider; 146 private boolean mFirstLayoutHappened = false; 147 private long mLastInteractionTime = 0; 148 private long mLastScrollTime; 149 private int mStackMode; 150 private int mFramePadding; 151 private final Rect stackInvalidateRect = new Rect(); 152 153 /** 154 * {@inheritDoc} 155 */ 156 public StackView(Context context) { 157 this(context, null); 158 } 159 160 /** 161 * {@inheritDoc} 162 */ 163 public StackView(Context context, AttributeSet attrs) { 164 this(context, attrs, com.android.internal.R.attr.stackViewStyle); 165 } 166 167 /** 168 * {@inheritDoc} 169 */ 170 public StackView(Context context, AttributeSet attrs, int defStyleAttr) { 171 this(context, attrs, defStyleAttr, 0); 172 } 173 174 /** 175 * {@inheritDoc} 176 */ 177 public StackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 178 super(context, attrs, defStyleAttr, defStyleRes); 179 final TypedArray a = context.obtainStyledAttributes( 180 attrs, com.android.internal.R.styleable.StackView, 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(Canvas.CLIP_SAVE_FLAG); 555 canvas.clipRect(stackInvalidateRect, Region.Op.UNION); 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.sqrt(Math.pow(viewLp.horizontalOffset, 2) + 1054 Math.pow(viewLp.verticalOffset, 2)); 1055 float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) + 1056 Math.pow(0.4f * mSlideAmount, 2)); 1057 1058 if (velocity == 0) { 1059 return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION; 1060 } else { 1061 float duration = invert ? d / Math.abs(velocity) : 1062 (maxd - d) / Math.abs(velocity); 1063 if (duration < MINIMUM_ANIMATION_DURATION || 1064 duration > DEFAULT_ANIMATION_DURATION) { 1065 return getDuration(invert, 0); 1066 } else { 1067 return duration; 1068 } 1069 } 1070 } 1071 return 0; 1072 } 1073 1074 // Used for animations 1075 @SuppressWarnings({"UnusedDeclaration"}) 1076 public float getYProgress() { 1077 return mYProgress; 1078 } 1079 1080 // Used for animations 1081 @SuppressWarnings({"UnusedDeclaration"}) 1082 public float getXProgress() { 1083 return mXProgress; 1084 } 1085 } 1086 1087 LayoutParams createOrReuseLayoutParams(View v) { 1088 final ViewGroup.LayoutParams currentLp = v.getLayoutParams(); 1089 if (currentLp instanceof LayoutParams) { 1090 LayoutParams lp = (LayoutParams) currentLp; 1091 lp.setHorizontalOffset(0); 1092 lp.setVerticalOffset(0); 1093 lp.width = 0; 1094 lp.width = 0; 1095 return lp; 1096 } 1097 return new LayoutParams(v); 1098 } 1099 1100 @Override 1101 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1102 checkForAndHandleDataChanged(); 1103 1104 final int childCount = getChildCount(); 1105 for (int i = 0; i < childCount; i++) { 1106 final View child = getChildAt(i); 1107 1108 int childRight = mPaddingLeft + child.getMeasuredWidth(); 1109 int childBottom = mPaddingTop + child.getMeasuredHeight(); 1110 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1111 1112 child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset, 1113 childRight + lp.horizontalOffset, childBottom + lp.verticalOffset); 1114 1115 } 1116 onLayout(); 1117 } 1118 1119 @Override 1120 public void advance() { 1121 long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime; 1122 1123 if (mAdapter == null) return; 1124 final int adapterCount = getCount(); 1125 if (adapterCount == 1 && mLoopViews) return; 1126 1127 if (mSwipeGestureType == GESTURE_NONE && 1128 timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) { 1129 showNext(); 1130 } 1131 } 1132 1133 private void measureChildren() { 1134 final int count = getChildCount(); 1135 1136 final int measuredWidth = getMeasuredWidth(); 1137 final int measuredHeight = getMeasuredHeight(); 1138 1139 final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X)) 1140 - mPaddingLeft - mPaddingRight; 1141 final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y)) 1142 - mPaddingTop - mPaddingBottom; 1143 1144 int maxWidth = 0; 1145 int maxHeight = 0; 1146 1147 for (int i = 0; i < count; i++) { 1148 final View child = getChildAt(i); 1149 child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST), 1150 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST)); 1151 1152 if (child != mHighlight && child != mClickFeedback) { 1153 final int childMeasuredWidth = child.getMeasuredWidth(); 1154 final int childMeasuredHeight = child.getMeasuredHeight(); 1155 if (childMeasuredWidth > maxWidth) { 1156 maxWidth = childMeasuredWidth; 1157 } 1158 if (childMeasuredHeight > maxHeight) { 1159 maxHeight = childMeasuredHeight; 1160 } 1161 } 1162 } 1163 1164 mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth; 1165 mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight; 1166 1167 // If we have extra space, we try and spread the items out 1168 if (maxWidth > 0 && count > 0 && maxWidth < childWidth) { 1169 mNewPerspectiveShiftX = measuredWidth - maxWidth; 1170 } 1171 1172 if (maxHeight > 0 && count > 0 && maxHeight < childHeight) { 1173 mNewPerspectiveShiftY = measuredHeight - maxHeight; 1174 } 1175 } 1176 1177 @Override 1178 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1179 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 1180 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 1181 final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 1182 final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 1183 1184 boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1); 1185 1186 // We need to deal with the case where our parent hasn't told us how 1187 // big we should be. In this case we should 1188 float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y); 1189 if (heightSpecMode == MeasureSpec.UNSPECIFIED) { 1190 heightSpecSize = haveChildRefSize ? 1191 Math.round(mReferenceChildHeight * (1 + factorY)) + 1192 mPaddingTop + mPaddingBottom : 0; 1193 } else if (heightSpecMode == MeasureSpec.AT_MOST) { 1194 if (haveChildRefSize) { 1195 int height = Math.round(mReferenceChildHeight * (1 + factorY)) 1196 + mPaddingTop + mPaddingBottom; 1197 if (height <= heightSpecSize) { 1198 heightSpecSize = height; 1199 } else { 1200 heightSpecSize |= MEASURED_STATE_TOO_SMALL; 1201 1202 } 1203 } else { 1204 heightSpecSize = 0; 1205 } 1206 } 1207 1208 float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X); 1209 if (widthSpecMode == MeasureSpec.UNSPECIFIED) { 1210 widthSpecSize = haveChildRefSize ? 1211 Math.round(mReferenceChildWidth * (1 + factorX)) + 1212 mPaddingLeft + mPaddingRight : 0; 1213 } else if (heightSpecMode == MeasureSpec.AT_MOST) { 1214 if (haveChildRefSize) { 1215 int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight; 1216 if (width <= widthSpecSize) { 1217 widthSpecSize = width; 1218 } else { 1219 widthSpecSize |= MEASURED_STATE_TOO_SMALL; 1220 } 1221 } else { 1222 widthSpecSize = 0; 1223 } 1224 } 1225 setMeasuredDimension(widthSpecSize, heightSpecSize); 1226 measureChildren(); 1227 } 1228 1229 @Override 1230 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1231 super.onInitializeAccessibilityEvent(event); 1232 event.setClassName(StackView.class.getName()); 1233 } 1234 1235 @Override 1236 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1237 super.onInitializeAccessibilityNodeInfo(info); 1238 info.setClassName(StackView.class.getName()); 1239 info.setScrollable(getChildCount() > 1); 1240 if (isEnabled()) { 1241 if (getDisplayedChild() < getChildCount() - 1) { 1242 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 1243 } 1244 if (getDisplayedChild() > 0) { 1245 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 1246 } 1247 } 1248 } 1249 1250 @Override 1251 public boolean performAccessibilityAction(int action, Bundle arguments) { 1252 if (super.performAccessibilityAction(action, arguments)) { 1253 return true; 1254 } 1255 if (!isEnabled()) { 1256 return false; 1257 } 1258 switch (action) { 1259 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 1260 if (getDisplayedChild() < getChildCount() - 1) { 1261 showNext(); 1262 return true; 1263 } 1264 } return false; 1265 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 1266 if (getDisplayedChild() > 0) { 1267 showPrevious(); 1268 return true; 1269 } 1270 } return false; 1271 } 1272 return false; 1273 } 1274 1275 class LayoutParams extends ViewGroup.LayoutParams { 1276 int horizontalOffset; 1277 int verticalOffset; 1278 View mView; 1279 private final Rect parentRect = new Rect(); 1280 private final Rect invalidateRect = new Rect(); 1281 private final RectF invalidateRectf = new RectF(); 1282 private final Rect globalInvalidateRect = new Rect(); 1283 1284 LayoutParams(View view) { 1285 super(0, 0); 1286 width = 0; 1287 height = 0; 1288 horizontalOffset = 0; 1289 verticalOffset = 0; 1290 mView = view; 1291 } 1292 1293 LayoutParams(Context c, AttributeSet attrs) { 1294 super(c, attrs); 1295 horizontalOffset = 0; 1296 verticalOffset = 0; 1297 width = 0; 1298 height = 0; 1299 } 1300 1301 void invalidateGlobalRegion(View v, Rect r) { 1302 // We need to make a new rect here, so as not to modify the one passed 1303 globalInvalidateRect.set(r); 1304 globalInvalidateRect.union(0, 0, getWidth(), getHeight()); 1305 View p = v; 1306 if (!(v.getParent() != null && v.getParent() instanceof View)) return; 1307 1308 boolean firstPass = true; 1309 parentRect.set(0, 0, 0, 0); 1310 while (p.getParent() != null && p.getParent() instanceof View 1311 && !parentRect.contains(globalInvalidateRect)) { 1312 if (!firstPass) { 1313 globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop() 1314 - p.getScrollY()); 1315 } 1316 firstPass = false; 1317 p = (View) p.getParent(); 1318 parentRect.set(p.getScrollX(), p.getScrollY(), 1319 p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY()); 1320 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top, 1321 globalInvalidateRect.right, globalInvalidateRect.bottom); 1322 } 1323 1324 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top, 1325 globalInvalidateRect.right, globalInvalidateRect.bottom); 1326 } 1327 1328 Rect getInvalidateRect() { 1329 return invalidateRect; 1330 } 1331 1332 void resetInvalidateRect() { 1333 invalidateRect.set(0, 0, 0, 0); 1334 } 1335 1336 // This is public so that ObjectAnimator can access it 1337 public void setVerticalOffset(int newVerticalOffset) { 1338 setOffsets(horizontalOffset, newVerticalOffset); 1339 } 1340 1341 public void setHorizontalOffset(int newHorizontalOffset) { 1342 setOffsets(newHorizontalOffset, verticalOffset); 1343 } 1344 1345 public void setOffsets(int newHorizontalOffset, int newVerticalOffset) { 1346 int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset; 1347 horizontalOffset = newHorizontalOffset; 1348 int verticalOffsetDelta = newVerticalOffset - verticalOffset; 1349 verticalOffset = newVerticalOffset; 1350 1351 if (mView != null) { 1352 mView.requestLayout(); 1353 int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft()); 1354 int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight()); 1355 int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop()); 1356 int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom()); 1357 1358 invalidateRectf.set(left, top, right, bottom); 1359 1360 float xoffset = -invalidateRectf.left; 1361 float yoffset = -invalidateRectf.top; 1362 invalidateRectf.offset(xoffset, yoffset); 1363 mView.getMatrix().mapRect(invalidateRectf); 1364 invalidateRectf.offset(-xoffset, -yoffset); 1365 1366 invalidateRect.set((int) Math.floor(invalidateRectf.left), 1367 (int) Math.floor(invalidateRectf.top), 1368 (int) Math.ceil(invalidateRectf.right), 1369 (int) Math.ceil(invalidateRectf.bottom)); 1370 1371 invalidateGlobalRegion(mView, invalidateRect); 1372 } 1373 } 1374 } 1375 1376 private static class HolographicHelper { 1377 private final Paint mHolographicPaint = new Paint(); 1378 private final Paint mErasePaint = new Paint(); 1379 private final Paint mBlurPaint = new Paint(); 1380 private static final int RES_OUT = 0; 1381 private static final int CLICK_FEEDBACK = 1; 1382 private float mDensity; 1383 private BlurMaskFilter mSmallBlurMaskFilter; 1384 private BlurMaskFilter mLargeBlurMaskFilter; 1385 private final Canvas mCanvas = new Canvas(); 1386 private final Canvas mMaskCanvas = new Canvas(); 1387 private final int[] mTmpXY = new int[2]; 1388 private final Matrix mIdentityMatrix = new Matrix(); 1389 1390 HolographicHelper(Context context) { 1391 mDensity = context.getResources().getDisplayMetrics().density; 1392 1393 mHolographicPaint.setFilterBitmap(true); 1394 mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30)); 1395 mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); 1396 mErasePaint.setFilterBitmap(true); 1397 1398 mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL); 1399 mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL); 1400 } 1401 1402 Bitmap createClickOutline(View v, int color) { 1403 return createOutline(v, CLICK_FEEDBACK, color); 1404 } 1405 1406 Bitmap createResOutline(View v, int color) { 1407 return createOutline(v, RES_OUT, color); 1408 } 1409 1410 Bitmap createOutline(View v, int type, int color) { 1411 mHolographicPaint.setColor(color); 1412 if (type == RES_OUT) { 1413 mBlurPaint.setMaskFilter(mSmallBlurMaskFilter); 1414 } else if (type == CLICK_FEEDBACK) { 1415 mBlurPaint.setMaskFilter(mLargeBlurMaskFilter); 1416 } 1417 1418 if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) { 1419 return null; 1420 } 1421 1422 Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(), 1423 v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888); 1424 mCanvas.setBitmap(bitmap); 1425 1426 float rotationX = v.getRotationX(); 1427 float rotation = v.getRotation(); 1428 float translationY = v.getTranslationY(); 1429 float translationX = v.getTranslationX(); 1430 v.setRotationX(0); 1431 v.setRotation(0); 1432 v.setTranslationY(0); 1433 v.setTranslationX(0); 1434 v.draw(mCanvas); 1435 v.setRotationX(rotationX); 1436 v.setRotation(rotation); 1437 v.setTranslationY(translationY); 1438 v.setTranslationX(translationX); 1439 1440 drawOutline(mCanvas, bitmap); 1441 mCanvas.setBitmap(null); 1442 return bitmap; 1443 } 1444 1445 void drawOutline(Canvas dest, Bitmap src) { 1446 final int[] xy = mTmpXY; 1447 Bitmap mask = src.extractAlpha(mBlurPaint, xy); 1448 mMaskCanvas.setBitmap(mask); 1449 mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint); 1450 dest.drawColor(0, PorterDuff.Mode.CLEAR); 1451 dest.setMatrix(mIdentityMatrix); 1452 dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint); 1453 mMaskCanvas.setBitmap(null); 1454 mask.recycle(); 1455 } 1456 } 1457 } 1458