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