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