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 android.animation.ObjectAnimator.ofFloat; 21 22 import static com.android.app.animation.Interpolators.DECELERATE_1_5; 23 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; 24 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; 25 import static com.android.launcher3.Utilities.mapRange; 26 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; 27 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent; 28 29 import android.animation.Animator; 30 import android.animation.ObjectAnimator; 31 import android.animation.TimeInterpolator; 32 import android.animation.TypeEvaluator; 33 import android.content.Context; 34 import android.content.res.Resources; 35 import android.graphics.Canvas; 36 import android.graphics.Rect; 37 import android.util.AttributeSet; 38 import android.view.KeyEvent; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.accessibility.AccessibilityEvent; 42 import android.view.accessibility.AccessibilityManager; 43 import android.view.animation.Interpolator; 44 45 import androidx.annotation.Nullable; 46 47 import com.android.app.animation.Interpolators; 48 import com.android.launcher3.AbstractFloatingView; 49 import com.android.launcher3.DropTargetBar; 50 import com.android.launcher3.Launcher; 51 import com.android.launcher3.R; 52 import com.android.launcher3.ShortcutAndWidgetContainer; 53 import com.android.launcher3.ShortcutAndWidgetContainer.TranslationProvider; 54 import com.android.launcher3.Utilities; 55 import com.android.launcher3.Workspace; 56 import com.android.launcher3.anim.PendingAnimation; 57 import com.android.launcher3.anim.SpringProperty; 58 import com.android.launcher3.celllayout.CellLayoutLayoutParams; 59 import com.android.launcher3.folder.Folder; 60 import com.android.launcher3.graphics.Scrim; 61 import com.android.launcher3.keyboard.ViewGroupFocusHelper; 62 import com.android.launcher3.views.BaseDragLayer; 63 import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks; 64 65 import java.util.ArrayList; 66 67 /** 68 * A ViewGroup that coordinates dragging across its descendants 69 */ 70 public class DragLayer extends BaseDragLayer<Launcher> implements LauncherOverlayCallbacks { 71 72 public static final int ALPHA_INDEX_OVERLAY = 0; 73 74 public static final int ALPHA_INDEX_LOADER = 1; 75 private static final int ALPHA_CHANNEL_COUNT = 2; 76 77 public static final int ANIMATION_END_DISAPPEAR = 0; 78 public static final int ANIMATION_END_REMAIN_VISIBLE = 2; 79 80 private final boolean mIsRtl; 81 82 private DragController mDragController; 83 84 // Variables relating to animation of views after drop 85 private Animator mDropAnim = null; 86 87 private DragView mDropView = null; 88 89 private boolean mHoverPointClosesFolder = false; 90 91 private int mTopViewIndex; 92 private int mChildCountOnLastUpdate = -1; 93 94 // Related to adjacent page hints 95 private final ViewGroupFocusHelper mFocusIndicatorHelper; 96 private Scrim mWorkspaceDragScrim; 97 98 /** 99 * Used to create a new DragLayer from XML. 100 * 101 * @param context The application's context. 102 * @param attrs The attributes set containing the Workspace's customization values. 103 */ DragLayer(Context context, AttributeSet attrs)104 public DragLayer(Context context, AttributeSet attrs) { 105 super(context, attrs, ALPHA_CHANNEL_COUNT); 106 107 // Disable multitouch across the workspace/all apps/customize tray 108 setMotionEventSplittingEnabled(false); 109 setChildrenDrawingOrderEnabled(true); 110 111 mFocusIndicatorHelper = new ViewGroupFocusHelper(this); 112 mIsRtl = Utilities.isRtl(getResources()); 113 } 114 115 /** 116 * Set up the drag layer with the parameters. 117 */ setup(DragController dragController, Workspace<?> workspace)118 public void setup(DragController dragController, Workspace<?> workspace) { 119 mDragController = dragController; 120 recreateControllers(); 121 mWorkspaceDragScrim = new Scrim(this); 122 workspace.addOverlayCallback(this); 123 } 124 125 @Override recreateControllers()126 public void recreateControllers() { 127 mControllers = mContainer.createTouchControllers(); 128 } 129 getFocusIndicatorHelper()130 public ViewGroupFocusHelper getFocusIndicatorHelper() { 131 return mFocusIndicatorHelper; 132 } 133 134 @Override dispatchKeyEvent(KeyEvent event)135 public boolean dispatchKeyEvent(KeyEvent event) { 136 return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); 137 } 138 isEventOverAccessibleDropTargetBar(MotionEvent ev)139 private boolean isEventOverAccessibleDropTargetBar(MotionEvent ev) { 140 return isInAccessibleDrag() && isEventOverView(mContainer.getDropTargetBar(), ev); 141 } 142 143 @Override onInterceptHoverEvent(MotionEvent ev)144 public boolean onInterceptHoverEvent(MotionEvent ev) { 145 if (mContainer == null || mContainer.getWorkspace() == null) { 146 return false; 147 } 148 AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer); 149 if (!(topView instanceof Folder)) { 150 return false; 151 } else { 152 AccessibilityManager accessibilityManager = (AccessibilityManager) 153 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 154 if (accessibilityManager.isTouchExplorationEnabled()) { 155 Folder currentFolder = (Folder) topView; 156 final int action = ev.getAction(); 157 boolean isOverFolderOrSearchBar; 158 switch (action) { 159 case MotionEvent.ACTION_HOVER_ENTER: 160 isOverFolderOrSearchBar = isEventOverView(topView, ev) || 161 isEventOverAccessibleDropTargetBar(ev); 162 if (!isOverFolderOrSearchBar) { 163 sendTapOutsideFolderAccessibilityEvent( 164 currentFolder.getIsEditingName()); 165 mHoverPointClosesFolder = true; 166 return true; 167 } 168 mHoverPointClosesFolder = false; 169 break; 170 case MotionEvent.ACTION_HOVER_MOVE: 171 isOverFolderOrSearchBar = isEventOverView(topView, ev) || 172 isEventOverAccessibleDropTargetBar(ev); 173 if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) { 174 sendTapOutsideFolderAccessibilityEvent( 175 currentFolder.getIsEditingName()); 176 mHoverPointClosesFolder = true; 177 return true; 178 } else if (!isOverFolderOrSearchBar) { 179 return true; 180 } 181 mHoverPointClosesFolder = false; 182 } 183 } 184 } 185 return false; 186 } 187 sendTapOutsideFolderAccessibilityEvent(boolean isEditingName)188 private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) { 189 int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close; 190 sendCustomAccessibilityEvent( 191 this, AccessibilityEvent.TYPE_VIEW_FOCUSED, getContext().getString(stringId)); 192 } 193 194 @Override onHoverEvent(MotionEvent ev)195 public boolean onHoverEvent(MotionEvent ev) { 196 // If we've received this, we've already done the necessary handling 197 // in onInterceptHoverEvent. Return true to consume the event. 198 return false; 199 } 200 201 isInAccessibleDrag()202 private boolean isInAccessibleDrag() { 203 return mContainer.getAccessibilityDelegate().isInAccessibleDrag(); 204 } 205 206 @Override onRequestSendAccessibilityEvent(View child, AccessibilityEvent event)207 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 208 if (isInAccessibleDrag() && child instanceof DropTargetBar) { 209 return true; 210 } 211 return super.onRequestSendAccessibilityEvent(child, event); 212 } 213 214 @Override addChildrenForAccessibility(ArrayList<View> childrenForAccessibility)215 public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) { 216 View topView = AbstractFloatingView.getTopOpenViewWithType(mContainer, 217 AbstractFloatingView.TYPE_ACCESSIBLE); 218 if (topView != null) { 219 addAccessibleChildToList(topView, childrenForAccessibility); 220 if (isInAccessibleDrag()) { 221 addAccessibleChildToList(mContainer.getDropTargetBar(), childrenForAccessibility); 222 } 223 } else { 224 super.addChildrenForAccessibility(childrenForAccessibility); 225 } 226 } 227 228 @Override dispatchTouchEvent(MotionEvent ev)229 public boolean dispatchTouchEvent(MotionEvent ev) { 230 ev.offsetLocation(getTranslationX(), 0); 231 try { 232 return super.dispatchTouchEvent(ev); 233 } finally { 234 ev.offsetLocation(-getTranslationX(), 0); 235 } 236 } 237 animateViewIntoPosition(DragView dragView, final int[] pos, float alpha, float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable, int duration)238 public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha, 239 float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable, 240 int duration) { 241 animateViewIntoPosition(dragView, pos[0], pos[1], alpha, scaleX, scaleY, 242 onFinishRunnable, animationEndStyle, duration, null); 243 } 244 animateViewIntoPosition(DragView dragView, final View child, View anchorView)245 public void animateViewIntoPosition(DragView dragView, final View child, View anchorView) { 246 animateViewIntoPosition(dragView, child, -1, anchorView); 247 } 248 animateViewIntoPosition(DragView dragView, final View child, int duration, View anchorView)249 public void animateViewIntoPosition(DragView dragView, final View child, int duration, 250 View anchorView) { 251 252 ShortcutAndWidgetContainer childParent = (ShortcutAndWidgetContainer) child.getParent(); 253 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams(); 254 childParent.measureChild(child); 255 childParent.layoutChild(child); 256 257 float coord[] = new float[2]; 258 float childScale = child.getScaleX(); 259 260 coord[0] = lp.x + (child.getMeasuredWidth() * (1 - childScale) / 2); 261 coord[1] = lp.y + (child.getMeasuredHeight() * (1 - childScale) / 2); 262 TranslationProvider translationProvider = childParent.getTranslationProvider(); 263 if (translationProvider != null) { 264 coord[0] = coord[0] + translationProvider.getTranslationX(lp.getCellX()); 265 } 266 267 // Since the child hasn't necessarily been laid out, we force the lp to be updated with 268 // the correct coordinates (above) and use these to determine the final location 269 float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord); 270 271 // We need to account for the scale of the child itself, as the above only accounts for 272 // the scale in parents. 273 scale *= childScale; 274 int toX = Math.round(coord[0]); 275 int toY = Math.round(coord[1]); 276 277 float toScale = scale; 278 279 if (child instanceof DraggableView) { 280 // This code is fairly subtle. Please verify drag and drop is pixel-perfect in a number 281 // of scenarios before modifying (from all apps, from workspace, different grid-sizes, 282 // shortcuts from in and out of Launcher etc). 283 DraggableView d = (DraggableView) child; 284 Rect destRect = new Rect(); 285 d.getWorkspaceVisualDragBounds(destRect); 286 287 // In most cases this additional scale factor should be a no-op (1). It mainly accounts 288 // for alternate grids where the source and destination icon sizes are different 289 toScale *= ((1f * destRect.width()) 290 / (dragView.getMeasuredWidth() - dragView.getBlurSizeOutline())); 291 292 // This accounts for the offset of the DragView created by scaling it about its 293 // center as it animates into place. 294 float scaleShiftX = dragView.getMeasuredWidth() * (1 - toScale) / 2; 295 float scaleShiftY = dragView.getMeasuredHeight() * (1 - toScale) / 2; 296 297 toX += scale * destRect.left - toScale * dragView.getBlurSizeOutline() / 2 - scaleShiftX; 298 toY += scale * destRect.top - toScale * dragView.getBlurSizeOutline() / 2 - scaleShiftY; 299 } 300 301 child.setVisibility(INVISIBLE); 302 Runnable onCompleteRunnable = () -> child.setVisibility(VISIBLE); 303 animateViewIntoPosition(dragView, toX, toY, 1, toScale, toScale, 304 onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView); 305 } 306 307 /** 308 * This method animates a view at the end of a drag and drop animation. 309 */ animateViewIntoPosition(final DragView view, final int toX, final int toY, float finalAlpha, float finalScaleX, float finalScaleY, Runnable onCompleteRunnable, int animationEndStyle, int duration, View anchorView)310 public void animateViewIntoPosition(final DragView view, 311 final int toX, final int toY, float finalAlpha, 312 float finalScaleX, float finalScaleY, Runnable onCompleteRunnable, 313 int animationEndStyle, int duration, View anchorView) { 314 Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight()); 315 animateView(view, to, finalAlpha, finalScaleX, finalScaleY, duration, 316 null, onCompleteRunnable, animationEndStyle, anchorView); 317 } 318 319 /** 320 * This method animates a view at the end of a drag and drop animation. 321 * @param view The view to be animated. This view is drawn directly into DragLayer, and so 322 * doesn't need to be a child of DragLayer. 323 * @param to The final location of the view. Only the left and top parameters are used. This 324 * location doesn't account for scaling, and so should be centered about the desired 325 * final location (including scaling). 326 * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates. 327 * @param finalScaleX The final scale of the view. The view is scaled about its center. 328 * @param finalScaleY The final scale of the view. The view is scaled about its center. 329 * @param duration The duration of the animation. 330 * @param motionInterpolator The interpolator to use for the location of the view. 331 * @param onCompleteRunnable Optional runnable to run on animation completion. 332 * @param animationEndStyle Whether or not to fade out the view once the animation completes. 333 * {@link #ANIMATION_END_DISAPPEAR} or {@link #ANIMATION_END_REMAIN_VISIBLE}. 334 * @param anchorView If not null, this represents the view which the animated view stays 335 */ animateView(final DragView view, final Rect to, final float finalAlpha, final float finalScaleX, final float finalScaleY, int duration, final Interpolator motionInterpolator, final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView)336 public void animateView(final DragView view, final Rect to, 337 final float finalAlpha, final float finalScaleX, final float finalScaleY, int duration, 338 final Interpolator motionInterpolator, final Runnable onCompleteRunnable, 339 final int animationEndStyle, View anchorView) { 340 view.cancelAnimation(); 341 view.requestLayout(); 342 343 final int[] from = getViewLocationRelativeToSelf(view); 344 345 // Calculate the duration of the animation based on the object's distance 346 final float dist = (float) Math.hypot(to.left - from[0], to.top - from[1]); 347 final Resources res = getResources(); 348 final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist); 349 350 // If duration < 0, this is a cue to compute the duration based on the distance 351 if (duration < 0) { 352 duration = res.getInteger(R.integer.config_dropAnimMaxDuration); 353 if (dist < maxDist) { 354 duration *= DECELERATE_1_5.getInterpolation(dist / maxDist); 355 } 356 duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration)); 357 } 358 359 // Fall back to cubic ease out interpolator for the animation if none is specified 360 TimeInterpolator interpolator = 361 motionInterpolator == null ? DECELERATE_1_5 : motionInterpolator; 362 363 // Animate the view 364 PendingAnimation anim = new PendingAnimation(duration); 365 anim.add(ofFloat(view, View.SCALE_X, finalScaleX), interpolator, SpringProperty.DEFAULT); 366 anim.add(ofFloat(view, View.SCALE_Y, finalScaleY), interpolator, SpringProperty.DEFAULT); 367 anim.setViewAlpha(view, finalAlpha, interpolator); 368 anim.setFloat(view, VIEW_TRANSLATE_Y, to.top, interpolator); 369 370 ObjectAnimator xMotion = ofFloat(view, VIEW_TRANSLATE_X, to.left); 371 if (anchorView != null) { 372 final int startScroll = anchorView.getScrollX(); 373 TypeEvaluator<Float> evaluator = (f, s, e) -> mapRange(f, s, e) 374 + (anchorView.getScaleX() * (startScroll - anchorView.getScrollX())); 375 xMotion.setEvaluator(evaluator); 376 } 377 anim.add(xMotion, interpolator, SpringProperty.DEFAULT); 378 if (onCompleteRunnable != null) { 379 anim.addListener(forEndCallback(onCompleteRunnable)); 380 } 381 playDropAnimation(view, anim.buildAnim(), animationEndStyle); 382 } 383 384 /** 385 * Runs a previously constructed drop animation 386 */ playDropAnimation(final DragView view, Animator animator, int animationEndStyle)387 public void playDropAnimation(final DragView view, Animator animator, int animationEndStyle) { 388 // Clean up the previous animations 389 if (mDropAnim != null) mDropAnim.cancel(); 390 391 // Show the drop view if it was previously hidden 392 mDropView = view; 393 // Create and start the animation 394 mDropAnim = animator; 395 mDropAnim.addListener(forEndCallback(() -> mDropAnim = null)); 396 if (animationEndStyle == ANIMATION_END_DISAPPEAR) { 397 mDropAnim.addListener(forEndCallback(this::clearAnimatedView)); 398 } 399 mDropAnim.start(); 400 } 401 402 /** 403 * Remove the drop view and end the drag animation. 404 * 405 * @return {@link DragView} that is removed. 406 */ 407 @Nullable clearAnimatedView()408 public DragView clearAnimatedView() { 409 if (mDropAnim != null) { 410 mDropAnim.cancel(); 411 } 412 mDropAnim = null; 413 if (mDropView != null) { 414 mDragController.onDeferredEndDrag(mDropView); 415 } 416 DragView ret = mDropView; 417 mDropView = null; 418 invalidate(); 419 return ret; 420 } 421 getAnimatedView()422 public View getAnimatedView() { 423 return mDropView; 424 } 425 426 @Override onViewAdded(View child)427 public void onViewAdded(View child) { 428 super.onViewAdded(child); 429 updateChildIndices(); 430 mContainer.onDragLayerHierarchyChanged(); 431 } 432 433 @Override onViewRemoved(View child)434 public void onViewRemoved(View child) { 435 super.onViewRemoved(child); 436 updateChildIndices(); 437 mContainer.onDragLayerHierarchyChanged(); 438 } 439 440 @Override bringChildToFront(View child)441 public void bringChildToFront(View child) { 442 super.bringChildToFront(child); 443 updateChildIndices(); 444 } 445 updateChildIndices()446 private void updateChildIndices() { 447 mTopViewIndex = -1; 448 int childCount = getChildCount(); 449 for (int i = 0; i < childCount; i++) { 450 if (getChildAt(i) instanceof DragView) { 451 mTopViewIndex = i; 452 } 453 } 454 mChildCountOnLastUpdate = childCount; 455 } 456 457 @Override getChildDrawingOrder(int childCount, int i)458 protected int getChildDrawingOrder(int childCount, int i) { 459 if (mChildCountOnLastUpdate != childCount) { 460 // between platform versions 17 and 18, behavior for onChildViewRemoved / Added changed. 461 // Pre-18, the child was not added / removed by the time of those callbacks. We need to 462 // force update our representation of things here to avoid crashing on pre-18 devices 463 // in certain instances. 464 updateChildIndices(); 465 } 466 467 // i represents the current draw iteration 468 if (mTopViewIndex == -1) { 469 // in general we do nothing 470 return i; 471 } else if (i == childCount - 1) { 472 // if we have a top index, we return it when drawing last item (highest z-order) 473 return mTopViewIndex; 474 } else if (i < mTopViewIndex) { 475 return i; 476 } else { 477 // for indexes greater than the top index, we fetch one item above to shift for the 478 // displacement of the top index 479 return i + 1; 480 } 481 } 482 483 @Override dispatchDraw(Canvas canvas)484 protected void dispatchDraw(Canvas canvas) { 485 // Draw the background below children. 486 mWorkspaceDragScrim.draw(canvas); 487 mFocusIndicatorHelper.draw(canvas); 488 super.dispatchDraw(canvas); 489 } 490 getWorkspaceDragScrim()491 public Scrim getWorkspaceDragScrim() { 492 return mWorkspaceDragScrim; 493 } 494 495 @Override onOverlayScrollChanged(float progress)496 public void onOverlayScrollChanged(float progress) { 497 float alpha = 1 - Interpolators.DECELERATE_3.getInterpolation(progress); 498 float transX = getMeasuredWidth() * progress; 499 500 if (mIsRtl) { 501 transX = -transX; 502 } 503 setTranslationX(transX); 504 getAlphaProperty(ALPHA_INDEX_OVERLAY).setValue(alpha); 505 } 506 } 507