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