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