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