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         ((DragLayer.LayoutParams) frame.getLayoutParams()).customPosition = true;
220 
221         dl.addView(frame);
222         frame.mIsOpen = true;
223         frame.post(() -> frame.snapToWidget(false));
224     }
225 
setCornerRadiusFromWidget()226     private void setCornerRadiusFromWidget() {
227         if (mWidgetView != null && mWidgetView.hasEnforcedCornerRadius()) {
228             float enforcedCornerRadius = mWidgetView.getEnforcedCornerRadius();
229             ImageView imageView = findViewById(R.id.widget_resize_frame);
230             Drawable d = imageView.getDrawable();
231             if (d instanceof GradientDrawable) {
232                 GradientDrawable gd = (GradientDrawable) d.mutate();
233                 gd.setCornerRadius(enforcedCornerRadius);
234             }
235         }
236     }
237 
setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer)238     private void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout,
239             DragLayer dragLayer) {
240         mCellLayout = cellLayout;
241         mWidgetView = widgetView;
242         LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo)
243                 widgetView.getAppWidgetInfo();
244         mDragLayer = dragLayer;
245 
246         mMinHSpan = info.minSpanX;
247         mMinVSpan = info.minSpanY;
248         mMaxHSpan = info.maxSpanX;
249         mMaxVSpan = info.maxSpanY;
250 
251         // Only show resize handles for the directions in which resizing is possible.
252         InvariantDeviceProfile idp = LauncherAppState.getIDP(cellLayout.getContext());
253         mVerticalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0
254                 && mMinVSpan < idp.numRows && mMaxVSpan > 1
255                 && mMinVSpan < mMaxVSpan;
256         if (!mVerticalResizeActive) {
257             mDragHandles[INDEX_TOP].setVisibility(GONE);
258             mDragHandles[INDEX_BOTTOM].setVisibility(GONE);
259         }
260         mHorizontalResizeActive = (info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0
261                 && mMinHSpan < idp.numColumns && mMaxHSpan > 1
262                 && mMinHSpan < mMaxHSpan;
263         if (!mHorizontalResizeActive) {
264             mDragHandles[INDEX_LEFT].setVisibility(GONE);
265             mDragHandles[INDEX_RIGHT].setVisibility(GONE);
266         }
267 
268         mReconfigureButton = (ImageButton) findViewById(R.id.widget_reconfigure_button);
269         if (info.isReconfigurable()) {
270             mReconfigureButton.setVisibility(VISIBLE);
271             mReconfigureButton.setOnClickListener(view -> {
272                 mLauncher.setWaitingForResult(
273                         PendingRequestArgs.forWidgetInfo(
274                                 mWidgetView.getAppWidgetId(),
275                                 // Widget add handler is null since we're reconfiguring an existing
276                                 // widget.
277                                 /* widgetHandler= */ null,
278                                 (ItemInfo) mWidgetView.getTag()));
279                 mLauncher
280                         .getAppWidgetHolder()
281                         .startConfigActivity(
282                                 mLauncher,
283                                 mWidgetView.getAppWidgetId(),
284                                 ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET);
285             });
286             if (!hasSeenReconfigurableWidgetEducationTip()) {
287                 post(() -> {
288                     if (showReconfigurableWidgetEducationTip() != null) {
289                         LauncherPrefs.get(getContext()).put(
290                                 RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN, true);
291                     }
292                 });
293             }
294         }
295 
296         if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) {
297             mWidgetView.setCellChildViewPreLayoutListener(mCellChildViewPreLayoutListener);
298             mWidgetViewOldRect.set(mWidgetView.getLeft(), mWidgetView.getTop(),
299                     mWidgetView.getRight(),
300                     mWidgetView.getBottom());
301             mWidgetViewNewRect.set(mWidgetViewOldRect);
302         }
303 
304         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) mWidgetView.getLayoutParams();
305         ItemInfo widgetInfo = (ItemInfo) mWidgetView.getTag();
306         CellPos presenterPos = mLauncher.getCellPosMapper().mapModelToPresenter(widgetInfo);
307         lp.setCellX(presenterPos.cellX);
308         lp.setTmpCellX(presenterPos.cellX);
309         lp.setCellY(presenterPos.cellY);
310         lp.setTmpCellY(presenterPos.cellY);
311         lp.cellHSpan = widgetInfo.spanX;
312         lp.cellVSpan = widgetInfo.spanY;
313         lp.isLockedToGrid = true;
314 
315         // When we create the resize frame, we first mark all cells as unoccupied. The appropriate
316         // cells (same if not resized, or different) will be marked as occupied when the resize
317         // frame is dismissed.
318         mCellLayout.markCellsAsUnoccupiedForView(mWidgetView);
319 
320         mLauncher.getStatsLogManager()
321                 .logger()
322                 .withInstanceId(logInstanceId)
323                 .withItemInfo(widgetInfo)
324                 .log(LAUNCHER_WIDGET_RESIZE_STARTED);
325 
326         setOnKeyListener(this);
327 
328         setCornerRadiusFromWidget();
329         mWidgetView.addOnLayoutChangeListener(mWidgetViewLayoutListener);
330     }
331 
332     public boolean beginResizeIfPointInRegion(int x, int y) {
333         mLeftBorderActive = (x < mTouchTargetWidth) && mHorizontalResizeActive;
334         mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && mHorizontalResizeActive;
335         mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment)
336                 && mVerticalResizeActive;
337         mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment)
338                 && mVerticalResizeActive;
339 
340         boolean anyBordersActive = mLeftBorderActive || mRightBorderActive
341                 || mTopBorderActive || mBottomBorderActive;
342 
343         if (anyBordersActive) {
344             mDragHandles[INDEX_LEFT].setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
345             mDragHandles[INDEX_RIGHT].setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA);
346             mDragHandles[INDEX_TOP].setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
347             mDragHandles[INDEX_BOTTOM].setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
348         }
349 
350         if (mLeftBorderActive) {
351             mDeltaXRange.set(-getLeft(), getWidth() - 2 * mTouchTargetWidth);
352         } else if (mRightBorderActive) {
353             mDeltaXRange.set(2 * mTouchTargetWidth - getWidth(), mDragLayer.getWidth() - getRight());
354         } else {
355             mDeltaXRange.set(0, 0);
356         }
357         mBaselineX.set(getLeft(), getRight());
358 
359         if (mTopBorderActive) {
360             mDeltaYRange.set(-getTop(), getHeight() - 2 * mTouchTargetWidth);
361         } else if (mBottomBorderActive) {
362             mDeltaYRange.set(2 * mTouchTargetWidth - getHeight(), mDragLayer.getHeight() - getBottom());
363         } else {
364             mDeltaYRange.set(0, 0);
365         }
366         mBaselineY.set(getTop(), getBottom());
367 
368         return anyBordersActive;
369     }
370 
371     /**
372      *  Based on the deltas, we resize the frame.
373      */
374     public void visualizeResizeForDelta(int deltaX, int deltaY) {
375         mDeltaX = mDeltaXRange.clamp(deltaX);
376         mDeltaY = mDeltaYRange.clamp(deltaY);
377 
378         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
379         mDeltaX = mDeltaXRange.clamp(deltaX);
380         mBaselineX.applyDelta(mLeftBorderActive, mRightBorderActive, mDeltaX, mTempRange1);
381         lp.x = mTempRange1.start;
382         lp.width = mTempRange1.size();
383 
384         mDeltaY = mDeltaYRange.clamp(deltaY);
385         mBaselineY.applyDelta(mTopBorderActive, mBottomBorderActive, mDeltaY, mTempRange1);
386         lp.y = mTempRange1.start;
387         lp.height = mTempRange1.size();
388 
389         resizeWidgetIfNeeded(false);
390 
391         // Handle invalid resize across CellLayouts in the two panel UI.
392         if (mCellLayout.getParent() instanceof Workspace) {
393             Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent();
394             CellLayout pairedCellLayout = workspace.getScreenPair(mCellLayout);
395             if (pairedCellLayout != null) {
396                 Rect focusedCellLayoutBound = sTmpRect;
397                 mDragLayerRelativeCoordinateHelper.viewToRect(mCellLayout, focusedCellLayoutBound);
398                 Rect resizeFrameBound = sTmpRect2;
399                 findViewById(R.id.widget_resize_frame).getGlobalVisibleRect(resizeFrameBound);
400                 float progress = 1f;
401                 if (workspace.indexOfChild(pairedCellLayout) < workspace.indexOfChild(mCellLayout)
402                         && mDeltaX < 0
403                         && resizeFrameBound.left < focusedCellLayoutBound.left) {
404                     // Resize from right to left.
405                     progress = (mDragAcrossTwoPanelOpacityMargin + mDeltaX)
406                             / mDragAcrossTwoPanelOpacityMargin;
407                 } else if (workspace.indexOfChild(pairedCellLayout)
408                                 > workspace.indexOfChild(mCellLayout)
409                         && mDeltaX > 0
410                         && resizeFrameBound.right > focusedCellLayoutBound.right) {
411                     // Resize from left to right.
412                     progress = (mDragAcrossTwoPanelOpacityMargin - mDeltaX)
413                             / mDragAcrossTwoPanelOpacityMargin;
414                 }
415                 float alpha = Math.max(MIN_OPACITY_FOR_CELL_LAYOUT_DURING_INVALID_RESIZE, progress);
416                 float springLoadedProgress = Math.min(1f, 1f - progress);
417                 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, alpha,
418                         springLoadedProgress);
419             }
420         }
421 
422         requestLayout();
423     }
424 
425     private static int getSpanIncrement(float deltaFrac) {
426         return Math.abs(deltaFrac) > RESIZE_THRESHOLD ? Math.round(deltaFrac) : 0;
427     }
428 
429     /**
430      *  Based on the current deltas, we determine if and how to resize the widget.
431      */
432     private void resizeWidgetIfNeeded(boolean onDismiss) {
433         ViewGroup.LayoutParams wlp = mWidgetView.getLayoutParams();
434         if (!(wlp instanceof CellLayoutLayoutParams)) {
435             return;
436         }
437         DeviceProfile dp = mLauncher.getDeviceProfile();
438         float xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x;
439         float yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y;
440 
441         int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc);
442         int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc);
443 
444         if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return;
445 
446         mDirectionVector[0] = 0;
447         mDirectionVector[1] = 0;
448 
449         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) wlp;
450 
451         int spanX = lp.cellHSpan;
452         int spanY = lp.cellVSpan;
453         int cellX = lp.useTmpCoords ? lp.getTmpCellX() : lp.getCellX();
454         int cellY = lp.useTmpCoords ? lp.getTmpCellY() : lp.getCellY();
455 
456         // For each border, we bound the resizing based on the minimum width, and the maximum
457         // expandability.
458         mTempRange1.set(cellX, spanX + cellX);
459         int hSpanDelta = mTempRange1.applyDeltaAndBound(mLeftBorderActive, mRightBorderActive,
460                 hSpanInc, mMinHSpan, mMaxHSpan, mCellLayout.getCountX(), mTempRange2);
461         cellX = mTempRange2.start;
462         spanX = mTempRange2.size();
463         if (hSpanDelta != 0) {
464             mDirectionVector[0] = mLeftBorderActive ? -1 : 1;
465         }
466 
467         mTempRange1.set(cellY, spanY + cellY);
468         int vSpanDelta = mTempRange1.applyDeltaAndBound(mTopBorderActive, mBottomBorderActive,
469                 vSpanInc, mMinVSpan, mMaxVSpan, mCellLayout.getCountY(), mTempRange2);
470         cellY = mTempRange2.start;
471         spanY = mTempRange2.size();
472         if (vSpanDelta != 0) {
473             mDirectionVector[1] = mTopBorderActive ? -1 : 1;
474         }
475 
476         if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return;
477 
478         // We always want the final commit to match the feedback, so we make sure to use the
479         // last used direction vector when committing the resize / reorder.
480         if (onDismiss) {
481             mDirectionVector[0] = mLastDirectionVector[0];
482             mDirectionVector[1] = mLastDirectionVector[1];
483         } else {
484             mLastDirectionVector[0] = mDirectionVector[0];
485             mLastDirectionVector[1] = mDirectionVector[1];
486         }
487 
488         // We don't want to evaluate resize if a widget was pending config activity and was already
489         // occupying a space on the screen. This otherwise will cause reorder algorithm evaluate a
490         // different location for the widget and cause a jump.
491         if (!(mWidgetView instanceof PendingAppWidgetHostView) && mCellLayout.createAreaForResize(
492                 cellX, cellY, spanX, spanY, mWidgetView, mDirectionVector, onDismiss)) {
493             if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) {
494                 mStateAnnouncer.announce(
495                         mLauncher.getString(R.string.widget_resized, spanX, spanY));
496             }
497 
498             lp.setTmpCellX(cellX);
499             lp.setTmpCellY(cellY);
500             lp.cellHSpan = spanX;
501             lp.cellVSpan = spanY;
502             mRunningVInc += vSpanDelta;
503             mRunningHInc += hSpanDelta;
504 
505             if (!onDismiss) {
506                 WidgetSizes.updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
507             }
508         }
509         mWidgetView.requestLayout();
510     }
511 
512     @Override
513     protected void onDetachedFromWindow() {
514         super.onDetachedFromWindow();
515 
516         // We are done with resizing the widget. Save the widget size & position to LauncherModel
517         resizeWidgetIfNeeded(true);
518         mLauncher.getStatsLogManager()
519                 .logger()
520                 .withInstanceId(logInstanceId)
521                 .withItemInfo((ItemInfo) mWidgetView.getTag())
522                 .log(LAUNCHER_WIDGET_RESIZE_COMPLETED);
523     }
524 
525     private void onTouchUp() {
526         DeviceProfile dp = mLauncher.getDeviceProfile();
527         int xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacePx.x;
528         int yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacePx.y;
529 
530         mDeltaXAddOn = mRunningHInc * xThreshold;
531         mDeltaYAddOn = mRunningVInc * yThreshold;
532         mDeltaX = 0;
533         mDeltaY = 0;
534 
535         post(() -> snapToWidget(true));
536     }
537 
538     /**
539      * Returns the rect of this view when the frame is snapped around the widget, with the bounds
540      * relative to the {@link DragLayer}.
541      */
542     private void getSnappedRectRelativeToDragLayer(@NonNull Rect out) {
543         float scale = mWidgetView.getScaleToFit();
544         if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) {
545             getViewRectRelativeToDragLayer(out);
546         } else {
547             mDragLayer.getViewRectRelativeToSelf(mWidgetView, out);
548         }
549 
550         int width = 2 * mBackgroundPadding + Math.round(scale * out.width());
551         int height = 2 * mBackgroundPadding + Math.round(scale * out.height());
552         int x = out.left - mBackgroundPadding;
553         int y = out.top - mBackgroundPadding;
554 
555         out.left = x;
556         out.top = y;
557         out.right = out.left + width;
558         out.bottom = out.top + height;
559     }
560 
561     private void getViewRectRelativeToDragLayer(@NonNull Rect out) {
562         int[] afterPos = getViewPosRelativeToDragLayer();
563         out.set(afterPos[0], afterPos[1], afterPos[0] + mWidgetViewNewRect.width(),
564                 afterPos[1] + mWidgetViewNewRect.height());
565     }
566 
567     /** Returns the relative x and y values of the widget view after the layout transition */
568     private int[] getViewPosRelativeToDragLayer() {
569         mDragLayer.getLocationInWindow(sDragLayerLoc);
570         int x = sDragLayerLoc[0];
571         int y = sDragLayerLoc[1];
572 
573         if (mWidgetViewWindowPos == null) {
574             mWidgetViewWindowPos = new int[2];
575             mWidgetView.getLocationInWindow(mWidgetViewWindowPos);
576         }
577 
578         int leftOffset = mWidgetViewNewRect.left - mWidgetViewOldRect.left;
579         int topOffset = mWidgetViewNewRect.top - mWidgetViewOldRect.top;
580 
581         return new int[] {mWidgetViewWindowPos[0] - x + leftOffset,
582                 mWidgetViewWindowPos[1] - y + topOffset};
583     }
584 
585     private void snapToWidget(boolean animate) {
586         // The widget is guaranteed to be attached to the cell layout at this point, thus setting
587         // the transition here
588         if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()
589                 && mWidgetView.getLayoutTransition() == null) {
590             final LayoutTransition transition = new LayoutTransition();
591             transition.setDuration(RESIZE_TRANSITION_DURATION_MS);
592             transition.enableTransitionType(LayoutTransition.CHANGING);
593             mWidgetView.setLayoutTransition(transition);
594         }
595 
596         getSnappedRectRelativeToDragLayer(sTmpRect);
597         int newWidth = sTmpRect.width();
598         int newHeight = sTmpRect.height();
599         int newX = sTmpRect.left;
600         int newY = sTmpRect.top;
601 
602         // We need to make sure the frame's touchable regions lie fully within the bounds of the
603         // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions
604         // down accordingly to provide a proper touch target.
605         if (newY < 0) {
606             // In this case we shift the touch region down to start at the top of the DragLayer
607             mTopTouchRegionAdjustment = -newY;
608         } else {
609             mTopTouchRegionAdjustment = 0;
610         }
611         if (newY + newHeight > mDragLayer.getHeight()) {
612             // In this case we shift the touch region up to end at the bottom of the DragLayer
613             mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight());
614         } else {
615             mBottomTouchRegionAdjustment = 0;
616         }
617 
618         final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
619         final CellLayout pairedCellLayout;
620         if (mCellLayout.getParent() instanceof Workspace) {
621             Workspace<?> workspace = (Workspace<?>) mCellLayout.getParent();
622             pairedCellLayout = workspace.getScreenPair(mCellLayout);
623         } else {
624             pairedCellLayout = null;
625         }
626         if (!animate) {
627             lp.width = newWidth;
628             lp.height = newHeight;
629             lp.x = newX;
630             lp.y = newY;
631             for (int i = 0; i < HANDLE_COUNT; i++) {
632                 mDragHandles[i].setAlpha(1f);
633             }
634             if (pairedCellLayout != null) {
635                 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f,
636                         /* springLoadedProgress= */ 0f);
637             }
638             requestLayout();
639         } else {
640             ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(lp,
641                     PropertyValuesHolder.ofInt(LAYOUT_WIDTH, lp.width, newWidth),
642                     PropertyValuesHolder.ofInt(LAYOUT_HEIGHT, lp.height, newHeight),
643                     PropertyValuesHolder.ofInt(LAYOUT_X, lp.x, newX),
644                     PropertyValuesHolder.ofInt(LAYOUT_Y, lp.y, newY));
645             mFirstFrameAnimatorHelper.addTo(oa).addUpdateListener(a -> requestLayout());
646 
647             AnimatorSet set = new AnimatorSet();
648             set.play(oa);
649             for (int i = 0; i < HANDLE_COUNT; i++) {
650                 set.play(mFirstFrameAnimatorHelper.addTo(
651                         ObjectAnimator.ofFloat(mDragHandles[i], ALPHA, 1f)));
652             }
653             if (pairedCellLayout != null) {
654                 updateInvalidResizeEffect(mCellLayout, pairedCellLayout, /* alpha= */ 1f,
655                         /* springLoadedProgress= */ 0f, /* animatorSet= */ set);
656             }
657             set.setDuration(SNAP_DURATION_MS);
658             set.start();
659         }
660 
661         setFocusableInTouchMode(true);
662         requestFocus();
663     }
664 
665     @Override
666     public boolean onKey(View v, int keyCode, KeyEvent event) {
667         // Clear the frame and give focus to the widget host view when a directional key is pressed.
668         if (shouldConsume(keyCode)) {
669             close(false);
670             mWidgetView.requestFocus();
671             return true;
672         }
673         return false;
674     }
675 
676     private boolean handleTouchDown(MotionEvent ev) {
677         Rect hitRect = new Rect();
678         int x = (int) ev.getX();
679         int y = (int) ev.getY();
680 
681         getHitRect(hitRect);
682         if (hitRect.contains(x, y)) {
683             if (beginResizeIfPointInRegion(x - getLeft(), y - getTop())) {
684                 mXDown = x;
685                 mYDown = y;
686                 return true;
687             }
688         }
689         return false;
690     }
691 
692     private boolean isTouchOnReconfigureButton(MotionEvent ev) {
693         int xFrame = (int) ev.getX() - getLeft();
694         int yFrame = (int) ev.getY() - getTop();
695         mReconfigureButton.getHitRect(sTmpRect);
696         return sTmpRect.contains(xFrame, yFrame);
697     }
698 
699     @Override
700     public boolean onControllerTouchEvent(MotionEvent ev) {
701         int action = ev.getAction();
702         int x = (int) ev.getX();
703         int y = (int) ev.getY();
704 
705         switch (action) {
706             case MotionEvent.ACTION_DOWN:
707                 return handleTouchDown(ev);
708             case MotionEvent.ACTION_MOVE:
709                 visualizeResizeForDelta(x - mXDown, y - mYDown);
710                 break;
711             case MotionEvent.ACTION_CANCEL:
712             case MotionEvent.ACTION_UP:
713                 visualizeResizeForDelta(x - mXDown, y - mYDown);
714                 onTouchUp();
715                 mXDown = mYDown = 0;
716                 break;
717         }
718         return true;
719     }
720 
721     @Override
722     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
723         if (ev.getAction() == MotionEvent.ACTION_DOWN && handleTouchDown(ev)) {
724             return true;
725         }
726         // Keep the resize frame open but let a click on the reconfigure button fall through to the
727         // button's OnClickListener.
728         if (isTouchOnReconfigureButton(ev)) {
729             return false;
730         }
731         close(false);
732         return false;
733     }
734 
735     @Override
736     protected void handleClose(boolean animate) {
737         if (FeatureFlags.ENABLE_WIDGET_TRANSITION_FOR_RESIZING.get()) {
738             mWidgetView.clearCellChildViewPreLayoutListener();
739             mWidgetView.setLayoutTransition(null);
740         }
741         mDragLayer.removeView(this);
742         mWidgetView.removeOnLayoutChangeListener(mWidgetViewLayoutListener);
743     }
744 
745     private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout,
746             float alpha, float springLoadedProgress) {
747         updateInvalidResizeEffect(cellLayout, pairedCellLayout, alpha,
748                 springLoadedProgress, /* animatorSet= */ null);
749     }
750 
751     private void updateInvalidResizeEffect(CellLayout cellLayout, CellLayout pairedCellLayout,
752             float alpha, float springLoadedProgress, @Nullable AnimatorSet animatorSet) {
753         int childCount = pairedCellLayout.getChildCount();
754         for (int i = 0; i < childCount; i++) {
755             View child = pairedCellLayout.getChildAt(i);
756             if (animatorSet != null) {
757                 animatorSet.play(
758                         mFirstFrameAnimatorHelper.addTo(
759                                 ObjectAnimator.ofFloat(child, ALPHA, alpha)));
760             } else {
761                 child.setAlpha(alpha);
762             }
763         }
764         if (animatorSet != null) {
765             animatorSet.play(mFirstFrameAnimatorHelper.addTo(
766                     ObjectAnimator.ofFloat(cellLayout, SPRING_LOADED_PROGRESS,
767                             springLoadedProgress)));
768             animatorSet.play(mFirstFrameAnimatorHelper.addTo(
769                     ObjectAnimator.ofFloat(pairedCellLayout, SPRING_LOADED_PROGRESS,
770                             springLoadedProgress)));
771         } else {
772             cellLayout.setSpringLoadedProgress(springLoadedProgress);
773             pairedCellLayout.setSpringLoadedProgress(springLoadedProgress);
774         }
775 
776         boolean shouldShowCellLayoutBorder = springLoadedProgress > 0f;
777         if (animatorSet != null) {
778             animatorSet.addListener(new AnimatorListenerAdapter() {
779                 @Override
780                 public void onAnimationEnd(Animator animator) {
781                     cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
782                     pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
783                 }
784             });
785         } else {
786             cellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
787             pairedCellLayout.setIsDragOverlapping(shouldShowCellLayoutBorder);
788         }
789     }
790 
791     @Override
792     protected boolean isOfType(int type) {
793         return (type & TYPE_WIDGET_RESIZE_FRAME) != 0;
794     }
795 
796     /**
797      * A mutable class for describing the range of two int values.
798      */
799     private static class IntRange {
800 
801         public int start, end;
802 
803         public int clamp(int value) {
804             return Utilities.boundToRange(value, start, end);
805         }
806 
807         public void set(int s, int e) {
808             start = s;
809             end = e;
810         }
811 
812         public int size() {
813             return end - start;
814         }
815 
816         /**
817          * Moves either the start or end edge (but never both) by {@param delta} and  sets the
818          * result in {@param out}
819          */
820         public void applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out) {
821             out.start = moveStart ? start + delta : start;
822             out.end = moveEnd ? end + delta : end;
823         }
824 
825         /**
826          * Applies delta similar to {@link #applyDelta(boolean, boolean, int, IntRange)},
827          * with extra conditions.
828          * @param minSize minimum size after with the moving edge should not be shifted any further.
829          *                For eg, if delta = -3 when moving the endEdge brings the size to less than
830          *                minSize, only delta = -2 will applied
831          * @param maxSize maximum size after with the moving edge should not be shifted any further.
832          *                For eg, if delta = -3 when moving the endEdge brings the size to greater
833          *                than maxSize, only delta = -2 will applied
834          * @param maxEnd The maximum value to the end edge (start edge is always restricted to 0)
835          * @return the amount of increase when endEdge was moves and the amount of decrease when
836          * the start edge was moved.
837          */
838         public int applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta,
839                 int minSize, int maxSize, int maxEnd, IntRange out) {
840             applyDelta(moveStart, moveEnd, delta, out);
841             if (out.start < 0) {
842                 out.start = 0;
843             }
844             if (out.end > maxEnd) {
845                 out.end = maxEnd;
846             }
847             if (out.size() < minSize) {
848                 if (moveStart) {
849                     out.start = out.end - minSize;
850                 } else if (moveEnd) {
851                     out.end = out.start + minSize;
852                 }
853             }
854             if (out.size() > maxSize) {
855                 if (moveStart) {
856                     out.start = out.end - maxSize;
857                 } else if (moveEnd) {
858                     out.end = out.start + maxSize;
859                 }
860             }
861             return moveEnd ? out.size() - size() : size() - out.size();
862         }
863     }
864 
865     /**
866      * Returns true only if this utility class handles the key code.
867      */
868     public static boolean shouldConsume(int keyCode) {
869         return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
870                 || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN
871                 || keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END
872                 || keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN);
873     }
874 
875     @Nullable private ArrowTipView showReconfigurableWidgetEducationTip() {
876         Rect rect = new Rect();
877         if (!mReconfigureButton.getGlobalVisibleRect(rect)) {
878             return null;
879         }
880         @Px int tipMargin = mLauncher.getResources()
881                 .getDimensionPixelSize(R.dimen.widget_reconfigure_tip_top_margin);
882         return new ArrowTipView(mLauncher, /* isPointingUp= */ true)
883                 .showAroundRect(
884                         getContext().getString(R.string.reconfigurable_widget_education_tip),
885                         /* arrowXCoord= */ rect.left + mReconfigureButton.getWidth() / 2,
886                         /* rect= */ rect,
887                         /* margin= */ tipMargin);
888     }
889 
890     private boolean hasSeenReconfigurableWidgetEducationTip() {
891         return LauncherPrefs.get(getContext()).get(RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN)
892                 || Utilities.isRunningInTestHarness();
893     }
894 }
895