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