1 2 /* 3 * Copyright (C) 2008 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.launcher3.dragndrop; 19 20 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.TimeInterpolator; 25 import android.animation.ValueAnimator; 26 import android.animation.ValueAnimator.AnimatorUpdateListener; 27 import android.content.Context; 28 import android.content.res.Resources; 29 import android.graphics.Canvas; 30 import android.graphics.Rect; 31 import android.util.AttributeSet; 32 import android.view.KeyEvent; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.accessibility.AccessibilityEvent; 36 import android.view.accessibility.AccessibilityManager; 37 import android.view.animation.Interpolator; 38 import android.widget.TextView; 39 40 import com.android.launcher3.AbstractFloatingView; 41 import com.android.launcher3.CellLayout; 42 import com.android.launcher3.DropTargetBar; 43 import com.android.launcher3.Launcher; 44 import com.android.launcher3.R; 45 import com.android.launcher3.ShortcutAndWidgetContainer; 46 import com.android.launcher3.Workspace; 47 import com.android.launcher3.anim.Interpolators; 48 import com.android.launcher3.folder.Folder; 49 import com.android.launcher3.folder.FolderIcon; 50 import com.android.launcher3.graphics.ViewScrim; 51 import com.android.launcher3.graphics.WorkspaceAndHotseatScrim; 52 import com.android.launcher3.keyboard.ViewGroupFocusHelper; 53 import com.android.launcher3.uioverrides.UiFactory; 54 import com.android.launcher3.util.Thunk; 55 import com.android.launcher3.views.BaseDragLayer; 56 57 import java.util.ArrayList; 58 59 /** 60 * A ViewGroup that coordinates dragging across its descendants 61 */ 62 public class DragLayer extends BaseDragLayer<Launcher> { 63 64 public static final int ALPHA_INDEX_OVERLAY = 0; 65 public static final int ALPHA_INDEX_LAUNCHER_LOAD = 1; 66 public static final int ALPHA_INDEX_TRANSITIONS = 2; 67 public static final int ALPHA_INDEX_SWIPE_UP = 3; 68 private static final int ALPHA_CHANNEL_COUNT = 4; 69 70 public static final int ANIMATION_END_DISAPPEAR = 0; 71 public static final int ANIMATION_END_REMAIN_VISIBLE = 2; 72 73 @Thunk DragController mDragController; 74 75 // Variables relating to animation of views after drop 76 private ValueAnimator mDropAnim = null; 77 private final TimeInterpolator mCubicEaseOutInterpolator = Interpolators.DEACCEL_1_5; 78 @Thunk DragView mDropView = null; 79 @Thunk int mAnchorViewInitialScrollX = 0; 80 @Thunk View mAnchorView = null; 81 82 private boolean mHoverPointClosesFolder = false; 83 84 private int mTopViewIndex; 85 private int mChildCountOnLastUpdate = -1; 86 87 // Related to adjacent page hints 88 private final ViewGroupFocusHelper mFocusIndicatorHelper; 89 private final WorkspaceAndHotseatScrim mScrim; 90 91 /** 92 * Used to create a new DragLayer from XML. 93 * 94 * @param context The application's context. 95 * @param attrs The attributes set containing the Workspace's customization values. 96 */ 97 public DragLayer(Context context, AttributeSet attrs) { 98 super(context, attrs, ALPHA_CHANNEL_COUNT); 99 100 // Disable multitouch across the workspace/all apps/customize tray 101 setMotionEventSplittingEnabled(false); 102 setChildrenDrawingOrderEnabled(true); 103 104 mFocusIndicatorHelper = new ViewGroupFocusHelper(this); 105 mScrim = new WorkspaceAndHotseatScrim(this); 106 } 107 108 public void setup(DragController dragController, Workspace workspace) { 109 mDragController = dragController; 110 mScrim.setWorkspace(workspace); 111 recreateControllers(); 112 } 113 114 public void recreateControllers() { 115 mControllers = UiFactory.createTouchControllers(mActivity); 116 } 117 118 public ViewGroupFocusHelper getFocusIndicatorHelper() { 119 return mFocusIndicatorHelper; 120 } 121 122 @Override 123 public boolean dispatchKeyEvent(KeyEvent event) { 124 return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); 125 } 126 127 @Override 128 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 129 ViewScrim scrim = ViewScrim.get(child); 130 if (scrim != null) { 131 scrim.draw(canvas, getWidth(), getHeight()); 132 } 133 return super.drawChild(canvas, child, drawingTime); 134 } 135 136 @Override 137 protected boolean findActiveController(MotionEvent ev) { 138 if (mActivity.getStateManager().getState().disableInteraction) { 139 // You Shall Not Pass!!! 140 mActiveController = null; 141 return true; 142 } 143 return super.findActiveController(ev); 144 } 145 146 private boolean isEventOverAccessibleDropTargetBar(MotionEvent ev) { 147 return isInAccessibleDrag() && isEventOverView(mActivity.getDropTargetBar(), ev); 148 } 149 150 @Override 151 public boolean onInterceptHoverEvent(MotionEvent ev) { 152 if (mActivity == null || mActivity.getWorkspace() == null) { 153 return false; 154 } 155 AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); 156 if (!(topView instanceof Folder)) { 157 return false; 158 } else { 159 AccessibilityManager accessibilityManager = (AccessibilityManager) 160 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 161 if (accessibilityManager.isTouchExplorationEnabled()) { 162 Folder currentFolder = (Folder) topView; 163 final int action = ev.getAction(); 164 boolean isOverFolderOrSearchBar; 165 switch (action) { 166 case MotionEvent.ACTION_HOVER_ENTER: 167 isOverFolderOrSearchBar = isEventOverView(topView, ev) || 168 isEventOverAccessibleDropTargetBar(ev); 169 if (!isOverFolderOrSearchBar) { 170 sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); 171 mHoverPointClosesFolder = true; 172 return true; 173 } 174 mHoverPointClosesFolder = false; 175 break; 176 case MotionEvent.ACTION_HOVER_MOVE: 177 isOverFolderOrSearchBar = isEventOverView(topView, ev) || 178 isEventOverAccessibleDropTargetBar(ev); 179 if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) { 180 sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); 181 mHoverPointClosesFolder = true; 182 return true; 183 } else if (!isOverFolderOrSearchBar) { 184 return true; 185 } 186 mHoverPointClosesFolder = false; 187 } 188 } 189 } 190 return false; 191 } 192 193 private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) { 194 int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close; 195 sendCustomAccessibilityEvent( 196 this, AccessibilityEvent.TYPE_VIEW_FOCUSED, getContext().getString(stringId)); 197 } 198 199 @Override 200 public boolean onHoverEvent(MotionEvent ev) { 201 // If we've received this, we've already done the necessary handling 202 // in onInterceptHoverEvent. Return true to consume the event. 203 return false; 204 } 205 206 207 private boolean isInAccessibleDrag() { 208 return mActivity.getAccessibilityDelegate().isInAccessibleDrag(); 209 } 210 211 @Override 212 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 213 if (isInAccessibleDrag() && child instanceof DropTargetBar) { 214 return true; 215 } 216 return super.onRequestSendAccessibilityEvent(child, event); 217 } 218 219 @Override 220 public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) { 221 View topView = AbstractFloatingView.getTopOpenViewWithType(mActivity, 222 AbstractFloatingView.TYPE_ACCESSIBLE); 223 if (topView != null) { 224 addAccessibleChildToList(topView, childrenForAccessibility); 225 if (isInAccessibleDrag()) { 226 addAccessibleChildToList(mActivity.getDropTargetBar(), childrenForAccessibility); 227 } 228 } else { 229 super.addChildrenForAccessibility(childrenForAccessibility); 230 } 231 } 232 233 @Override 234 public boolean dispatchUnhandledMove(View focused, int direction) { 235 return super.dispatchUnhandledMove(focused, direction) 236 || mDragController.dispatchUnhandledMove(focused, direction); 237 } 238 239 @Override 240 public boolean dispatchTouchEvent(MotionEvent ev) { 241 ev.offsetLocation(getTranslationX(), 0); 242 try { 243 return super.dispatchTouchEvent(ev); 244 } finally { 245 ev.offsetLocation(-getTranslationX(), 0); 246 } 247 } 248 249 public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha, 250 float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable, 251 int duration) { 252 Rect r = new Rect(); 253 getViewRectRelativeToSelf(dragView, r); 254 final int fromX = r.left; 255 final int fromY = r.top; 256 257 animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY, 258 onFinishRunnable, animationEndStyle, duration, null); 259 } 260 261 public void animateViewIntoPosition(DragView dragView, final View child, View anchorView) { 262 animateViewIntoPosition(dragView, child, -1, anchorView); 263 } 264 265 public void animateViewIntoPosition(DragView dragView, final View child, int duration, 266 View anchorView) { 267 ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent(); 268 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); 269 parentChildren.measureChild(child); 270 271 Rect r = new Rect(); 272 getViewRectRelativeToSelf(dragView, r); 273 274 int coord[] = new int[2]; 275 float childScale = child.getScaleX(); 276 coord[0] = lp.x + (int) (child.getMeasuredWidth() * (1 - childScale) / 2); 277 coord[1] = lp.y + (int) (child.getMeasuredHeight() * (1 - childScale) / 2); 278 279 // Since the child hasn't necessarily been laid out, we force the lp to be updated with 280 // the correct coordinates (above) and use these to determine the final location 281 float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord); 282 // We need to account for the scale of the child itself, as the above only accounts for 283 // for the scale in parents. 284 scale *= childScale; 285 int toX = coord[0]; 286 int toY = coord[1]; 287 float toScale = scale; 288 if (child instanceof TextView) { 289 TextView tv = (TextView) child; 290 // Account for the source scale of the icon (ie. from AllApps to Workspace, in which 291 // the workspace may have smaller icon bounds). 292 toScale = scale / dragView.getIntrinsicIconScaleFactor(); 293 294 // The child may be scaled (always about the center of the view) so to account for it, 295 // we have to offset the position by the scaled size. Once we do that, we can center 296 // the drag view about the scaled child view. 297 toY += Math.round(toScale * tv.getPaddingTop()); 298 toY -= dragView.getMeasuredHeight() * (1 - toScale) / 2; 299 if (dragView.getDragVisualizeOffset() != null) { 300 toY -= Math.round(toScale * dragView.getDragVisualizeOffset().y); 301 } 302 303 toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; 304 } else if (child instanceof FolderIcon) { 305 // Account for holographic blur padding on the drag view 306 toY += Math.round(scale * (child.getPaddingTop() - dragView.getDragRegionTop())); 307 toY -= scale * dragView.getBlurSizeOutline() / 2; 308 toY -= (1 - scale) * dragView.getMeasuredHeight() / 2; 309 // Center in the x coordinate about the target's drawable 310 toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; 311 } else { 312 toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2; 313 toX -= (Math.round(scale * (dragView.getMeasuredWidth() 314 - child.getMeasuredWidth()))) / 2; 315 } 316 317 final int fromX = r.left; 318 final int fromY = r.top; 319 child.setVisibility(INVISIBLE); 320 Runnable onCompleteRunnable = () -> child.setVisibility(VISIBLE); 321 animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, toScale, toScale, 322 onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView); 323 } 324 325 public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY, 326 final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY, 327 float finalScaleX, float finalScaleY, Runnable onCompleteRunnable, 328 int animationEndStyle, int duration, View anchorView) { 329 Rect from = new Rect(fromX, fromY, fromX + 330 view.getMeasuredWidth(), fromY + view.getMeasuredHeight()); 331 Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight()); 332 animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration, 333 null, null, onCompleteRunnable, animationEndStyle, anchorView); 334 } 335 336 /** 337 * This method animates a view at the end of a drag and drop animation. 338 * 339 * @param view The view to be animated. This view is drawn directly into DragLayer, and so 340 * doesn't need to be a child of DragLayer. 341 * @param from The initial location of the view. Only the left and top parameters are used. 342 * @param to The final location of the view. Only the left and top parameters are used. This 343 * location doesn't account for scaling, and so should be centered about the desired 344 * final location (including scaling). 345 * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates. 346 * @param finalScaleX The final scale of the view. The view is scaled about its center. 347 * @param finalScaleY The final scale of the view. The view is scaled about its center. 348 * @param duration The duration of the animation. 349 * @param motionInterpolator The interpolator to use for the location of the view. 350 * @param alphaInterpolator The interpolator to use for the alpha of the view. 351 * @param onCompleteRunnable Optional runnable to run on animation completion. 352 * @param animationEndStyle Whether or not to fade out the view once the animation completes. 353 * {@link #ANIMATION_END_DISAPPEAR} or {@link #ANIMATION_END_REMAIN_VISIBLE}. 354 * @param anchorView If not null, this represents the view which the animated view stays 355 * anchored to in case scrolling is currently taking place. Note: currently this is 356 * only used for the X dimension for the case of the workspace. 357 */ 358 public void animateView(final DragView view, final Rect from, final Rect to, 359 final float finalAlpha, final float initScaleX, final float initScaleY, 360 final float finalScaleX, final float finalScaleY, int duration, 361 final Interpolator motionInterpolator, final Interpolator alphaInterpolator, 362 final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) { 363 364 // Calculate the duration of the animation based on the object's distance 365 final float dist = (float) Math.hypot(to.left - from.left, to.top - from.top); 366 final Resources res = getResources(); 367 final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist); 368 369 // If duration < 0, this is a cue to compute the duration based on the distance 370 if (duration < 0) { 371 duration = res.getInteger(R.integer.config_dropAnimMaxDuration); 372 if (dist < maxDist) { 373 duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist); 374 } 375 duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration)); 376 } 377 378 // Fall back to cubic ease out interpolator for the animation if none is specified 379 TimeInterpolator interpolator = null; 380 if (alphaInterpolator == null || motionInterpolator == null) { 381 interpolator = mCubicEaseOutInterpolator; 382 } 383 384 // Animate the view 385 final float initAlpha = view.getAlpha(); 386 final float dropViewScale = view.getScaleX(); 387 AnimatorUpdateListener updateCb = new AnimatorUpdateListener() { 388 @Override 389 public void onAnimationUpdate(ValueAnimator animation) { 390 final float percent = (Float) animation.getAnimatedValue(); 391 final int width = view.getMeasuredWidth(); 392 final int height = view.getMeasuredHeight(); 393 394 float alphaPercent = alphaInterpolator == null ? percent : 395 alphaInterpolator.getInterpolation(percent); 396 float motionPercent = motionInterpolator == null ? percent : 397 motionInterpolator.getInterpolation(percent); 398 399 float initialScaleX = initScaleX * dropViewScale; 400 float initialScaleY = initScaleY * dropViewScale; 401 float scaleX = finalScaleX * percent + initialScaleX * (1 - percent); 402 float scaleY = finalScaleY * percent + initialScaleY * (1 - percent); 403 float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent); 404 405 float fromLeft = from.left + (initialScaleX - 1f) * width / 2; 406 float fromTop = from.top + (initialScaleY - 1f) * height / 2; 407 408 int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent))); 409 int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent))); 410 411 int anchorAdjust = mAnchorView == null ? 0 : (int) (mAnchorView.getScaleX() * 412 (mAnchorViewInitialScrollX - mAnchorView.getScrollX())); 413 414 int xPos = x - mDropView.getScrollX() + anchorAdjust; 415 int yPos = y - mDropView.getScrollY(); 416 417 mDropView.setTranslationX(xPos); 418 mDropView.setTranslationY(yPos); 419 mDropView.setScaleX(scaleX); 420 mDropView.setScaleY(scaleY); 421 mDropView.setAlpha(alpha); 422 } 423 }; 424 animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle, 425 anchorView); 426 } 427 428 public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration, 429 TimeInterpolator interpolator, final Runnable onCompleteRunnable, 430 final int animationEndStyle, View anchorView) { 431 // Clean up the previous animations 432 if (mDropAnim != null) mDropAnim.cancel(); 433 434 // Show the drop view if it was previously hidden 435 mDropView = view; 436 mDropView.cancelAnimation(); 437 mDropView.requestLayout(); 438 439 // Set the anchor view if the page is scrolling 440 if (anchorView != null) { 441 mAnchorViewInitialScrollX = anchorView.getScrollX(); 442 } 443 mAnchorView = anchorView; 444 445 // Create and start the animation 446 mDropAnim = new ValueAnimator(); 447 mDropAnim.setInterpolator(interpolator); 448 mDropAnim.setDuration(duration); 449 mDropAnim.setFloatValues(0f, 1f); 450 mDropAnim.addUpdateListener(updateCb); 451 mDropAnim.addListener(new AnimatorListenerAdapter() { 452 public void onAnimationEnd(Animator animation) { 453 if (onCompleteRunnable != null) { 454 onCompleteRunnable.run(); 455 } 456 switch (animationEndStyle) { 457 case ANIMATION_END_DISAPPEAR: 458 clearAnimatedView(); 459 break; 460 case ANIMATION_END_REMAIN_VISIBLE: 461 break; 462 } 463 mDropAnim = null; 464 } 465 }); 466 mDropAnim.start(); 467 } 468 469 public void clearAnimatedView() { 470 if (mDropAnim != null) { 471 mDropAnim.cancel(); 472 } 473 mDropAnim = null; 474 if (mDropView != null) { 475 mDragController.onDeferredEndDrag(mDropView); 476 } 477 mDropView = null; 478 invalidate(); 479 } 480 481 public View getAnimatedView() { 482 return mDropView; 483 } 484 485 @Override 486 public void onViewAdded(View child) { 487 super.onViewAdded(child); 488 updateChildIndices(); 489 UiFactory.onLauncherStateOrFocusChanged(mActivity); 490 } 491 492 @Override 493 public void onViewRemoved(View child) { 494 super.onViewRemoved(child); 495 updateChildIndices(); 496 UiFactory.onLauncherStateOrFocusChanged(mActivity); 497 } 498 499 @Override 500 public void bringChildToFront(View child) { 501 super.bringChildToFront(child); 502 updateChildIndices(); 503 } 504 505 private void updateChildIndices() { 506 mTopViewIndex = -1; 507 int childCount = getChildCount(); 508 for (int i = 0; i < childCount; i++) { 509 if (getChildAt(i) instanceof DragView) { 510 mTopViewIndex = i; 511 } 512 } 513 mChildCountOnLastUpdate = childCount; 514 } 515 516 @Override 517 protected int getChildDrawingOrder(int childCount, int i) { 518 if (mChildCountOnLastUpdate != childCount) { 519 // between platform versions 17 and 18, behavior for onChildViewRemoved / Added changed. 520 // Pre-18, the child was not added / removed by the time of those callbacks. We need to 521 // force update our representation of things here to avoid crashing on pre-18 devices 522 // in certain instances. 523 updateChildIndices(); 524 } 525 526 // i represents the current draw iteration 527 if (mTopViewIndex == -1) { 528 // in general we do nothing 529 return i; 530 } else if (i == childCount - 1) { 531 // if we have a top index, we return it when drawing last item (highest z-order) 532 return mTopViewIndex; 533 } else if (i < mTopViewIndex) { 534 return i; 535 } else { 536 // for indexes greater than the top index, we fetch one item above to shift for the 537 // displacement of the top index 538 return i + 1; 539 } 540 } 541 542 @Override 543 protected void dispatchDraw(Canvas canvas) { 544 // Draw the background below children. 545 mScrim.draw(canvas); 546 mFocusIndicatorHelper.draw(canvas); 547 super.dispatchDraw(canvas); 548 } 549 550 @Override 551 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 552 super.onSizeChanged(w, h, oldw, oldh); 553 mScrim.setSize(w, h); 554 } 555 556 @Override 557 public void setInsets(Rect insets) { 558 super.setInsets(insets); 559 mScrim.onInsetsChanged(insets); 560 } 561 562 public WorkspaceAndHotseatScrim getScrim() { 563 return mScrim; 564 } 565 } 566