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