Add widget layout transition when resizing

Also getting rid of part of the logic that updates the frame size in multi-window mode since multi-window is no longer supported

Test: N/A
Bug: 268553314
Flag: ENABLE_WIDGET_TRANSITION_FOR_RESIZING. OFF
Change-Id: I081b11441b562fccec7feb12cec0b28b9a0ea3a2
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index 7131452..7b0d71b 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -11,6 +11,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
+import android.animation.LayoutTransition;
 import android.animation.ObjectAnimator;
 import android.animation.PropertyValuesHolder;
 import android.appwidget.AppWidgetProviderInfo;
@@ -26,12 +27,14 @@
 import android.widget.ImageButton;
 import android.widget.ImageView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Px;
 
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.logging.InstanceId;
@@ -47,15 +50,18 @@
 import java.util.List;
 
 public class AppWidgetResizeFrame extends AbstractFloatingView implements View.OnKeyListener {
-    private static final int SNAP_DURATION = 150;
+    private static final int SNAP_DURATION_MS = 150;
     private static final float DIMMED_HANDLE_ALPHA = 0f;
     private static final float RESIZE_THRESHOLD = 0.66f;
+    private static final int RESIZE_TRANSITION_DURATION_MS = 150;
 
     private static final String KEY_RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN =
             "launcher.reconfigurable_widget_education_tip_seen";
     private static final Rect sTmpRect = new Rect();
     private static final Rect sTmpRect2 = new Rect();
 
+    private static final int[] sDragLayerLoc = new int[2];
+
     private static final int HANDLE_COUNT = 4;
     private static final int INDEX_LEFT = 0;
     private static final int INDEX_TOP = 1;
@@ -124,6 +130,12 @@
     private int mTopTouchRegionAdjustment = 0;
     private int mBottomTouchRegionAdjustment = 0;
 
+    private int[] mWidgetViewWindowPos;
+    private final Rect mWidgetViewOldRect = new Rect();
+    private final Rect mWidgetViewNewRect = new Rect();
+    private final @Nullable LauncherAppWidgetHostView.CellChildViewPreLayoutListener
+            mCellChildViewPreLayoutListener;
+
     private int mXDown, mYDown;
 
     public AppWidgetResizeFrame(Context context) {
@@ -140,6 +152,18 @@
         mLauncher = Launcher.getLauncher(context);
         mStateAnnouncer = DragViewStateAnnouncer.createFor(this);
 
+        mCellChildViewPreLayoutListener = FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()
+                ? (v, left, top, right, bottom) -> {
+                            if (mWidgetViewWindowPos == null) {
+                                mWidgetViewWindowPos = new int[2];
+                            }
+                            v.getLocationInWindow(mWidgetViewWindowPos);
+                            mWidgetViewOldRect.set(v.getLeft(), v.getTop(), v.getRight(),
+                                    v.getBottom());
+                            mWidgetViewNewRect.set(left, top, right, bottom);
+                        }
+                : null;
+
         mBackgroundPadding = getResources()
                 .getDimensionPixelSize(R.dimen.resize_frame_background_padding);
         mTouchTargetWidth = 2 * mBackgroundPadding;
@@ -260,6 +284,14 @@
             }
         }
 
+        if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) {
+            mWidgetView.setCellChildViewPreLayoutListener(mCellChildViewPreLayoutListener);
+            mWidgetViewOldRect.set(mWidgetView.getLeft(), mWidgetView.getTop(),
+                    mWidgetView.getRight(),
+                    mWidgetView.getBottom());
+            mWidgetViewNewRect.set(mWidgetViewOldRect);
+        }
+
         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) mWidgetView.getLayoutParams();
         ItemInfo widgetInfo = (ItemInfo) mWidgetView.getTag();
         CellPos presenterPos = mLauncher.getCellPosMapper().mapModelToPresenter(widgetInfo);
@@ -344,22 +376,6 @@
 
         resizeWidgetIfNeeded(false);
 
