• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.launcher3;
2 
3 import com.android.launcher3.dragndrop.DragLayer;
4 
5 import android.animation.AnimatorSet;
6 import android.animation.ObjectAnimator;
7 import android.animation.PropertyValuesHolder;
8 import android.animation.ValueAnimator;
9 import android.animation.ValueAnimator.AnimatorUpdateListener;
10 import android.appwidget.AppWidgetHostView;
11 import android.appwidget.AppWidgetProviderInfo;
12 import android.content.Context;
13 import android.content.res.Resources;
14 import android.graphics.Point;
15 import android.graphics.Rect;
16 import android.view.Gravity;
17 import android.view.KeyEvent;
18 import android.view.View;
19 import android.widget.FrameLayout;
20 import android.widget.ImageView;
21 
22 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
23 import com.android.launcher3.util.FocusLogic;
24 
25 public class AppWidgetResizeFrame extends FrameLayout implements View.OnKeyListener {
26     private static final int SNAP_DURATION = 150;
27     private static final float DIMMED_HANDLE_ALPHA = 0f;
28     private static final float RESIZE_THRESHOLD = 0.66f;
29 
30     private static final Rect sTmpRect = new Rect();
31 
32     // Represents the cell size on the grid in the two orientations.
33     private static Point[] sCellSize;
34 
35     private final Launcher mLauncher;
36     private final LauncherAppWidgetHostView mWidgetView;
37     private final CellLayout mCellLayout;
38     private final DragLayer mDragLayer;
39 
40     private final ImageView mLeftHandle;
41     private final ImageView mRightHandle;
42     private final ImageView mTopHandle;
43     private final ImageView mBottomHandle;
44 
45     private final Rect mWidgetPadding;
46 
47     private final int mBackgroundPadding;
48     private final int mTouchTargetWidth;
49 
50     private final int[] mDirectionVector = new int[2];
51     private final int[] mLastDirectionVector = new int[2];
52     private final int[] mTmpPt = new int[2];
53 
54     private final DragViewStateAnnouncer mStateAnnouncer;
55 
56     private boolean mLeftBorderActive;
57     private boolean mRightBorderActive;
58     private boolean mTopBorderActive;
59     private boolean mBottomBorderActive;
60 
61     private int mBaselineWidth;
62     private int mBaselineHeight;
63     private int mBaselineX;
64     private int mBaselineY;
65     private int mResizeMode;
66 
67     private int mRunningHInc;
68     private int mRunningVInc;
69     private int mMinHSpan;
70     private int mMinVSpan;
71     private int mDeltaX;
72     private int mDeltaY;
73     private int mDeltaXAddOn;
74     private int mDeltaYAddOn;
75 
76     private int mTopTouchRegionAdjustment = 0;
77     private int mBottomTouchRegionAdjustment = 0;
78 
AppWidgetResizeFrame(Context context, LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer)79     public AppWidgetResizeFrame(Context context,
80             LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer) {
81 
82         super(context);
83         mLauncher = Launcher.getLauncher(context);
84         mCellLayout = cellLayout;
85         mWidgetView = widgetView;
86         LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo)
87                 widgetView.getAppWidgetInfo();
88         mResizeMode = info.resizeMode;
89         mDragLayer = dragLayer;
90 
91         mMinHSpan = info.minSpanX;
92         mMinVSpan = info.minSpanY;
93 
94         mStateAnnouncer = DragViewStateAnnouncer.createFor(this);
95 
96         setBackgroundResource(R.drawable.widget_resize_shadow);
97         setForeground(getResources().getDrawable(R.drawable.widget_resize_frame));
98         setPadding(0, 0, 0, 0);
99 
100         final int handleMargin = getResources().getDimensionPixelSize(R.dimen.widget_handle_margin);
101         LayoutParams lp;
102         mLeftHandle = new ImageView(context);
103         mLeftHandle.setImageResource(R.drawable.ic_widget_resize_handle);
104         lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
105                 Gravity.LEFT | Gravity.CENTER_VERTICAL);
106         lp.leftMargin = handleMargin;
107         addView(mLeftHandle, lp);
108 
109         mRightHandle = new ImageView(context);
110         mRightHandle.setImageResource(R.drawable.ic_widget_resize_handle);
111         lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
112                 Gravity.RIGHT | Gravity.CENTER_VERTICAL);
113         lp.rightMargin = handleMargin;
114         addView(mRightHandle, lp);
115 
116         mTopHandle = new ImageView(context);
117         mTopHandle.setImageResource(R.drawable.ic_widget_resize_handle);
118         lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
119                 Gravity.CENTER_HORIZONTAL | Gravity.TOP);
120         lp.topMargin = handleMargin;
121         addView(mTopHandle, lp);
122 
123         mBottomHandle = new ImageView(context);
124         mBottomHandle.setImageResource(R.drawable.ic_widget_resize_handle);
125         lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
126                 Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
127         lp.bottomMargin = handleMargin;
128         addView(mBottomHandle, lp);
129 
130         if (!info.isCustomWidget) {
131             mWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context,
132                     widgetView.getAppWidgetInfo().provider, null);
133         } else {
134             Resources r = context.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             mTopHandle.setVisibility(GONE);
141             mBottomHandle.setVisibility(GONE);
142         } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) {
143             mLeftHandle.setVisibility(GONE);
144             mRightHandle.setVisibility(GONE);
145         }
146 
147         mBackgroundPadding = getResources()
148                 .getDimensionPixelSize(R.dimen.resize_frame_background_padding);
149         mTouchTargetWidth = 2 * mBackgroundPadding;
150 
151         // When we create the resize frame, we first mark all cells as unoccupied. The appropriate
152         // cells (same if not resized, or different) will be marked as occupied when the resize
153         // frame is dismissed.
154         mCellLayout.markCellsAsUnoccupiedForView(mWidgetView);
155 
156         setOnKeyListener(this);
157     }
158 
beginResizeIfPointInRegion(int x, int y)159     public boolean beginResizeIfPointInRegion(int x, int y) {
160         boolean horizontalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
161         boolean verticalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
162 
163         mLeftBorderActive = (x < mTouchTargetWidth) && horizontalActive;
164         mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && horizontalActive;
165         mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) && verticalActive;
166         mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment)
167                 && verticalActive;
168 
169         boolean anyBordersActive = mLeftBorderActive || mRightBorderActive
170                 || mTopBorderActive || mBottomBorderActive;
171 
172         mBaselineWidth = getMeasuredWidth();
173         mBaselineHeight = getMeasuredHeight();
174         mBaselineX = getLeft();
175         mBaselineY = getTop();
176 
177         if (anyBordersActive) {
178             mLeftHandle.setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
179             mRightHandle.setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA);
180             mTopHandle.setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
181             mBottomHandle.setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
182         }
183         return anyBordersActive;
184     }
185 
186     /**
187      *  Here we bound the deltas such that the frame cannot be stretched beyond the extents
188      *  of the CellLayout, and such that the frame's borders can't cross.
189      */
updateDeltas(int deltaX, int deltaY)190     public void updateDeltas(int deltaX, int deltaY) {
191         if (mLeftBorderActive) {
192             mDeltaX = Math.max(-mBaselineX, deltaX);
193             mDeltaX = Math.min(mBaselineWidth - 2 * mTouchTargetWidth, mDeltaX);
194         } else if (mRightBorderActive) {
195             mDeltaX = Math.min(mDragLayer.getWidth() - (mBaselineX + mBaselineWidth), deltaX);
196             mDeltaX = Math.max(-mBaselineWidth + 2 * mTouchTargetWidth, mDeltaX);
197         }
198 
199         if (mTopBorderActive) {
200             mDeltaY = Math.max(-mBaselineY, deltaY);
201             mDeltaY = Math.min(mBaselineHeight - 2 * mTouchTargetWidth, mDeltaY);
202         } else if (mBottomBorderActive) {
203             mDeltaY = Math.min(mDragLayer.getHeight() - (mBaselineY + mBaselineHeight), deltaY);
204             mDeltaY = Math.max(-mBaselineHeight + 2 * mTouchTargetWidth, mDeltaY);
205         }
206     }
207 
visualizeResizeForDelta(int deltaX, int deltaY)208     public void visualizeResizeForDelta(int deltaX, int deltaY) {
209         visualizeResizeForDelta(deltaX, deltaY, false);
210     }
211 
212     /**
213      *  Based on the deltas, we resize the frame, and, if needed, we resize the widget.
214      */
visualizeResizeForDelta(int deltaX, int deltaY, boolean onDismiss)215     private void visualizeResizeForDelta(int deltaX, int deltaY, boolean onDismiss) {
216         updateDeltas(deltaX, deltaY);
217         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
218 
219         if (mLeftBorderActive) {
220             lp.x = mBaselineX + mDeltaX;
221             lp.width = mBaselineWidth - mDeltaX;
222         } else if (mRightBorderActive) {
223             lp.width = mBaselineWidth + mDeltaX;
224         }
225 
226         if (mTopBorderActive) {
227             lp.y = mBaselineY + mDeltaY;
228             lp.height = mBaselineHeight - mDeltaY;
229         } else if (mBottomBorderActive) {
230             lp.height = mBaselineHeight + mDeltaY;
231         }
232 
233         resizeWidgetIfNeeded(onDismiss);
234         requestLayout();
235     }
236 
237     /**
238      *  Based on the current deltas, we determine if and how to resize the widget.
239      */
resizeWidgetIfNeeded(boolean onDismiss)240     private void resizeWidgetIfNeeded(boolean onDismiss) {
241         int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap();
242         int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap();
243 
244         int deltaX = mDeltaX + mDeltaXAddOn;
245         int deltaY = mDeltaY + mDeltaYAddOn;
246 
247         float hSpanIncF = 1.0f * deltaX / xThreshold - mRunningHInc;
248         float vSpanIncF = 1.0f * deltaY / yThreshold - mRunningVInc;
249 
250         int hSpanInc = 0;
251         int vSpanInc = 0;
252         int cellXInc = 0;
253         int cellYInc = 0;
254 
255         int countX = mCellLayout.getCountX();
256         int countY = mCellLayout.getCountY();
257 
258         if (Math.abs(hSpanIncF) > RESIZE_THRESHOLD) {
259             hSpanInc = Math.round(hSpanIncF);
260         }
261         if (Math.abs(vSpanIncF) > RESIZE_THRESHOLD) {
262             vSpanInc = Math.round(vSpanIncF);
263         }
264 
265         if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return;
266 
267 
268         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams();
269 
270         int spanX = lp.cellHSpan;
271         int spanY = lp.cellVSpan;
272         int cellX = lp.useTmpCoords ? lp.tmpCellX : lp.cellX;
273         int cellY = lp.useTmpCoords ? lp.tmpCellY : lp.cellY;
274 
275         int hSpanDelta = 0;
276         int vSpanDelta = 0;
277 
278         // For each border, we bound the resizing based on the minimum width, and the maximum
279         // expandability.
280         if (mLeftBorderActive) {
281             cellXInc = Math.max(-cellX, hSpanInc);
282             cellXInc = Math.min(lp.cellHSpan - mMinHSpan, cellXInc);
283             hSpanInc *= -1;
284             hSpanInc = Math.min(cellX, hSpanInc);
285             hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc);
286             hSpanDelta = -hSpanInc;
287 
288         } else if (mRightBorderActive) {
289             hSpanInc = Math.min(countX - (cellX + spanX), hSpanInc);
290             hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc);
291             hSpanDelta = hSpanInc;
292         }
293 
294         if (mTopBorderActive) {
295             cellYInc = Math.max(-cellY, vSpanInc);
296             cellYInc = Math.min(lp.cellVSpan - mMinVSpan, cellYInc);
297             vSpanInc *= -1;
298             vSpanInc = Math.min(cellY, vSpanInc);
299             vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc);
300             vSpanDelta = -vSpanInc;
301         } else if (mBottomBorderActive) {
302             vSpanInc = Math.min(countY - (cellY + spanY), vSpanInc);
303             vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc);
304             vSpanDelta = vSpanInc;
305         }
306 
307         mDirectionVector[0] = 0;
308         mDirectionVector[1] = 0;
309         // Update the widget's dimensions and position according to the deltas computed above
310         if (mLeftBorderActive || mRightBorderActive) {
311             spanX += hSpanInc;
312             cellX += cellXInc;
313             if (hSpanDelta != 0) {
314                 mDirectionVector[0] = mLeftBorderActive ? -1 : 1;
315             }
316         }
317 
318         if (mTopBorderActive || mBottomBorderActive) {
319             spanY += vSpanInc;
320             cellY += cellYInc;
321             if (vSpanDelta != 0) {
322                 mDirectionVector[1] = mTopBorderActive ? -1 : 1;
323             }
324         }
325 
326         if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return;
327 
328         // We always want the final commit to match the feedback, so we make sure to use the
329         // last used direction vector when committing the resize / reorder.
330         if (onDismiss) {
331             mDirectionVector[0] = mLastDirectionVector[0];
332             mDirectionVector[1] = mLastDirectionVector[1];
333         } else {
334             mLastDirectionVector[0] = mDirectionVector[0];
335             mLastDirectionVector[1] = mDirectionVector[1];
336         }
337 
338         if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView,
339                 mDirectionVector, onDismiss)) {
340             if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) {
341                 mStateAnnouncer.announce(
342                         mLauncher.getString(R.string.widget_resized, spanX, spanY));
343             }
344 
345             lp.tmpCellX = cellX;
346             lp.tmpCellY = cellY;
347             lp.cellHSpan = spanX;
348             lp.cellVSpan = spanY;
349             mRunningVInc += vSpanDelta;
350             mRunningHInc += hSpanDelta;
351 
352             if (!onDismiss) {
353                 updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
354             }
355         }
356         mWidgetView.requestLayout();
357     }
358 
updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher, int spanX, int spanY)359     static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
360             int spanX, int spanY) {
361         getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect);
362         widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top,
363                 sTmpRect.right, sTmpRect.bottom);
364     }
365 
getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect)366     public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect) {
367         if (sCellSize == null) {
368             InvariantDeviceProfile inv = LauncherAppState.getInstance().getInvariantDeviceProfile();
369 
370             // Initiate cell sizes.
371             sCellSize = new Point[2];
372             sCellSize[0] = inv.landscapeProfile.getCellSize();
373             sCellSize[1] = inv.portraitProfile.getCellSize();
374         }
375 
376         if (rect == null) {
377             rect = new Rect();
378         }
379         final float density = context.getResources().getDisplayMetrics().density;
380 
381         // Compute landscape size
382         int landWidth = (int) ((spanX * sCellSize[0].x) / density);
383         int landHeight = (int) ((spanY * sCellSize[0].y) / density);
384 
385         // Compute portrait size
386         int portWidth = (int) ((spanX * sCellSize[1].x) / density);
387         int portHeight = (int) ((spanY * sCellSize[1].y) / density);
388         rect.set(portWidth, landHeight, landWidth, portHeight);
389         return rect;
390     }
391 
392     /**
393      * This is the final step of the resize. Here we save the new widget size and position
394      * to LauncherModel and animate the resize frame.
395      */
commitResize()396     public void commitResize() {
397         resizeWidgetIfNeeded(true);
398         requestLayout();
399     }
400 
onTouchUp()401     public void onTouchUp() {
402         int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap();
403         int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap();
404 
405         mDeltaXAddOn = mRunningHInc * xThreshold;
406         mDeltaYAddOn = mRunningVInc * yThreshold;
407         mDeltaX = 0;
408         mDeltaY = 0;
409 
410         post(new Runnable() {
411             @Override
412             public void run() {
413                 snapToWidget(true);
414             }
415         });
416     }
417 
snapToWidget(boolean animate)418     public void snapToWidget(boolean animate) {
419         final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
420         int newWidth = mWidgetView.getWidth() + 2 * mBackgroundPadding
421                 - mWidgetPadding.left - mWidgetPadding.right;
422         int newHeight = mWidgetView.getHeight() + 2 * mBackgroundPadding
423                 - mWidgetPadding.top - mWidgetPadding.bottom;
424 
425         mTmpPt[0] = mWidgetView.getLeft();
426         mTmpPt[1] = mWidgetView.getTop();
427         mDragLayer.getDescendantCoordRelativeToSelf(mCellLayout.getShortcutsAndWidgets(), mTmpPt);
428 
429         int newX = mTmpPt[0] - mBackgroundPadding + mWidgetPadding.left;
430         int newY = mTmpPt[1] - mBackgroundPadding + mWidgetPadding.top;
431 
432         // We need to make sure the frame's touchable regions lie fully within the bounds of the
433         // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions
434         // down accordingly to provide a proper touch target.
435         if (newY < 0) {
436             // In this case we shift the touch region down to start at the top of the DragLayer
437             mTopTouchRegionAdjustment = -newY;
438         } else {
439             mTopTouchRegionAdjustment = 0;
440         }
441         if (newY + newHeight > mDragLayer.getHeight()) {
442             // In this case we shift the touch region up to end at the bottom of the DragLayer
443             mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight());
444         } else {
445             mBottomTouchRegionAdjustment = 0;
446         }
447 
448         if (!animate) {
449             lp.width = newWidth;
450             lp.height = newHeight;
451             lp.x = newX;
452             lp.y = newY;
453             mLeftHandle.setAlpha(1.0f);
454             mRightHandle.setAlpha(1.0f);
455             mTopHandle.setAlpha(1.0f);
456             mBottomHandle.setAlpha(1.0f);
457             requestLayout();
458         } else {
459             PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", lp.width, newWidth);
460             PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", lp.height,
461                     newHeight);
462             PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX);
463             PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY);
464             ObjectAnimator oa =
465                     LauncherAnimUtils.ofPropertyValuesHolder(lp, this, width, height, x, y);
466             ObjectAnimator leftOa = LauncherAnimUtils.ofFloat(mLeftHandle, ALPHA, 1.0f);
467             ObjectAnimator rightOa = LauncherAnimUtils.ofFloat(mRightHandle, ALPHA, 1.0f);
468             ObjectAnimator topOa = LauncherAnimUtils.ofFloat(mTopHandle, ALPHA, 1.0f);
469             ObjectAnimator bottomOa = LauncherAnimUtils.ofFloat(mBottomHandle, ALPHA, 1.0f);
470             oa.addUpdateListener(new AnimatorUpdateListener() {
471                 public void onAnimationUpdate(ValueAnimator animation) {
472                     requestLayout();
473                 }
474             });
475             AnimatorSet set = LauncherAnimUtils.createAnimatorSet();
476             if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) {
477                 set.playTogether(oa, topOa, bottomOa);
478             } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) {
479                 set.playTogether(oa, leftOa, rightOa);
480             } else {
481                 set.playTogether(oa, leftOa, rightOa, topOa, bottomOa);
482             }
483 
484             set.setDuration(SNAP_DURATION);
485             set.start();
486         }
487 
488         setFocusableInTouchMode(true);
489         requestFocus();
490     }
491 
492     @Override
onKey(View v, int keyCode, KeyEvent event)493     public boolean onKey(View v, int keyCode, KeyEvent event) {
494         // Clear the frame and give focus to the widget host view when a directional key is pressed.
495         if (FocusLogic.shouldConsume(keyCode)) {
496             mDragLayer.clearAllResizeFrames();
497             mWidgetView.requestFocus();
498             return true;
499         }
500         return false;
501     }
502 }
503