StackView.java revision 32a42f1587db77b958d62c3de4f2734eb0a3b965
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.widget; 18 19import java.util.WeakHashMap; 20 21import android.animation.PropertyAnimator; 22import android.content.Context; 23import android.graphics.Bitmap; 24import android.graphics.Canvas; 25import android.graphics.Matrix; 26import android.graphics.Paint; 27import android.graphics.PorterDuff; 28import android.graphics.PorterDuffXfermode; 29import android.graphics.Rect; 30import android.util.AttributeSet; 31import android.util.Log; 32import android.view.MotionEvent; 33import android.view.VelocityTracker; 34import android.view.View; 35import android.view.ViewConfiguration; 36import android.view.ViewGroup; 37import android.widget.RemoteViews.RemoteView; 38 39@RemoteView 40/** 41 * A view that displays its children in a stack and allows users to discretely swipe 42 * through the children. 43 */ 44public class StackView extends AdapterViewAnimator { 45 private final String TAG = "StackView"; 46 47 /** 48 * Default animation parameters 49 */ 50 private final int DEFAULT_ANIMATION_DURATION = 400; 51 private final int MINIMUM_ANIMATION_DURATION = 50; 52 53 /** 54 * These specify the different gesture states 55 */ 56 private final int GESTURE_NONE = 0; 57 private final int GESTURE_SLIDE_UP = 1; 58 private final int GESTURE_SLIDE_DOWN = 2; 59 60 /** 61 * Specifies how far you need to swipe (up or down) before it 62 * will be consider a completed gesture when you lift your finger 63 */ 64 private final float SWIPE_THRESHOLD_RATIO = 0.35f; 65 private final float SLIDE_UP_RATIO = 0.7f; 66 67 private final WeakHashMap<View, Float> mRotations = new WeakHashMap<View, Float>(); 68 private final WeakHashMap<View, Integer> 69 mChildrenToApplyTransformsTo = new WeakHashMap<View, Integer>(); 70 71 /** 72 * Sentinel value for no current active pointer. 73 * Used by {@link #mActivePointerId}. 74 */ 75 private static final int INVALID_POINTER = -1; 76 77 /** 78 * These variables are all related to the current state of touch interaction 79 * with the stack 80 */ 81 private float mInitialY; 82 private float mInitialX; 83 private int mActivePointerId; 84 private int mYVelocity = 0; 85 private int mSwipeGestureType = GESTURE_NONE; 86 private int mViewHeight; 87 private int mSwipeThreshold; 88 private int mTouchSlop; 89 private int mMaximumVelocity; 90 private VelocityTracker mVelocityTracker; 91 92 private ImageView mHighlight; 93 private StackSlider mStackSlider; 94 private boolean mFirstLayoutHappened = false; 95 96 // TODO: temp hack to get this thing started 97 int mIndex = 5; 98 99 public StackView(Context context) { 100 super(context); 101 initStackView(); 102 } 103 104 public StackView(Context context, AttributeSet attrs) { 105 super(context, attrs); 106 initStackView(); 107 } 108 109 private void initStackView() { 110 configureViewAnimator(4, 2); 111 setStaticTransformationsEnabled(true); 112 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 113 mTouchSlop = configuration.getScaledTouchSlop();// + 5; 114 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 115 mActivePointerId = INVALID_POINTER; 116 117 mHighlight = new ImageView(getContext()); 118 mHighlight.setLayoutParams(new LayoutParams(mHighlight)); 119 addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight)); 120 mStackSlider = new StackSlider(); 121 122 if (!sPaintsInitialized) { 123 initializePaints(getContext()); 124 } 125 } 126 127 /** 128 * Animate the views between different relative indexes within the {@link AdapterViewAnimator} 129 */ 130 void animateViewForTransition(int fromIndex, int toIndex, View view) { 131 if (fromIndex == -1 && toIndex == 0) { 132 // Fade item in 133 if (view.getAlpha() == 1) { 134 view.setAlpha(0); 135 } 136 PropertyAnimator fadeIn = new PropertyAnimator(DEFAULT_ANIMATION_DURATION, 137 view, "alpha", view.getAlpha(), 1.0f); 138 fadeIn.start(); 139 } else if (fromIndex == mNumActiveViews - 1 && toIndex == mNumActiveViews - 2) { 140 // Slide item in 141 view.setVisibility(VISIBLE); 142 143 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 144 145 int largestDuration = (int) Math.round( 146 (lp.verticalOffset*1.0f/-mViewHeight)*DEFAULT_ANIMATION_DURATION); 147 int duration = largestDuration; 148 if (mYVelocity != 0) { 149 duration = 1000*(0 - lp.verticalOffset)/Math.abs(mYVelocity); 150 } 151 152 duration = Math.min(duration, largestDuration); 153 duration = Math.max(duration, MINIMUM_ANIMATION_DURATION); 154 155 PropertyAnimator slideInY = new PropertyAnimator(duration, mStackSlider, 156 "YProgress", mStackSlider.getYProgress(), 0); 157 slideInY.start(); 158 PropertyAnimator slideInX = new PropertyAnimator(duration, mStackSlider, 159 "XProgress", mStackSlider.getXProgress(), 0); 160 slideInX.start(); 161 162 } else if (fromIndex == mNumActiveViews - 2 && toIndex == mNumActiveViews - 1) { 163 // Slide item out 164 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 165 166 int largestDuration = (int) Math.round(mStackSlider.getYProgress()*DEFAULT_ANIMATION_DURATION); 167 int duration = largestDuration; 168 if (mYVelocity != 0) { 169 duration = 1000*(lp.verticalOffset + mViewHeight)/Math.abs(mYVelocity); 170 } 171 172 duration = Math.min(duration, largestDuration); 173 duration = Math.max(duration, MINIMUM_ANIMATION_DURATION); 174 175 PropertyAnimator slideOutY = new PropertyAnimator(duration, mStackSlider, 176 "YProgress", mStackSlider.getYProgress(), 1); 177 slideOutY.start(); 178 PropertyAnimator slideOutX = new PropertyAnimator(duration, mStackSlider, 179 "XProgress", mStackSlider.getXProgress(), 0); 180 slideOutX.start(); 181 182 } else if (fromIndex == -1 && toIndex == mNumActiveViews - 1) { 183 // Make sure this view that is "waiting in the wings" is invisible 184 view.setAlpha(0.0f); 185 } else if (toIndex == -1) { 186 // Fade item out 187 PropertyAnimator fadeOut = new PropertyAnimator(DEFAULT_ANIMATION_DURATION, 188 view, "alpha", view.getAlpha(), 0); 189 fadeOut.start(); 190 } 191 } 192 193 /** 194 * Apply any necessary tranforms for the child that is being added. 195 */ 196 void applyTransformForChildAtIndex(View child, int relativeIndex) { 197 float rotation; 198 199 if (!mRotations.containsKey(child)) { 200 rotation = (float) (Math.random()*26 - 13); 201 mRotations.put(child, rotation); 202 } else { 203 rotation = mRotations.get(child); 204 } 205 206 // Child has been removed 207 if (relativeIndex == -1) { 208 if (mRotations.containsKey(child)) { 209 mRotations.remove(child); 210 } 211 if (mChildrenToApplyTransformsTo.containsKey(child)) { 212 mChildrenToApplyTransformsTo.remove(child); 213 } 214 } 215 216 // if this view is already in the layout, we need to 217 // wait until layout has finished in order to set the 218 // pivot point of the rotation (requiring getMeasuredWidth/Height()) 219 mChildrenToApplyTransformsTo.put(child, relativeIndex); 220 } 221 222 @Override 223 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 224 super.onLayout(changed, left, top, right, bottom); 225 226 if (!mChildrenToApplyTransformsTo.isEmpty()) { 227 for (View child: mChildrenToApplyTransformsTo.keySet()) { 228 if (mRotations.containsKey(child)) { 229 child.setPivotX(child.getMeasuredWidth()/2); 230 child.setPivotY(child.getMeasuredHeight()/2); 231 child.setRotation(mRotations.get(child)); 232 } 233 } 234 mChildrenToApplyTransformsTo.clear(); 235 } 236 237 if (!mFirstLayoutHappened) { 238 mViewHeight = (int) Math.round(SLIDE_UP_RATIO*getMeasuredHeight()); 239 mSwipeThreshold = (int) Math.round(SWIPE_THRESHOLD_RATIO*mViewHeight); 240 241 // TODO: Right now this walks all the way up the view hierarchy and disables 242 // ClipChildren and ClipToPadding. We're probably going to want to reset 243 // these flags as well. 244 setClipChildren(false); 245 ViewGroup view = this; 246 while (view.getParent() != null && view.getParent() instanceof ViewGroup) { 247 view = (ViewGroup) view.getParent(); 248 view.setClipChildren(false); 249 view.setClipToPadding(false); 250 } 251 mFirstLayoutHappened = true; 252 } 253 } 254 255 @Override 256 public boolean onInterceptTouchEvent(MotionEvent ev) { 257 int action = ev.getAction(); 258 switch(action & MotionEvent.ACTION_MASK) { 259 260 case MotionEvent.ACTION_DOWN: { 261 if (mActivePointerId == INVALID_POINTER) { 262 mInitialX = ev.getX(); 263 mInitialY = ev.getY(); 264 mActivePointerId = ev.getPointerId(0); 265 } 266 break; 267 } 268 case MotionEvent.ACTION_MOVE: { 269 int pointerIndex = ev.findPointerIndex(mActivePointerId); 270 if (pointerIndex == INVALID_POINTER) { 271 // no data for our primary pointer, this shouldn't happen, log it 272 Log.d(TAG, "Error: No data for our primary pointer."); 273 return false; 274 } 275 float newY = ev.getY(pointerIndex); 276 float deltaY = newY - mInitialY; 277 278 beginGestureIfNeeded(deltaY); 279 break; 280 } 281 case MotionEvent.ACTION_POINTER_UP: { 282 onSecondaryPointerUp(ev); 283 break; 284 } 285 case MotionEvent.ACTION_UP: 286 case MotionEvent.ACTION_CANCEL: { 287 mActivePointerId = INVALID_POINTER; 288 mSwipeGestureType = GESTURE_NONE; 289 } 290 } 291 292 return mSwipeGestureType != GESTURE_NONE; 293 } 294 295 private void beginGestureIfNeeded(float deltaY) { 296 if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) { 297 mSwipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN; 298 cancelLongPress(); 299 requestDisallowInterceptTouchEvent(true); 300 301 int activeIndex = mSwipeGestureType == GESTURE_SLIDE_DOWN ? mNumActiveViews - 1 302 : mNumActiveViews - 2; 303 304 View v = getViewAtRelativeIndex(activeIndex); 305 if (v != null) { 306 mHighlight.setImageBitmap(createOutline(v)); 307 mHighlight.bringToFront(); 308 v.bringToFront(); 309 mStackSlider.setView(v); 310 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) 311 v.setVisibility(VISIBLE); 312 } 313 } 314 } 315 316 @Override 317 public boolean onTouchEvent(MotionEvent ev) { 318 int action = ev.getAction(); 319 int pointerIndex = ev.findPointerIndex(mActivePointerId); 320 if (pointerIndex == INVALID_POINTER) { 321 // no data for our primary pointer, this shouldn't happen, log it 322 Log.d(TAG, "Error: No data for our primary pointer."); 323 return false; 324 } 325 326 float newY = ev.getY(pointerIndex); 327 float newX = ev.getX(pointerIndex); 328 float deltaY = newY - mInitialY; 329 float deltaX = newX - mInitialX; 330 if (mVelocityTracker == null) { 331 mVelocityTracker = VelocityTracker.obtain(); 332 } 333 mVelocityTracker.addMovement(ev); 334 335 switch (action & MotionEvent.ACTION_MASK) { 336 case MotionEvent.ACTION_MOVE: { 337 beginGestureIfNeeded(deltaY); 338 339 float rx = 0.3f*deltaX/(mViewHeight*1.0f); 340 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 341 float r = (deltaY-mTouchSlop*1.0f)/mViewHeight*1.0f; 342 mStackSlider.setYProgress(1 - r); 343 mStackSlider.setXProgress(rx); 344 return true; 345 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) { 346 float r = -(deltaY + mTouchSlop*1.0f)/mViewHeight*1.0f; 347 mStackSlider.setYProgress(r); 348 mStackSlider.setXProgress(rx); 349 return true; 350 } 351 352 break; 353 } 354 case MotionEvent.ACTION_UP: { 355 handlePointerUp(ev); 356 break; 357 } 358 case MotionEvent.ACTION_POINTER_UP: { 359 onSecondaryPointerUp(ev); 360 break; 361 } 362 case MotionEvent.ACTION_CANCEL: { 363 mActivePointerId = INVALID_POINTER; 364 mSwipeGestureType = GESTURE_NONE; 365 break; 366 } 367 } 368 return true; 369 } 370 371 private final Rect touchRect = new Rect(); 372 private void onSecondaryPointerUp(MotionEvent ev) { 373 final int activePointerIndex = ev.getActionIndex(); 374 final int pointerId = ev.getPointerId(activePointerIndex); 375 if (pointerId == mActivePointerId) { 376 377 int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? mNumActiveViews - 1 378 : mNumActiveViews - 2; 379 380 View v = getViewAtRelativeIndex(activeViewIndex); 381 if (v == null) return; 382 383 // Our primary pointer has gone up -- let's see if we can find 384 // another pointer on the view. If so, then we should replace 385 // our primary pointer with this new pointer and adjust things 386 // so that the view doesn't jump 387 for (int index = 0; index < ev.getPointerCount(); index++) { 388 if (index != activePointerIndex) { 389 390 float x = ev.getX(index); 391 float y = ev.getY(index); 392 393 touchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); 394 if (touchRect.contains((int) Math.round(x), (int) Math.round(y))) { 395 float oldX = ev.getX(activePointerIndex); 396 float oldY = ev.getY(activePointerIndex); 397 398 // adjust our frame of reference to avoid a jump 399 mInitialY += (y - oldY); 400 mInitialX += (x - oldX); 401 402 mActivePointerId = ev.getPointerId(index); 403 if (mVelocityTracker != null) { 404 mVelocityTracker.clear(); 405 } 406 // ok, we're good, we found a new pointer which is touching the active view 407 return; 408 } 409 } 410 } 411 // if we made it this far, it means we didn't find a satisfactory new pointer :(, 412 // so end the 413 handlePointerUp(ev); 414 } 415 } 416 417 private void handlePointerUp(MotionEvent ev) { 418 int pointerIndex = ev.findPointerIndex(mActivePointerId); 419 float newY = ev.getY(pointerIndex); 420 int deltaY = (int) (newY - mInitialY); 421 422 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 423 mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); 424 425 if (mVelocityTracker != null) { 426 mVelocityTracker.recycle(); 427 mVelocityTracker = null; 428 } 429 430 if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN) { 431 // Swipe threshold exceeded, swipe down 432 showNext(); 433 mHighlight.bringToFront(); 434 } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP) { 435 // Swipe threshold exceeded, swipe up 436 showPrevious(); 437 mHighlight.bringToFront(); 438 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) { 439 // Didn't swipe up far enough, snap back down 440 int duration = (int) Math.round(mStackSlider.getYProgress()*DEFAULT_ANIMATION_DURATION); 441 442 PropertyAnimator snapBackY = new PropertyAnimator(duration, mStackSlider, 443 "YProgress", mStackSlider.getYProgress(), 0); 444 snapBackY.start(); 445 PropertyAnimator snapBackX = new PropertyAnimator(duration, mStackSlider, 446 "XProgress", mStackSlider.getXProgress(), 0); 447 snapBackX.start(); 448 } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { 449 // Didn't swipe down far enough, snap back up 450 int duration = (int) Math.round((1 - 451 mStackSlider.getYProgress())*DEFAULT_ANIMATION_DURATION); 452 PropertyAnimator snapBackY = new PropertyAnimator(duration, mStackSlider, 453 "YProgress", mStackSlider.getYProgress(), 1); 454 snapBackY.start(); 455 PropertyAnimator snapBackX = new PropertyAnimator(duration, mStackSlider, 456 "XProgress", mStackSlider.getXProgress(), 0); 457 snapBackX.start(); 458 } 459 460 mActivePointerId = INVALID_POINTER; 461 mSwipeGestureType = GESTURE_NONE; 462 } 463 464 private class StackSlider { 465 View mView; 466 float mYProgress; 467 float mXProgress; 468 469 private float cubic(float r) { 470 return (float) (Math.pow(2*r-1, 3) + 1)/2.0f; 471 } 472 473 private float highlightAlphaInterpolator(float r) { 474 float pivot = 0.4f; 475 if (r < pivot) { 476 return 0.85f*cubic(r/pivot); 477 } else { 478 return 0.85f*cubic(1 - (r-pivot)/(1-pivot)); 479 } 480 } 481 482 private float viewAlphaInterpolator(float r) { 483 float pivot = 0.3f; 484 if (r > pivot) { 485 return (r - pivot)/(1 - pivot); 486 } else { 487 return 0; 488 } 489 } 490 491 void setView(View v) { 492 mView = v; 493 } 494 495 public void setYProgress(float r) { 496 // enforce r between 0 and 1 497 r = Math.min(1.0f, r); 498 r = Math.max(0, r); 499 500 mYProgress = r; 501 502 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 503 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 504 505 viewLp.setVerticalOffset((int) Math.round(-r*mViewHeight)); 506 highlightLp.setVerticalOffset((int) Math.round(-r*mViewHeight)); 507 mHighlight.setAlpha(highlightAlphaInterpolator(r)); 508 mView.setAlpha(viewAlphaInterpolator(1-r)); 509 } 510 511 public void setXProgress(float r) { 512 // enforce r between 0 and 1 513 r = Math.min(1.0f, r); 514 r = Math.max(-1.0f, r); 515 516 mXProgress = r; 517 518 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); 519 final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); 520 521 viewLp.setHorizontalOffset((int) Math.round(r*mViewHeight)); 522 highlightLp.setHorizontalOffset((int) Math.round(r*mViewHeight)); 523 } 524 525 float getYProgress() { 526 return mYProgress; 527 } 528 529 float getXProgress() { 530 return mXProgress; 531 } 532 } 533 534 @Override 535 public void onRemoteAdapterConnected() { 536 super.onRemoteAdapterConnected(); 537 setDisplayedChild(mIndex); 538 } 539 540 private static final Paint sHolographicPaint = new Paint(); 541 private static final Paint sErasePaint = new Paint(); 542 private static boolean sPaintsInitialized = false; 543 private static final float STROKE_WIDTH = 3.0f; 544 545 static void initializePaints(Context context) { 546 sHolographicPaint.setColor(0xff6699ff); 547 sHolographicPaint.setFilterBitmap(true); 548 sErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); 549 sErasePaint.setFilterBitmap(true); 550 sPaintsInitialized = true; 551 } 552 553 static Bitmap createOutline(View v) { 554 Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(), 555 Bitmap.Config.ARGB_8888); 556 Canvas canvas = new Canvas(bitmap); 557 558 canvas.concat(v.getMatrix()); 559 v.draw(canvas); 560 561 Bitmap outlineBitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(), 562 Bitmap.Config.ARGB_8888); 563 Canvas outlineCanvas = new Canvas(outlineBitmap); 564 drawOutline(outlineCanvas, v.getMeasuredWidth(), v.getMeasuredHeight(), bitmap); 565 bitmap.recycle(); 566 return outlineBitmap; 567 } 568 569 static void drawOutline(Canvas dest, int destWidth, int destHeight, Bitmap src) { 570 dest.drawColor(0, PorterDuff.Mode.CLEAR); 571 572 Bitmap mask = src.extractAlpha(); 573 Matrix id = new Matrix(); 574 575 Matrix m = new Matrix(); 576 float xScale = STROKE_WIDTH*2/(src.getWidth()); 577 float yScale = STROKE_WIDTH*2/(src.getHeight()); 578 m.preScale(1+xScale, 1+yScale, src.getWidth()/2, src.getHeight()/2); 579 dest.drawBitmap(mask, m, sHolographicPaint); 580 581 dest.drawBitmap(src, id, sErasePaint); 582 mask.recycle(); 583 } 584} 585