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:
Tony Wickham 2018-08-21 11:40:23 -07:00
parent 1654c9e1c0
commit 6a71a5bd77
21 changed files with 344 additions and 53 deletions

41
res/layout/snackbar.xml Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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");
}

View File

@ -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));
}

View File

@ -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;

View File

@ -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);

View File

@ -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 {

View File

@ -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

View File

@ -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);
}
/**

View File

@ -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;

View File

@ -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.

View File

@ -181,7 +181,7 @@ public class LoaderResults {
public void run() {
Callbacks callbacks = mCallbacks.get();
if (callbacks != null) {
callbacks.finishBindingItems();
callbacks.finishBindingItems(currentScreen);
}
}
};

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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;
}
}