• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.graphics.Point;
12 import android.graphics.Rect;
13 import android.util.AttributeSet;
14 import android.view.KeyEvent;
15 import android.view.MotionEvent;
16 import android.view.View;
17 import android.view.ViewGroup;
18 
19 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
20 import com.android.launcher3.dragndrop.DragLayer;
21 import com.android.launcher3.util.FocusLogic;
22 import com.android.launcher3.widget.LauncherAppWidgetHostView;
23 
24 public 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 
AppWidgetResizeFrame(Context context)87     public AppWidgetResizeFrame(Context context) {
88         this(context, null);
89     }
90 
AppWidgetResizeFrame(Context context, AttributeSet attrs)91     public AppWidgetResizeFrame(Context context, AttributeSet attrs) {
92         this(context, attrs, 0);
93     }
94 
AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr)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
onFinishInflate()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 
showForWidget(LauncherAppWidgetHostView widget, CellLayout cellLayout)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 
setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer)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 
beginResizeIfPointInRegion(int x, int y)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      */
visualizeResizeForDelta(int deltaX, int deltaY)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 
getSpanIncrement(float deltaFrac)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      */
resizeWidgetIfNeeded(boolean onDismiss)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 
updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher, int spanX, int spanY)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 
getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect)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
onDetachedFromWindow()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 
onTouchUp()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      */
getSnappedRectRelativeToDragLayer(Rect out)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 
snapToWidget(boolean animate)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
onKey(View v, int keyCode, KeyEvent event)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 
handleTouchDown(MotionEvent ev)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
onControllerTouchEvent(MotionEvent ev)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
onControllerInterceptTouchEvent(MotionEvent ev)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
handleClose(boolean animate)521     protected void handleClose(boolean animate) {
522         mDragLayer.removeView(this);
523     }
524 
525     @Override
logActionCommand(int command)526     public void logActionCommand(int command) {
527         // TODO: Log this case.
528     }
529 
530     @Override
isOfType(int type)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 
clamp(int value)542         public int clamp(int value) {
543             return Utilities.boundToRange(value, start, end);
544         }
545 
set(int s, int e)546         public void set(int s, int e) {
547             start = s;
548             end = e;
549         }
550 
size()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          */
applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out)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          */
applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta, int minSize, int maxEnd, IntRange out)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