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