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