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