AppWidgetResizeFrame.java revision 4459d6b4db75c49f25ac6a43925a8ea212113218
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
61    final int SNAP_DURATION = 150;
62    final int BACKGROUND_PADDING = 24;
63    final float DIMMED_HANDLE_ALPHA = 0f;
64    final float RESIZE_THRESHOLD = 0.66f;
65
66    public static final int LEFT = 0;
67    public static final int TOP = 1;
68    public static final int RIGHT = 2;
69    public static final int BOTTOM = 3;
70
71    private Launcher mLauncher;
72
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        mResizeMode = widgetView.getAppWidgetInfo().resizeMode;
81        mDragLayer = dragLayer;
82        mWorkspace = (Workspace) dragLayer.findViewById(R.id.workspace);
83
84        final AppWidgetProviderInfo info = widgetView.getAppWidgetInfo();
85        int[] result = Launcher.getMinSpanForWidget(mLauncher, info);
86        mMinHSpan = result[0];
87        mMinVSpan = result[1];
88
89        setBackgroundResource(R.drawable.widget_resize_frame_holo);
90        setPadding(0, 0, 0, 0);
91
92        LayoutParams lp;
93        mLeftHandle = new ImageView(context);
94        mLeftHandle.setImageResource(R.drawable.widget_resize_handle_left);
95        lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
96                Gravity.LEFT | Gravity.CENTER_VERTICAL);
97        addView(mLeftHandle, lp);
98
99        mRightHandle = new ImageView(context);
100        mRightHandle.setImageResource(R.drawable.widget_resize_handle_right);
101        lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
102                Gravity.RIGHT | Gravity.CENTER_VERTICAL);
103        addView(mRightHandle, lp);
104
105        mTopHandle = new ImageView(context);
106        mTopHandle.setImageResource(R.drawable.widget_resize_handle_top);
107        lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
108                Gravity.CENTER_HORIZONTAL | Gravity.TOP);
109        addView(mTopHandle, lp);
110
111        mBottomHandle = new ImageView(context);
112        mBottomHandle.setImageResource(R.drawable.widget_resize_handle_bottom);
113        lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
114                Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
115        addView(mBottomHandle, lp);
116
117        Rect p = AppWidgetHostView.getDefaultPaddingForWidget(context,
118                widgetView.getAppWidgetInfo().provider, null);
119        mWidgetPaddingLeft = p.left;
120        mWidgetPaddingTop = p.top;
121        mWidgetPaddingRight = p.right;
122        mWidgetPaddingBottom = p.bottom;
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        final float density = mLauncher.getResources().getDisplayMetrics().density;
133        mBackgroundPadding = (int) Math.ceil(density * 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            mDirectionVector[0] = mLeftBorderActive ? -1 : 1;
297        }
298
299        if (mTopBorderActive || mBottomBorderActive) {
300            spanY += vSpanInc;
301            cellY += cellYInc;
302            mDirectionVector[1] = mTopBorderActive ? -1 : 1;
303        }
304
305        if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return;
306
307        if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView,
308                mDirectionVector, onDismiss)) {
309            lp.tmpCellX = cellX;
310            lp.tmpCellY = cellY;
311            lp.cellHSpan = spanX;
312            lp.cellVSpan = spanY;
313            mRunningVInc += vSpanDelta;
314            mRunningHInc += hSpanDelta;
315            if (!onDismiss) {
316                updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
317            }
318        }
319        mWidgetView.requestLayout();
320    }
321
322    static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
323            int spanX, int spanY) {
324        Rect landMetrics = Workspace.getCellLayoutMetrics(launcher, CellLayout.LANDSCAPE);
325        Rect portMetrics = Workspace.getCellLayoutMetrics(launcher, CellLayout.PORTRAIT);
326        final float density = launcher.getResources().getDisplayMetrics().density;
327
328        // Compute landscape size
329        int cellWidth = landMetrics.left;
330        int cellHeight = landMetrics.top;
331        int widthGap = landMetrics.right;
332        int heightGap = landMetrics.bottom;
333        int landWidth = (int) ((spanX * cellWidth + (spanX - 1) * widthGap) / density);
334        int landHeight = (int) ((spanY * cellHeight + (spanY - 1) * heightGap) / density);
335
336        // Compute portrait size
337        cellWidth = portMetrics.left;
338        cellHeight = portMetrics.top;
339        widthGap = portMetrics.right;
340        heightGap = portMetrics.bottom;
341        int portWidth = (int) ((spanX * cellWidth + (spanX - 1) * widthGap) / density);
342        int portHeight = (int) ((spanY * cellHeight + (spanY - 1) * heightGap) / density);
343
344        widgetView.updateAppWidgetSize(null, portWidth, landHeight, landWidth, portHeight);
345    }
346
347    /**
348     * This is the final step of the resize. Here we save the new widget size and position
349     * to LauncherModel and animate the resize frame.
350     */
351    public void commitResize() {
352        resizeWidgetIfNeeded(true);
353        requestLayout();
354    }
355
356    public void onTouchUp() {
357        int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap();
358        int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap();
359
360        mDeltaXAddOn = mRunningHInc * xThreshold;
361        mDeltaYAddOn = mRunningVInc * yThreshold;
362        mDeltaX = 0;
363        mDeltaY = 0;
364
365        post(new Runnable() {
366            @Override
367            public void run() {
368                snapToWidget(true);
369            }
370        });
371    }
372
373    public void snapToWidget(boolean animate) {
374        final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
375        int xOffset = mCellLayout.getLeft() + mCellLayout.getPaddingLeft() - mWorkspace.getScrollX();
376        int yOffset = mCellLayout.getTop() + mCellLayout.getPaddingTop() - mWorkspace.getScrollY();
377
378        int newWidth = mWidgetView.getWidth() + 2 * mBackgroundPadding - mWidgetPaddingLeft -
379                mWidgetPaddingRight;
380        int newHeight = mWidgetView.getHeight() + 2 * mBackgroundPadding - mWidgetPaddingTop -
381                mWidgetPaddingBottom;
382
383        int newX = mWidgetView.getLeft() - mBackgroundPadding + xOffset + mWidgetPaddingLeft;
384        int newY = mWidgetView.getTop() - mBackgroundPadding + yOffset + mWidgetPaddingTop;
385
386        // We need to make sure the frame's touchable regions lie fully within the bounds of the
387        // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions
388        // down accordingly to provide a proper touch target.
389        if (newY < 0) {
390            // In this case we shift the touch region down to start at the top of the DragLayer
391            mTopTouchRegionAdjustment = -newY;
392        } else {
393            mTopTouchRegionAdjustment = 0;
394        }
395        if (newY + newHeight > mDragLayer.getHeight()) {
396            // In this case we shift the touch region up to end at the bottom of the DragLayer
397            mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight());
398        } else {
399            mBottomTouchRegionAdjustment = 0;
400        }
401
402        if (!animate) {
403            lp.width = newWidth;
404            lp.height = newHeight;
405            lp.x = newX;
406            lp.y = newY;
407            mLeftHandle.setAlpha(1.0f);
408            mRightHandle.setAlpha(1.0f);
409            mTopHandle.setAlpha(1.0f);
410            mBottomHandle.setAlpha(1.0f);
411            requestLayout();
412        } else {
413            PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", lp.width, newWidth);
414            PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", lp.height,
415                    newHeight);
416            PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX);
417            PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY);
418            ObjectAnimator oa = LauncherAnimUtils.ofPropertyValuesHolder(lp, width, height, x, y);
419            ObjectAnimator leftOa = LauncherAnimUtils.ofFloat(mLeftHandle, "alpha", 1.0f);
420            ObjectAnimator rightOa = LauncherAnimUtils.ofFloat(mRightHandle, "alpha", 1.0f);
421            ObjectAnimator topOa = LauncherAnimUtils.ofFloat(mTopHandle, "alpha", 1.0f);
422            ObjectAnimator bottomOa = LauncherAnimUtils.ofFloat(mBottomHandle, "alpha", 1.0f);
423            oa.addUpdateListener(new AnimatorUpdateListener() {
424                public void onAnimationUpdate(ValueAnimator animation) {
425                    requestLayout();
426                }
427            });
428            AnimatorSet set = LauncherAnimUtils.createAnimatorSet();
429            if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) {
430                set.playTogether(oa, topOa, bottomOa);
431            } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) {
432                set.playTogether(oa, leftOa, rightOa);
433            } else {
434                set.playTogether(oa, leftOa, rightOa, topOa, bottomOa);
435            }
436
437            set.setDuration(SNAP_DURATION);
438            set.start();
439        }
440    }
441}
442