• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.launcher3;
2 
3 import android.animation.AnimatorSet;
4 import android.animation.ObjectAnimator;
5 import android.animation.PropertyValuesHolder;
6 import android.animation.ValueAnimator;
7 import android.animation.ValueAnimator.AnimatorUpdateListener;
8 import android.appwidget.AppWidgetHostView;
9 import android.appwidget.AppWidgetProviderInfo;
10 import android.content.Context;
11 import android.content.res.Resources;
12 import android.graphics.Point;
13 import android.graphics.Rect;
14 import android.util.AttributeSet;
15 import android.view.KeyEvent;
16 import android.view.MotionEvent;
17 import android.view.View;
18 import android.widget.FrameLayout;
19 
20 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
21 import com.android.launcher3.dragndrop.DragLayer;
22 import com.android.launcher3.util.FocusLogic;
23 import com.android.launcher3.util.TouchController;
24 
25 public class AppWidgetResizeFrame extends FrameLayout
26         implements View.OnKeyListener, TouchController {
27     private static final int SNAP_DURATION = 150;
28     private static final float DIMMED_HANDLE_ALPHA = 0f;
29     private static final float RESIZE_THRESHOLD = 0.66f;
30 
31     private static final Rect sTmpRect = new Rect();
32 
33     // Represents the cell size on the grid in the two orientations.
34     private static Point[] sCellSize;
35 
36     private static final int HANDLE_COUNT = 4;
37     private static final int INDEX_LEFT = 0;
38     private static final int INDEX_TOP = 1;
39     private static final int INDEX_RIGHT = 2;
40     private static final int INDEX_BOTTOM = 3;
41 
42     private final Launcher mLauncher;
43     private final DragViewStateAnnouncer mStateAnnouncer;
44 
45     private final View[] mDragHandles = new View[HANDLE_COUNT];
46 
47     private LauncherAppWidgetHostView mWidgetView;
48     private CellLayout mCellLayout;
49     private DragLayer mDragLayer;
50 
51     private Rect mWidgetPadding;
52 
53     private final int mBackgroundPadding;
54     private final int mTouchTargetWidth;
55 
56     private final int[] mDirectionVector = new int[2];
57     private final int[] mLastDirectionVector = new int[2];
58     private final int[] mTmpPt = new int[2];
59 
60     private final IntRange mTempRange1 = new IntRange();
61     private final IntRange mTempRange2 = new IntRange();
62 
63     private final IntRange mDeltaXRange = new IntRange();
64     private final IntRange mBaselineX = new IntRange();
65 
66     private final IntRange mDeltaYRange = new IntRange();
67     private final IntRange mBaselineY = new IntRange();
68 
69     private boolean mLeftBorderActive;
70     private boolean mRightBorderActive;
71     private boolean mTopBorderActive;
72     private boolean mBottomBorderActive;
73 
74     private int mResizeMode;
75 
76     private int mRunningHInc;
77     private int mRunningVInc;
78     private int mMinHSpan;
79     private int mMinVSpan;
80     private int mDeltaX;
81     private int mDeltaY;
82     private int mDeltaXAddOn;
83     private int mDeltaYAddOn;
84 
85     private int mTopTouchRegionAdjustment = 0;
86     private int mBottomTouchRegionAdjustment = 0;
87 
88     private int mXDown, mYDown;
89 
AppWidgetResizeFrame(Context context)90     public AppWidgetResizeFrame(Context context) {
91         this(context, null);
92     }
93 
AppWidgetResizeFrame(Context context, AttributeSet attrs)94     public AppWidgetResizeFrame(Context context, AttributeSet attrs) {
95         this(context, attrs, 0);
96     }
97 
AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr)98     public AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr) {
99         super(context, attrs, defStyleAttr);
100 
101         mLauncher = Launcher.getLauncher(context);
102         mStateAnnouncer = DragViewStateAnnouncer.createFor(this);
103 
104         mBackgroundPadding = getResources()
105                 .getDimensionPixelSize(R.dimen.resize_frame_background_padding);
106         mTouchTargetWidth = 2 * mBackgroundPadding;
107     }
108 
109     @Override
onFinishInflate()110     protected void onFinishInflate() {
111         super.onFinishInflate();
112 
113         for (int i = 0; i < HANDLE_COUNT; i ++) {
114             mDragHandles[i] = getChildAt(i);
115         }
116     }
117 
setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer)118     public void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout,
119             DragLayer dragLayer) {
120         mCellLayout = cellLayout;
121         mWidgetView = widgetView;
122         LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo)
123                 widgetView.getAppWidgetInfo();
124         mResizeMode = info.resizeMode;
125         mDragLayer = dragLayer;
126 
127         mMinHSpan = info.minSpanX;
128         mMinVSpan = info.minSpanY;
129 
130         if (!info.isCustomWidget) {
131             mWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(getContext(),
132                     widgetView.getAppWidgetInfo().provider, null);
133         } else {
134             Resources r = getContext().getResources();
135             int padding = r.getDimensionPixelSize(R.dimen.default_widget_padding);
136             mWidgetPadding = new Rect(padding, padding, padding, padding);
137         }
138 
139         if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) {
140             mDragHandles[INDEX_TOP].setVisibility(GONE);
141             mDragHandles[INDEX_BOTTOM].setVisibility(GONE);
142         } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) {
143             mDragHandles[INDEX_LEFT].setVisibility(GONE);
144             mDragHandles[INDEX_RIGHT].setVisibility(GONE);
145         }
146 
147         // When we create the resize frame, we first mark all cells as unoccupied. The appropriate
148         // cells (same if not resized, or different) will be marked as occupied when the resize
149         // frame is dismissed.
150         mCellLayout.markCellsAsUnoccupiedForView(mWidgetView);
151 
152         setOnKeyListener(this);
153     }
154 
beginResizeIfPointInRegion(int x, int y)155     public boolean beginResizeIfPointInRegion(int x, int y) {
156         boolean horizontalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
157         boolean verticalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
158 
159         mLeftBorderActive = (x < mTouchTargetWidth) && horizontalActive;
160         mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && horizontalActive;
161         mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) && verticalActive;
162         mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment)
163                 && verticalActive;
164 
165         boolean anyBordersActive = mLeftBorderActive || mRightBorderActive
166                 || mTopBorderActive || mBottomBorderActive;
167 
168         if (anyBordersActive) {
169             mDragHandles[INDEX_LEFT].setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
170             mDragHandles[INDEX_RIGHT].setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA);
171             mDragHandles[INDEX_TOP].setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
172             mDragHandles[INDEX_BOTTOM].setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
173         }
174 
175         if (mLeftBorderActive) {
176             mDeltaXRange.set(-getLeft(), getWidth() - 2 * mTouchTargetWidth);
177         } else if (mRightBorderActive) {
178             mDeltaXRange.set(2 * mTouchTargetWidth - getWidth(), mDragLayer.getWidth() - getRight());
179         } else {
180             mDeltaXRange.set(0, 0);
181         }
182         mBaselineX.set(getLeft(), getRight());
183 
184         if (mTopBorderActive) {
185             mDeltaYRange.set(-getTop(), getHeight() - 2 * mTouchTargetWidth);
186         } else if (mBottomBorderActive) {
187             mDeltaYRange.set(2 * mTouchTargetWidth - getHeight(), mDragLayer.getHeight() - getBottom());
188         } else {
189             mDeltaYRange.set(0, 0);
190         }
191         mBaselineY.set(getTop(), getBottom());
192 
193         return anyBordersActive;
194     }
195 
196     /**
197      *  Based on the deltas, we resize the frame.
198      */
visualizeResizeForDelta(int deltaX, int deltaY)199     public void visualizeResizeForDelta(int deltaX, int deltaY) {
200         mDeltaX = mDeltaXRange.clamp(deltaX);
201         mDeltaY = mDeltaYRange.clamp(deltaY);
202 
203         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
204         mDeltaX = mDeltaXRange.clamp(deltaX);
205         mBaselineX.applyDelta(mLeftBorderActive, mRightBorderActive, mDeltaX, mTempRange1);
206         lp.x = mTempRange1.start;
207         lp.width = mTempRange1.size();
208 
209         mDeltaY = mDeltaYRange.clamp(deltaY);
210         mBaselineY.applyDelta(mTopBorderActive, mBottomBorderActive, mDeltaY, mTempRange1);
211         lp.y = mTempRange1.start;
212         lp.height = mTempRange1.size();
213 
214         resizeWidgetIfNeeded(false);
215 
216         // When the widget resizes in multi-window mode, the translation value changes to maintain
217         // a center fit. These overrides ensure the resize frame always aligns with the widget view.
218         getSnappedRectRelativeToDragLayer(sTmpRect);
219         if (mLeftBorderActive) {
220             lp.width = sTmpRect.width() + sTmpRect.left - lp.x;
221         }
222         if (mTopBorderActive) {
223             lp.height = sTmpRect.height() + sTmpRect.top - lp.y;
224         }
225         if (mRightBorderActive) {
226             lp.x = sTmpRect.left;
227         }
228         if (mBottomBorderActive) {
229             lp.y = sTmpRect.top;
230         }
231 
232         requestLayout();
233     }
234 
getSpanIncrement(float deltaFrac)235     private static int getSpanIncrement(float deltaFrac) {
236         return Math.abs(deltaFrac) > RESIZE_THRESHOLD ? Math.round(deltaFrac) : 0;
237     }
238 
239     /**
240      *  Based on the current deltas, we determine if and how to resize the widget.
241      */
resizeWidgetIfNeeded(boolean onDismiss)242     private void resizeWidgetIfNeeded(boolean onDismiss) {
243         float xThreshold = mCellLayout.getCellWidth();
244         float yThreshold = mCellLayout.getCellHeight();
245 
246         int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc);
247         int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc);
248 
249         if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return;
250 
251         mDirectionVector[0] = 0;
252         mDirectionVector[1] = 0;
253 
254         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams();
255 
256         int spanX = lp.cellHSpan;
257         int spanY = lp.cellVSpan;
258         int cellX = lp.useTmpCoords ? lp.tmpCellX : lp.cellX;
259         int cellY = lp.useTmpCoords ? lp.tmpCellY : lp.cellY;
260 
261         // For each border, we bound the resizing based on the minimum width, and the maximum
262         // expandability.
263         mTempRange1.set(cellX, spanX + cellX);
264         int hSpanDelta = mTempRange1.applyDeltaAndBound(mLeftBorderActive, mRightBorderActive,
265                 hSpanInc, mMinHSpan, mCellLayout.getCountX(), mTempRange2);
266         cellX = mTempRange2.start;
267         spanX = mTempRange2.size();
268         if (hSpanDelta != 0) {
269             mDirectionVector[0] = mLeftBorderActive ? -1 : 1;
270         }
271 
272         mTempRange1.set(cellY, spanY + cellY);
273         int vSpanDelta = mTempRange1.applyDeltaAndBound(mTopBorderActive, mBottomBorderActive,
274                 vSpanInc, mMinVSpan, mCellLayout.getCountY(), mTempRange2);
275         cellY = mTempRange2.start;
276         spanY = mTempRange2.size();
277         if (vSpanDelta != 0) {
278             mDirectionVector[1] = mTopBorderActive ? -1 : 1;
279         }
280 
281         if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return;
282 
283         // We always want the final commit to match the feedback, so we make sure to use the
284         // last used direction vector when committing the resize / reorder.
285         if (onDismiss) {
286             mDirectionVector[0] = mLastDirectionVector[0];
287             mDirectionVector[1] = mLastDirectionVector[1];
288         } else {
289             mLastDirectionVector[0] = mDirectionVector[0];
290             mLastDirectionVector[1] = mDirectionVector[1];
291         }
292 
293         if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView,
294                 mDirectionVector, onDismiss)) {
295             if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) {
296                 mStateAnnouncer.announce(
297                         mLauncher.getString(R.string.widget_resized, spanX, spanY));
298             }
299 
300             lp.tmpCellX = cellX;
301             lp.tmpCellY = cellY;
302             lp.cellHSpan = spanX;
303             lp.cellVSpan = spanY;
304             mRunningVInc += vSpanDelta;
305             mRunningHInc += hSpanDelta;
306 
307             if (!onDismiss) {
308                 updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
309             }
310         }
311         mWidgetView.requestLayout();
312     }
313 
updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher, int spanX, int spanY)314     static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
315             int spanX, int spanY) {
316         getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect);
317         widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top,
318                 sTmpRect.right, sTmpRect.bottom);
319     }
320 
getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect)321     public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect) {
322         if (sCellSize == null) {
323             InvariantDeviceProfile inv = LauncherAppState.getIDP(context);
324 
325             // Initiate cell sizes.
326             sCellSize = new Point[2];
327             sCellSize[0] = inv.landscapeProfile.getCellSize();
328             sCellSize[1] = inv.portraitProfile.getCellSize();
329         }
330 
331         if (rect == null) {
332             rect = new Rect();
333         }
334         final float density = context.getResources().getDisplayMetrics().density;
335 
336         // Compute landscape size
337         int landWidth = (int) ((spanX * sCellSize[0].x) / density);
338         int landHeight = (int) ((spanY * sCellSize[0].y) / density);
339 
340         // Compute portrait size
341         int portWidth = (int) ((spanX * sCellSize[1].x) / density);
342         int portHeight = (int) ((spanY * sCellSize[1].y) / density);
343         rect.set(portWidth, landHeight, landWidth, portHeight);
344         return rect;
345     }
346 
347     /**
348      * This is the final step of the resize. Here we save the new widget size and position
349      * to LauncherModel and animate the resize frame.
350      */
commitResize()351     public void commitResize() {
352         resizeWidgetIfNeeded(true);
353         requestLayout();
354     }
355 
onTouchUp()356     private void onTouchUp() {
357         int xThreshold = mCellLayout.getCellWidth();
358         int yThreshold = mCellLayout.getCellHeight();
359 
360         mDeltaXAddOn = mRunningHInc * xThreshold;
361         mDeltaYAddOn = mRunningVInc * yThreshold;
362         mDeltaX = 0;
363         mDeltaY = 0;
364 
365         post(new Runnable() {
366             @Override
367             public void run() {
368                 snapToWidget(true);
369             }
370         });
371     }
372 
373     /**
374      * Returns the rect of this view when the frame is snapped around the widget, with the bounds
375      * relative to the {@link DragLayer}.
376      */
getSnappedRectRelativeToDragLayer(Rect out)377     private void getSnappedRectRelativeToDragLayer(Rect out) {
378         float scale = mWidgetView.getScaleToFit();
379 
380         mDragLayer.getViewRectRelativeToSelf(mWidgetView, out);
381 
382         int width = 2 * mBackgroundPadding
383                 + (int) (scale * (out.width() - mWidgetPadding.left - mWidgetPadding.right));
384         int height = 2 * mBackgroundPadding
385                 + (int) (scale * (out.height() - mWidgetPadding.top - mWidgetPadding.bottom));
386 
387         int x = (int) (out.left - mBackgroundPadding + scale * mWidgetPadding.left);
388         int y = (int) (out.top - mBackgroundPadding + scale * mWidgetPadding.top);
389 
390         out.left = x;
391         out.top = y;
392         out.right = out.left + width;
393         out.bottom = out.top + height;
394     }
395 
snapToWidget(boolean animate)396     public void snapToWidget(boolean animate) {
397         getSnappedRectRelativeToDragLayer(sTmpRect);
398         int newWidth = sTmpRect.width();
399         int newHeight = sTmpRect.height();
400         int newX = sTmpRect.left;
401         int newY = sTmpRect.top;
402 
403         // We need to make sure the frame's touchable regions lie fully within the bounds of the
404         // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions
405         // down accordingly to provide a proper touch target.
406         if (newY < 0) {
407             // In this case we shift the touch region down to start at the top of the DragLayer
408             mTopTouchRegionAdjustment = -newY;
409         } else {
410             mTopTouchRegionAdjustment = 0;
411         }
412         if (newY + newHeight > mDragLayer.getHeight()) {
413             // In this case we shift the touch region up to end at the bottom of the DragLayer
414             mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight());
415         } else {
416             mBottomTouchRegionAdjustment = 0;
417         }
418 
419         final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
420         if (!animate) {
421             lp.width = newWidth;
422             lp.height = newHeight;
423             lp.x = newX;
424             lp.y = newY;
425             for (int i = 0; i < HANDLE_COUNT; i++) {
426                 mDragHandles[i].setAlpha(1.0f);
427             }
428             requestLayout();
429         } else {
430             PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", lp.width, newWidth);
431             PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", lp.height,
432                     newHeight);
433             PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX);
434             PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY);
435             ObjectAnimator oa =
436                     LauncherAnimUtils.ofPropertyValuesHolder(lp, this, width, height, x, y);
437             oa.addUpdateListener(new AnimatorUpdateListener() {
438                 public void onAnimationUpdate(ValueAnimator animation) {
439                     requestLayout();
440                 }
441             });
442             AnimatorSet set = LauncherAnimUtils.createAnimatorSet();
443             set.play(oa);
444             for (int i = 0; i < HANDLE_COUNT; i++) {
445                 set.play(LauncherAnimUtils.ofFloat(mDragHandles[i], ALPHA, 1.0f));
446             }
447 
448             set.setDuration(SNAP_DURATION);
449             set.start();
450         }
451 
452         setFocusableInTouchMode(true);
453         requestFocus();
454     }
455 
456     @Override
onKey(View v, int keyCode, KeyEvent event)457     public boolean onKey(View v, int keyCode, KeyEvent event) {
458         // Clear the frame and give focus to the widget host view when a directional key is pressed.
459         if (FocusLogic.shouldConsume(keyCode)) {
460             mDragLayer.clearResizeFrame();
461             mWidgetView.requestFocus();
462             return true;
463         }
464         return false;
465     }
466 
handleTouchDown(MotionEvent ev)467     private boolean handleTouchDown(MotionEvent ev) {
468         Rect hitRect = new Rect();
469         int x = (int) ev.getX();
470         int y = (int) ev.getY();
471 
472         getHitRect(hitRect);
473         if (hitRect.contains(x, y)) {
474             if (beginResizeIfPointInRegion(x - getLeft(), y - getTop())) {
475                 mXDown = x;
476                 mYDown = y;
477                 return true;
478             }
479         }
480         return false;
481     }
482 
483     @Override
onControllerTouchEvent(MotionEvent ev)484     public boolean onControllerTouchEvent(MotionEvent ev) {
485         int action = ev.getAction();
486         int x = (int) ev.getX();
487         int y = (int) ev.getY();
488 
489         switch (action) {
490             case MotionEvent.ACTION_DOWN:
491                 return handleTouchDown(ev);
492             case MotionEvent.ACTION_MOVE:
493                 visualizeResizeForDelta(x - mXDown, y - mYDown);
494                 break;
495             case MotionEvent.ACTION_CANCEL:
496             case MotionEvent.ACTION_UP:
497                 visualizeResizeForDelta(x - mXDown, y - mYDown);
498                 onTouchUp();
499                 mXDown = mYDown = 0;
500                 break;
501         }
502         return true;
503     }
504 
505     @Override
onControllerInterceptTouchEvent(MotionEvent ev)506     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
507         if (ev.getAction() == MotionEvent.ACTION_DOWN && handleTouchDown(ev)) {
508             return true;
509         }
510         return false;
511     }
512 
513     /**
514      * A mutable class for describing the range of two int values.
515      */
516     private static class IntRange {
517 
518         public int start, end;
519 
clamp(int value)520         public int clamp(int value) {
521             return Utilities.boundToRange(value, start, end);
522         }
523 
set(int s, int e)524         public void set(int s, int e) {
525             start = s;
526             end = e;
527         }
528 
size()529         public int size() {
530             return end - start;
531         }
532 
533         /**
534          * Moves either the start or end edge (but never both) by {@param delta} and  sets the
535          * result in {@param out}
536          */
applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out)537         public void applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out) {
538             out.start = moveStart ? start + delta : start;
539             out.end = moveEnd ? end + delta : end;
540         }
541 
542         /**
543          * Applies delta similar to {@link #applyDelta(boolean, boolean, int, IntRange)},
544          * with extra conditions.
545          * @param minSize minimum size after with the moving edge should not be shifted any further.
546          *                For eg, if delta = -3 when moving the endEdge brings the size to less than
547          *                minSize, only delta = -2 will applied
548          * @param maxEnd The maximum value to the end edge (start edge is always restricted to 0)
549          * @return the amount of increase when endEdge was moves and the amount of decrease when
550          * the start edge was moved.
551          */
applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta, int minSize, int maxEnd, IntRange out)552         public int applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta,
553                 int minSize, int maxEnd, IntRange out) {
554             applyDelta(moveStart, moveEnd, delta, out);
555             if (out.start < 0) {
556                 out.start = 0;
557             }
558             if (out.end > maxEnd) {
559                 out.end = maxEnd;
560             }
561             if (out.size() < minSize) {
562                 if (moveStart) {
563                     out.start = out.end - minSize;
564                 } else if (moveEnd) {
565                     out.end = out.start + minSize;
566                 }
567             }
568             return moveEnd ? out.size() - size() : size() - out.size();
569         }
570     }
571 }
572