| |
| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.launcher3.dragndrop; |
| |
| import static android.animation.ObjectAnimator.ofFloat; |
| |
| import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; |
| import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; |
| import static com.android.launcher3.Utilities.mapRange; |
| import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; |
| import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5; |
| import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent; |
| |
| import android.animation.Animator; |
| import android.animation.ObjectAnimator; |
| import android.animation.TimeInterpolator; |
| import android.animation.TypeEvaluator; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Rect; |
| import android.util.AttributeSet; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityManager; |
| import android.view.animation.Interpolator; |
| |
| import com.android.launcher3.AbstractFloatingView; |
| import com.android.launcher3.CellLayout; |
| import com.android.launcher3.DropTargetBar; |
| import com.android.launcher3.Launcher; |
| import com.android.launcher3.R; |
| import com.android.launcher3.ShortcutAndWidgetContainer; |
| import com.android.launcher3.Workspace; |
| import com.android.launcher3.anim.PendingAnimation; |
| import com.android.launcher3.anim.SpringProperty; |
| import com.android.launcher3.folder.Folder; |
| import com.android.launcher3.graphics.Scrim; |
| import com.android.launcher3.keyboard.ViewGroupFocusHelper; |
| import com.android.launcher3.util.TouchController; |
| import com.android.launcher3.views.BaseDragLayer; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * A ViewGroup that coordinates dragging across its descendants |
| */ |
| public class DragLayer extends BaseDragLayer<Launcher> { |
| |
| public static final int ALPHA_INDEX_OVERLAY = 0; |
| public static final int ALPHA_INDEX_LAUNCHER_LOAD = 1; |
| public static final int ALPHA_INDEX_TRANSITIONS = 2; |
| private static final int ALPHA_CHANNEL_COUNT = 3; |
| |
| public static final int ANIMATION_END_DISAPPEAR = 0; |
| public static final int ANIMATION_END_REMAIN_VISIBLE = 2; |
| |
| private DragController mDragController; |
| |
| // Variables relating to animation of views after drop |
| private Animator mDropAnim = null; |
| |
| private DragView mDropView = null; |
| |
| private boolean mHoverPointClosesFolder = false; |
| |
| private int mTopViewIndex; |
| private int mChildCountOnLastUpdate = -1; |
| |
| // Related to adjacent page hints |
| private final ViewGroupFocusHelper mFocusIndicatorHelper; |
| private Scrim mWorkspaceDragScrim; |
| |
| /** |
| * Used to create a new DragLayer from XML. |
| * |
| * @param context The application's context. |
| * @param attrs The attributes set containing the Workspace's customization values. |
| */ |
| public DragLayer(Context context, AttributeSet attrs) { |
| super(context, attrs, ALPHA_CHANNEL_COUNT); |
| |
| // Disable multitouch across the workspace/all apps/customize tray |
| setMotionEventSplittingEnabled(false); |
| setChildrenDrawingOrderEnabled(true); |
| |
| mFocusIndicatorHelper = new ViewGroupFocusHelper(this); |
| } |
| |
| public void setup(DragController dragController, Workspace workspace) { |
| mDragController = dragController; |
| recreateControllers(); |
| mWorkspaceDragScrim = new Scrim(this); |
| } |
| |
| @Override |
| public void recreateControllers() { |
| mControllers = mActivity.createTouchControllers(); |
| } |
| |
| public ViewGroupFocusHelper getFocusIndicatorHelper() { |
| return mFocusIndicatorHelper; |
| } |
| |
| @Override |
| public boolean dispatchKeyEvent(KeyEvent event) { |
| return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); |
| } |
| |
| private boolean isEventOverAccessibleDropTargetBar(MotionEvent ev) { |
| return isInAccessibleDrag() && isEventOverView(mActivity.getDropTargetBar(), ev); |
| } |
| |
| @Override |
| public boolean onInterceptHoverEvent(MotionEvent ev) { |
| if (mActivity == null || mActivity.getWorkspace() == null) { |
| return false; |
| } |
| AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); |
| if (!(topView instanceof Folder)) { |
| return false; |
| } else { |
| AccessibilityManager accessibilityManager = (AccessibilityManager) |
| getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); |
| if (accessibilityManager.isTouchExplorationEnabled()) { |
| Folder currentFolder = (Folder) topView; |
| final int action = ev.getAction(); |
| boolean isOverFolderOrSearchBar; |
| switch (action) { |
| case MotionEvent.ACTION_HOVER_ENTER: |
| isOverFolderOrSearchBar = isEventOverView(topView, ev) || |
| isEventOverAccessibleDropTargetBar(ev); |
| if (!isOverFolderOrSearchBar) { |
| sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); |
| mHoverPointClosesFolder = true; |
| return true; |
| } |
| mHoverPointClosesFolder = false; |
| break; |
| case MotionEvent.ACTION_HOVER_MOVE: |
| isOverFolderOrSearchBar = isEventOverView(topView, ev) || |
| isEventOverAccessibleDropTargetBar(ev); |
| if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) { |
| sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); |
| mHoverPointClosesFolder = true; |
| return true; |
| } else if (!isOverFolderOrSearchBar) { |
| return true; |
| } |
| mHoverPointClosesFolder = false; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) { |
| int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close; |
| sendCustomAccessibilityEvent( |
| this, AccessibilityEvent.TYPE_VIEW_FOCUSED, getContext().getString(stringId)); |
| } |
| |
| @Override |
| public boolean onHoverEvent(MotionEvent ev) { |
| // If we've received this, we've already done the necessary handling |
| // in onInterceptHoverEvent. Return true to consume the event. |
| return false; |
| } |
| |
| |
| private boolean isInAccessibleDrag() { |
| return mActivity.getAccessibilityDelegate().isInAccessibleDrag(); |
| } |
| |
| @Override |
| public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { |
| if (isInAccessibleDrag() && child instanceof DropTargetBar) { |
| return true; |
| } |
| return super.onRequestSendAccessibilityEvent(child, event); |
| } |
| |
| @Override |
| public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) { |
| View topView = AbstractFloatingView.getTopOpenViewWithType(mActivity, |
| AbstractFloatingView.TYPE_ACCESSIBLE); |
| if (topView != null) { |
| addAccessibleChildToList(topView, childrenForAccessibility); |
| if (isInAccessibleDrag()) { |
| addAccessibleChildToList(mActivity.getDropTargetBar(), childrenForAccessibility); |
| } |
| } else { |
| super.addChildrenForAccessibility(childrenForAccessibility); |
| } |
| } |
| |
| @Override |
| public boolean dispatchTouchEvent(MotionEvent ev) { |
| ev.offsetLocation(getTranslationX(), 0); |
| try { |
| return super.dispatchTouchEvent(ev); |
| } finally { |
| ev.offsetLocation(-getTranslationX(), 0); |
| } |
| } |
| |
| public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha, |
| float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable, |
| int duration) { |
| animateViewIntoPosition(dragView, pos[0], pos[1], alpha, scaleX, scaleY, |
| onFinishRunnable, animationEndStyle, duration, null); |
| } |
| |
| public void animateViewIntoPosition(DragView dragView, final View child, View anchorView) { |
| animateViewIntoPosition(dragView, child, -1, anchorView); |
| } |
| |
| public void animateViewIntoPosition(DragView dragView, final View child, int duration, |
| View anchorView) { |
| |
| ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent(); |
| CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); |
| parentChildren.measureChild(child); |
| parentChildren.layoutChild(child); |
| |
| float coord[] = new float[2]; |
| float childScale = child.getScaleX(); |
| |
| coord[0] = lp.x + (child.getMeasuredWidth() * (1 - childScale) / 2); |
| coord[1] = lp.y + (child.getMeasuredHeight() * (1 - childScale) / 2); |
| |
| // Since the child hasn't necessarily been laid out, we force the lp to be updated with |
| // the correct coordinates (above) and use these to determine the final location |
| float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord); |
| |
| // We need to account for the scale of the child itself, as the above only accounts for |
| // for the scale in parents. |
| scale *= childScale; |
| int toX = Math.round(coord[0]); |
| int toY = Math.round(coord[1]); |
| |
| float toScale = scale; |
| |
| if (child instanceof DraggableView) { |
| // This code is fairly subtle. Please verify drag and drop is pixel-perfect in a number |
| // of scenarios before modifying (from all apps, from workspace, different grid-sizes, |
| // shortcuts from in and out of Launcher etc). |
| DraggableView d = (DraggableView) child; |
| Rect destRect = new Rect(); |
| d.getWorkspaceVisualDragBounds(destRect); |
| |
| // In most cases this additional scale factor should be a no-op (1). It mainly accounts |
| // for alternate grids where the source and destination icon sizes are different |
| toScale *= ((1f * destRect.width()) |
| / (dragView.getMeasuredWidth() - dragView.getBlurSizeOutline())); |
| |
| // This accounts for the offset of the DragView created by scaling it about its |
| // center as it animates into place. |
| float scaleShiftX = dragView.getMeasuredWidth() * (1 - toScale) / 2; |
| float scaleShiftY = dragView.getMeasuredHeight() * (1 - toScale) / 2; |
| |
| toX += scale * destRect.left - toScale * dragView.getBlurSizeOutline() / 2 - scaleShiftX; |
| toY += scale * destRect.top - toScale * dragView.getBlurSizeOutline() / 2 - scaleShiftY; |
| } |
| |
| child.setVisibility(INVISIBLE); |
| Runnable onCompleteRunnable = () -> child.setVisibility(VISIBLE); |
| animateViewIntoPosition(dragView, toX, toY, 1, toScale, toScale, |
| onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView); |
| } |
| |
| /** |
| * This method animates a view at the end of a drag and drop animation. |
| */ |
| public void animateViewIntoPosition(final DragView view, |
| final int toX, final int toY, float finalAlpha, |
| float finalScaleX, float finalScaleY, Runnable onCompleteRunnable, |
| int animationEndStyle, int duration, View anchorView) { |
| Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight()); |
| animateView(view, to, finalAlpha, finalScaleX, finalScaleY, duration, |
| null, onCompleteRunnable, animationEndStyle, anchorView); |
| } |
| |
| /** |
| * This method animates a view at the end of a drag and drop animation. |
| * @param view The view to be animated. This view is drawn directly into DragLayer, and so |
| * doesn't need to be a child of DragLayer. |
| * @param to The final location of the view. Only the left and top parameters are used. This |
| * location doesn't account for scaling, and so should be centered about the desired |
| * final location (including scaling). |
| * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates. |
| * @param finalScaleX The final scale of the view. The view is scaled about its center. |
| * @param finalScaleY The final scale of the view. The view is scaled about its center. |
| * @param duration The duration of the animation. |
| * @param motionInterpolator The interpolator to use for the location of the view. |
| * @param onCompleteRunnable Optional runnable to run on animation completion. |
| * @param animationEndStyle Whether or not to fade out the view once the animation completes. |
| * {@link #ANIMATION_END_DISAPPEAR} or {@link #ANIMATION_END_REMAIN_VISIBLE}. |
| * @param anchorView If not null, this represents the view which the animated view stays |
| */ |
| public void 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) { |
| view.cancelAnimation(); |
| view.requestLayout(); |
| |
| final int[] from = getViewLocationRelativeToSelf(view); |
| |
| // Calculate the duration of the animation based on the object's distance |
| final float dist = (float) Math.hypot(to.left - from[0], to.top - from[1]); |
| final Resources res = getResources(); |
| final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist); |
| |
| // If duration < 0, this is a cue to compute the duration based on the distance |
| if (duration < 0) { |
| duration = res.getInteger(R.integer.config_dropAnimMaxDuration); |
| if (dist < maxDist) { |
| duration *= DEACCEL_1_5.getInterpolation(dist / maxDist); |
| } |
| duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration)); |
| } |
| |
| // Fall back to cubic ease out interpolator for the animation if none is specified |
| TimeInterpolator interpolator = |
| motionInterpolator == null ? DEACCEL_1_5 : motionInterpolator; |
| |
| // Animate the view |
| PendingAnimation anim = new PendingAnimation(duration); |
| anim.add(ofFloat(view, View.SCALE_X, finalScaleX), interpolator, SpringProperty.DEFAULT); |
| anim.add(ofFloat(view, View.SCALE_Y, finalScaleY), interpolator, SpringProperty.DEFAULT); |
| anim.setViewAlpha(view, finalAlpha, interpolator); |
| anim.setFloat(view, VIEW_TRANSLATE_Y, to.top, interpolator); |
| |
| ObjectAnimator xMotion = ofFloat(view, VIEW_TRANSLATE_X, to.left); |
| if (anchorView != null) { |
| final int startScroll = anchorView.getScrollX(); |
| TypeEvaluator<Float> evaluator = (f, s, e) -> mapRange(f, s, e) |
| + (anchorView.getScaleX() * (startScroll - anchorView.getScrollX())); |
| xMotion.setEvaluator(evaluator); |
| } |
| anim.add(xMotion, interpolator, SpringProperty.DEFAULT); |
| if (onCompleteRunnable != null) { |
| anim.addListener(forEndCallback(onCompleteRunnable)); |
| } |
| playDropAnimation(view, anim.buildAnim(), animationEndStyle); |
| } |
| |
| /** |
| * Runs a previously constructed drop animation |
| */ |
| public void playDropAnimation(final DragView view, Animator animator, int animationEndStyle) { |
| // Clean up the previous animations |
| if (mDropAnim != null) mDropAnim.cancel(); |
| |
| // Show the drop view if it was previously hidden |
| mDropView = view; |
| // Create and start the animation |
| mDropAnim = animator; |
| mDropAnim.addListener(forEndCallback(() -> mDropAnim = null)); |
| if (animationEndStyle == ANIMATION_END_DISAPPEAR) { |
| mDropAnim.addListener(forEndCallback(this::clearAnimatedView)); |
| } |
| mDropAnim.start(); |
| } |
| |
| public void clearAnimatedView() { |
| if (mDropAnim != null) { |
| mDropAnim.cancel(); |
| } |
| mDropAnim = null; |
| if (mDropView != null) { |
| mDragController.onDeferredEndDrag(mDropView); |
| } |
| mDropView = null; |
| invalidate(); |
| } |
| |
| public View getAnimatedView() { |
| return mDropView; |
| } |
| |
| @Override |
| public void onViewAdded(View child) { |
| super.onViewAdded(child); |
| updateChildIndices(); |
| mActivity.onDragLayerHierarchyChanged(); |
| } |
| |
| @Override |
| public void onViewRemoved(View child) { |
| super.onViewRemoved(child); |
| updateChildIndices(); |
| mActivity.onDragLayerHierarchyChanged(); |
| } |
| |
| @Override |
| public void bringChildToFront(View child) { |
| super.bringChildToFront(child); |
| updateChildIndices(); |
| } |
| |
| private void updateChildIndices() { |
| mTopViewIndex = -1; |
| int childCount = getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| if (getChildAt(i) instanceof DragView) { |
| mTopViewIndex = i; |
| } |
| } |
| mChildCountOnLastUpdate = childCount; |
| } |
| |
| @Override |
| protected int getChildDrawingOrder(int childCount, int i) { |
| if (mChildCountOnLastUpdate != childCount) { |
| // between platform versions 17 and 18, behavior for onChildViewRemoved / Added changed. |
| // Pre-18, the child was not added / removed by the time of those callbacks. We need to |
| // force update our representation of things here to avoid crashing on pre-18 devices |
| // in certain instances. |
| updateChildIndices(); |
| } |
| |
| // i represents the current draw iteration |
| if (mTopViewIndex == -1) { |
| // in general we do nothing |
| return i; |
| } else if (i == childCount - 1) { |
| // if we have a top index, we return it when drawing last item (highest z-order) |
| return mTopViewIndex; |
| } else if (i < mTopViewIndex) { |
| return i; |
| } else { |
| // for indexes greater than the top index, we fetch one item above to shift for the |
| // displacement of the top index |
| return i + 1; |
| } |
| } |
| |
| @Override |
| protected void dispatchDraw(Canvas canvas) { |
| // Draw the background below children. |
| mWorkspaceDragScrim.draw(canvas); |
| mFocusIndicatorHelper.draw(canvas); |
| super.dispatchDraw(canvas); |
| } |
| |
| public Scrim getWorkspaceDragScrim() { |
| return mWorkspaceDragScrim; |
| } |
| |
| /** |
| * Called when one handed mode state changed. |
| * @param activated true if one handed mode activated, false otherwise. |
| */ |
| public void onOneHandedModeStateChanged(boolean activated) { |
| for (TouchController controller : mControllers) { |
| controller.onOneHandedModeStateChanged(activated); |
| } |
| } |
| } |