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