1package com.android.launcher2;
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.launcher.R;
17
18public 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
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
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     */
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
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     */
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     */
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
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
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     */
378    public void commitResize() {
379        resizeWidgetIfNeeded(true);
380        requestLayout();
381    }
382
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
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