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