1/* 2 * Copyright (C) 2008 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 com.android.launcher3; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.TimeInterpolator; 22import android.animation.ValueAnimator; 23import android.animation.ValueAnimator.AnimatorUpdateListener; 24import android.content.Context; 25import android.content.res.Resources; 26import android.graphics.Canvas; 27import android.graphics.Rect; 28import android.graphics.drawable.Drawable; 29import android.util.AttributeSet; 30import android.view.KeyEvent; 31import android.view.MotionEvent; 32import android.view.View; 33import android.view.ViewGroup; 34import android.view.accessibility.AccessibilityEvent; 35import android.view.accessibility.AccessibilityManager; 36import android.view.animation.DecelerateInterpolator; 37import android.view.animation.Interpolator; 38import android.widget.FrameLayout; 39import android.widget.TextView; 40 41import java.util.ArrayList; 42 43/** 44 * A ViewGroup that coordinates dragging across its descendants 45 */ 46public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChangeListener { 47 private DragController mDragController; 48 private int[] mTmpXY = new int[2]; 49 50 private int mXDown, mYDown; 51 private Launcher mLauncher; 52 53 // Variables relating to resizing widgets 54 private final ArrayList<AppWidgetResizeFrame> mResizeFrames = 55 new ArrayList<AppWidgetResizeFrame>(); 56 private AppWidgetResizeFrame mCurrentResizeFrame; 57 58 // Variables relating to animation of views after drop 59 private ValueAnimator mDropAnim = null; 60 private ValueAnimator mFadeOutAnim = null; 61 private TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); 62 private DragView mDropView = null; 63 private int mAnchorViewInitialScrollX = 0; 64 private View mAnchorView = null; 65 66 private boolean mHoverPointClosesFolder = false; 67 private Rect mHitRect = new Rect(); 68 public static final int ANIMATION_END_DISAPPEAR = 0; 69 public static final int ANIMATION_END_FADE_OUT = 1; 70 public static final int ANIMATION_END_REMAIN_VISIBLE = 2; 71 72 private TouchCompleteListener mTouchCompleteListener; 73 74 private final Rect mInsets = new Rect(); 75 76 private View mOverlayView; 77 private int mTopViewIndex; 78 private int mChildCountOnLastUpdate = -1; 79 80 // Darkening scrim 81 private Drawable mBackground; 82 private float mBackgroundAlpha = 0; 83 84 // Related to adjacent page hints 85 private boolean mInScrollArea; 86 private boolean mShowPageHints; 87 private Drawable mLeftHoverDrawable; 88 private Drawable mRightHoverDrawable; 89 private Drawable mLeftHoverDrawableActive; 90 private Drawable mRightHoverDrawableActive; 91 92 /** 93 * Used to create a new DragLayer from XML. 94 * 95 * @param context The application's context. 96 * @param attrs The attributes set containing the Workspace's customization values. 97 */ 98 public DragLayer(Context context, AttributeSet attrs) { 99 super(context, attrs); 100 101 // Disable multitouch across the workspace/all apps/customize tray 102 setMotionEventSplittingEnabled(false); 103 setChildrenDrawingOrderEnabled(true); 104 setOnHierarchyChangeListener(this); 105 106 final Resources res = getResources(); 107 mLeftHoverDrawable = res.getDrawable(R.drawable.page_hover_left); 108 mRightHoverDrawable = res.getDrawable(R.drawable.page_hover_right); 109 mLeftHoverDrawableActive = res.getDrawable(R.drawable.page_hover_left_active); 110 mRightHoverDrawableActive = res.getDrawable(R.drawable.page_hover_right_active); 111 mBackground = res.getDrawable(R.drawable.apps_customize_bg); 112 } 113 114 public void setup(Launcher launcher, DragController controller) { 115 mLauncher = launcher; 116 mDragController = controller; 117 } 118 119 @Override 120 public boolean dispatchKeyEvent(KeyEvent event) { 121 return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); 122 } 123 124 @Override 125 protected boolean fitSystemWindows(Rect insets) { 126 final int n = getChildCount(); 127 for (int i = 0; i < n; i++) { 128 final View child = getChildAt(i); 129 setInsets(child, insets, mInsets); 130 } 131 mInsets.set(insets); 132 return true; // I'll take it from here 133 } 134 135 Rect getInsets() { 136 return mInsets; 137 } 138 139 @Override 140 public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) { 141 super.addView(child, index, params); 142 setInsets(child, mInsets, new Rect()); 143 } 144 145 public void showOverlayView(View overlayView) { 146 LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 147 mOverlayView = overlayView; 148 addView(overlayView, lp); 149 150 // ensure that the overlay view stays on top. we can't use drawing order for this 151 // because in API level 16 touch dispatch doesn't respect drawing order. 152 mOverlayView.bringToFront(); 153 } 154 155 public void dismissOverlayView() { 156 removeView(mOverlayView); 157 } 158 159 private void setInsets(View child, Rect newInsets, Rect oldInsets) { 160 final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) child.getLayoutParams(); 161 if (child instanceof Insettable) { 162 ((Insettable) child).setInsets(newInsets); 163 } else { 164 flp.topMargin += (newInsets.top - oldInsets.top); 165 flp.leftMargin += (newInsets.left - oldInsets.left); 166 flp.rightMargin += (newInsets.right - oldInsets.right); 167 flp.bottomMargin += (newInsets.bottom - oldInsets.bottom); 168 } 169 child.setLayoutParams(flp); 170 } 171 172 private boolean isEventOverFolderTextRegion(Folder folder, MotionEvent ev) { 173 getDescendantRectRelativeToSelf(folder.getEditTextRegion(), mHitRect); 174 if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { 175 return true; 176 } 177 return false; 178 } 179 180 private boolean isEventOverFolder(Folder folder, MotionEvent ev) { 181 getDescendantRectRelativeToSelf(folder, mHitRect); 182 if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { 183 return true; 184 } 185 return false; 186 } 187 188 private boolean handleTouchDown(MotionEvent ev, boolean intercept) { 189 Rect hitRect = new Rect(); 190 int x = (int) ev.getX(); 191 int y = (int) ev.getY(); 192 193 for (AppWidgetResizeFrame child: mResizeFrames) { 194 child.getHitRect(hitRect); 195 if (hitRect.contains(x, y)) { 196 if (child.beginResizeIfPointInRegion(x - child.getLeft(), y - child.getTop())) { 197 mCurrentResizeFrame = child; 198 mXDown = x; 199 mYDown = y; 200 requestDisallowInterceptTouchEvent(true); 201 return true; 202 } 203 } 204 } 205 206 Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); 207 if (currentFolder != null && intercept) { 208 if (currentFolder.isEditingName()) { 209 if (!isEventOverFolderTextRegion(currentFolder, ev)) { 210 currentFolder.dismissEditingName(); 211 return true; 212 } 213 } 214 215 getDescendantRectRelativeToSelf(currentFolder, hitRect); 216 if (!isEventOverFolder(currentFolder, ev)) { 217 mLauncher.closeFolder(); 218 return true; 219 } 220 } 221 return false; 222 } 223 224 @Override 225 public boolean onInterceptTouchEvent(MotionEvent ev) { 226 int action = ev.getAction(); 227 228 if (action == MotionEvent.ACTION_DOWN) { 229 if (handleTouchDown(ev, true)) { 230 return true; 231 } 232 } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 233 if (mTouchCompleteListener != null) { 234 mTouchCompleteListener.onTouchComplete(); 235 } 236 mTouchCompleteListener = null; 237 } 238 clearAllResizeFrames(); 239 return mDragController.onInterceptTouchEvent(ev); 240 } 241 242 @Override 243 public boolean onInterceptHoverEvent(MotionEvent ev) { 244 if (mLauncher == null || mLauncher.getWorkspace() == null) { 245 return false; 246 } 247 Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); 248 if (currentFolder == null) { 249 return false; 250 } else { 251 AccessibilityManager accessibilityManager = (AccessibilityManager) 252 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 253 if (accessibilityManager.isTouchExplorationEnabled()) { 254 final int action = ev.getAction(); 255 boolean isOverFolder; 256 switch (action) { 257 case MotionEvent.ACTION_HOVER_ENTER: 258 isOverFolder = isEventOverFolder(currentFolder, ev); 259 if (!isOverFolder) { 260 sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); 261 mHoverPointClosesFolder = true; 262 return true; 263 } 264 mHoverPointClosesFolder = false; 265 break; 266 case MotionEvent.ACTION_HOVER_MOVE: 267 isOverFolder = isEventOverFolder(currentFolder, ev); 268 if (!isOverFolder && !mHoverPointClosesFolder) { 269 sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); 270 mHoverPointClosesFolder = true; 271 return true; 272 } else if (!isOverFolder) { 273 return true; 274 } 275 mHoverPointClosesFolder = false; 276 } 277 } 278 } 279 return false; 280 } 281 282 private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) { 283 AccessibilityManager accessibilityManager = (AccessibilityManager) 284 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 285 if (accessibilityManager.isEnabled()) { 286 int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close; 287 AccessibilityEvent event = AccessibilityEvent.obtain( 288 AccessibilityEvent.TYPE_VIEW_FOCUSED); 289 onInitializeAccessibilityEvent(event); 290 event.getText().add(getContext().getString(stringId)); 291 accessibilityManager.sendAccessibilityEvent(event); 292 } 293 } 294 295 @Override 296 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 297 Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); 298 if (currentFolder != null) { 299 if (child == currentFolder) { 300 return super.onRequestSendAccessibilityEvent(child, event); 301 } 302 // Skip propagating onRequestSendAccessibilityEvent all for other children 303 // when a folder is open 304 return false; 305 } 306 return super.onRequestSendAccessibilityEvent(child, event); 307 } 308 309 @Override 310 public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) { 311 Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); 312 if (currentFolder != null) { 313 // Only add the folder as a child for accessibility when it is open 314 childrenForAccessibility.add(currentFolder); 315 } else { 316 super.addChildrenForAccessibility(childrenForAccessibility); 317 } 318 } 319 320 @Override 321 public boolean onHoverEvent(MotionEvent ev) { 322 // If we've received this, we've already done the necessary handling 323 // in onInterceptHoverEvent. Return true to consume the event. 324 return false; 325 } 326 327 @Override 328 public boolean onTouchEvent(MotionEvent ev) { 329 boolean handled = false; 330 int action = ev.getAction(); 331 332 int x = (int) ev.getX(); 333 int y = (int) ev.getY(); 334 335 if (action == MotionEvent.ACTION_DOWN) { 336 if (handleTouchDown(ev, false)) { 337 return true; 338 } 339 } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 340 if (mTouchCompleteListener != null) { 341 mTouchCompleteListener.onTouchComplete(); 342 } 343 mTouchCompleteListener = null; 344 } 345 346 if (mCurrentResizeFrame != null) { 347 handled = true; 348 switch (action) { 349 case MotionEvent.ACTION_MOVE: 350 mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown); 351 break; 352 case MotionEvent.ACTION_CANCEL: 353 case MotionEvent.ACTION_UP: 354 mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown); 355 mCurrentResizeFrame.onTouchUp(); 356 mCurrentResizeFrame = null; 357 } 358 } 359 if (handled) return true; 360 return mDragController.onTouchEvent(ev); 361 } 362 363 /** 364 * Determine the rect of the descendant in this DragLayer's coordinates 365 * 366 * @param descendant The descendant whose coordinates we want to find. 367 * @param r The rect into which to place the results. 368 * @return The factor by which this descendant is scaled relative to this DragLayer. 369 */ 370 public float getDescendantRectRelativeToSelf(View descendant, Rect r) { 371 mTmpXY[0] = 0; 372 mTmpXY[1] = 0; 373 float scale = getDescendantCoordRelativeToSelf(descendant, mTmpXY); 374 375 r.set(mTmpXY[0], mTmpXY[1], 376 (int) (mTmpXY[0] + scale * descendant.getMeasuredWidth()), 377 (int) (mTmpXY[1] + scale * descendant.getMeasuredHeight())); 378 return scale; 379 } 380 381 public float getLocationInDragLayer(View child, int[] loc) { 382 loc[0] = 0; 383 loc[1] = 0; 384 return getDescendantCoordRelativeToSelf(child, loc); 385 } 386 387 public float getDescendantCoordRelativeToSelf(View descendant, int[] coord) { 388 return getDescendantCoordRelativeToSelf(descendant, coord, false); 389 } 390 391 /** 392 * Given a coordinate relative to the descendant, find the coordinate in this DragLayer's 393 * coordinates. 394 * 395 * @param descendant The descendant to which the passed coordinate is relative. 396 * @param coord The coordinate that we want mapped. 397 * @param includeRootScroll Whether or not to account for the scroll of the root descendant: 398 * sometimes this is relevant as in a child's coordinates within the root descendant. 399 * @return The factor by which this descendant is scaled relative to this DragLayer. Caution 400 * this scale factor is assumed to be equal in X and Y, and so if at any point this 401 * assumption fails, we will need to return a pair of scale factors. 402 */ 403 public float getDescendantCoordRelativeToSelf(View descendant, int[] coord, 404 boolean includeRootScroll) { 405 return Utilities.getDescendantCoordRelativeToParent(descendant, this, 406 coord, includeRootScroll); 407 } 408 409 /** 410 * Inverse of {@link #getDescendantCoordRelativeToSelf(View, int[])}. 411 */ 412 public float mapCoordInSelfToDescendent(View descendant, int[] coord) { 413 return Utilities.mapCoordInSelfToDescendent(descendant, this, coord); 414 } 415 416 public void getViewRectRelativeToSelf(View v, Rect r) { 417 int[] loc = new int[2]; 418 getLocationInWindow(loc); 419 int x = loc[0]; 420 int y = loc[1]; 421 422 v.getLocationInWindow(loc); 423 int vX = loc[0]; 424 int vY = loc[1]; 425 426 int left = vX - x; 427 int top = vY - y; 428 r.set(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight()); 429 } 430 431 @Override 432 public boolean dispatchUnhandledMove(View focused, int direction) { 433 return mDragController.dispatchUnhandledMove(focused, direction); 434 } 435 436 public static class LayoutParams extends FrameLayout.LayoutParams { 437 public int x, y; 438 public boolean customPosition = false; 439 440 /** 441 * {@inheritDoc} 442 */ 443 public LayoutParams(int width, int height) { 444 super(width, height); 445 } 446 447 public void setWidth(int width) { 448 this.width = width; 449 } 450 451 public int getWidth() { 452 return width; 453 } 454 455 public void setHeight(int height) { 456 this.height = height; 457 } 458 459 public int getHeight() { 460 return height; 461 } 462 463 public void setX(int x) { 464 this.x = x; 465 } 466 467 public int getX() { 468 return x; 469 } 470 471 public void setY(int y) { 472 this.y = y; 473 } 474 475 public int getY() { 476 return y; 477 } 478 } 479 480 protected void onLayout(boolean changed, int l, int t, int r, int b) { 481 super.onLayout(changed, l, t, r, b); 482 int count = getChildCount(); 483 for (int i = 0; i < count; i++) { 484 View child = getChildAt(i); 485 final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) child.getLayoutParams(); 486 if (flp instanceof LayoutParams) { 487 final LayoutParams lp = (LayoutParams) flp; 488 if (lp.customPosition) { 489 child.layout(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height); 490 } 491 } 492 } 493 } 494 495 public void clearAllResizeFrames() { 496 if (mResizeFrames.size() > 0) { 497 for (AppWidgetResizeFrame frame: mResizeFrames) { 498 frame.commitResize(); 499 removeView(frame); 500 } 501 mResizeFrames.clear(); 502 } 503 } 504 505 public boolean hasResizeFrames() { 506 return mResizeFrames.size() > 0; 507 } 508 509 public boolean isWidgetBeingResized() { 510 return mCurrentResizeFrame != null; 511 } 512 513 public void addResizeFrame(ItemInfo itemInfo, LauncherAppWidgetHostView widget, 514 CellLayout cellLayout) { 515 AppWidgetResizeFrame resizeFrame = new AppWidgetResizeFrame(getContext(), 516 widget, cellLayout, this); 517 518 LayoutParams lp = new LayoutParams(-1, -1); 519 lp.customPosition = true; 520 521 addView(resizeFrame, lp); 522 mResizeFrames.add(resizeFrame); 523 524 resizeFrame.snapToWidget(false); 525 } 526 527 public void animateViewIntoPosition(DragView dragView, final View child) { 528 animateViewIntoPosition(dragView, child, null, null); 529 } 530 531 public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha, 532 float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable, 533 int duration) { 534 Rect r = new Rect(); 535 getViewRectRelativeToSelf(dragView, r); 536 final int fromX = r.left; 537 final int fromY = r.top; 538 539 animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY, 540 onFinishRunnable, animationEndStyle, duration, null); 541 } 542 543 public void animateViewIntoPosition(DragView dragView, final View child, 544 final Runnable onFinishAnimationRunnable, View anchorView) { 545 animateViewIntoPosition(dragView, child, -1, onFinishAnimationRunnable, anchorView); 546 } 547 548 public void animateViewIntoPosition(DragView dragView, final View child, int duration, 549 final Runnable onFinishAnimationRunnable, View anchorView) { 550 ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent(); 551 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); 552 parentChildren.measureChild(child); 553 554 Rect r = new Rect(); 555 getViewRectRelativeToSelf(dragView, r); 556 557 int coord[] = new int[2]; 558 float childScale = child.getScaleX(); 559 coord[0] = lp.x + (int) (child.getMeasuredWidth() * (1 - childScale) / 2); 560 coord[1] = lp.y + (int) (child.getMeasuredHeight() * (1 - childScale) / 2); 561 562 // Since the child hasn't necessarily been laid out, we force the lp to be updated with 563 // the correct coordinates (above) and use these to determine the final location 564 float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord); 565 // We need to account for the scale of the child itself, as the above only accounts for 566 // for the scale in parents. 567 scale *= childScale; 568 int toX = coord[0]; 569 int toY = coord[1]; 570 float toScale = scale; 571 if (child instanceof TextView) { 572 TextView tv = (TextView) child; 573 // Account for the source scale of the icon (ie. from AllApps to Workspace, in which 574 // the workspace may have smaller icon bounds). 575 toScale = scale / dragView.getIntrinsicIconScaleFactor(); 576 577 // The child may be scaled (always about the center of the view) so to account for it, 578 // we have to offset the position by the scaled size. Once we do that, we can center 579 // the drag view about the scaled child view. 580 toY += Math.round(toScale * tv.getPaddingTop()); 581 toY -= dragView.getMeasuredHeight() * (1 - toScale) / 2; 582 if (dragView.getDragVisualizeOffset() != null) { 583 toY -= Math.round(toScale * dragView.getDragVisualizeOffset().y); 584 } 585 586 toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; 587 } else if (child instanceof FolderIcon) { 588 // Account for holographic blur padding on the drag view 589 toY += Math.round(scale * (child.getPaddingTop() - dragView.getDragRegionTop())); 590 toY -= scale * Workspace.DRAG_BITMAP_PADDING / 2; 591 toY -= (1 - scale) * dragView.getMeasuredHeight() / 2; 592 // Center in the x coordinate about the target's drawable 593 toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; 594 } else { 595 toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2; 596 toX -= (Math.round(scale * (dragView.getMeasuredWidth() 597 - child.getMeasuredWidth()))) / 2; 598 } 599 600 final int fromX = r.left; 601 final int fromY = r.top; 602 child.setVisibility(INVISIBLE); 603 Runnable onCompleteRunnable = new Runnable() { 604 public void run() { 605 child.setVisibility(VISIBLE); 606 if (onFinishAnimationRunnable != null) { 607 onFinishAnimationRunnable.run(); 608 } 609 } 610 }; 611 animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, toScale, toScale, 612 onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView); 613 } 614 615 public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY, 616 final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY, 617 float finalScaleX, float finalScaleY, Runnable onCompleteRunnable, 618 int animationEndStyle, int duration, View anchorView) { 619 Rect from = new Rect(fromX, fromY, fromX + 620 view.getMeasuredWidth(), fromY + view.getMeasuredHeight()); 621 Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight()); 622 animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration, 623 null, null, onCompleteRunnable, animationEndStyle, anchorView); 624 } 625 626 /** 627 * This method animates a view at the end of a drag and drop animation. 628 * 629 * @param view The view to be animated. This view is drawn directly into DragLayer, and so 630 * doesn't need to be a child of DragLayer. 631 * @param from The initial location of the view. Only the left and top parameters are used. 632 * @param to The final location of the view. Only the left and top parameters are used. This 633 * location doesn't account for scaling, and so should be centered about the desired 634 * final location (including scaling). 635 * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates. 636 * @param finalScale The final scale of the view. The view is scaled about its center. 637 * @param duration The duration of the animation. 638 * @param motionInterpolator The interpolator to use for the location of the view. 639 * @param alphaInterpolator The interpolator to use for the alpha of the view. 640 * @param onCompleteRunnable Optional runnable to run on animation completion. 641 * @param fadeOut Whether or not to fade out the view once the animation completes. If true, 642 * the runnable will execute after the view is faded out. 643 * @param anchorView If not null, this represents the view which the animated view stays 644 * anchored to in case scrolling is currently taking place. Note: currently this is 645 * only used for the X dimension for the case of the workspace. 646 */ 647 public void animateView(final DragView view, final Rect from, final Rect to, 648 final float finalAlpha, final float initScaleX, final float initScaleY, 649 final float finalScaleX, final float finalScaleY, int duration, 650 final Interpolator motionInterpolator, final Interpolator alphaInterpolator, 651 final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) { 652 653 // Calculate the duration of the animation based on the object's distance 654 final float dist = (float) Math.sqrt(Math.pow(to.left - from.left, 2) + 655 Math.pow(to.top - from.top, 2)); 656 final Resources res = getResources(); 657 final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist); 658 659 // If duration < 0, this is a cue to compute the duration based on the distance 660 if (duration < 0) { 661 duration = res.getInteger(R.integer.config_dropAnimMaxDuration); 662 if (dist < maxDist) { 663 duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist); 664 } 665 duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration)); 666 } 667 668 // Fall back to cubic ease out interpolator for the animation if none is specified 669 TimeInterpolator interpolator = null; 670 if (alphaInterpolator == null || motionInterpolator == null) { 671 interpolator = mCubicEaseOutInterpolator; 672 } 673 674 // Animate the view 675 final float initAlpha = view.getAlpha(); 676 final float dropViewScale = view.getScaleX(); 677 AnimatorUpdateListener updateCb = new AnimatorUpdateListener() { 678 @Override 679 public void onAnimationUpdate(ValueAnimator animation) { 680 final float percent = (Float) animation.getAnimatedValue(); 681 final int width = view.getMeasuredWidth(); 682 final int height = view.getMeasuredHeight(); 683 684 float alphaPercent = alphaInterpolator == null ? percent : 685 alphaInterpolator.getInterpolation(percent); 686 float motionPercent = motionInterpolator == null ? percent : 687 motionInterpolator.getInterpolation(percent); 688 689 float initialScaleX = initScaleX * dropViewScale; 690 float initialScaleY = initScaleY * dropViewScale; 691 float scaleX = finalScaleX * percent + initialScaleX * (1 - percent); 692 float scaleY = finalScaleY * percent + initialScaleY * (1 - percent); 693 float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent); 694 695 float fromLeft = from.left + (initialScaleX - 1f) * width / 2; 696 float fromTop = from.top + (initialScaleY - 1f) * height / 2; 697 698 int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent))); 699 int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent))); 700 701 int anchorAdjust = mAnchorView == null ? 0 : (int) (mAnchorView.getScaleX() * 702 (mAnchorViewInitialScrollX - mAnchorView.getScrollX())); 703 704 int xPos = x - mDropView.getScrollX() + anchorAdjust; 705 int yPos = y - mDropView.getScrollY(); 706 707 mDropView.setTranslationX(xPos); 708 mDropView.setTranslationY(yPos); 709 mDropView.setScaleX(scaleX); 710 mDropView.setScaleY(scaleY); 711 mDropView.setAlpha(alpha); 712 } 713 }; 714 animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle, 715 anchorView); 716 } 717 718 public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration, 719 TimeInterpolator interpolator, final Runnable onCompleteRunnable, 720 final int animationEndStyle, View anchorView) { 721 // Clean up the previous animations 722 if (mDropAnim != null) mDropAnim.cancel(); 723 if (mFadeOutAnim != null) mFadeOutAnim.cancel(); 724 725 // Show the drop view if it was previously hidden 726 mDropView = view; 727 mDropView.cancelAnimation(); 728 mDropView.resetLayoutParams(); 729 730 // Set the anchor view if the page is scrolling 731 if (anchorView != null) { 732 mAnchorViewInitialScrollX = anchorView.getScrollX(); 733 } 734 mAnchorView = anchorView; 735 736 // Create and start the animation 737 mDropAnim = new ValueAnimator(); 738 mDropAnim.setInterpolator(interpolator); 739 mDropAnim.setDuration(duration); 740 mDropAnim.setFloatValues(0f, 1f); 741 mDropAnim.addUpdateListener(updateCb); 742 mDropAnim.addListener(new AnimatorListenerAdapter() { 743 public void onAnimationEnd(Animator animation) { 744 if (onCompleteRunnable != null) { 745 onCompleteRunnable.run(); 746 } 747 switch (animationEndStyle) { 748 case ANIMATION_END_DISAPPEAR: 749 clearAnimatedView(); 750 break; 751 case ANIMATION_END_FADE_OUT: 752 fadeOutDragView(); 753 break; 754 case ANIMATION_END_REMAIN_VISIBLE: 755 break; 756 } 757 } 758 }); 759 mDropAnim.start(); 760 } 761 762 public void clearAnimatedView() { 763 if (mDropAnim != null) { 764 mDropAnim.cancel(); 765 } 766 if (mDropView != null) { 767 mDragController.onDeferredEndDrag(mDropView); 768 } 769 mDropView = null; 770 invalidate(); 771 } 772 773 public View getAnimatedView() { 774 return mDropView; 775 } 776 777 private void fadeOutDragView() { 778 mFadeOutAnim = new ValueAnimator(); 779 mFadeOutAnim.setDuration(150); 780 mFadeOutAnim.setFloatValues(0f, 1f); 781 mFadeOutAnim.removeAllUpdateListeners(); 782 mFadeOutAnim.addUpdateListener(new AnimatorUpdateListener() { 783 public void onAnimationUpdate(ValueAnimator animation) { 784 final float percent = (Float) animation.getAnimatedValue(); 785 786 float alpha = 1 - percent; 787 mDropView.setAlpha(alpha); 788 } 789 }); 790 mFadeOutAnim.addListener(new AnimatorListenerAdapter() { 791 public void onAnimationEnd(Animator animation) { 792 if (mDropView != null) { 793 mDragController.onDeferredEndDrag(mDropView); 794 } 795 mDropView = null; 796 invalidate(); 797 } 798 }); 799 mFadeOutAnim.start(); 800 } 801 802 @Override 803 public void onChildViewAdded(View parent, View child) { 804 if (mOverlayView != null) { 805 // ensure that the overlay view stays on top. we can't use drawing order for this 806 // because in API level 16 touch dispatch doesn't respect drawing order. 807 mOverlayView.bringToFront(); 808 } 809 updateChildIndices(); 810 } 811 812 @Override 813 public void onChildViewRemoved(View parent, View child) { 814 updateChildIndices(); 815 } 816 817 @Override 818 public void bringChildToFront(View child) { 819 super.bringChildToFront(child); 820 if (child != mOverlayView && mOverlayView != null) { 821 // ensure that the overlay view stays on top. we can't use drawing order for this 822 // because in API level 16 touch dispatch doesn't respect drawing order. 823 mOverlayView.bringToFront(); 824 } 825 updateChildIndices(); 826 } 827 828 private void updateChildIndices() { 829 mTopViewIndex = -1; 830 int childCount = getChildCount(); 831 for (int i = 0; i < childCount; i++) { 832 if (getChildAt(i) instanceof DragView) { 833 mTopViewIndex = i; 834 } 835 } 836 mChildCountOnLastUpdate = childCount; 837 } 838 839 @Override 840 protected int getChildDrawingOrder(int childCount, int i) { 841 if (mChildCountOnLastUpdate != childCount) { 842 // between platform versions 17 and 18, behavior for onChildViewRemoved / Added changed. 843 // Pre-18, the child was not added / removed by the time of those callbacks. We need to 844 // force update our representation of things here to avoid crashing on pre-18 devices 845 // in certain instances. 846 updateChildIndices(); 847 } 848 849 // i represents the current draw iteration 850 if (mTopViewIndex == -1) { 851 // in general we do nothing 852 return i; 853 } else if (i == childCount - 1) { 854 // if we have a top index, we return it when drawing last item (highest z-order) 855 return mTopViewIndex; 856 } else if (i < mTopViewIndex) { 857 return i; 858 } else { 859 // for indexes greater than the top index, we fetch one item above to shift for the 860 // displacement of the top index 861 return i + 1; 862 } 863 } 864 865 void onEnterScrollArea(int direction) { 866 mInScrollArea = true; 867 invalidate(); 868 } 869 870 void onExitScrollArea() { 871 mInScrollArea = false; 872 invalidate(); 873 } 874 875 void showPageHints() { 876 mShowPageHints = true; 877 invalidate(); 878 } 879 880 void hidePageHints() { 881 mShowPageHints = false; 882 invalidate(); 883 } 884 885 /** 886 * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. 887 */ 888 private boolean isLayoutRtl() { 889 return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); 890 } 891 892 @Override 893 protected void dispatchDraw(Canvas canvas) { 894 // Draw the background gradient below children. 895 if (mBackground != null && mBackgroundAlpha > 0.0f) { 896 int alpha = (int) (mBackgroundAlpha * 255); 897 mBackground.setAlpha(alpha); 898 mBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight()); 899 mBackground.draw(canvas); 900 } 901 902 super.dispatchDraw(canvas); 903 } 904 905 private void drawPageHints(Canvas canvas) { 906 if (mShowPageHints) { 907 Workspace workspace = mLauncher.getWorkspace(); 908 int width = getMeasuredWidth(); 909 Rect childRect = new Rect(); 910 getDescendantRectRelativeToSelf(workspace.getChildAt(workspace.getChildCount() - 1), 911 childRect); 912 913 int page = workspace.getNextPage(); 914 final boolean isRtl = isLayoutRtl(); 915 CellLayout leftPage = (CellLayout) workspace.getChildAt(isRtl ? page + 1 : page - 1); 916 CellLayout rightPage = (CellLayout) workspace.getChildAt(isRtl ? page - 1 : page + 1); 917 918 if (leftPage != null && leftPage.isDragTarget()) { 919 Drawable left = mInScrollArea && leftPage.getIsDragOverlapping() ? 920 mLeftHoverDrawableActive : mLeftHoverDrawable; 921 left.setBounds(0, childRect.top, 922 left.getIntrinsicWidth(), childRect.bottom); 923 left.draw(canvas); 924 } 925 if (rightPage != null && rightPage.isDragTarget()) { 926 Drawable right = mInScrollArea && rightPage.getIsDragOverlapping() ? 927 mRightHoverDrawableActive : mRightHoverDrawable; 928 right.setBounds(width - right.getIntrinsicWidth(), 929 childRect.top, width, childRect.bottom); 930 right.draw(canvas); 931 } 932 } 933 } 934 935 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 936 boolean ret = super.drawChild(canvas, child, drawingTime); 937 938 // We want to draw the page hints above the workspace, but below the drag view. 939 if (child instanceof Workspace) { 940 drawPageHints(canvas); 941 } 942 return ret; 943 } 944 945 public void setBackgroundAlpha(float alpha) { 946 if (alpha != mBackgroundAlpha) { 947 mBackgroundAlpha = alpha; 948 invalidate(); 949 } 950 } 951 952 public float getBackgroundAlpha() { 953 return mBackgroundAlpha; 954 } 955 956 public void setTouchCompleteListener(TouchCompleteListener listener) { 957 mTouchCompleteListener = listener; 958 } 959 960 public interface TouchCompleteListener { 961 public void onTouchComplete(); 962 } 963} 964