diff options
author | Tony Wickham <twickham@google.com> | 2018-08-21 11:40:23 -0700 |
---|---|---|
committer | Tony Wickham <twickham@google.com> | 2018-08-30 16:22:25 -0700 |
commit | 6a71a5bd7787a2dc4a585e9e6ea0a440d0458a54 (patch) | |
tree | b3bdc0c63c2540f57042c0d09d82786ec8d1869c /src | |
parent | 1654c9e1c00cb41de392880adf7c3a5a558270b3 (diff) | |
download | android_packages_apps_Trebuchet-6a71a5bd7787a2dc4a585e9e6ea0a440d0458a54.tar.gz android_packages_apps_Trebuchet-6a71a5bd7787a2dc4a585e9e6ea0a440d0458a54.tar.bz2 android_packages_apps_Trebuchet-6a71a5bd7787a2dc4a585e9e6ea0a440d0458a54.zip |
Add undo snackbar for deleting items
- Add methods to ModelWriter to prepareForUndoDelete, then
enqueueDeleteRunnable, followed by commitDelete or abortDelete.
- Add Snackbar floating view
- Show Undo snackbar when dropping or flinging to delete target; if the
undo action is clicked, we abort the delete, otherwise we commit it.
Bug: 24238108
Change-Id: I9997235e1f8525cbb8b1fa2338099609e7358426
Diffstat (limited to 'src')
18 files changed, 293 insertions, 52 deletions
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java index 1c07ea4a7..4fe60991c 100644 --- a/src/com/android/launcher3/AbstractFloatingView.java +++ b/src/com/android/launcher3/AbstractFloatingView.java @@ -19,7 +19,6 @@ package com.android.launcher3; import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED; import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; - import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled; import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent; @@ -54,6 +53,7 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch TYPE_WIDGETS_FULL_SHEET, TYPE_ON_BOARD_POPUP, TYPE_DISCOVERY_BOUNCE, + TYPE_SNACKBAR, TYPE_QUICKSTEP_PREVIEW, TYPE_TASK_MENU, @@ -68,23 +68,25 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch public static final int TYPE_WIDGETS_FULL_SHEET = 1 << 4; public static final int TYPE_ON_BOARD_POPUP = 1 << 5; public static final int TYPE_DISCOVERY_BOUNCE = 1 << 6; + public static final int TYPE_SNACKBAR = 1 << 7; // Popups related to quickstep UI - public static final int TYPE_QUICKSTEP_PREVIEW = 1 << 7; - public static final int TYPE_TASK_MENU = 1 << 8; - public static final int TYPE_OPTIONS_POPUP = 1 << 9; + public static final int TYPE_QUICKSTEP_PREVIEW = 1 << 8; + public static final int TYPE_TASK_MENU = 1 << 9; + public static final int TYPE_OPTIONS_POPUP = 1 << 10; public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET | TYPE_QUICKSTEP_PREVIEW | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU - | TYPE_OPTIONS_POPUP; + | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR; // Type of popups which should be kept open during launcher rebind public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET | TYPE_QUICKSTEP_PREVIEW | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE; // Usually we show the back button when a floating view is open. Instead, hide for these types. - public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE; + public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE + | TYPE_SNACKBAR; public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_QUICKSTEP_PREVIEW; diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java index 2ec7e012c..7b565273f 100644 --- a/src/com/android/launcher3/BaseActivity.java +++ b/src/com/android/launcher3/BaseActivity.java @@ -17,7 +17,6 @@ package com.android.launcher3; import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW; - import static java.lang.annotation.RetentionPolicy.SOURCE; import android.app.Activity; diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index ed8c42d07..dd63ebce0 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -264,6 +264,10 @@ public abstract class ButtonDropTarget extends TextView */ @Override public void onDrop(final DragObject d, final DragOptions options) { + if (options.isFlingToDelete) { + // FlingAnimation handles the animation and then calls completeDrop(). + return; + } final DragLayer dragLayer = mLauncher.getDragLayer(); final Rect from = new Rect(); dragLayer.getViewRectRelativeToSelf(d.dragView, from); diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index 64a58fb8c..c80f96bb8 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -23,10 +23,11 @@ import android.view.View; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.dragndrop.DragOptions; -import com.android.launcher3.folder.Folder; import com.android.launcher3.logging.LoggerUtils; +import com.android.launcher3.model.ModelWriter; import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType; import com.android.launcher3.userevent.nano.LauncherLogProto.Target; +import com.android.launcher3.views.Snackbar; public class DeleteDropTarget extends ButtonDropTarget { @@ -81,13 +82,17 @@ public class DeleteDropTarget extends ButtonDropTarget { */ private void setTextBasedOnDragSource(ItemInfo item) { if (!TextUtils.isEmpty(mText)) { - mText = getResources().getString(item.id != ItemInfo.NO_ID + mText = getResources().getString(canRemove(item) ? R.string.remove_drop_target_label : android.R.string.cancel); requestLayout(); } } + private boolean canRemove(ItemInfo item) { + return item.id != ItemInfo.NO_ID; + } + /** * Set mControlType depending on the drag item. */ @@ -97,10 +102,21 @@ public class DeleteDropTarget extends ButtonDropTarget { } @Override + public void onDrop(DragObject d, DragOptions options) { + if (canRemove(d.dragInfo)) { + mLauncher.getModelWriter().prepareToUndoDelete(); + } + super.onDrop(d, options); + } + + @Override public void completeDrop(DragObject d) { ItemInfo item = d.dragInfo; - if ((d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder)) { + if (canRemove(item)) { onAccessibilityDrop(null, item); + ModelWriter modelWriter = mLauncher.getModelWriter(); + Snackbar.show(mLauncher, R.string.item_removed, R.string.undo, + modelWriter::commitDelete, modelWriter::abortDelete); } } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index f0ddd53ed..55074f813 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -19,7 +19,7 @@ package com.android.launcher3; import static android.content.pm.ActivityInfo.CONFIG_LOCALE; import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; - +import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR; import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; @@ -48,7 +48,6 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.sqlite.SQLiteDatabase; import android.graphics.Point; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -1549,7 +1548,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, final LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) itemInfo; mWorkspace.removeWorkspaceItem(v); if (deleteFromDb) { - deleteWidgetInfo(widgetInfo); + getModelWriter().deleteWidgetInfo(widgetInfo, getAppWidgetHost()); } } else { return false; @@ -1557,23 +1556,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, return true; } - /** - * Deletes the widget info and the widget id. - */ - private void deleteWidgetInfo(final LauncherAppWidgetInfo widgetInfo) { - final LauncherAppWidgetHost appWidgetHost = getAppWidgetHost(); - if (appWidgetHost != null && !widgetInfo.isCustomWidget() && widgetInfo.isWidgetIdAllocated()) { - // Deleting an app widget ID is a void call but writes to disk before returning - // to the caller... - new AsyncTask<Void, Void, Void>() { - public Void doInBackground(Void ... args) { - appWidgetHost.deleteAppWidgetId(widgetInfo.appWidgetId); - return null; - } - }.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR); - } - getModelWriter().deleteItemFromDatabase(widgetInfo); - } + @Override public boolean dispatchKeyEvent(KeyEvent event) { @@ -1808,6 +1791,17 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, } @Override + public void preAddApps() { + // If there's an undo snackbar, force it to complete to ensure empty screens are removed + // before trying to add new items. + mModelWriter.commitDelete(); + AbstractFloatingView snackbar = AbstractFloatingView.getOpenView(this, TYPE_SNACKBAR); + if (snackbar != null) { + snackbar.post(() -> snackbar.close(true)); + } + } + + @Override public void bindAppsAdded(ArrayList<Long> newScreens, ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated) { // Add the new screens @@ -2040,7 +2034,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, // Verify that we own the widget if (appWidgetInfo == null) { FileLog.e(TAG, "Removing invalid widget: id=" + item.appWidgetId); - deleteWidgetInfo(item); + getModelWriter().deleteWidgetInfo(item, getAppWidgetHost()); return null; } @@ -2130,7 +2124,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, * * Implementation of the method from LauncherModel.Callbacks. */ - public void finishBindingItems() { + public void finishBindingItems(int currentScreen) { TraceHelper.beginSection("finishBindingItems"); mWorkspace.restoreInstanceStateForRemainingPages(); @@ -2145,6 +2139,8 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, InstallShortcutReceiver.disableAndFlushInstallQueue( InstallShortcutReceiver.FLAG_LOADER_RUNNING, this); + mWorkspace.setCurrentPage(currentScreen); + TraceHelper.endSection("finishBindingItems"); } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index fe9f901c4..7a90a55d4 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -144,9 +144,10 @@ public class LauncherModel extends BroadcastReceiver public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons); public void bindScreens(ArrayList<Long> orderedScreenIds); public void finishFirstPageBind(ViewOnDrawExecutor executor); - public void finishBindingItems(); + public void finishBindingItems(int currentScreen); public void bindAllApplications(ArrayList<AppInfo> apps); public void bindAppsAddedOrUpdated(ArrayList<AppInfo> apps); + public void preAddApps(); public void bindAppsAdded(ArrayList<Long> newScreens, ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated); @@ -196,6 +197,10 @@ public class LauncherModel extends BroadcastReceiver * Adds the provided items to the workspace. */ public void addAndBindAddedWorkspaceItems(List<Pair<ItemInfo, Object>> itemList) { + Callbacks callbacks = getCallback(); + if (callbacks != null) { + callbacks.preAddApps(); + } enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList)); } diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java index 76e85e255..6083415d5 100644 --- a/src/com/android/launcher3/SecondaryDropTarget.java +++ b/src/com/android/launcher3/SecondaryDropTarget.java @@ -2,7 +2,6 @@ package com.android.launcher3; import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID; import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE; - import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_MASK; import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_NO; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP; @@ -10,7 +9,6 @@ import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate. import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.UNINSTALL; import android.appwidget.AppWidgetHostView; -import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index d652fe0cd..353916fbb 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -819,7 +819,9 @@ public class Workspace extends PagedView<WorkspacePageIndicator> if (!removeScreens.isEmpty()) { // Update the model if we have changed any screens - LauncherModel.updateWorkspaceScreenOrder(mLauncher, mScreenOrder); + mLauncher.getModelWriter().enqueueDeleteRunnable( + () -> LauncherModel.updateWorkspaceScreenOrder(mLauncher, mScreenOrder)); + } if (pageShift >= 0) { @@ -2848,7 +2850,6 @@ public class Workspace extends PagedView<WorkspacePageIndicator> */ public void onDropCompleted(final View target, final DragObject d, final boolean success) { - if (success) { if (target != this && mDragInfo != null) { removeWorkspaceItem(mDragInfo.cell); diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java index 80077549a..03dc66ee2 100644 --- a/src/com/android/launcher3/dragndrop/DragController.java +++ b/src/com/android/launcher3/dragndrop/DragController.java @@ -396,7 +396,7 @@ public class DragController implements DragDriver.EventListener, TouchController @Override public void onDriverDragEnd(float x, float y) { DropTarget dropTarget; - Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject); + Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject, mOptions); if (flingAnimation != null) { dropTarget = mFlingToDeleteHelper.getDropTarget(); } else { diff --git a/src/com/android/launcher3/dragndrop/DragOptions.java b/src/com/android/launcher3/dragndrop/DragOptions.java index f108f8b53..2d19f3642 100644 --- a/src/com/android/launcher3/dragndrop/DragOptions.java +++ b/src/com/android/launcher3/dragndrop/DragOptions.java @@ -37,6 +37,8 @@ public class DragOptions { /** Scale of the icons over the workspace icon size. */ public float intrinsicIconScaleFactor = 1f; + public boolean isFlingToDelete; + /** * Specifies a condition that must be met before DragListener#onDragStart() is called. * By default, there is no condition and onDragStart() is called immediately following diff --git a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java index e79474483..589ad25e4 100644 --- a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java +++ b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java @@ -91,12 +91,13 @@ public class FlingToDeleteHelper { return mDropTarget; } - public Runnable getFlingAnimation(DropTarget.DragObject dragObject) { + public Runnable getFlingAnimation(DropTarget.DragObject dragObject, DragOptions options) { PointF vel = isFlingingToDelete(); - if (vel == null) { + options.isFlingToDelete = vel != null; + if (!options.isFlingToDelete) { return null; } - return new FlingAnimation(dragObject, vel, mDropTarget, mLauncher); + return new FlingAnimation(dragObject, vel, mDropTarget, mLauncher, options); } /** diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index a059627df..c4d105800 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -25,7 +25,6 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.annotation.SuppressLint; import android.content.Context; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.text.InputType; @@ -41,7 +40,6 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; -import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java index 6ef798d62..1277a2090 100644 --- a/src/com/android/launcher3/folder/FolderAnimationManager.java +++ b/src/com/android/launcher3/folder/FolderAnimationManager.java @@ -155,6 +155,7 @@ public class FolderAnimationManager { final int finalColor = Themes.getAttrColor(mContext, android.R.attr.colorPrimary); final int initialColor = ColorUtils.setAlphaComponent(finalColor, mPreviewBackground.getBackgroundAlpha()); + mFolderBackground.mutate(); mFolderBackground.setColor(mIsOpening ? initialColor : finalColor); // Set up the reveal animation that clips the Folder. diff --git a/src/com/android/launcher3/model/LoaderResults.java b/src/com/android/launcher3/model/LoaderResults.java index 0fd9b735e..57789363a 100644 --- a/src/com/android/launcher3/model/LoaderResults.java +++ b/src/com/android/launcher3/model/LoaderResults.java @@ -181,7 +181,7 @@ public class LoaderResults { public void run() { Callbacks callbacks = mCallbacks.get(); if (callbacks != null) { - callbacks.finishBindingItems(); + callbacks.finishBindingItems(currentScreen); } } }; diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java index eba751524..9f6e4f85d 100644 --- a/src/com/android/launcher3/model/ModelWriter.java +++ b/src/com/android/launcher3/model/ModelWriter.java @@ -28,6 +28,8 @@ import android.util.Log; import com.android.launcher3.FolderInfo; import com.android.launcher3.ItemInfo; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetHost; +import com.android.launcher3.LauncherAppWidgetInfo; import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherModel.Callbacks; import com.android.launcher3.LauncherProvider; @@ -35,13 +37,14 @@ import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.LauncherSettings.Settings; import com.android.launcher3.ShortcutInfo; -import com.android.launcher3.logging.FileLog; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.ContentWriter; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.LooperExecutor; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.concurrent.Executor; /** @@ -60,6 +63,10 @@ public class ModelWriter { private final boolean mHasVerticalHotseat; private final boolean mVerifyChanges; + // Keep track of delete operations that occur when an Undo option is present; we may not commit. + private final List<Runnable> mDeleteRunnables = new ArrayList<>(); + private boolean mPreparingToUndo; + public ModelWriter(Context context, LauncherModel model, BgDataModel dataModel, boolean hasVerticalHotseat, boolean verifyChanges) { mContext = context; @@ -152,7 +159,7 @@ public class ModelWriter { .put(Favorites.RANK, item.rank) .put(Favorites.SCREEN, item.screenId); - mWorkerExecutor.execute(new UpdateItemRunnable(item, writer)); + enqueueDeleteRunnable(new UpdateItemRunnable(item, writer)); } /** @@ -176,7 +183,7 @@ public class ModelWriter { contentValues.add(values); } - mWorkerExecutor.execute(new UpdateItemsRunnable(items, contentValues)); + enqueueDeleteRunnable(new UpdateItemsRunnable(items, contentValues)); } /** @@ -258,7 +265,7 @@ public class ModelWriter { public void deleteItemsFromDatabase(final Iterable<? extends ItemInfo> items) { ModelVerifier verifier = new ModelVerifier(); - mWorkerExecutor.execute(() -> { + enqueueDeleteRunnable(() -> { for (ItemInfo item : items) { final Uri uri = Favorites.getContentUri(item.id); mContext.getContentResolver().delete(uri, null, null); @@ -275,7 +282,7 @@ public class ModelWriter { public void deleteFolderAndContentsFromDatabase(final FolderInfo info) { ModelVerifier verifier = new ModelVerifier(); - mWorkerExecutor.execute(() -> { + enqueueDeleteRunnable(() -> { ContentResolver cr = mContext.getContentResolver(); cr.delete(LauncherSettings.Favorites.CONTENT_URI, LauncherSettings.Favorites.CONTAINER + "=" + info.id, null); @@ -288,6 +295,63 @@ public class ModelWriter { }); } + /** + * Deletes the widget info and the widget id. + */ + public void deleteWidgetInfo(final LauncherAppWidgetInfo info, LauncherAppWidgetHost host) { + if (host != null && !info.isCustomWidget() && info.isWidgetIdAllocated()) { + // Deleting an app widget ID is a void call but writes to disk before returning + // to the caller... + enqueueDeleteRunnable(() -> host.deleteAppWidgetId(info.appWidgetId)); + } + deleteItemFromDatabase(info); + } + + /** + * Delete operations tracked using {@link #enqueueDeleteRunnable} will only be called + * if {@link #commitDelete} is called. Note that one of {@link #commitDelete()} or + * {@link #abortDelete()} MUST be called after this method, or else all delete + * operations will remain uncommitted indefinitely. + */ + public void prepareToUndoDelete() { + if (!mPreparingToUndo) { + if (!mDeleteRunnables.isEmpty() && FeatureFlags.IS_DOGFOOD_BUILD) { + throw new IllegalStateException("There are still uncommitted delete operations!"); + } + mDeleteRunnables.clear(); + mPreparingToUndo = true; + } + } + + /** + * If {@link #prepareToUndoDelete} has been called, we store the Runnable to be run when + * {@link #commitDelete()} is called (or abandoned if {@link #abortDelete()} is called). + * Otherwise, we run the Runnable immediately. + */ + public void enqueueDeleteRunnable(Runnable r) { + if (mPreparingToUndo) { + mDeleteRunnables.add(r); + } else { + mWorkerExecutor.execute(r); + } + } + + public void commitDelete() { + mPreparingToUndo = false; + for (Runnable runnable : mDeleteRunnables) { + mWorkerExecutor.execute(runnable); + } + mDeleteRunnables.clear(); + } + + public void abortDelete() { + mPreparingToUndo = false; + mDeleteRunnables.clear(); + // We do a full reload here instead of just a rebind because Folders change their internal + // state when dragging an item out, which clobbers the rebind unless we load from the DB. + mModel.forceReload(); + } + private class UpdateItemRunnable extends UpdateItemBaseRunnable { private final ItemInfo mItem; private final ContentWriter mWriter; diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java index 6f012f69a..babbcdd16 100644 --- a/src/com/android/launcher3/touch/ItemLongClickListener.java +++ b/src/com/android/launcher3/touch/ItemLongClickListener.java @@ -17,7 +17,6 @@ package com.android.launcher3.touch; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; - import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; @@ -30,7 +29,6 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.DropTarget; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherState; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.folder.Folder; diff --git a/src/com/android/launcher3/util/FlingAnimation.java b/src/com/android/launcher3/util/FlingAnimation.java index fe0571b8a..9d0ad22e9 100644 --- a/src/com/android/launcher3/util/FlingAnimation.java +++ b/src/com/android/launcher3/util/FlingAnimation.java @@ -14,6 +14,7 @@ import com.android.launcher3.ButtonDropTarget; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.Launcher; import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.dragndrop.DragView; public class FlingAnimation implements AnimatorUpdateListener, Runnable { @@ -28,6 +29,7 @@ public class FlingAnimation implements AnimatorUpdateListener, Runnable { private final Launcher mLauncher; protected final DragObject mDragObject; + protected final DragOptions mDragOptions; protected final DragLayer mDragLayer; protected final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); protected final float mUX, mUY; @@ -39,13 +41,15 @@ public class FlingAnimation implements AnimatorUpdateListener, Runnable { protected float mAX, mAY; - public FlingAnimation(DragObject d, PointF vel, ButtonDropTarget dropTarget, Launcher launcher) { + public FlingAnimation(DragObject d, PointF vel, ButtonDropTarget dropTarget, Launcher launcher, + DragOptions options) { mDropTarget = dropTarget; mLauncher = launcher; mDragObject = d; mUX = vel.x / 1000; mUY = vel.y / 1000; mDragLayer = mLauncher.getDragLayer(); + mDragOptions = options; } @Override @@ -102,6 +106,7 @@ public class FlingAnimation implements AnimatorUpdateListener, Runnable { } }; + mDropTarget.onDrop(mDragObject, mDragOptions); mDragLayer.animateView(mDragObject.dragView, this, duration, tInterpolator, onAnimationEndRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null); } diff --git a/src/com/android/launcher3/views/Snackbar.java b/src/com/android/launcher3/views/Snackbar.java new file mode 100644 index 000000000..f515360e6 --- /dev/null +++ b/src/com/android/launcher3/views/Snackbar.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2018 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.views; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.widget.TextView; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.dragndrop.DragLayer; + +/** + * A toast-like UI at the bottom of the screen with a label, button action, and dismiss action. + */ +public class Snackbar extends AbstractFloatingView { + + private static final long SHOW_DURATION_MS = 180; + private static final long HIDE_DURATION_MS = 180; + private static final long TIMEOUT_DURATION_MS = 4000; + + private final Launcher mLauncher; + private Runnable mOnDismissed; + + public Snackbar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public Snackbar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLauncher = Launcher.getLauncher(context); + inflate(context, R.layout.snackbar, this); + } + + public static void show(Launcher launcher, int labelStringResId, int actionStringResId, + Runnable onDismissed, Runnable onActionClicked) { + closeOpenViews(launcher, true, TYPE_SNACKBAR); + Snackbar snackbar = new Snackbar(launcher, null); + // Set some properties here since inflated xml only contains the children. + snackbar.setOrientation(HORIZONTAL); + snackbar.setGravity(Gravity.CENTER_VERTICAL); + Resources res = launcher.getResources(); + snackbar.setElevation(res.getDimension(R.dimen.deep_shortcuts_elevation)); + int padding = res.getDimensionPixelSize(R.dimen.snackbar_padding); + snackbar.setPadding(padding, padding, padding, padding); + snackbar.setBackgroundResource(R.drawable.round_rect_primary); + + snackbar.mIsOpen = true; + DragLayer dragLayer = launcher.getDragLayer(); + dragLayer.addView(snackbar); + + DragLayer.LayoutParams params = (DragLayer.LayoutParams) snackbar.getLayoutParams(); + params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + params.height = res.getDimensionPixelSize(R.dimen.snackbar_height); + int margin = res.getDimensionPixelSize(R.dimen.snackbar_margin); + Rect insets = launcher.getDeviceProfile().getInsets(); + params.width = dragLayer.getWidth() - margin * 2 - insets.left - insets.right; + params.setMargins(0, margin + insets.top, 0, margin + insets.bottom); + + ((TextView) snackbar.findViewById(R.id.label)).setText(labelStringResId); + ((TextView) snackbar.findViewById(R.id.action)).setText(actionStringResId); + snackbar.findViewById(R.id.action).setOnClickListener(v -> { + if (onActionClicked != null) { + onActionClicked.run(); + } + snackbar.mOnDismissed = null; + snackbar.close(true); + }); + snackbar.mOnDismissed = onDismissed; + + snackbar.setAlpha(0); + snackbar.setScaleX(0.8f); + snackbar.setScaleY(0.8f); + snackbar.animate() + .alpha(1f) + .withLayer() + .scaleX(1) + .scaleY(1) + .setDuration(SHOW_DURATION_MS) + .setInterpolator(Interpolators.ACCEL_DEACCEL) + .start(); + snackbar.postDelayed(() -> snackbar.close(true), TIMEOUT_DURATION_MS); + } + + @Override + protected void handleClose(boolean animate) { + if (mIsOpen) { + if (animate) { + animate().alpha(0f) + .withLayer() + .setStartDelay(0) + .setDuration(HIDE_DURATION_MS) + .setInterpolator(Interpolators.ACCEL) + .withEndAction(this::onClosed) + .start(); + } else { + animate().cancel(); + onClosed(); + } + mIsOpen = false; + } + } + + private void onClosed() { + mLauncher.getDragLayer().removeView(this); + if (mOnDismissed != null) { + mOnDismissed.run(); + } + } + + @Override + public void logActionCommand(int command) { + // TODO + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_SNACKBAR) != 0; + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + DragLayer dl = mLauncher.getDragLayer(); + if (!dl.isEventOverView(this, ev)) { + close(true); + } + } + return false; + } +} |