1package com.android.launcher3;
2
3import android.animation.AnimatorSet;
4import android.animation.ObjectAnimator;
5import android.animation.PropertyValuesHolder;
6import android.animation.ValueAnimator;
7import android.animation.ValueAnimator.AnimatorUpdateListener;
8import android.appwidget.AppWidgetHostView;
9import android.appwidget.AppWidgetProviderInfo;
10import android.content.Context;
11import android.content.res.Resources;
12import android.graphics.Rect;
13import android.view.Gravity;
14import android.widget.FrameLayout;
15import android.widget.ImageView;
16
17public 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
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.getMinSpanX(mLauncher);
79        mMinVSpan = info.getMinSpanY(mLauncher);
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
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     */
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
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     */
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     */
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
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
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     */
374    public void commitResize() {
375        resizeWidgetIfNeeded(true);
376        requestLayout();
377    }
378
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
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