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