-        // When the widget resizes in multi-window mode, the translation value changes to maintain
-        // a center fit. These overrides ensure the resize frame always aligns with the widget view.
-        getSnappedRectRelativeToDragLayer(sTmpRect);
-        if (mLeftBorderActive) {
-            lp.width = sTmpRect.width() + sTmpRect.left - lp.x;
-        }
-        if (mTopBorderActive) {
-            lp.height = sTmpRect.height() + sTmpRect.top - lp.y;
-        }
-        if (mRightBorderActive) {
-            lp.x = sTmpRect.left;
-        }
-        if (mBottomBorderActive) {
-            lp.y = sTmpRect.top;
-        }
-
         // Handle invalid resize across CellLayouts in the two panel UI.
         if (mCellLayout.getParent() instanceof Workspace) {
             Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent();
@@ -508,9 +524,13 @@
      * Returns the rect of this view when the frame is snapped around the widget, with the bounds
      * relative to the {@link DragLayer}.
      */
-    private void getSnappedRectRelativeToDragLayer(Rect out) {
+    private void getSnappedRectRelativeToDragLayer(@NonNull Rect out) {
         float scale = mWidgetView.getScaleToFit();
-        mDragLayer.getViewRectRelativeToSelf(mWidgetView, out);
+        if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) {
+            getViewRectRelativeToDragLayer(out);
+        } else {
+            mDragLayer.getViewRectRelativeToSelf(mWidgetView, out);
+        }
 
         int width = 2 * mBackgroundPadding + Math.round(scale * out.width());
         int height = 2 * mBackgroundPadding + Math.round(scale * out.height());
@@ -523,7 +543,41 @@
         out.bottom = out.top + height;
     }
 
+    private void getViewRectRelativeToDragLayer(@NonNull Rect out) {
+        int[] afterPos = getViewPosRelativeToDragLayer();
+        out.set(afterPos[0], afterPos[1], afterPos[0] + mWidgetViewNewRect.width(),
+                afterPos[1] + mWidgetViewNewRect.height());
+    }
+
+    /** Returns the relative x and y values of the widget view after the layout transition */
+    private int[] getViewPosRelativeToDragLayer() {
+        mDragLayer.getLocationInWindow(sDragLayerLoc);
+        int x = sDragLayerLoc[0];
+        int y = sDragLayerLoc[1];
+
+        if (mWidgetViewWindowPos == null) {
+            mWidgetViewWindowPos = new int[2];
+            mWidgetView.getLocationInWindow(mWidgetViewWindowPos);
+        }
+
+        int leftOffset = mWidgetViewNewRect.left - mWidgetViewOldRect.left;
+        int topOffset = mWidgetViewNewRect.top - mWidgetViewOldRect.top;
+
+        return new int[] {mWidgetViewWindowPos[0] - x + leftOffset,
+                mWidgetViewWindowPos[1] - y + topOffset};
+    }
+
     private void snapToWidget(boolean animate) {
+        // The widget is guaranteed to be attached to the cell layout at this point, thus setting
+        // the transition here
+        if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()
+                && mWidgetView.getLayoutTransition() == null) {
+            final LayoutTransition transition = new LayoutTransition();
+            transition.setDuration(RESIZE_TRANSITION_DURATION_MS);
+            transition.enableTransitionType(LayoutTransition.CHANGING);
+            mWidgetView.setLayoutTransition(transition);
+        }
+
         getSnappedRectRelativeToDragLayer(sTmpRect);
         int newWidth = sTmpRect.width();
         int newHeight = sTmpRect.height();
@@ -585,7 +639,7 @@
                 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f,
                         /* springLoadedProgress= */ 0f, /* animatorSet= */ set);
             }
-            set.setDuration(SNAP_DURATION);
+            set.setDuration(SNAP_DURATION_MS);
             set.start();
         }
 
@@ -665,6 +719,10 @@
 
     @Override
     protected void handleClose(boolean animate) {
+        if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) {
+            mWidgetView.clearCellChildViewPreLayoutListener();
+            mWidgetView.setLayoutTransition(null);
+        }
         mDragLayer.removeView(this);
     }
 
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index f0fea61..5e7f21b 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -38,6 +38,7 @@
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.launcher3.widget.NavigableAppWidgetHostView;
 
 public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon.FolderIconParent {
@@ -217,6 +218,16 @@
 
         int childLeft = lp.x;
         int childTop = lp.y;
+
+        // We want to get the layout position of the widget, but layout() is a final function in
+        // ViewGroup which makes it impossible to be overridden. Overriding onLayout() will have no
+        // effect since it will not be called when the transition is enabled. The only possible
+        // solution here seems to be sending the positions when CellLayout is laying out the views
+        if (child instanceof LauncherAppWidgetHostView widgetView
+                && widgetView.getCellChildViewPreLayoutListener() != null) {
+            widgetView.getCellChildViewPreLayoutListener().notifyBoundChangeOnPreLayout(child,
+                    childLeft, childTop, childLeft + lp.width, childTop + lp.height);
+        }
         child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height);
 
         if (lp.dropped) {
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 98d854e..76a044d 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -37,6 +37,7 @@
 import android.widget.Advanceable;
 import android.widget.RemoteViews;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.CheckLongPressHelper;
@@ -63,6 +64,8 @@
     private static final long ADVANCE_INTERVAL = 20000;
     private static final long ADVANCE_STAGGER = 250;
 
+    private @Nullable CellChildViewPreLayoutListener mCellChildViewPreLayoutListener;
+
     // Maintains a list of widget ids which are supposed to be auto advanced.
     private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray();
     // Maximum duration for which updates can be deferred.
@@ -335,6 +338,26 @@
         requestLayout();
     }
 
+    /**
+     * Set the pre-layout listener
+     * @param listener The listener to be notified when {@code CellLayout} is to layout this view
+     */
+    public void setCellChildViewPreLayoutListener(
+            @NonNull CellChildViewPreLayoutListener listener) {
+        mCellChildViewPreLayoutListener = listener;
+    }
+
+    /** @return The current cell layout listener */
+    @Nullable
+    public CellChildViewPreLayoutListener getCellChildViewPreLayoutListener() {
+        return mCellChildViewPreLayoutListener;
+    }
+
+    /** Clear the listener for the pre-layout in CellLayout */
+    public void clearCellChildViewPreLayoutListener() {
+        mCellChildViewPreLayoutListener = null;
+    }
+
     @Override
     public void onColorsChanged(SparseIntArray colors) {
         if (isDeferringUpdates()) {
@@ -460,4 +483,19 @@
         }
         return false;
     }
+
+    /**
+     * Listener interface to be called when {@code CellLayout} is about to layout this child view
+     */
+    public interface CellChildViewPreLayoutListener {
+        /**
+         * Notify the bound changes to this view on pre-layout
+         * @param v The view which the listener is set for
+         * @param left The new left coordinate of this view
+         * @param top The new top coordinate of this view
+         * @param right The new right coordinate of this view
+         * @param bottom The new bottom coordinate of this view
+         */
+        void notifyBoundChangeOnPreLayout(View v, int left, int top, int right, int bottom);
+    }
 }