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