Avoid flicker to drop a widget that needs a config activity.
When dropping a widget that requires an config activity, drop a
PendingAppWidgetHostView to workspace
Adb command to try the feature:
adb shell device_config put launcher com.android.launcher3.enable_add_app_widget_via_config_activity_v2 true
Fix: 284236964
Test: manual
Flag: aconfig launcher.enable_add_app_widget_via_config_activity_v2 DISABLED
Change-Id: Ifd0be5c607a388cf8a8f6d77b46c03112e3e599f
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 496cb4e..a74cffb 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -27,6 +27,7 @@
import static com.android.launcher3.AbstractFloatingView.TYPE_ICON_SURFACE;
import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
+import static com.android.launcher3.Flags.enableAddAppWidgetViaConfigActivityV2;
import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY;
import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WIDGET_TRANSITION;
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
@@ -116,6 +117,8 @@
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.RectF;
@@ -829,7 +832,7 @@
announceForAccessibility(R.string.item_added_to_workspace);
break;
case REQUEST_CREATE_APPWIDGET:
- completeAddAppWidget(appWidgetId, info, null, null);
+ completeAddAppWidget(appWidgetId, info, null, null, false, null);
break;
case REQUEST_RECONFIGURE_APPWIDGET:
getStatsLogManager().logger().withItemInfo(info).log(LAUNCHER_WIDGET_RECONFIGURED);
@@ -1016,11 +1019,18 @@
AppWidgetHostView boundWidget = null;
if (resultCode == RESULT_OK) {
animationType = Workspace.COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION;
- final AppWidgetHostView layout = mAppWidgetHolder.createView(appWidgetId,
- requestArgs.getWidgetHandler().getProviderInfo(this));
+
+ // Now that we are exiting the config activity with RESULT_OK.
+ // If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled, we can retrieve the
+ // PendingAppWidgetHostView from LauncherWidgetHolder (it was added to
+ // LauncherWidgetHolder when starting the config activity).
+ final AppWidgetHostView layout = enableAddAppWidgetViaConfigActivityV2()
+ ? getWorkspace().getWidgetForAppWidgetId(appWidgetId)
+ : mAppWidgetHolder.createView(appWidgetId,
+ requestArgs.getWidgetHandler().getProviderInfo(this));
boundWidget = layout;
onCompleteRunnable = () -> {
- completeAddAppWidget(appWidgetId, requestArgs, layout, null);
+ completeAddAppWidget(appWidgetId, requestArgs, layout, null, false, null);
if (!isInState(EDIT_MODE)) {
mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
}
@@ -1450,14 +1460,15 @@
*/
@Thunk
void completeAddAppWidget(int appWidgetId, ItemInfo itemInfo,
- AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) {
+ @Nullable AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo,
+ boolean showPendingWidget, @Nullable Bitmap widgetPreviewBitmap) {
if (appWidgetInfo == null) {
appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(appWidgetId,
itemInfo.getTargetComponent());
}
- if (hostView == null) {
+ if (hostView == null && !showPendingWidget) {
// Perform actual inflation because we're live
hostView = mAppWidgetHolder.createView(appWidgetId, appWidgetInfo);
}
@@ -1471,39 +1482,71 @@
launcherInfo.minSpanX = itemInfo.minSpanX;
launcherInfo.minSpanY = itemInfo.minSpanY;
launcherInfo.user = appWidgetInfo.getProfile();
+ CellPos presenterPos = getCellPosMapper().mapModelToPresenter(itemInfo);
+ if (showPendingWidget) {
+ launcherInfo.restoreStatus = LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
+ PendingAppWidgetHostView pendingAppWidgetHostView =
+ new PendingAppWidgetHostView(this, launcherInfo, appWidgetInfo);
+ pendingAppWidgetHostView.setPreviewBitmap(widgetPreviewBitmap);
+ hostView = pendingAppWidgetHostView;
+ } else if (hostView instanceof PendingAppWidgetHostView) {
+ ((PendingAppWidgetHostView) hostView).setPreviewBitmap(null);
+ // User has selected a widget config and exited the config activity, we can trigger
+ // re-inflation of PendingAppWidgetHostView to replace it with
+ // LauncherAppWidgetHostView in workspace.
+ completeRestoreAppWidget(appWidgetId, LauncherAppWidgetInfo.RESTORE_COMPLETED);
+
+ // Show resize frame on the newly inflated LauncherAppWidgetHostView.
+ LauncherAppWidgetHostView reInflatedHostView =
+ getWorkspace().getWidgetForAppWidgetId(appWidgetId);
+ showWidgetResizeFrame(
+ reInflatedHostView,
+ (LauncherAppWidgetInfo) reInflatedHostView.getTag(),
+ presenterPos);
+ return;
+ }
if (itemInfo instanceof PendingAddWidgetInfo) {
launcherInfo.sourceContainer = ((PendingAddWidgetInfo) itemInfo).sourceContainer;
} else if (itemInfo instanceof PendingRequestArgs) {
launcherInfo.sourceContainer =
((PendingRequestArgs) itemInfo).getWidgetSourceContainer();
}
- CellPos presenterPos = getCellPosMapper().mapModelToPresenter(itemInfo);
getModelWriter().addItemToDatabase(launcherInfo,
itemInfo.container, presenterPos.screenId, presenterPos.cellX, presenterPos.cellY);
hostView.setVisibility(View.VISIBLE);
mItemInflater.prepareAppWidget(hostView, launcherInfo);
- mWorkspace.addInScreen(hostView, launcherInfo);
+ if (!enableAddAppWidgetViaConfigActivityV2() || hostView.getParent() == null) {
+ mWorkspace.addInScreen(hostView, launcherInfo);
+ }
announceForAccessibility(R.string.item_added_to_workspace);
// Show the widget resize frame.
if (hostView instanceof LauncherAppWidgetHostView) {
final LauncherAppWidgetHostView launcherHostView = (LauncherAppWidgetHostView) hostView;
- CellLayout cellLayout = getCellLayout(launcherInfo.container, presenterPos.screenId);
- if (mStateManager.getState() == NORMAL) {
- AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
- } else {
- mStateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- if ((mPrevLauncherState == SPRING_LOADED || mPrevLauncherState == EDIT_MODE)
- && finalState == NORMAL) {
- AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
- mStateManager.removeStateListener(this);
- }
+ showWidgetResizeFrame(launcherHostView, launcherInfo, presenterPos);
+ }
+ }
+
+ /** Show widget resize frame. */
+ private void showWidgetResizeFrame(
+ LauncherAppWidgetHostView launcherHostView,
+ LauncherAppWidgetInfo launcherInfo,
+ CellPos presenterPos) {
+ CellLayout cellLayout = getCellLayout(launcherInfo.container, presenterPos.screenId);
+ if (mStateManager.getState() == NORMAL) {
+ AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
+ } else {
+ mStateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ if ((mPrevLauncherState == SPRING_LOADED || mPrevLauncherState == EDIT_MODE)
+ && finalState == NORMAL) {
+ AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
+ mStateManager.removeStateListener(this);
}
- });
- }
+ }
+ });
}
}
@@ -1760,19 +1803,39 @@
addAppWidgetImpl(appWidgetId, info, boundWidget, addFlowHandler, 0);
}
+ /**
+ * If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled, we always add widget
+ * host view to workspace, otherwise we only add widget to host view if config activity is
+ * not started.
+ */
void addAppWidgetImpl(int appWidgetId, ItemInfo info,
AppWidgetHostView boundWidget, WidgetAddFlowHandler addFlowHandler, int delay) {
- if (!addFlowHandler.startConfigActivity(this, appWidgetId, info,
- REQUEST_CREATE_APPWIDGET)) {
- // If the configuration flow was not started, add the widget
+ final boolean isActivityStarted = addFlowHandler.startConfigActivity(
+ this, appWidgetId, info, REQUEST_CREATE_APPWIDGET);
- // Exit spring loaded mode if necessary after adding the widget
- Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null
- : () -> mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
- completeAddAppWidget(appWidgetId, info, boundWidget,
- addFlowHandler.getProviderInfo(this));
- mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete);
+ if (!enableAddAppWidgetViaConfigActivityV2() && isActivityStarted) {
+ return;
}
+
+ // If FLAG_ENABLE_ADD_APP_WIDGET_VIA_CONFIG_ACTIVITY_V2 is enabled and config activity is
+ // started, we should remove the dropped AppWidgetHostView from drag layer and extract the
+ // Bitmap that shows the preview. Then pass the Bitmap to completeAddAppWidget() to create
+ // a PendingWidgetHostView.
+ Bitmap widgetPreviewBitmap = null;
+ if (isActivityStarted) {
+ DragView dropView = getDragLayer().clearAnimatedView();
+ if (dropView != null && dropView.containsAppWidgetHostView()) {
+ widgetPreviewBitmap = getBitmapFromView(dropView.getContentView());
+ }
+ }
+
+ // Exit spring loaded mode if necessary after adding the widget
+ Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null
+ : () -> mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
+ completeAddAppWidget(appWidgetId, info, boundWidget,
+ addFlowHandler.getProviderInfo(this), addFlowHandler.needsConfigure(),
+ widgetPreviewBitmap);
+ mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete);
}
public void addPendingItem(PendingAddItemInfo info, int container, int screenId,
@@ -2373,6 +2436,18 @@
return null;
}
+ /** Convert a {@link View} to {@link Bitmap}. */
+ private static Bitmap getBitmapFromView(@Nullable View view) {
+ if (view == null) {
+ return null;
+ }
+ Bitmap returnedBitmap =
+ Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(returnedBitmap);
+ view.draw(canvas);
+ return returnedBitmap;
+ }
+
/**
* Returns the first view matching the operator in the given ViewGroups, or null if none.
* Forward iteration matters.
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index ac0d7ce..b4745c9 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -2988,7 +2988,7 @@
}
public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, final DragView dragView,
- final Runnable onCompleteRunnable, int animationType, final View finalView,
+ final Runnable onCompleteRunnable, int animationType, @Nullable final View finalView,
boolean external) {
int[] finalPos = new int[2];
float scaleXY[] = new float[2];
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index f18f900..db693f0 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -42,6 +42,8 @@
import android.view.accessibility.AccessibilityManager;
import android.view.animation.Interpolator;
+import androidx.annotation.Nullable;
+
import com.android.app.animation.Interpolators;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DropTargetBar;
@@ -388,7 +390,13 @@
mDropAnim.start();
}
- public void clearAnimatedView() {
+ /**
+ * Remove the drop view and end the drag animation.
+ *
+ * @return {@link DragView} that is removed.
+ */
+ @Nullable
+ public DragView clearAnimatedView() {
if (mDropAnim != null) {
mDropAnim.cancel();
}
@@ -396,8 +404,10 @@
if (mDropView != null) {
mDragController.onDeferredEndDrag(mDropView);
}
+ DragView ret = mDropView;
mDropView = null;
invalidate();
+ return ret;
}
public View getAnimatedView() {
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 1f07352..bcee442 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -30,6 +30,7 @@
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.TargetApi;
+import android.appwidget.AppWidgetHostView;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -563,6 +564,11 @@
return mContentViewParent;
}
+ /** Return true if {@link mContent} is a {@link AppWidgetHostView}. */
+ public boolean containsAppWidgetHostView() {
+ return mContent instanceof AppWidgetHostView;
+ }
+
private static class SpringFloatValue {
private static final FloatPropertyCompat<SpringFloatValue> VALUE =
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 25979c2..adf85c7 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -16,13 +16,20 @@
package com.android.launcher3.widget;
+import static android.graphics.Paint.ANTI_ALIAS_FLAG;
+import static android.graphics.Paint.DITHER_FLAG;
+import static android.graphics.Paint.FILTER_BITMAP_FLAG;
+
import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
+import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
@@ -85,8 +92,12 @@
private boolean mDrawableSizeChanged;
private final TextPaint mPaint;
+
+ private final Paint mPreviewPaint;
private Layout mSetupTextLayout;
+ @Nullable private Bitmap mPreviewBitmap;
+
public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
@Nullable LauncherAppWidgetProviderInfo appWidget) {
this(context, info, appWidget,
@@ -116,6 +127,15 @@
mDrawableSizeChanged = true;
}
+ /** Set {@link Bitmap} of widget preview. */
+ public void setPreviewBitmap(@Nullable Bitmap previewBitmap) {
+ if (this.mPreviewBitmap == previewBitmap) {
+ return;
+ }
+ this.mPreviewBitmap = previewBitmap;
+ invalidate();
+ }
+
private PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
LauncherAppWidgetProviderInfo appwidget, CharSequence label) {
super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme));
@@ -130,12 +150,17 @@
mPaint.setColor(Themes.getAttrColor(getContext(), android.R.attr.textColorPrimary));
mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
mLauncher.getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics()));
-
+ mPreviewPaint = new Paint(ANTI_ALIAS_FLAG | DITHER_FLAG | FILTER_BITMAP_FLAG);
setWillNotDraw(false);
setBackgroundResource(R.drawable.pending_widget_bg);
}
@Override
+ public AppWidgetProviderInfo getAppWidgetInfo() {
+ return mAppwidget;
+ }
+
+ @Override
public void updateAppWidget(RemoteViews remoteViews) {
checkIfRestored();
}
@@ -393,6 +418,11 @@
@Override
protected void onDraw(Canvas canvas) {
+ if (mPreviewBitmap != null
+ && (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0) {
+ canvas.drawBitmap(mPreviewBitmap, 0, 0, mPreviewPaint);
+ return;
+ }
if (mCenterDrawable == null) {
// Nothing to draw
return;