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