DragLayer.java revision 3371da0159cc54ff8ae1b1b26effb96445f208d5
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.launcher2; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.animation.TimeInterpolator; 23import android.animation.ValueAnimator; 24import android.animation.ValueAnimator.AnimatorUpdateListener; 25import android.content.Context; 26import android.content.res.Resources; 27import android.graphics.Canvas; 28import android.graphics.Rect; 29import android.graphics.drawable.Drawable; 30import android.util.AttributeSet; 31import android.view.KeyEvent; 32import android.view.MotionEvent; 33import android.view.View; 34import android.view.ViewParent; 35import android.view.accessibility.AccessibilityEvent; 36import android.view.accessibility.AccessibilityManager; 37import android.view.animation.DecelerateInterpolator; 38import android.view.animation.Interpolator; 39import android.widget.FrameLayout; 40import android.widget.TextView; 41 42import com.android.launcher.R; 43 44import java.util.ArrayList; 45 46/** 47 * A ViewGroup that coordinates dragging across its descendants 48 */ 49public class DragLayer extends FrameLayout { 50 private DragController mDragController; 51 private int[] mTmpXY = new int[2]; 52 53 private int mXDown, mYDown; 54 private Launcher mLauncher; 55 56 // Variables relating to resizing widgets 57 private final ArrayList<AppWidgetResizeFrame> mResizeFrames = 58 new ArrayList<AppWidgetResizeFrame>(); 59 private AppWidgetResizeFrame mCurrentResizeFrame; 60 61 // Variables relating to animation of views after drop 62 private ValueAnimator mDropAnim = null; 63 private ValueAnimator mFadeOutAnim = null; 64 private TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); 65 private View mDropView = null; 66 67 private int[] mDropViewPos = new int[2]; 68 private float mDropViewScale; 69 private float mDropViewAlpha; 70 private boolean mHoverPointClosesFolder = false; 71 private Rect mHitRect = new Rect(); 72 73 /** 74 * Used to create a new DragLayer from XML. 75 * 76 * @param context The application's context. 77 * @param attrs The attributes set containing the Workspace's customization values. 78 */ 79 public DragLayer(Context context, AttributeSet attrs) { 80 super(context, attrs); 81 82 // Disable multitouch across the workspace/all apps/customize tray 83 setMotionEventSplittingEnabled(false); 84 } 85 86 public void setup(Launcher launcher, DragController controller) { 87 mLauncher = launcher; 88 mDragController = controller; 89 } 90 91 @Override 92 public boolean dispatchKeyEvent(KeyEvent event) { 93 return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); 94 } 95 96 private boolean isEventOverFolderTextRegion(Folder folder, MotionEvent ev) { 97 getDescendantRectRelativeToSelf(folder.getEditTextRegion(), mHitRect); 98 if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { 99 return true; 100 } 101 return false; 102 } 103 104 private boolean isEventOverFolder(Folder folder, MotionEvent ev) { 105 getDescendantRectRelativeToSelf(folder, mHitRect); 106 if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { 107 return true; 108 } 109 return false; 110 } 111 112 private boolean handleTouchDown(MotionEvent ev, boolean intercept) { 113 Rect hitRect = new Rect(); 114 int x = (int) ev.getX(); 115 int y = (int) ev.getY(); 116 117 for (AppWidgetResizeFrame child: mResizeFrames) { 118 child.getHitRect(hitRect); 119 if (hitRect.contains(x, y)) { 120 if (child.beginResizeIfPointInRegion(x - child.getLeft(), y - child.getTop())) { 121 mCurrentResizeFrame = child; 122 mXDown = x; 123 mYDown = y; 124 requestDisallowInterceptTouchEvent(true); 125 return true; 126 } 127 } 128 } 129 130 Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); 131 if (currentFolder != null && !mLauncher.isFolderClingVisible() && intercept) { 132 if (currentFolder.isEditingName()) { 133 if (!isEventOverFolderTextRegion(currentFolder, ev)) { 134 currentFolder.dismissEditingName(); 135 return true; 136 } 137 } 138 139 getDescendantRectRelativeToSelf(currentFolder, hitRect); 140 if (!isEventOverFolder(currentFolder, ev)) { 141 mLauncher.closeFolder(); 142 return true; 143 } 144 } 145 return false; 146 } 147 148 @Override 149 public boolean onInterceptTouchEvent(MotionEvent ev) { 150 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 151 if (handleTouchDown(ev, true)) { 152 return true; 153 } 154 } 155 clearAllResizeFrames(); 156 return mDragController.onInterceptTouchEvent(ev); 157 } 158 159 @Override 160 public boolean onInterceptHoverEvent(MotionEvent ev) { 161 Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); 162 if (currentFolder == null) { 163 return false; 164 } else { 165 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 166 final int action = ev.getAction(); 167 boolean isOverFolder; 168 switch (action) { 169 case MotionEvent.ACTION_HOVER_ENTER: 170 isOverFolder = isEventOverFolder(currentFolder, ev); 171 if (!isOverFolder) { 172 sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); 173 mHoverPointClosesFolder = true; 174 return true; 175 } else if (isOverFolder) { 176 mHoverPointClosesFolder = false; 177 } else { 178 return true; 179 } 180 case MotionEvent.ACTION_HOVER_MOVE: 181 isOverFolder = isEventOverFolder(currentFolder, ev); 182 if (!isOverFolder && !mHoverPointClosesFolder) { 183 sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); 184 mHoverPointClosesFolder = true; 185 return true; 186 } else if (isOverFolder) { 187 mHoverPointClosesFolder = false; 188 } else { 189 return true; 190 } 191 } 192 } 193 } 194 return false; 195 } 196 197 private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) { 198 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 199 Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); 200 int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close; 201 AccessibilityEvent event = AccessibilityEvent.obtain( 202 AccessibilityEvent.TYPE_VIEW_FOCUSED); 203 onInitializeAccessibilityEvent(event); 204 event.getText().add(mContext.getString(stringId)); 205 AccessibilityManager.getInstance(mContext).sendAccessibilityEvent(event); 206 } 207 } 208 209 @Override 210 public boolean onHoverEvent(MotionEvent ev) { 211 // If we've received this, we've already done the necessary handling 212 // in onInterceptHoverEvent. Return true to consume the event. 213 return false; 214 } 215 216 @Override 217 public boolean onTouchEvent(MotionEvent ev) { 218 boolean handled = false; 219 int action = ev.getAction(); 220 221 int x = (int) ev.getX(); 222 int y = (int) ev.getY(); 223 224 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 225 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 226 if (handleTouchDown(ev, false)) { 227 return true; 228 } 229 } 230 } 231 232 if (mCurrentResizeFrame != null) { 233 handled = true; 234 switch (action) { 235 case MotionEvent.ACTION_MOVE: 236 mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown); 237 break; 238 case MotionEvent.ACTION_CANCEL: 239 case MotionEvent.ACTION_UP: 240 mCurrentResizeFrame.commitResizeForDelta(x - mXDown, y - mYDown); 241 mCurrentResizeFrame = null; 242 } 243 } 244 if (handled) return true; 245 return mDragController.onTouchEvent(ev); 246 } 247 248 /** 249 * Determine the rect of the descendant in this DragLayer's coordinates 250 * 251 * @param descendant The descendant whose coordinates we want to find. 252 * @param r The rect into which to place the results. 253 * @return The factor by which this descendant is scaled relative to this DragLayer. 254 */ 255 public float getDescendantRectRelativeToSelf(View descendant, Rect r) { 256 mTmpXY[0] = 0; 257 mTmpXY[1] = 0; 258 float scale = getDescendantCoordRelativeToSelf(descendant, mTmpXY); 259 r.set(mTmpXY[0], mTmpXY[1], 260 mTmpXY[0] + descendant.getWidth(), mTmpXY[1] + descendant.getHeight()); 261 return scale; 262 } 263 264 public void getLocationInDragLayer(View child, int[] loc) { 265 loc[0] = 0; 266 loc[1] = 0; 267 getDescendantCoordRelativeToSelf(child, loc); 268 } 269 270 /** 271 * Given a coordinate relative to the descendant, find the coordinate in this DragLayer's 272 * coordinates. 273 * 274 * @param descendant The descendant to which the passed coordinate is relative. 275 * @param coord The coordinate that we want mapped. 276 * @return The factor by which this descendant is scaled relative to this DragLayer. 277 */ 278 public float getDescendantCoordRelativeToSelf(View descendant, int[] coord) { 279 float scale = 1.0f; 280 float[] pt = {coord[0], coord[1]}; 281 descendant.getMatrix().mapPoints(pt); 282 scale *= descendant.getScaleX(); 283 pt[0] += descendant.getLeft(); 284 pt[1] += descendant.getTop(); 285 ViewParent viewParent = descendant.getParent(); 286 while (viewParent instanceof View && viewParent != this) { 287 final View view = (View)viewParent; 288 view.getMatrix().mapPoints(pt); 289 scale *= view.getScaleX(); 290 pt[0] += view.getLeft() - view.getScrollX(); 291 pt[1] += view.getTop() - view.getScrollY(); 292 viewParent = view.getParent(); 293 } 294 coord[0] = (int) Math.round(pt[0]); 295 coord[1] = (int) Math.round(pt[1]); 296 return scale; 297 } 298 299 public void getViewRectRelativeToSelf(View v, Rect r) { 300 int[] loc = new int[2]; 301 getLocationInWindow(loc); 302 int x = loc[0]; 303 int y = loc[1]; 304 305 v.getLocationInWindow(loc); 306 int vX = loc[0]; 307 int vY = loc[1]; 308 309 int left = vX - x; 310 int top = vY - y; 311 r.set(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight()); 312 } 313 314 @Override 315 public boolean dispatchUnhandledMove(View focused, int direction) { 316 return mDragController.dispatchUnhandledMove(focused, direction); 317 } 318 319 public static class LayoutParams extends FrameLayout.LayoutParams { 320 public int x, y; 321 public boolean customPosition = false; 322 323 /** 324 * {@inheritDoc} 325 */ 326 public LayoutParams(int width, int height) { 327 super(width, height); 328 } 329 330 public void setWidth(int width) { 331 this.width = width; 332 } 333 334 public int getWidth() { 335 return width; 336 } 337 338 public void setHeight(int height) { 339 this.height = height; 340 } 341 342 public int getHeight() { 343 return height; 344 } 345 346 public void setX(int x) { 347 this.x = x; 348 } 349 350 public int getX() { 351 return x; 352 } 353 354 public void setY(int y) { 355 this.y = y; 356 } 357 358 public int getY() { 359 return y; 360 } 361 } 362 363 protected void onLayout(boolean changed, int l, int t, int r, int b) { 364 super.onLayout(changed, l, t, r, b); 365 int count = getChildCount(); 366 for (int i = 0; i < count; i++) { 367 View child = getChildAt(i); 368 final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) child.getLayoutParams(); 369 if (flp instanceof LayoutParams) { 370 final LayoutParams lp = (LayoutParams) flp; 371 if (lp.customPosition) { 372 child.layout(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height); 373 } 374 } 375 } 376 } 377 378 public void clearAllResizeFrames() { 379 if (mResizeFrames.size() > 0) { 380 for (AppWidgetResizeFrame frame: mResizeFrames) { 381 removeView(frame); 382 } 383 mResizeFrames.clear(); 384 } 385 } 386 387 public boolean hasResizeFrames() { 388 return mResizeFrames.size() > 0; 389 } 390 391 public boolean isWidgetBeingResized() { 392 return mCurrentResizeFrame != null; 393 } 394 395 public void addResizeFrame(ItemInfo itemInfo, LauncherAppWidgetHostView widget, 396 CellLayout cellLayout) { 397 AppWidgetResizeFrame resizeFrame = new AppWidgetResizeFrame(getContext(), 398 itemInfo, widget, cellLayout, this); 399 400 LayoutParams lp = new LayoutParams(-1, -1); 401 lp.customPosition = true; 402 403 addView(resizeFrame, lp); 404 mResizeFrames.add(resizeFrame); 405 406 resizeFrame.snapToWidget(false); 407 } 408 409 public void animateViewIntoPosition(DragView dragView, final View child) { 410 animateViewIntoPosition(dragView, child, null); 411 } 412 413 public void animateViewIntoPosition(DragView dragView, final int[] pos, float scale, 414 Runnable onFinishRunnable) { 415 Rect r = new Rect(); 416 getViewRectRelativeToSelf(dragView, r); 417 final int fromX = r.left; 418 final int fromY = r.top; 419 420 animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], scale, 421 onFinishRunnable, true, -1); 422 } 423 424 public void animateViewIntoPosition(DragView dragView, final View child, 425 final Runnable onFinishAnimationRunnable) { 426 animateViewIntoPosition(dragView, child, -1, onFinishAnimationRunnable); 427 } 428 429 public void animateViewIntoPosition(DragView dragView, final View child, int duration, 430 final Runnable onFinishAnimationRunnable) { 431 ((CellLayoutChildren) child.getParent()).measureChild(child); 432 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); 433 434 Rect r = new Rect(); 435 getViewRectRelativeToSelf(dragView, r); 436 437 int coord[] = new int[2]; 438 coord[0] = lp.x; 439 coord[1] = lp.y; 440 // Since the child hasn't necessarily been laid out, we force the lp to be updated with 441 // the correct coordinates (above) and use these to determine the final location 442 float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord); 443 int toX = coord[0]; 444 int toY = coord[1]; 445 if (child instanceof TextView) { 446 TextView tv = (TextView) child; 447 Drawable d = tv.getCompoundDrawables()[1]; 448 449 // Center in the y coordinate about the target's drawable 450 toY += Math.round(scale * tv.getPaddingTop()); 451 toY -= (dragView.getHeight() - (int) Math.round(scale * d.getIntrinsicHeight())) / 2; 452 // Center in the x coordinate about the target's drawable 453 toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; 454 } else if (child instanceof FolderIcon) { 455 // Account for holographic blur padding on the drag view 456 toY -= HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS / 2; 457 // Center in the x coordinate about the target's drawable 458 toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; 459 } else { 460 toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2; 461 toX -= (Math.round(scale * (dragView.getMeasuredWidth() 462 - child.getMeasuredWidth()))) / 2; 463 } 464 465 final int fromX = r.left; 466 final int fromY = r.top; 467 child.setVisibility(INVISIBLE); 468 child.setAlpha(0); 469 Runnable onCompleteRunnable = new Runnable() { 470 public void run() { 471 child.setVisibility(VISIBLE); 472 ObjectAnimator oa = ObjectAnimator.ofFloat(child, "alpha", 0f, 1f); 473 oa.setDuration(60); 474 oa.addListener(new AnimatorListenerAdapter() { 475 @Override 476 public void onAnimationEnd(android.animation.Animator animation) { 477 if (onFinishAnimationRunnable != null) { 478 onFinishAnimationRunnable.run(); 479 } 480 } 481 }); 482 oa.start(); 483 } 484 }; 485 animateViewIntoPosition(dragView, fromX, fromY, toX, toY, scale, 486 onCompleteRunnable, true, duration); 487 } 488 489 private void animateViewIntoPosition(final View view, final int fromX, final int fromY, 490 final int toX, final int toY, float finalScale, Runnable onCompleteRunnable, 491 boolean fadeOut, int duration) { 492 Rect from = new Rect(fromX, fromY, fromX + 493 view.getMeasuredWidth(), fromY + view.getMeasuredHeight()); 494 Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight()); 495 animateView(view, from, to, 1f, finalScale, duration, null, null, onCompleteRunnable, true); 496 } 497 498 /** 499 * This method animates a view at the end of a drag and drop animation. 500 * 501 * @param view The view to be animated. This view is drawn directly into DragLayer, and so 502 * doesn't need to be a child of DragLayer. 503 * @param from The initial location of the view. Only the left and top parameters are used. 504 * @param to The final location of the view. Only the left and top parameters are used. This 505 * location doesn't account for scaling, and so should be centered about the desired 506 * final location (including scaling). 507 * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates. 508 * @param finalScale The final scale of the view. The view is scaled about its center. 509 * @param duration The duration of the animation. 510 * @param motionInterpolator The interpolator to use for the location of the view. 511 * @param alphaInterpolator The interpolator to use for the alpha of the view. 512 * @param onCompleteRunnable Optional runnable to run on animation completion. 513 * @param fadeOut Whether or not to fade out the view once the animation completes. If true, 514 * the runnable will execute after the view is faded out. 515 */ 516 public void animateView(final View view, final Rect from, final Rect to, final float finalAlpha, 517 final float finalScale, int duration, final Interpolator motionInterpolator, 518 final Interpolator alphaInterpolator, final Runnable onCompleteRunnable, 519 final boolean fadeOut) { 520 // Calculate the duration of the animation based on the object's distance 521 final float dist = (float) Math.sqrt(Math.pow(to.left - from.left, 2) + 522 Math.pow(to.top - from.top, 2)); 523 final Resources res = getResources(); 524 final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist); 525 526 // If duration < 0, this is a cue to compute the duration based on the distance 527 if (duration < 0) { 528 duration = res.getInteger(R.integer.config_dropAnimMaxDuration); 529 if (dist < maxDist) { 530 duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist); 531 } 532 } 533 534 if (mDropAnim != null) { 535 mDropAnim.cancel(); 536 } 537 538 if (mFadeOutAnim != null) { 539 mFadeOutAnim.cancel(); 540 } 541 542 mDropView = view; 543 final float initialAlpha = view.getAlpha(); 544 mDropAnim = new ValueAnimator(); 545 if (alphaInterpolator == null || motionInterpolator == null) { 546 mDropAnim.setInterpolator(mCubicEaseOutInterpolator); 547 } 548 549 mDropAnim.setDuration(duration); 550 mDropAnim.setFloatValues(0.0f, 1.0f); 551 mDropAnim.removeAllUpdateListeners(); 552 mDropAnim.addUpdateListener(new AnimatorUpdateListener() { 553 public void onAnimationUpdate(ValueAnimator animation) { 554 final float percent = (Float) animation.getAnimatedValue(); 555 // Invalidate the old position 556 int width = view.getMeasuredWidth(); 557 int height = view.getMeasuredHeight(); 558 invalidate(mDropViewPos[0], mDropViewPos[1], 559 mDropViewPos[0] + width, mDropViewPos[1] + height); 560 561 float alphaPercent = alphaInterpolator == null ? percent : 562 alphaInterpolator.getInterpolation(percent); 563 float motionPercent = motionInterpolator == null ? percent : 564 motionInterpolator.getInterpolation(percent); 565 566 mDropViewPos[0] = from.left + (int) Math.round(((to.left - from.left) * motionPercent)); 567 mDropViewPos[1] = from.top + (int) Math.round(((to.top - from.top) * motionPercent)); 568 mDropViewScale = percent * finalScale + (1 - percent); 569 mDropViewAlpha = alphaPercent * finalAlpha + (1 - alphaPercent) * initialAlpha; 570 invalidate(mDropViewPos[0], mDropViewPos[1], 571 mDropViewPos[0] + width, mDropViewPos[1] + height); 572 } 573 }); 574 mDropAnim.addListener(new AnimatorListenerAdapter() { 575 public void onAnimationEnd(Animator animation) { 576 if (onCompleteRunnable != null) { 577 onCompleteRunnable.run(); 578 } 579 if (fadeOut) { 580 fadeOutDragView(); 581 } else { 582 mDropView = null; 583 } 584 } 585 }); 586 mDropAnim.start(); 587 } 588 589 private void fadeOutDragView() { 590 mFadeOutAnim = new ValueAnimator(); 591 mFadeOutAnim.setDuration(150); 592 mFadeOutAnim.setFloatValues(0f, 1f); 593 mFadeOutAnim.removeAllUpdateListeners(); 594 mFadeOutAnim.addUpdateListener(new AnimatorUpdateListener() { 595 public void onAnimationUpdate(ValueAnimator animation) { 596 final float percent = (Float) animation.getAnimatedValue(); 597 mDropViewAlpha = 1 - percent; 598 int width = mDropView.getMeasuredWidth(); 599 int height = mDropView.getMeasuredHeight(); 600 invalidate(mDropViewPos[0], mDropViewPos[1], 601 mDropViewPos[0] + width, mDropViewPos[1] + height); 602 } 603 }); 604 mFadeOutAnim.addListener(new AnimatorListenerAdapter() { 605 public void onAnimationEnd(Animator animation) { 606 mDropView = null; 607 } 608 }); 609 mFadeOutAnim.start(); 610 } 611 612 @Override 613 protected void dispatchDraw(Canvas canvas) { 614 super.dispatchDraw(canvas); 615 if (mDropView != null) { 616 // We are animating an item that was just dropped on the home screen. 617 // Render its View in the current animation position. 618 canvas.save(Canvas.MATRIX_SAVE_FLAG); 619 final int xPos = mDropViewPos[0] - mDropView.getScrollX(); 620 final int yPos = mDropViewPos[1] - mDropView.getScrollY(); 621 int width = mDropView.getMeasuredWidth(); 622 int height = mDropView.getMeasuredHeight(); 623 canvas.translate(xPos, yPos); 624 canvas.translate((1 - mDropViewScale) * width / 2, (1 - mDropViewScale) * height / 2); 625 canvas.scale(mDropViewScale, mDropViewScale); 626 mDropView.setAlpha(mDropViewAlpha); 627 mDropView.draw(canvas); 628 canvas.restore(); 629 } 630 } 631} 632