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