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