• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.launcher3;
2 
3 import static com.android.launcher3.CellLayout.SPRING_LOADED_PROGRESS;
4 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_HEIGHT;
5 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_WIDTH;
6 import static com.android.launcher3.LauncherPrefs.RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN;
7 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RESIZE_COMPLETED;
8 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RESIZE_STARTED;
9 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_X;
10 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_Y;
11 
12 import android.animation.Animator;
13 import android.animation.AnimatorListenerAdapter;
14 import android.animation.AnimatorSet;
15 import android.animation.LayoutTransition;
16 import android.animation.ObjectAnimator;
17 import android.animation.PropertyValuesHolder;
18 import android.appwidget.AppWidgetProviderInfo;
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.graphics.drawable.Drawable;
22 import android.graphics.drawable.GradientDrawable;
23 import android.util.AttributeSet;
24 import android.view.KeyEvent;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.widget.ImageButton;
29 import android.widget.ImageView;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.Px;
34 
35 import com.android.launcher3.LauncherConstants.ActivityCodes;
36 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
37 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
38 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
39 import com.android.launcher3.config.FeatureFlags;
40 import com.android.launcher3.dragndrop.DragLayer;
41 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
42 import com.android.launcher3.logging.InstanceId;
43 import com.android.launcher3.logging.InstanceIdSequence;
44 import com.android.launcher3.model.data.ItemInfo;
45 import com.android.launcher3.util.PendingRequestArgs;
46 import com.android.launcher3.views.ArrowTipView;
47 import com.android.launcher3.widget.LauncherAppWidgetHostView;
48 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
49 import com.android.launcher3.widget.PendingAppWidgetHostView;
50 import com.android.launcher3.widget.util.WidgetSizes;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 public class AppWidgetResizeFrame extends AbstractFloatingView implements View.OnKeyListener {
56     private static final int SNAP_DURATION_MS = 150;
57     private static final float DIMMED_HANDLE_ALPHA = 0f;
58     private static final float RESIZE_THRESHOLD = 0.66f;
59     private static final int RESIZE_TRANSITION_DURATION_MS = 150;
60 
61     private static final Rect sTmpRect = new Rect();
62     private static final Rect sTmpRect2 = new Rect();
63 
64     private static final int[] sDragLayerLoc = new int[2];
65 
66     private static final int HANDLE_COUNT = 4;
67     private static final int INDEX_LEFT = 0;
68     private static final int INDEX_TOP = 1;
69     private static final int INDEX_RIGHT = 2;
70     private static final int INDEX_BOTTOM = 3;
71     private static final float MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE = 0.5f;
72 
73     private final Launcher mLauncher;
74     private final DragViewStateAnnouncer mStateAnnouncer;
75     private final FirstFrameAnimatorHelper mFirstFrameAnimatorHelper;
76 
77     private final View[] mDragHandles = new View[HANDLE_COUNT];
78     private final List<Rect> mSystemGestureExclusionRects = new ArrayList<>(HANDLE_COUNT);
79 
80     private LauncherAppWidgetHostView mWidgetView;
81     private CellLayout mCellLayout;
82     private DragLayer mDragLayer;
83     private ImageButton mReconfigureButton;
84 
85     private final int mBackgroundPadding;
86     private final int mTouchTargetWidth;
87 
88     private final int[] mDirectionVector = new int[2];
89     private final int[] mLastDirectionVector = new int[2];
90 
91     private final IntRange mTempRange1 = new IntRange();
92     private final IntRange mTempRange2 = new IntRange();
93 
94     private final IntRange mDeltaXRange = new IntRange();
95     private final IntRange mBaselineX = new IntRange();
96 
97     private final IntRange mDeltaYRange = new IntRange();
98     private final IntRange mBaselineY = new IntRange();
99 
100     private final InstanceId logInstanceId = new InstanceIdSequence().newInstanceId();
101 
102     private final ViewGroupFocusHelper mDragLayerRelativeCoordinateHelper;
103 
104     /**
105      * In the two panel UI, it is not possible to resize a widget to cross its host
106      * {@link CellLayout}'s sibling. When this happens, we gradually reduce the opacity of the
107      * sibling {@link CellLayout} from 1f to
108      * {@link #MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE}.
109      */
110     private final float mDragAcrossTwoPanelOpacityMargin;
111 
112     private boolean mLeftBorderActive;
113     private boolean mRightBorderActive;
114     private boolean mTopBorderActive;
115     private boolean mBottomBorderActive;
116 
117     private boolean mHorizontalResizeActive;
118     private boolean mVerticalResizeActive;
119 
120     private int mRunningHInc;
121     private int mRunningVInc;
122     private int mMinHSpan;
123     private int mMinVSpan;
124     private int mMaxHSpan;
125     private int mMaxVSpan;
126     private int mDeltaX;
127     private int mDeltaY;
128     private int mDeltaXAddOn;
129     private int mDeltaYAddOn;
130 
131     private int mTopTouchRegionAdjustment = 0;
132     private int mBottomTouchRegionAdjustment = 0;
133 
134     private int[] mWidgetViewWindowPos;
135     private final Rect mWidgetViewOldRect = new Rect();
136     private final Rect mWidgetViewNewRect = new Rect();
137     private final @Nullable LauncherAppWidgetHostView.CellChildViewPreLayoutListener
138             mCellChildViewPreLayoutListener;
139     private final @NonNull OnLayoutChangeListener mWidgetViewLayoutListener;
140 
141     private int mXDown, mYDown;
142 
AppWidgetResizeFrame(Context context)143     public AppWidgetResizeFrame(Context context) {
144         this(context, null);
145     }
146 
AppWidgetResizeFrame(Context context, AttributeSet attrs)147     public AppWidgetResizeFrame(Context context, AttributeSet attrs) {
148         this(context, attrs, 0);
149     }
150 
AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr)151     public AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr) {
152         super(context, attrs, defStyleAttr);
153 
154         mLauncher = Launcher.getLauncher(context);
155         mStateAnnouncer = DragViewStateAnnouncer.createFor(this);
156 
157         mCellChildViewPreLayoutListener = FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()
158                 ? (v, left, top, right, bottom) -> {
159                             if (mWidgetViewWindowPos == null) {
160                                 mWidgetViewWindowPos = new int[2];
161                             }
162                             v.getLocationInWindow(mWidgetViewWindowPos);
163                             mWidgetViewOldRect.set(v.getLeft(), v.getTop(), v.getRight(),
164                                     v.getBottom());
165                             mWidgetViewNewRect.set(left, top, right, bottom);
166                         }
167                 : null;
168 
169         mBackgroundPadding = getResources()
170                 .getDimensionPixelSize(R.dimen.resize_frame_background_padding);
171         mTouchTargetWidth = 2 * mBackgroundPadding;
172         mFirstFrameAnimatorHelper = new FirstFrameAnimatorHelper(this);
173 
174         for (int i = 0; i < HANDLE_COUNT; i++) {
175             mSystemGestureExclusionRects.add(new Rect());
176         }
177 
178         mDragAcrossTwoPanelOpacityMargin = mLauncher.getResources().getDimensionPixelSize(
179                 R.dimen.resize_frame_invalid_drag_across_two_panel_opacity_margin);
180         mDragLayerRelativeCoordinateHelper = new ViewGroupFocusHelper(mLauncher.getDragLayer());
181 
182         mWidgetViewLayoutListener =
183                 (v, l, t, r, b, oldL, oldT, oldR, oldB) -> setCornerRadiusFromWidget();
184     }
185 
186     @Override
onFinishInflate()187     protected void onFinishInflate() {
188         super.onFinishInflate();
189 
190         mDragHandles[INDEX_LEFT] = findViewById(R.id.widget_resize_left_handle);
191         mDragHandles[INDEX_TOP] = findViewById(R.id.widget_resize_top_handle);
192         mDragHandles[INDEX_RIGHT] = findViewById(R.id.widget_resize_right_handle);
193         mDragHandles[INDEX_BOTTOM] = findViewById(R.id.widget_resize_bottom_handle);
194     }
195 
196     @Override
onLayout(boolean changed, int l, int t, int r, int b)197     protected void onLayout(boolean changed, int l, int t, int r, int b) {
198         super.onLayout(changed, l, t, r, b);
199         for (int i = 0; i < HANDLE_COUNT; i++) {
200             View dragHandle = mDragHandles[i];
201             mSystemGestureExclusionRects.get(i).set(dragHandle.getLeft(), dragHandle.getTop(),
202                     dragHandle.getRight(), dragHandle.getBottom());
203         }
204         setSystemGestureExclusionRects(mSystemGestureExclusionRects);
205     }
206 
showForWidget(LauncherAppWidgetHostView widget, CellLayout cellLayout)207     public static void showForWidget(LauncherAppWidgetHostView widget, CellLayout cellLayout) {
208         // If widget is not added to view hierarchy, we cannot show resize frame at correct location
209         if (widget.getParent() == null) {
210             return;
211         }
212         Launcher launcher = Launcher.getLauncher(cellLayout.getContext());
213         AbstractFloatingView.closeAllOpenViews(launcher);
214 
215         DragLayer dl = launcher.getDragLayer();
216         AppWidgetResizeFrame frame = (AppWidgetResizeFrame) launcher.getLayoutInflater()
217                 .inflate(R.layout.app_widget_resize_frame, dl, false);
218         frame.setupForWidget(widget, cellLayout, dl);
219         // Save widget item info as tag on resize frame; so that, the accessibility delegate can
220         // attach actions that typically happen on widget (e.g. resize, move) also on the resize
221         // frame.
222         frame.setTag(widget.getTag());
223         frame.setAccessibilityDelegate(launcher.getAccessibilityDelegate());
224         frame.setContentDescription(launcher.asContext().getString(R.string.widget_frame_name,
225                 widget.getContentDescription()));
226         ((DragLayer.LayoutParams) frame.getLayoutParams()).customPosition = true;
227 
228         dl.addView(frame);
229         frame.mIsOpen = true;
230         frame.post(() -> frame.snapToWidget(false));
231     }
232 
setCornerRadiusFromWidget()233     private void setCornerRadiusFromWidget() {
234         if (mWidgetView != null && mWidgetView.hasEnforcedCornerRadius()) {
235             float enforcedCornerRadius = mWidgetView.getEnforcedCornerRadius();
236             ImageView imageView = findViewById(R.id.widget_resize_frame);
237             Drawable d = imageView.getDrawable();
238             if (d instanceof GradientDrawable) {
239                 GradientDrawable gd = (GradientDrawable) d.mutate();
240                 gd.setCornerRadius(enforcedCornerRadius);
241             }
242         }
243     }
244 
245     /**
246      *  Retrieves the view where accessibility actions happen.
247      */
getViewForAccessibility()248     public View getViewForAccessibility() {
249         return mWidgetView;
250     }
251 
setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer)252     private void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout,
253             DragLayer dragLayer) {
254         mCellLayout = cellLayout;
255         mWidgetView = widgetView;
256         LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo)
257                 widgetView.getAppWidgetInfo();
258         mDragLayer = dragLayer;
259 
260         mMinHSpan = info.minSpanX;
261         mMinVSpan = info.minSpanY;
262         mMaxHSpan = info.maxSpanX;
263         mMaxVSpan = info.maxSpanY;
264 
265         // Only show resize handles for the directions in which resizing is possible.
266         InvariantDeviceProfile idp = LauncherAppState.getIDP(cellLayout.getContext());
267         mVerticalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0
268                 && mMinVSpan < idp.numRows && mMaxVSpan > 1
269                 && mMinVSpan < mMaxVSpan;
270         if (!mVerticalResizeActive) {
271             mDragHandles[INDEX_TOP].setVisibility(GONE);
272             mDragHandles[INDEX_BOTTOM].setVisibility(GONE);
273         }
274         mHorizontalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0
275                 && mMinHSpan < idp.numColumns && mMaxHSpan > 1
276                 && mMinHSpan < mMaxHSpan;
277         if (!mHorizontalResizeActive) {
278             mDragHandles[INDEX_LEFT].setVisibility(GONE);
279             mDragHandles[INDEX_RIGHT].setVisibility(GONE);
280         }
281 
282         mReconfigureButton = (ImageButton) findViewById(R.id.widget_reconfigure_button);
283         if (info.isReconfigurable()) {
284             mReconfigureButton.setVisibility(VISIBLE);
285             mReconfigureButton.setOnClickListener(view -> {
286                 mLauncher.setWaitingForResult(
287                         PendingRequestArgs.forWidgetInfo(
288                                 mWidgetView.getAppWidgetId(),
289                                 // Widget add handler is null since we're reconfiguring an existing
290                                 // widget.
291                                 /* widgetHandler= */ null,
292                                 (ItemInfo) mWidgetView.getTag()));
293                 mLauncher
294                         .getAppWidgetHolder()
295                         .startConfigActivity(
296                                 mLauncher,
297                                 mWidgetView.getAppWidgetId(),
298                                 ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET);
299             });
300             if (!hasSeenReconfigurableWidgetEducationTip()) {
301                 post(() -> {
302                     if (showReconfigurableWidgetEducationTip() != null) {
303                         LauncherPrefs.get(getContext()).put(
304                                 RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN, true);
305                     }
306                 });
307             }
308         }
309 
310         if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) {
311             mWidgetView.setCellChildViewPreLayoutListener(mCellChildViewPreLayoutListener);
312             mWidgetViewOldRect.set(mWidgetView.getLeft(), mWidgetView.getTop(),
313                     mWidgetView.getRight(),
314                     mWidgetView.getBottom());
315             mWidgetViewNewRect.set(mWidgetViewOldRect);
316         }
317 
318         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) mWidgetView.getLayoutParams();
319         ItemInfo widgetInfo = (ItemInfo) mWidgetView.getTag();
320         CellPos presenterPos = mLauncher.getCellPosMapper().mapModelToPresenter(widgetInfo);
321         lp.setCellX(presenterPos.cellX);
322         lp.setTmpCellX(presenterPos.cellX);
323         lp.setCellY(presenterPos.cellY);
324         lp.setTmpCellY(presenterPos.cellY);
325         lp.cellHSpan = widgetInfo.spanX;
326         lp.cellVSpan = widgetInfo.spanY;
327         lp.isLockedToGrid = true;
328 
329         // When we create the resize frame, we first mark all cells as unoccupied. The appropriate
330         // cells (same if not resized, or different) will be marked as occupied when the resize
331         // frame is dismissed.
332         mCellLayout.markCellsAsUnoccupiedForView(mWidgetView);
333 
334         mLauncher.getStatsLogManager()
335                 .logger()
336                 .withInstanceId(logInstanceId)
337                 .withItemInfo(widgetInfo)
338                 .log(LAUNCHER_WIDGET_RESIZE_STARTED);
339 
340         setOnKeyListener(this);
341 
342         setCornerRadiusFromWidget();
343         mWidgetView.addOnLayoutChangeListener(mWidgetViewLayoutListener);
344     }
345 
346     public boolean beginResizeIfPointInRegion(int x, int y) {
347         mLeftBorderActive = (x < mTouchTargetWidth) && mHorizontalResizeActive;
348         mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && mHorizontalResizeActive;
349         mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment)
350                 && mVerticalResizeActive;
351         mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment)
352                 && mVerticalResizeActive;
353 
354         boolean anyBordersActive = mLeftBorderActive || mRightBorderActive
355                 || mTopBorderActive || mBottomBorderActive;
356 
357         if (anyBordersActive) {
358             mDragHandles[INDEX_LEFT].setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
359             mDragHandles[INDEX_RIGHT].setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA);
360             mDragHandles[INDEX_TOP].setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
361             mDragHandles[INDEX_BOTTOM].setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
362         }
363 
364         if (mLeftBorderActive) {
365             mDeltaXRange.set(-getLeft(), getWidth() - 2 * mTouchTargetWidth);
366         } else if (mRightBorderActive) {
367             mDeltaXRange.set(2 * mTouchTargetWidth - getWidth(), mDragLayer.getWidth() - getRight());
368         } else {
369             mDeltaXRange.set(0, 0);
370         }
371         mBaselineX.set(getLeft(), getRight());
372 
373         if (mTopBorderActive) {
374             mDeltaYRange.set(-getTop(), getHeight() - 2 * mTouchTargetWidth);
375         } else if (mBottomBorderActive) {
376             mDeltaYRange.set(2 * mTouchTargetWidth - getHeight(), mDragLayer.getHeight() - getBottom());
377         } else {
378             mDeltaYRange.set(0, 0);
379         }
380         mBaselineY.set(getTop(), getBottom());
381 
382         return anyBordersActive;
383     }
384 
385     /**
386      *  Based on the deltas, we resize the frame.
387      */
388     public void visualizeResizeForDelta(int deltaX, int deltaY) {
389         mDeltaX = mDeltaXRange.clamp(deltaX);
390         mDeltaY = mDeltaYRange.clamp(deltaY);
391 
392         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
393         mDeltaX = mDeltaXRange.clamp(deltaX);
394         mBaselineX.applyDelta(mLeftBorderActive, mRightBorderActive, mDeltaX, mTempRange1);
395         lp.x = mTempRange1.start;
396         lp.width = mTempRange1.size();
397 
398         mDeltaY = mDeltaYRange.clamp(deltaY);
399         mBaselineY.applyDelta(mTopBorderActive, mBottomBorderActive, mDeltaY, mTempRange1);
400         lp.y = mTempRange1.start;
401         lp.height = mTempRange1.size();
402 
403         resizeWidgetIfNeeded(false);
404 
405         // Handle invalid resize across CellLayouts in the two panel UI.
406         if (mCellLayout.getParent() instanceof Workspace) {
407             Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent();
408             CellLayout pairedCellLayout = workspace.getScreenPair(mCellLayout);
409             if (pairedCellLayout != null) {
410                 Rect focusedCellLayoutBound = sTmpRect;
411                 mDragLayerRelativeCoordinateHelper.viewToRect(mCellLayout, focusedCellLayoutBound);
412                 Rect resizeFrameBound = sTmpRect2;
413                 findViewById(R.id.widget_resize_frame).getGlobalVisibleRect(resizeFrameBound);
414                 float progress = 1f;
415                 if (workspace.indexOfChild(pairedCellLayout) < workspace.indexOfChild(mCellLayout)
416                         && mDeltaX < 0
417                         && resizeFrameBound.left < focusedCellLayoutBound.left) {
418                     // Resize from right to left.
419                     progress = (mDragAcrossTwoPanelOpacityMargin + mDeltaX)
420                             / mDragAcrossTwoPanelOpacityMargin;
421                 } else if (workspace.indexOfChild(pairedCellLayout)
422                                 > workspace.indexOfChild(mCellLayout)
423                         && mDeltaX > 0
424                         && resizeFrameBound.right > focusedCellLayoutBound.right) {
425                     // Resize from left to right.
426                     progress = (mDragAcrossTwoPanelOpacityMargin - mDeltaX)
427                             / mDragAcrossTwoPanelOpacityMargin;
428                 }
429                 float alpha = Math.max(MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE, progress);
430                 float springLoadedProgress = Math.min(1f, 1f - progress);
431                 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, alpha,
432                         springLoadedProgress);
433             }
434         }
435 
436         requestLayout();
437     }
438 
439     private static int getSpanIncrement(float deltaFrac) {
440         return Math.abs(deltaFrac) > RESIZE_THRESHOLD ? Math.round(deltaFrac) : 0;
441     }
442 
443     /**
444      *  Based on the current deltas, we determine if and how to resize the widget.
445      */
446     private void resizeWidgetIfNeeded(boolean onDismiss) {
447         ViewGroup.LayoutParams wlp = mWidgetView.getLayoutParams();
448         if (!(wlp instanceof CellLayoutLayoutParams)) {
449             return;
450         }
451         DeviceProfile dp = mLauncher.getDeviceProfile();
452         float xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x;
453         float yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y;
454 
455         int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc);
456         int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc);
457 
458         if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return;
459 
460         mDirectionVector[0] = 0;
461         mDirectionVector[1] = 0;
462 
463         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) wlp;
464 
465         int spanX = lp.cellHSpan;
466         int spanY = lp.cellVSpan;
467         int cellX = lp.useTmpCoords ? lp.getTmpCellX() : lp.getCellX();
468         int cellY = lp.useTmpCoords ? lp.getTmpCellY() : lp.getCellY();
469 
470         // For each border, we bound the resizing based on the minimum width, and the maximum
471         // expandability.
472         mTempRange1.set(cellX, spanX + cellX);
473         int hSpanDelta = mTempRange1.applyDeltaAndBound(mLeftBorderActive, mRightBorderActive,
474                 hSpanInc, mMinHSpan, mMaxHSpan, mCellLayout.getCountX(), mTempRange2);
475         cellX = mTempRange2.start;
476         spanX = mTempRange2.size();
477         if (hSpanDelta != 0) {
478             mDirectionVector[0] = mLeftBorderActive ? -1 : 1;
479         }
480 
481         mTempRange1.set(cellY, spanY + cellY);
482         int vSpanDelta = mTempRange1.applyDeltaAndBound(mTopBorderActive, mBottomBorderActive,
483                 vSpanInc, mMinVSpan, mMaxVSpan, mCellLayout.getCountY(), mTempRange2);
484         cellY = mTempRange2.start;
485         spanY = mTempRange2.size();
486         if (vSpanDelta != 0) {
487             mDirectionVector[1] = mTopBorderActive ? -1 : 1;
488         }
489 
490         if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return;
491 
492         // We always want the final commit to match the feedback, so we make sure to use the
493         // last used direction vector when committing the resize / reorder.
494         if (onDismiss) {
495             mDirectionVector[0] = mLastDirectionVector[0];
496             mDirectionVector[1] = mLastDirectionVector[1];
497         } else {
498             mLastDirectionVector[0] = mDirectionVector[0];
499             mLastDirectionVector[1] = mDirectionVector[1];
500         }
501 
502         // We don't want to evaluate resize if a widget was pending config activity and was already
503         // occupying a space on the screen. This otherwise will cause reorder algorithm evaluate a
504         // different location for the widget and cause a jump.
505         if (!(mWidgetView instanceof PendingAppWidgetHostView) && mCellLayout.createAreaForResize(
506                 cellX, cellY, spanX, spanY, mWidgetView, mDirectionVector, onDismiss)) {
507             if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) {
508                 mStateAnnouncer.announce(
509                         mLauncher.getString(R.string.widget_resized, spanX, spanY));
510             }
511 
512             lp.setTmpCellX(cellX);
513             lp.setTmpCellY(cellY);
514             lp.cellHSpan = spanX;
515             lp.cellVSpan = spanY;
516             mRunningVInc += vSpanDelta;
517             mRunningHInc += hSpanDelta;
518 
519             if (!onDismiss) {
520                 WidgetSizes.updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
521             }
522         }
523         mWidgetView.requestLayout();
524     }
525 
526     @Override
527     protected void onDetachedFromWindow() {
528         super.onDetachedFromWindow();
529 
530         // We are done with resizing the widget. Save the widget size & position to LauncherModel
531         resizeWidgetIfNeeded(true);
532         mLauncher.getStatsLogManager()
533                 .logger()
534                 .withInstanceId(logInstanceId)
535                 .withItemInfo((ItemInfo) mWidgetView.getTag())
536                 .log(LAUNCHER_WIDGET_RESIZE_COMPLETED);
537     }
538 
539     private void onTouchUp() {
540         DeviceProfile dp = mLauncher.getDeviceProfile();
541         int xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x;
542         int yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y;
543 
544         mDeltaXAddOn = mRunningHInc * xThreshold;
545         mDeltaYAddOn = mRunningVInc * yThreshold;
546         mDeltaX = 0;
547         mDeltaY = 0;
548 
549         post(() -> snapToWidget(true));
550     }
551 
552     /**
553      * Returns the rect of this view when the frame is snapped around the widget, with the bounds
554      * relative to the {@link DragLayer}.
555      */
556     private void getSnappedRectRelativeToDragLayer(@NonNull Rect out) {
557         float scale = mWidgetView.getScaleToFit();
558         if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) {
559             getViewRectRelativeToDragLayer(out);
560         } else {
561             mDragLayer.getViewRectRelativeToSelf(mWidgetView, out);
562         }
563 
564         int width = 2 * mBackgroundPadding + Math.round(scale * out.width());
565         int height = 2 * mBackgroundPadding + Math.round(scale * out.height());
566         int x = out.left - mBackgroundPadding;
567         int y = out.top - mBackgroundPadding;
568 
569         out.left = x;
570         out.top = y;
571         out.right = out.left + width;
572         out.bottom = out.top + height;
573     }
574 
575     private void getViewRectRelativeToDragLayer(@NonNull Rect out) {
576         int[] afterPos = getViewPosRelativeToDragLayer();
577         out.set(afterPos[0], afterPos[1], afterPos[0] + mWidgetViewNewRect.width(),
578                 afterPos[1] + mWidgetViewNewRect.height());
579     }
580 
581     /** Returns the relative x and y values of the widget view after the layout transition */
582     private int[] getViewPosRelativeToDragLayer() {
583         mDragLayer.getLocationInWindow(sDragLayerLoc);
584         int x = sDragLayerLoc[0];
585         int y = sDragLayerLoc[1];
586 
587         if (mWidgetViewWindowPos == null) {
588             mWidgetViewWindowPos = new int[2];
589             mWidgetView.getLocationInWindow(mWidgetViewWindowPos);
590         }
591 
592         int leftOffset = mWidgetViewNewRect.left - mWidgetViewOldRect.left;
593         int topOffset = mWidgetViewNewRect.top - mWidgetViewOldRect.top;
594 
595         return new int[] {mWidgetViewWindowPos[0] - x + leftOffset,
596                 mWidgetViewWindowPos[1] - y + topOffset};
597     }
598 
599     private void snapToWidget(boolean animate) {
600         // The widget is guaranteed to be attached to the cell layout at this point, thus setting
601         // the transition here
602         if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()
603                 && mWidgetView.getLayoutTransition() == null) {
604             final LayoutTransition transition = new LayoutTransition();
605             transition.setDuration(RESIZE_TRANSITION_DURATION_MS);
606             transition.enableTransitionType(LayoutTransition.CHANGING);
607             mWidgetView.setLayoutTransition(transition);
608         }
609 
610         getSnappedRectRelativeToDragLayer(sTmpRect);
611         int newWidth = sTmpRect.width();
612         int newHeight = sTmpRect.height();
613         int newX = sTmpRect.left;
614         int newY = sTmpRect.top;
615 
616         // We need to make sure the frame's touchable regions lie fully within the bounds of the
617         // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions
618         // down accordingly to provide a proper touch target.
619         if (newY < 0) {
620             // In this case we shift the touch region down to start at the top of the DragLayer
621             mTopTouchRegionAdjustment = -newY;
622         } else {
623             mTopTouchRegionAdjustment = 0;
624         }
625         if (newY + newHeight > mDragLayer.getHeight()) {
626             // In this case we shift the touch region up to end at the bottom of the DragLayer
627             mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight());
628         } else {
629             mBottomTouchRegionAdjustment = 0;
630         }
631 
632         final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
633         final CellLayout pairedCellLayout;
634         if (mCellLayout.getParent() instanceof Workspace) {
635             Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent();
636             pairedCellLayout = workspace.getScreenPair(mCellLayout);
637         } else {
638             pairedCellLayout = null;
639         }
640         if (!animate) {
641             lp.width = newWidth;
642             lp.height = newHeight;
643             lp.x = newX;
644             lp.y = newY;
645             for (int i = 0; i < HANDLE_COUNT; i++) {
646                 mDragHandles[i].setAlpha(1f);
647             }
648             if (pairedCellLayout != null) {
649                 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f,
650                         /* springLoadedProgress= */ 0f);
651             }
652             requestLayout();
653         } else {
654             ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(lp,
655                     PropertyValuesHolder.ofInt(LAYOUT_WIDTH, lp.width, newWidth),
656                     PropertyValuesHolder.ofInt(LAYOUT_HEIGHT, lp.height, newHeight),
657                     PropertyValuesHolder.ofInt(LAYOUT_X, lp.x, newX),
658                     PropertyValuesHolder.ofInt(LAYOUT_Y, lp.y, newY));
659             mFirstFrameAnimatorHelper.addTo(oa).addUpdateListener(a -> requestLayout());
660 
661             AnimatorSet set = new AnimatorSet();
662             set.play(oa);
663             for (int i = 0; i < HANDLE_COUNT; i++) {
664                 set.play(mFirstFrameAnimatorHelper.addTo(
665                         ObjectAnimator.ofFloat(mDragHandles[i], ALPHA, 1f)));
666             }
667             if (pairedCellLayout != null) {
668                 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f,
669                         /* springLoadedProgress= */ 0f, /* animatorSet= */ set);
670             }
671             set.setDuration(SNAP_DURATION_MS);
672             set.start();
673         }
674 
675         setFocusableInTouchMode(true);
676         requestFocus();
677     }
678 
679     @Override
680     public boolean onKey(View v, int keyCode, KeyEvent event) {
681         // Clear the frame and give focus to the widget host view when a directional key is pressed.
682         if (shouldConsume(keyCode)) {
683             close(false);
684             mWidgetView.requestFocus();
685             return true;
686         }
687         return false;
688     }
689 
690     private boolean handleTouchDown(MotionEvent ev) {
691         Rect hitRect = new Rect();
692         int x = (int) ev.getX();
693         int y = (int) ev.getY();
694 
695         getHitRect(hitRect);
696         if (hitRect.contains(x, y)) {
697             if (beginResizeIfPointInRegion(x - getLeft(), y - getTop())) {
698                 mXDown = x;
699                 mYDown = y;
700                 return true;
701             }
702         }
703         return false;
704     }
705 
706     private boolean isTouchOnReconfigureButton(MotionEvent ev) {
707         int xFrame = (int) ev.getX() - getLeft();
708         int yFrame = (int) ev.getY() - getTop();
709         mReconfigureButton.getHitRect(sTmpRect);
710         return sTmpRect.contains(xFrame, yFrame);
711     }
712 
713     @Override
714     public boolean onControllerTouchEvent(MotionEvent ev) {
715         int action = ev.getAction();
716         int x = (int) ev.getX();
717         int y = (int) ev.getY();
718 
719         switch (action) {
720             case MotionEvent.ACTION_DOWN:
721                 return handleTouchDown(ev);
722             case MotionEvent.ACTION_MOVE:
723                 visualizeResizeForDelta(x - mXDown, y - mYDown);
724                 break;
725             case MotionEvent.ACTION_CANCEL:
726             case MotionEvent.ACTION_UP:
727                 visualizeResizeForDelta(x - mXDown, y - mYDown);
728                 onTouchUp();
729                 mXDown = mYDown = 0;
730                 break;
731         }
732         return true;
733     }
734 
735     @Override
736     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
737         if (ev.getAction() == MotionEvent.ACTION_DOWN && handleTouchDown(ev)) {
738             return true;
739         }
740         // Keep the resize frame open but let a click on the reconfigure button fall through to the
741         // button's OnClickListener.
742         if (isTouchOnReconfigureButton(ev)) {
743             return false;
744         }
745         close(false);
746         return false;
747     }
748 
749     @Override
750     protected void handleClose(boolean animate) {
751         if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) {
752             mWidgetView.clearCellChildViewPreLayoutListener();
753             mWidgetView.setLayoutTransition(null);
754         }
755         mDragLayer.removeView(this);
756         mWidgetView.removeOnLayoutChangeListener(mWidgetViewLayoutListener);
757     }
758 
759     private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout,
760             float alpha, float springLoadedProgress) {
761         updateInvalidResizeEffect(cellLayout, pairedCellLayout, alpha,
762                 springLoadedProgress, /* animatorSet= */ null);
763     }
764 
765     private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout,
766             float alpha, float springLoadedProgress, @Nullable AnimatorSet animatorSet) {
767         int childCount = pairedCellLayout.getChildCount();
768         for (int i = 0; i < childCount; i++) {
769             View child = pairedCellLayout.getChildAt(i);
770             if (animatorSet != null) {
771                 animatorSet.play(
772                         mFirstFrameAnimatorHelper.addTo(
773                                 ObjectAnimator.ofFloat(child, ALPHA, alpha)));
774             } else {
775                 child.setAlpha(alpha);
776             }
777         }
778         if (animatorSet != null) {
779             animatorSet.play(mFirstFrameAnimatorHelper.addTo(
780                     ObjectAnimator.ofFloat(cellLayout, SPRING_LOADED_PROGRESS,
781                             springLoadedProgress)));
782             animatorSet.play(mFirstFrameAnimatorHelper.addTo(
783                     ObjectAnimator.ofFloat(pairedCellLayout, SPRING_LOADED_PROGRESS,
784                             springLoadedProgress)));
785         } else {
786             cellLayout.setSpringLoadedProgress(springLoadedProgress);
787             pairedCellLayout.setSpringLoadedProgress(springLoadedProgress);
788         }
789 
790         boolean shouldShowCellLayoutBorder = springLoadedProgress > 0f;
791         if (animatorSet != null) {
792             animatorSet.addListener(new AnimatorListenerAdapter() {
793                 @Override
794                 public void onAnimationEnd(Animator animator) {
795                     cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
796                     pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
797                 }
798             });
799         } else {
800             cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
801             pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
802         }
803     }
804 
805     @Override
806     protected boolean isOfType(int type) {
807         return (type & TYPE_WIDGET_RESIZE_FRAME) != 0;
808     }
809 
810     /**
811      * A mutable class for describing the range of two int values.
812      */
813     private static class IntRange {
814 
815         public int start, end;
816 
817         public int clamp(int value) {
818             return Utilities.boundToRange(value, start, end);
819         }
820 
821         public void set(int s, int e) {
822             start = s;
823             end = e;
824         }
825 
826         public int size() {
827             return end - start;
828         }
829 
830         /**
831          * Moves either the start or end edge (but never both) by {@param delta} and  sets the
832          * result in {@param out}
833          */
834         public void applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out) {
835             out.start = moveStart ? start + delta : start;
836             out.end = moveEnd ? end + delta : end;
837         }
838 
839         /**
840          * Applies delta similar to {@link #applyDelta(boolean, boolean, int, IntRange)},
841          * with extra conditions.
842          * @param minSize minimum size after with the moving edge should not be shifted any further.
843          *                For eg, if delta = -3 when moving the endEdge brings the size to less than
844          *                minSize, only delta = -2 will applied
845          * @param maxSize maximum size after with the moving edge should not be shifted any further.
846          *                For eg, if delta = -3 when moving the endEdge brings the size to greater
847          *                than maxSize, only delta = -2 will applied
848          * @param maxEnd The maximum value to the end edge (start edge is always restricted to 0)
849          * @return the amount of increase when endEdge was moves and the amount of decrease when
850          * the start edge was moved.
851          */
852         public int applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta,
853                 int minSize, int maxSize, int maxEnd, IntRange out) {
854             applyDelta(moveStart, moveEnd, delta, out);
855             if (out.start < 0) {
856                 out.start = 0;
857             }
858             if (out.end > maxEnd) {
859                 out.end = maxEnd;
860             }
861             if (out.size() < minSize) {
862                 if (moveStart) {
863                     out.start = out.end - minSize;
864                 } else if (moveEnd) {
865                     out.end = out.start + minSize;
866                 }
867             }
868             if (out.size() > maxSize) {
869                 if (moveStart) {
870                     out.start = out.end - maxSize;
871                 } else if (moveEnd) {
872                     out.end = out.start + maxSize;
873                 }
874             }
875             return moveEnd ? out.size() - size() : size() - out.size();
876         }
877     }
878 
879     /**
880      * Returns true only if this utility class handles the key code.
881      */
882     public static boolean shouldConsume(int keyCode) {
883         return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
884                 || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN
885                 || keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END
886                 || keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN);
887     }
888 
889     @Nullable private ArrowTipView showReconfigurableWidgetEducationTip() {
890         Rect rect = new Rect();
891         if (!mReconfigureButton.getGlobalVisibleRect(rect)) {
892             return null;
893         }
894         @Px int tipMargin = mLauncher.getResources()
895                 .getDimensionPixelSize(R.dimen.widget_reconfigure_tip_top_margin);
896         return new ArrowTipView(mLauncher, /* isPointingUp= */ true)
897                 .showAroundRect(
898                         getContext().getString(R.string.reconfigurable_widget_education_tip),
899                         /* arrowXCoord= */ rect.left + mReconfigureButton.getWidth() / 2,
900                         /* rect= */ rect,
901                         /* margin= */ tipMargin);
902     }
903 
904     private boolean hasSeenReconfigurableWidgetEducationTip() {
905         return LauncherPrefs.get(getContext()).get(RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN)
906                 || Utilities.isRunningInTestHarness();
907     }
908 }
909