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.Point;
12import android.graphics.Rect;
13import android.util.AttributeSet;
14import android.view.KeyEvent;
15import android.view.MotionEvent;
16import android.view.View;
17import android.view.ViewGroup;
18
19import com.android.launcher3.accessibility.DragViewStateAnnouncer;
20import com.android.launcher3.dragndrop.DragLayer;
21import com.android.launcher3.util.FocusLogic;
22import com.android.launcher3.widget.LauncherAppWidgetHostView;
23
24public class AppWidgetResizeFrame extends AbstractFloatingView implements View.OnKeyListener {
25    private static final int SNAP_DURATION = 150;
26    private static final float DIMMED_HANDLE_ALPHA = 0f;
27    private static final float RESIZE_THRESHOLD = 0.66f;
28
29    private static final Rect sTmpRect = new Rect();
30
31    // Represents the cell size on the grid in the two orientations.
32    private static Point[] sCellSize;
33
34    private static final int HANDLE_COUNT = 4;
35    private static final int INDEX_LEFT = 0;
36    private static final int INDEX_TOP = 1;
37    private static final int INDEX_RIGHT = 2;
38    private static final int INDEX_BOTTOM = 3;
39
40    private final Launcher mLauncher;
41    private final DragViewStateAnnouncer mStateAnnouncer;
42
43    private final View[] mDragHandles = new View[HANDLE_COUNT];
44
45    private LauncherAppWidgetHostView mWidgetView;
46    private CellLayout mCellLayout;
47    private DragLayer mDragLayer;
48
49    private Rect mWidgetPadding;
50
51    private final int mBackgroundPadding;
52    private final int mTouchTargetWidth;
53
54    private final int[] mDirectionVector = new int[2];
55    private final int[] mLastDirectionVector = new int[2];
56
57    private final IntRange mTempRange1 = new IntRange();
58    private final IntRange mTempRange2 = new IntRange();
59
60    private final IntRange mDeltaXRange = new IntRange();
61    private final IntRange mBaselineX = new IntRange();
62
63    private final IntRange mDeltaYRange = new IntRange();
64    private final IntRange mBaselineY = new IntRange();
65
66    private boolean mLeftBorderActive;
67    private boolean mRightBorderActive;
68    private boolean mTopBorderActive;
69    private boolean mBottomBorderActive;
70
71    private int mResizeMode;
72
73    private int mRunningHInc;
74    private int mRunningVInc;
75    private int mMinHSpan;
76    private int mMinVSpan;
77    private int mDeltaX;
78    private int mDeltaY;
79    private int mDeltaXAddOn;
80    private int mDeltaYAddOn;
81
82    private int mTopTouchRegionAdjustment = 0;
83    private int mBottomTouchRegionAdjustment = 0;
84
85    private int mXDown, mYDown;
86
87    public AppWidgetResizeFrame(Context context) {
88        this(context, null);
89    }
90
91    public AppWidgetResizeFrame(Context context, AttributeSet attrs) {
92        this(context, attrs, 0);
93    }
94
95    public AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr) {
96        super(context, attrs, defStyleAttr);
97
98        mLauncher = Launcher.getLauncher(context);
99        mStateAnnouncer = DragViewStateAnnouncer.createFor(this);
100
101        mBackgroundPadding = getResources()
102                .getDimensionPixelSize(R.dimen.resize_frame_background_padding);
103        mTouchTargetWidth = 2 * mBackgroundPadding;
104    }
105
106    @Override
107    protected void onFinishInflate() {
108        super.onFinishInflate();
109
110        ViewGroup content = (ViewGroup) getChildAt(0);
111        for (int i = 0; i < HANDLE_COUNT; i ++) {
112            mDragHandles[i] = content.getChildAt(i);
113        }
114    }
115
116    public static void showForWidget(LauncherAppWidgetHostView widget, CellLayout cellLayout) {
117        Launcher launcher = Launcher.getLauncher(cellLayout.getContext());
118        AbstractFloatingView.closeAllOpenViews(launcher);
119
120        DragLayer dl = launcher.getDragLayer();
121        AppWidgetResizeFrame frame = (AppWidgetResizeFrame) launcher.getLayoutInflater()
122                .inflate(R.layout.app_widget_resize_frame, dl, false);
123        frame.setupForWidget(widget, cellLayout, dl);
124        ((DragLayer.LayoutParams) frame.getLayoutParams()).customPosition = true;
125
126        dl.addView(frame);
127        frame.mIsOpen = true;
128        frame.snapToWidget(false);
129    }
130
131    private void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout,
132            DragLayer dragLayer) {
133        mCellLayout = cellLayout;
134        mWidgetView = widgetView;
135        LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo)
136                widgetView.getAppWidgetInfo();
137        mResizeMode = info.resizeMode;
138        mDragLayer = dragLayer;
139
140        mMinHSpan = info.minSpanX;
141        mMinVSpan = info.minSpanY;
142
143        mWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(getContext(),
144                widgetView.getAppWidgetInfo().provider, null);
145
146        if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) {
147            mDragHandles[INDEX_TOP].setVisibility(GONE);
148            mDragHandles[INDEX_BOTTOM].setVisibility(GONE);
149        } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) {
150            mDragHandles[INDEX_LEFT].setVisibility(GONE);
151            mDragHandles[INDEX_RIGHT].setVisibility(GONE);
152        }
153
154        // When we create the resize frame, we first mark all cells as unoccupied. The appropriate
155        // cells (same if not resized, or different) will be marked as occupied when the resize
156        // frame is dismissed.
157        mCellLayout.markCellsAsUnoccupiedForView(mWidgetView);
158
159        setOnKeyListener(this);
160    }
161
162    public boolean beginResizeIfPointInRegion(int x, int y) {
163        boolean horizontalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
164        boolean verticalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
165
166        mLeftBorderActive = (x < mTouchTargetWidth) && horizontalActive;
167        mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && horizontalActive;
168        mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) && verticalActive;
169        mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment)
170                && verticalActive;
171
172        boolean anyBordersActive = mLeftBorderActive || mRightBorderActive
173                || mTopBorderActive || mBottomBorderActive;
174
175        if (anyBordersActive) {
176            mDragHandles[INDEX_LEFT].setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
177            mDragHandles[INDEX_RIGHT].setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA);
178            mDragHandles[INDEX_TOP].setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
179            mDragHandles[INDEX_BOTTOM].setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
180        }
181
182        if (mLeftBorderActive) {
183            mDeltaXRange.set(-getLeft(), getWidth() - 2 * mTouchTargetWidth);
184        } else if (mRightBorderActive) {
185            mDeltaXRange.set(2 * mTouchTargetWidth - getWidth(), mDragLayer.getWidth() - getRight());
186        } else {
187            mDeltaXRange.set(0, 0);
188        }
189        mBaselineX.set(getLeft(), getRight());
190
191        if (mTopBorderActive) {
192            mDeltaYRange.set(-getTop(), getHeight() - 2 * mTouchTargetWidth);
193        } else if (mBottomBorderActive) {
194            mDeltaYRange.set(2 * mTouchTargetWidth - getHeight(), mDragLayer.getHeight() - getBottom());
195        } else {
196            mDeltaYRange.set(0, 0);
197        }
198        mBaselineY.set(getTop(), getBottom());
199
200        return anyBordersActive;
201    }
202
203    /**
204     *  Based on the deltas, we resize the frame.
205     */
206    public void visualizeResizeForDelta(int deltaX, int deltaY) {
207        mDeltaX = mDeltaXRange.clamp(deltaX);
208        mDeltaY = mDeltaYRange.clamp(deltaY);
209
210        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
211        mDeltaX = mDeltaXRange.clamp(deltaX);
212        mBaselineX.applyDelta(mLeftBorderActive, mRightBorderActive, mDeltaX, mTempRange1);
213        lp.x = mTempRange1.start;
214        lp.width = mTempRange1.size();
215
216        mDeltaY = mDeltaYRange.clamp(deltaY);
217        mBaselineY.applyDelta(mTopBorderActive, mBottomBorderActive, mDeltaY, mTempRange1);
218        lp.y = mTempRange1.start;
219        lp.height = mTempRange1.size();
220
221        resizeWidgetIfNeeded(false);
222
223        // When the widget resizes in multi-window mode, the translation value changes to maintain
224        // a center fit. These overrides ensure the resize frame always aligns with the widget view.
225        getSnappedRectRelativeToDragLayer(sTmpRect);
226        if (mLeftBorderActive) {
227            lp.width = sTmpRect.width() + sTmpRect.left - lp.x;
228        }
229        if (mTopBorderActive) {
230            lp.height = sTmpRect.height() + sTmpRect.top - lp.y;
231        }
232        if (mRightBorderActive) {
233            lp.x = sTmpRect.left;
234        }
235        if (mBottomBorderActive) {
236            lp.y = sTmpRect.top;
237        }
238
239        requestLayout();
240    }
241
242    private static int getSpanIncrement(float deltaFrac) {
243        return Math.abs(deltaFrac) > RESIZE_THRESHOLD ? Math.round(deltaFrac) : 0;
244    }
245
246    /**
247     *  Based on the current deltas, we determine if and how to resize the widget.
248     */
249    private void resizeWidgetIfNeeded(boolean onDismiss) {
250        float xThreshold = mCellLayout.getCellWidth();
251        float yThreshold = mCellLayout.getCellHeight();
252
253        int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc);
254        int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc);
255
256        if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return;
257
258        mDirectionVector[0] = 0;
259        mDirectionVector[1] = 0;
260
261        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams();
262
263        int spanX = lp.cellHSpan;
264        int spanY = lp.cellVSpan;
265        int cellX = lp.useTmpCoords ? lp.tmpCellX : lp.cellX;
266        int cellY = lp.useTmpCoords ? lp.tmpCellY : lp.cellY;
267
268        // For each border, we bound the resizing based on the minimum width, and the maximum
269        // expandability.
270        mTempRange1.set(cellX, spanX + cellX);
271        int hSpanDelta = mTempRange1.applyDeltaAndBound(mLeftBorderActive, mRightBorderActive,
272                hSpanInc, mMinHSpan, mCellLayout.getCountX(), mTempRange2);
273        cellX = mTempRange2.start;
274        spanX = mTempRange2.size();
275        if (hSpanDelta != 0) {
276            mDirectionVector[0] = mLeftBorderActive ? -1 : 1;
277        }
278
279        mTempRange1.set(cellY, spanY + cellY);
280        int vSpanDelta = mTempRange1.applyDeltaAndBound(mTopBorderActive, mBottomBorderActive,
281                vSpanInc, mMinVSpan, mCellLayout.getCountY(), mTempRange2);
282        cellY = mTempRange2.start;
283        spanY = mTempRange2.size();
284        if (vSpanDelta != 0) {
285            mDirectionVector[1] = mTopBorderActive ? -1 : 1;
286        }
287
288        if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return;
289
290        // We always want the final commit to match the feedback, so we make sure to use the
291        // last used direction vector when committing the resize / reorder.
292        if (onDismiss) {
293            mDirectionVector[0] = mLastDirectionVector[0];
294            mDirectionVector[1] = mLastDirectionVector[1];
295        } else {
296            mLastDirectionVector[0] = mDirectionVector[0];
297            mLastDirectionVector[1] = mDirectionVector[1];
298        }
299
300        if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView,
301                mDirectionVector, onDismiss)) {
302            if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) {
303                mStateAnnouncer.announce(
304                        mLauncher.getString(R.string.widget_resized, spanX, spanY));
305            }
306
307            lp.tmpCellX = cellX;
308            lp.tmpCellY = cellY;
309            lp.cellHSpan = spanX;
310            lp.cellVSpan = spanY;
311            mRunningVInc += vSpanDelta;
312            mRunningHInc += hSpanDelta;
313
314            if (!onDismiss) {
315                updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
316            }
317        }
318        mWidgetView.requestLayout();
319    }
320
321    static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
322            int spanX, int spanY) {
323        getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect);
324        widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top,
325                sTmpRect.right, sTmpRect.bottom);
326    }
327
328    public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect) {
329        if (sCellSize == null) {
330            InvariantDeviceProfile inv = LauncherAppState.getIDP(context);
331
332            // Initiate cell sizes.
333            sCellSize = new Point[2];
334            sCellSize[0] = inv.landscapeProfile.getCellSize();
335            sCellSize[1] = inv.portraitProfile.getCellSize();
336        }
337
338        if (rect == null) {
339            rect = new Rect();
340        }
341        final float density = context.getResources().getDisplayMetrics().density;
342
343        // Compute landscape size
344        int landWidth = (int) ((spanX * sCellSize[0].x) / density);
345        int landHeight = (int) ((spanY * sCellSize[0].y) / density);
346
347        // Compute portrait size
348        int portWidth = (int) ((spanX * sCellSize[1].x) / density);
349        int portHeight = (int) ((spanY * sCellSize[1].y) / density);
350        rect.set(portWidth, landHeight, landWidth, portHeight);
351        return rect;
352    }
353
354    @Override
355    protected void onDetachedFromWindow() {
356        super.onDetachedFromWindow();
357
358        // We are done with resizing the widget. Save the widget size & position to LauncherModel
359        resizeWidgetIfNeeded(true);
360    }
361
362    private void onTouchUp() {
363        int xThreshold = mCellLayout.getCellWidth();
364        int yThreshold = mCellLayout.getCellHeight();
365
366        mDeltaXAddOn = mRunningHInc * xThreshold;
367        mDeltaYAddOn = mRunningVInc * yThreshold;
368        mDeltaX = 0;
369        mDeltaY = 0;
370
371        post(new Runnable() {
372            @Override
373            public void run() {
374                snapToWidget(true);
375            }
376        });
377    }
378
379    /**
380     * Returns the rect of this view when the frame is snapped around the widget, with the bounds
381     * relative to the {@link DragLayer}.
382     */
383    private void getSnappedRectRelativeToDragLayer(Rect out) {
384        float scale = mWidgetView.getScaleToFit();
385
386        mDragLayer.getViewRectRelativeToSelf(mWidgetView, out);
387
388        int width = 2 * mBackgroundPadding
389                + (int) (scale * (out.width() - mWidgetPadding.left - mWidgetPadding.right));
390        int height = 2 * mBackgroundPadding
391                + (int) (scale * (out.height() - mWidgetPadding.top - mWidgetPadding.bottom));
392
393        int x = (int) (out.left - mBackgroundPadding + scale * mWidgetPadding.left);
394        int y = (int) (out.top - mBackgroundPadding + scale * mWidgetPadding.top);
395
396        out.left = x;
397        out.top = y;
398        out.right = out.left + width;
399        out.bottom = out.top + height;
400    }
401
402    private void snapToWidget(boolean animate) {
403        getSnappedRectRelativeToDragLayer(sTmpRect);
404        int newWidth = sTmpRect.width();
405        int newHeight = sTmpRect.height();
406        int newX = sTmpRect.left;
407        int newY = sTmpRect.top;
408
409        // We need to make sure the frame's touchable regions lie fully within the bounds of the
410        // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions
411        // down accordingly to provide a proper touch target.
412        if (newY < 0) {
413            // In this case we shift the touch region down to start at the top of the DragLayer
414            mTopTouchRegionAdjustment = -newY;
415        } else {
416            mTopTouchRegionAdjustment = 0;
417        }
418        if (newY + newHeight > mDragLayer.getHeight()) {
419            // In this case we shift the touch region up to end at the bottom of the DragLayer
420            mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight());
421        } else {
422            mBottomTouchRegionAdjustment = 0;
423        }
424
425        final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
426        if (!animate) {
427            lp.width = newWidth;
428            lp.height = newHeight;
429            lp.x = newX;
430            lp.y = newY;
431            for (int i = 0; i < HANDLE_COUNT; i++) {
432                mDragHandles[i].setAlpha(1.0f);
433            }
434            requestLayout();
435        } else {
436            PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", lp.width, newWidth);
437            PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", lp.height,
438                    newHeight);
439            PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX);
440            PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY);
441            ObjectAnimator oa =
442                    LauncherAnimUtils.ofPropertyValuesHolder(lp, this, width, height, x, y);
443            oa.addUpdateListener(new AnimatorUpdateListener() {
444                public void onAnimationUpdate(ValueAnimator animation) {
445                    requestLayout();
446                }
447            });
448            AnimatorSet set = LauncherAnimUtils.createAnimatorSet();
449            set.play(oa);
450            for (int i = 0; i < HANDLE_COUNT; i++) {
451                set.play(LauncherAnimUtils.ofFloat(mDragHandles[i], ALPHA, 1.0f));
452            }
453
454            set.setDuration(SNAP_DURATION);
455            set.start();
456        }
457
458        setFocusableInTouchMode(true);
459        requestFocus();
460    }
461
462    @Override
463    public boolean onKey(View v, int keyCode, KeyEvent event) {
464        // Clear the frame and give focus to the widget host view when a directional key is pressed.
465        if (FocusLogic.shouldConsume(keyCode)) {
466            close(false);
467            mWidgetView.requestFocus();
468            return true;
469        }
470        return false;
471    }
472
473    private boolean handleTouchDown(MotionEvent ev) {
474        Rect hitRect = new Rect();
475        int x = (int) ev.getX();
476        int y = (int) ev.getY();
477
478        getHitRect(hitRect);
479        if (hitRect.contains(x, y)) {
480            if (beginResizeIfPointInRegion(x - getLeft(), y - getTop())) {
481                mXDown = x;
482                mYDown = y;
483                return true;
484            }
485        }
486        return false;
487    }
488
489    @Override
490    public boolean onControllerTouchEvent(MotionEvent ev) {
491        int action = ev.getAction();
492        int x = (int) ev.getX();
493        int y = (int) ev.getY();
494
495        switch (action) {
496            case MotionEvent.ACTION_DOWN:
497                return handleTouchDown(ev);
498            case MotionEvent.ACTION_MOVE:
499                visualizeResizeForDelta(x - mXDown, y - mYDown);
500                break;
501            case MotionEvent.ACTION_CANCEL:
502            case MotionEvent.ACTION_UP:
503                visualizeResizeForDelta(x - mXDown, y - mYDown);
504                onTouchUp();
505                mXDown = mYDown = 0;
506                break;
507        }
508        return true;
509    }
510
511    @Override
512    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
513        if (ev.getAction() == MotionEvent.ACTION_DOWN && handleTouchDown(ev)) {
514            return true;
515        }
516        close(false);
517        return false;
518    }
519
520    @Override
521    protected void handleClose(boolean animate) {
522        mDragLayer.removeView(this);
523    }
524
525    @Override
526    public void logActionCommand(int command) {
527        // TODO: Log this case.
528    }
529
530    @Override
531    protected boolean isOfType(int type) {
532        return (type & TYPE_WIDGET_RESIZE_FRAME) != 0;
533    }
534
535    /**
536     * A mutable class for describing the range of two int values.
537     */
538    private static class IntRange {
539
540        public int start, end;
541
542        public int clamp(int value) {
543            return Utilities.boundToRange(value, start, end);
544        }
545
546        public void set(int s, int e) {
547            start = s;
548            end = e;
549        }
550
551        public int size() {
552            return end - start;
553        }
554
555        /**
556         * Moves either the start or end edge (but never both) by {@param delta} and  sets the
557         * result in {@param out}
558         */
559        public void applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out) {
560            out.start = moveStart ? start + delta : start;
561            out.end = moveEnd ? end + delta : end;
562        }
563
564        /**
565         * Applies delta similar to {@link #applyDelta(boolean, boolean, int, IntRange)},
566         * with extra conditions.
567         * @param minSize minimum size after with the moving edge should not be shifted any further.
568         *                For eg, if delta = -3 when moving the endEdge brings the size to less than
569         *                minSize, only delta = -2 will applied
570         * @param maxEnd The maximum value to the end edge (start edge is always restricted to 0)
571         * @return the amount of increase when endEdge was moves and the amount of decrease when
572         * the start edge was moved.
573         */
574        public int applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta,
575                int minSize, int maxEnd, IntRange out) {
576            applyDelta(moveStart, moveEnd, delta, out);
577            if (out.start < 0) {
578                out.start = 0;
579            }
580            if (out.end > maxEnd) {
581                out.end = maxEnd;
582            }
583            if (out.size() < minSize) {
584                if (moveStart) {
585                    out.start = out.end - minSize;
586                } else if (moveEnd) {
587                    out.end = out.start + minSize;
588                }
589            }
590            return moveEnd ? out.size() - size() : size() - out.size();
591        }
592    }
593}
594