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