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
This commit is contained in:
parent
1654c9e1c0
commit
6a71a5bd77
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<TextView
|
||||
android:id="@+id/label"
|
||||
android:layout_height="@dimen/snackbar_content_height"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorPrimary"/>
|
||||
<TextView
|
||||
android:id="@+id/action"
|
||||
android:layout_height="@dimen/snackbar_content_height"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_weight="0"
|
||||
android:gravity="center"
|
||||
android:padding="8dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textAllCaps="true"/>
|
||||
</merge>
|
|
@ -225,4 +225,10 @@
|
|||
<!-- Overview -->
|
||||
<dimen name="options_menu_icon_size">24dp</dimen>
|
||||
<dimen name="options_menu_thumb_size">32dp</dimen>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<dimen name="snackbar_height">48dp</dimen>
|
||||
<dimen name="snackbar_content_height">32dp</dimen>
|
||||
<dimen name="snackbar_padding">8dp</dimen>
|
||||
<dimen name="snackbar_margin">16dp</dimen>
|
||||
</resources>
|
||||
|
|
|
@ -259,9 +259,12 @@
|
|||
<!-- Accessibility confirmation for item added to workspace. -->
|
||||
<string name="item_added_to_workspace">Item added to home screen</string>
|
||||
|
||||
<!-- Accessibility confirmation for item removed. -->
|
||||
<!-- Accessibility confirmation for item removed. [CHAR_LIMIT=50]-->
|
||||
<string name="item_removed">Item removed</string>
|
||||
|
||||
<!-- Action shown in snackbar to undo item removal. [CHAR_LIMIT=15] -->
|
||||
<string name="undo">Undo</string>
|
||||
|
||||
<!-- Accessibility action to move an item on the workspace. [CHAR_LIMIT=30] -->
|
||||
<string name="action_move">Move item</string>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
@ -96,11 +101,22 @@ public class DeleteDropTarget extends ButtonDropTarget {
|
|||
: ControlType.CANCEL_TARGET;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
@ -1807,6 +1790,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) {
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -181,7 +181,7 @@ public class LoaderResults {
|
|||
public void run() {
|
||||
Callbacks callbacks = mCallbacks.get();
|
||||
if (callbacks != null) {
|
||||
callbacks.finishBindingItems();
|
||||
callbacks.finishBindingItems(currentScreen);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue