From d139b0aa7d03f676dc7869dc5b39fd9f24ff0a1d Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Mon, 12 Sep 2016 14:36:30 -0700 Subject: Adding support for multiwindow drag and drop Change-Id: I95b46e3c3f1238307d3ef5a6c81a8e530ba0987a --- src/com/android/launcher3/ButtonDropTarget.java | 3 +- src/com/android/launcher3/ShortcutInfo.java | 2 +- .../dragndrop/AnotherWindowDragSource.java | 76 ++++++++++ .../launcher3/dragndrop/DragController.java | 14 +- .../android/launcher3/dragndrop/DragDriver.java | 165 +++++++++------------ src/com/android/launcher3/dragndrop/DragLayer.java | 61 +++++++- .../android/launcher3/dragndrop/DragOptions.java | 5 + .../dragndrop/ExternalDragPreviewProvider.java | 79 ++++++++++ .../com/android/launcher3/config/FeatureFlags.java | 2 +- 9 files changed, 301 insertions(+), 106 deletions(-) create mode 100644 src/com/android/launcher3/dragndrop/AnotherWindowDragSource.java create mode 100644 src/com/android/launcher3/dragndrop/ExternalDragPreviewProvider.java diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index 2dde7caa6..cf8abae2e 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -259,7 +259,8 @@ public abstract class ButtonDropTarget extends TextView } }; dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, - DRAG_VIEW_DROP_DURATION, new DecelerateInterpolator(2), + mLauncher.getDragController().isExternalDrag() ? 1 : DRAG_VIEW_DROP_DURATION, + new DecelerateInterpolator(2), new LinearInterpolator(), onAnimationEndRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null); } diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index 9d7be1693..fb9374314 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -158,7 +158,7 @@ public class ShortcutInfo extends ItemInfo { */ Intent promisedIntent; - ShortcutInfo() { + public ShortcutInfo() { itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; } diff --git a/src/com/android/launcher3/dragndrop/AnotherWindowDragSource.java b/src/com/android/launcher3/dragndrop/AnotherWindowDragSource.java new file mode 100644 index 000000000..156941a29 --- /dev/null +++ b/src/com/android/launcher3/dragndrop/AnotherWindowDragSource.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2016 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 android.content.Context; +import android.view.View; + +import com.android.launcher3.DragSource; +import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.userevent.nano.LauncherLogProto.Target; + +/** + * DragSource used when the drag started at another window. + */ +public class AnotherWindowDragSource implements DragSource { + + private final Context mContext; + + AnotherWindowDragSource(Context context) { + mContext = context; + } + + @Override + public boolean supportsFlingToDelete() { + return false; + } + + @Override + public boolean supportsAppInfoDropTarget() { + return false; + } + + @Override + public boolean supportsDeleteDropTarget() { + return false; + } + + @Override + public float getIntrinsicIconScaleFactor() { + return 1; + } + + @Override + public void onFlingToDeleteCompleted() { + } + + @Override + public void onDropCompleted(View target, DragObject d, + boolean isFlingToDelete, boolean success) { + if (!success) { + Launcher.getLauncher(mContext).exitSpringLoadedDragModeDelayed(false, 0, null); + } + + } + + @Override + public void fillInLaunchSourceData(View v, ItemInfo info, Target target, Target targetParent) { + // TODO: Probably log something + } +} diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java index 29e33e944..a93ee9019 100644 --- a/src/com/android/launcher3/dragndrop/DragController.java +++ b/src/com/android/launcher3/dragndrop/DragController.java @@ -25,7 +25,6 @@ import android.graphics.PointF; import android.graphics.Rect; import android.os.Handler; import android.os.IBinder; -import android.util.Log; import android.view.DragEvent; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; @@ -213,6 +212,12 @@ public class DragController implements DragDriver.EventListener, TouchController } mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0); + mOptions = options; + if (mOptions.systemDndStartPoint != null) { + mMotionDownX = mOptions.systemDndStartPoint.x; + mMotionDownY = mOptions.systemDndStartPoint.y; + } + final int registrationX = mMotionDownX - dragLayerX; final int registrationY = mMotionDownY - dragLayerY; @@ -221,7 +226,6 @@ public class DragController implements DragDriver.EventListener, TouchController mLastDropTarget = null; - mOptions = options; mDragObject = new DropTarget.DragObject(); final Resources res = mLauncher.getResources(); @@ -241,7 +245,7 @@ public class DragController implements DragDriver.EventListener, TouchController mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView); - mDragDriver = DragDriver.create(this, dragInfo, dragView); + mDragDriver = DragDriver.create(mLauncher, this, mDragObject, mOptions); } mDragObject.dragSource = source; @@ -293,6 +297,10 @@ public class DragController implements DragDriver.EventListener, TouchController return mDragDriver != null || (mOptions != null && mOptions.isAccessibleDrag); } + public boolean isExternalDrag() { + return (mOptions != null && mOptions.systemDndStartPoint != null); + } + /** * Stop dragging without dropping. */ diff --git a/src/com/android/launcher3/dragndrop/DragDriver.java b/src/com/android/launcher3/dragndrop/DragDriver.java index 2164708ba..4db8c07d5 100644 --- a/src/com/android/launcher3/dragndrop/DragDriver.java +++ b/src/com/android/launcher3/dragndrop/DragDriver.java @@ -17,18 +17,19 @@ package com.android.launcher3.dragndrop; import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Context; import android.content.Intent; -import android.graphics.Canvas; -import android.graphics.Point; import android.view.DragEvent; import android.view.MotionEvent; -import android.view.View; -import com.android.launcher3.AnotherWindowDropTarget; import com.android.launcher3.DropTarget; -import com.android.launcher3.ItemInfo; +import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.InstallShortcutReceiver; +import com.android.launcher3.ShortcutInfo; import com.android.launcher3.Utilities; -import com.android.launcher3.config.FeatureFlags; + +import java.util.ArrayList; /** * Base class for driving a drag/drop operation. @@ -50,7 +51,7 @@ public abstract class DragDriver { /** * Handles ending of the DragView animation. */ - public abstract void onDragViewAnimationEnd(); + public void onDragViewAnimationEnd() { } public boolean onTouchEvent(MotionEvent ev) { final int action = ev.getAction(); @@ -89,100 +90,46 @@ public abstract class DragDriver { return true; } - public static DragDriver create( - DragController dragController, ItemInfo dragInfo, DragView dragView) { - if (FeatureFlags.LAUNCHER3_USE_SYSTEM_DRAG_DRIVER && Utilities.isNycOrAbove()) { - return new SystemDragDriver(dragController, dragInfo.getIntent(), dragView); + public static DragDriver create(Context context, DragController dragController, + DragObject dragObject, DragOptions options) { + if (Utilities.isNycOrAbove() && options.systemDndStartPoint != null) { + return new SystemDragDriver(dragController, context, dragObject); } else { return new InternalDragDriver(dragController); } } - -}; +} /** * Class for driving a system (i.e. framework) drag/drop operation. */ class SystemDragDriver extends DragDriver { - /** Intent associated with the drag operation, or null is there no associated intent. */ - private final Intent mDragIntent; - private final DragView mDragView; - boolean mIsFrameworkDragActive = false; + private final DragObject mDragObject; + private final Context mContext; + boolean mReceivedDropEvent = false; float mLastX = 0; float mLastY = 0; - public SystemDragDriver(DragController dragController, Intent dragIntent, DragView dragView) { + public SystemDragDriver(DragController dragController, Context context, DragObject dragObject) { super(dragController); - mDragIntent = dragIntent; - mDragView = dragView; - } - - private static class ShadowBuilder extends View.DragShadowBuilder { - final DragView mDragView; - - public ShadowBuilder(DragView dragView) { - mDragView = dragView; - } - - @Override - public void onProvideShadowMetrics (Point size, Point touch) { - mDragView.provideDragShadowMetrics(size, touch); - } - - @Override - public void onDrawShadow(Canvas canvas) { - mDragView.drawDragShadow(canvas); - } - }; - - @Override - public void onDragViewAnimationEnd() { - // Clip data for the drag operation. If there is an intent, create an intent-based ClipData, - // which will be passed to a global DND. - // If there is no intent, craft a fake ClipData and start a local DND operation; this - // ClipData will be ignored. - final ClipData dragData = mDragIntent != null ? - ClipData.newIntent("", mDragIntent) : - ClipData.newPlainText("", ""); - - View.DragShadowBuilder shadowBuilder = new ShadowBuilder(mDragView); - // TODO: DND flags are in flux, once settled, use the appropriate constant. - final int flagGlobal = 1 << 0; - final int flagOpaque = 1 << 9; - final int flags = (mDragIntent != null ? flagGlobal : 0) | flagOpaque; - - mIsFrameworkDragActive = true; - - if (!mDragView.startDrag(dragData, shadowBuilder, null, flags)) { - mIsFrameworkDragActive = false; - mEventListener.onDriverDragCancel(); - return; - } - - // Starting from this point, the driver takes over showing the drag shadow, so hiding the - // drag view. - mDragView.setVisibility(View.INVISIBLE); + mDragObject = dragObject; + mContext = context; } @Override public boolean onTouchEvent(MotionEvent ev) { - return !mIsFrameworkDragActive && super.onTouchEvent(ev); + return false; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - return !mIsFrameworkDragActive && super.onInterceptTouchEvent(ev); + return false; } @Override public boolean onDragEvent (DragEvent event) { - if (!mIsFrameworkDragActive) { - // We are interested only in drag events started by this driver. - return false; - } - final int action = event.getAction(); switch (action) { @@ -192,8 +139,6 @@ class SystemDragDriver extends DragDriver { return true; case DragEvent.ACTION_DRAG_ENTERED: - mLastX = event.getX(); - mLastY = event.getY(); return true; case DragEvent.ACTION_DRAG_LOCATION: @@ -205,35 +150,66 @@ class SystemDragDriver extends DragDriver { case DragEvent.ACTION_DROP: mLastX = event.getX(); mLastY = event.getY(); - mReceivedDropEvent = true; - return true; + mReceivedDropEvent = + updateInfoFromClipData(event.getClipData(), event.getClipDescription()); + return mReceivedDropEvent; case DragEvent.ACTION_DRAG_EXITED: - mLastX = event.getX(); - mLastY = event.getY(); mEventListener.onDriverDragExitWindow(); return true; case DragEvent.ACTION_DRAG_ENDED: - final boolean dragAccepted = event.getResult(); - final boolean acceptedByAnotherWindow = dragAccepted && !mReceivedDropEvent; - - // When the system drag ends, its drag shadow disappears. Resume showing the drag - // view for the possible final animation. - mDragView.setVisibility(View.VISIBLE); - - final DropTarget dropTargetOverride = acceptedByAnotherWindow ? - new AnotherWindowDropTarget(mDragView.getContext()) : null; - - mEventListener.onDriverDragEnd(mLastX, mLastY, dropTargetOverride); - mIsFrameworkDragActive = false; + if (mReceivedDropEvent) { + mEventListener.onDriverDragEnd(mLastX, mLastY, null); + } else { + mEventListener.onDriverDragCancel(); + } return true; default: return false; } } -}; + + private boolean updateInfoFromClipData(ClipData data, ClipDescription desc) { + if (data == null) { + return false; + } + ArrayList intents = new ArrayList<>(); + int itemCount = data.getItemCount(); + for (int i = 0; i < itemCount; i++) { + Intent intent = data.getItemAt(i).getIntent(); + if (intent == null) { + continue; + } + + // Give preference to shortcut intents. + if (!Intent.ACTION_CREATE_SHORTCUT.equals(intent.getAction())) { + intents.add(intent); + continue; + } + ShortcutInfo info = InstallShortcutReceiver.fromShortcutIntent(mContext, intent); + if (info != null) { + mDragObject.dragInfo = info; + return true; + } + return true; + } + + // Try creating shortcuts just using the intent and label + Intent fullIntent = new Intent().putExtra(Intent.EXTRA_SHORTCUT_NAME, desc.getLabel()); + for (Intent intent : intents) { + fullIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent); + ShortcutInfo info = InstallShortcutReceiver.fromShortcutIntent(mContext, fullIntent); + if (info != null) { + mDragObject.dragInfo = info; + return true; + } + } + + return false; + } +} /** * Class for driving an internal (i.e. not using framework) drag/drop operation. @@ -243,9 +219,6 @@ class InternalDragDriver extends DragDriver { super(dragController); } - @Override - public void onDragViewAnimationEnd() {} - @Override public boolean onDragEvent (DragEvent event) { return false; } }; diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java index 58638288e..016347b17 100644 --- a/src/com/android/launcher3/dragndrop/DragLayer.java +++ b/src/com/android/launcher3/dragndrop/DragLayer.java @@ -21,14 +21,22 @@ import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; +import android.content.ClipDescription; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; +import android.os.Build; import android.util.AttributeSet; +import android.util.Log; import android.view.DragEvent; import android.view.KeyEvent; import android.view.MotionEvent; @@ -45,12 +53,14 @@ import com.android.launcher3.AppWidgetResizeFrame; import com.android.launcher3.CellLayout; import com.android.launcher3.DropTargetBar; import com.android.launcher3.InsettableFrameLayout; +import com.android.launcher3.InstallShortcutReceiver; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppWidgetHostView; import com.android.launcher3.PinchToOverviewListener; import com.android.launcher3.R; import com.android.launcher3.ShortcutAndWidgetContainer; +import com.android.launcher3.ShortcutInfo; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.allapps.AllAppsTransitionController; @@ -62,6 +72,7 @@ import com.android.launcher3.shortcuts.DeepShortcutsContainer; import com.android.launcher3.util.Thunk; import com.android.launcher3.util.TouchController; +import java.net.URISyntaxException; import java.util.ArrayList; /** @@ -431,8 +442,46 @@ public class DragLayer extends InsettableFrameLayout { return false; } + @TargetApi(Build.VERSION_CODES.N) + private void handleSystemDragStart(DragEvent event) { + if (!FeatureFlags.LAUNCHER3_USE_SYSTEM_DRAG_DRIVER || !Utilities.isNycOrAbove()) { + return; + } + if (mLauncher.isWorkspaceLocked()) { + return; + } + + ClipDescription description = event.getClipDescription(); + if (!description.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) { + return; + } + ShortcutInfo info = new ShortcutInfo(); + // Set a dummy intent until we get the final value + info.intent = new Intent(); + + // Since we are not going through the workspace for starting the drag, set drag related + // information on the workspace before starting the drag. + ExternalDragPreviewProvider previewProvider = + new ExternalDragPreviewProvider(mLauncher, info); + mLauncher.getWorkspace().prepareDragWithProvider(previewProvider); + + DragOptions options = new DragOptions(); + options.systemDndStartPoint = new Point((int) event.getX(), (int) event.getY()); + + int halfPadding = previewProvider.previewPadding / 2; + mDragController.startDrag( + Bitmap.createBitmap(1, 1, Config.ARGB_8888), + 0, 0, + new AnotherWindowDragSource(mLauncher), info, + new Point(- halfPadding, halfPadding), + previewProvider.getPreviewBounds(), 1f, options); + } + @Override public boolean onDragEvent (DragEvent event) { + if (event.getAction() == DragEvent.ACTION_DRAG_STARTED) { + handleSystemDragStart(event); + } return mDragController.onDragEvent(event); } @@ -758,11 +807,15 @@ public class DragLayer extends InsettableFrameLayout { // 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 *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist); + if (mDragController != null && mDragController.isExternalDrag()) { + duration = 1; + } else { + duration = res.getInteger(R.integer.config_dropAnimMaxDuration); + if (dist < maxDist) { + duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist); + } + duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration)); } - duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration)); } // Fall back to cubic ease out interpolator for the animation if none is specified diff --git a/src/com/android/launcher3/dragndrop/DragOptions.java b/src/com/android/launcher3/dragndrop/DragOptions.java index a7f28726a..3d52a48c6 100644 --- a/src/com/android/launcher3/dragndrop/DragOptions.java +++ b/src/com/android/launcher3/dragndrop/DragOptions.java @@ -16,6 +16,8 @@ package com.android.launcher3.dragndrop; +import android.graphics.Point; + /** * Set of options to control the drag and drop behavior. */ @@ -23,4 +25,7 @@ public class DragOptions { /** Whether or not an accessible drag operation is in progress. */ public boolean isAccessibleDrag = false; + + /** Specifies the start location for the system DnD, null when using internal DnD */ + public Point systemDndStartPoint = null; } diff --git a/src/com/android/launcher3/dragndrop/ExternalDragPreviewProvider.java b/src/com/android/launcher3/dragndrop/ExternalDragPreviewProvider.java new file mode 100644 index 000000000..6b14be714 --- /dev/null +++ b/src/com/android/launcher3/dragndrop/ExternalDragPreviewProvider.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2016 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 android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.HolographicOutlineHelper; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.graphics.DragPreviewProvider; + +/** + * Extension of {@link DragPreviewProvider} which provides a dummy outline when drag starts from + * a different window. + * It just draws an empty circle to a placeholder outline. + */ +public class ExternalDragPreviewProvider extends DragPreviewProvider { + + private final Launcher mLauncher; + private final ItemInfo mAddInfo; + + private final int[] mOutlineSize; + + public ExternalDragPreviewProvider(Launcher launcher, ItemInfo addInfo) { + super(null); + mLauncher = launcher; + mAddInfo = addInfo; + + mOutlineSize = mLauncher.getWorkspace().estimateItemSize(mAddInfo, false); + } + + public Rect getPreviewBounds() { + Rect rect = new Rect(); + DeviceProfile dp = mLauncher.getDeviceProfile(); + rect.left = DRAG_BITMAP_PADDING / 2; + rect.top = (mOutlineSize[1] - dp.cellHeightPx) / 2; + rect.right = rect.left + dp.iconSizePx; + rect.bottom = rect.top + dp.iconSizePx; + return rect; + } + + @Override + public Bitmap createDragOutline(Canvas canvas) { + final Bitmap b = Bitmap.createBitmap(mOutlineSize[0], mOutlineSize[1], Bitmap.Config.ALPHA_8); + canvas.setBitmap(b); + + Paint paint = new Paint(); + paint.setColor(Color.WHITE); + paint.setStyle(Paint.Style.FILL); + + // Use 0.9f times the radius for the actual circle to account for icon normalization. + float radius = getPreviewBounds().width() * 0.5f; + canvas.drawCircle(DRAG_BITMAP_PADDING / 2 + radius, + DRAG_BITMAP_PADDING / 2 + radius, radius * 0.9f, paint); + + HolographicOutlineHelper.obtain(mLauncher).applyExpensiveOutlineWithBlur(b, canvas); + canvas.setBitmap(null); + return b; + } +} diff --git a/src_config/com/android/launcher3/config/FeatureFlags.java b/src_config/com/android/launcher3/config/FeatureFlags.java index 69d8f0f5f..81fa3370f 100644 --- a/src_config/com/android/launcher3/config/FeatureFlags.java +++ b/src_config/com/android/launcher3/config/FeatureFlags.java @@ -27,7 +27,7 @@ public final class FeatureFlags { // As opposed to the new spring-loaded workspace. public static boolean LAUNCHER3_LEGACY_WORKSPACE_DND = false; public static boolean LAUNCHER3_LEGACY_FOLDER_ICON = false; - public static boolean LAUNCHER3_USE_SYSTEM_DRAG_DRIVER = false; + public static boolean LAUNCHER3_USE_SYSTEM_DRAG_DRIVER = true; public static boolean LAUNCHER3_DISABLE_PINCH_TO_OVERVIEW = false; public static boolean LAUNCHER3_ALL_APPS_PULL_UP = true; -- cgit v1.2.3