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