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