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