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