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