1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.documentsui.selection; 18 19 import static android.support.v4.util.Preconditions.checkArgument; 20 21 import android.graphics.Point; 22 import android.graphics.Rect; 23 import android.support.annotation.VisibleForTesting; 24 import android.support.v7.widget.RecyclerView; 25 import android.support.v7.widget.RecyclerView.OnScrollListener; 26 import android.util.Log; 27 import android.util.SparseArray; 28 import android.util.SparseBooleanArray; 29 import android.util.SparseIntArray; 30 31 import com.android.documentsui.selection.BandSelectionHelper.BandHost; 32 import com.android.documentsui.selection.SelectionHelper.SelectionPredicate; 33 import com.android.documentsui.selection.SelectionHelper.StableIdProvider; 34 35 import java.util.ArrayList; 36 import java.util.Collections; 37 import java.util.HashSet; 38 import java.util.List; 39 import java.util.Set; 40 41 /** 42 * Provides a band selection item model for views within a RecyclerView. This class queries the 43 * RecyclerView to determine where its items are placed; then, once band selection is underway, 44 * it alerts listeners of which items are covered by the selections. 45 */ 46 final class GridModel { 47 48 // Magical value indicating that a value has not been previously set. primitive null :) 49 static final int NOT_SET = -1; 50 51 // Enum values used to determine the corner at which the origin is located within the 52 private static final int UPPER = 0x00; 53 private static final int LOWER = 0x01; 54 private static final int LEFT = 0x00; 55 private static final int RIGHT = 0x02; 56 private static final int UPPER_LEFT = UPPER | LEFT; 57 private static final int UPPER_RIGHT = UPPER | RIGHT; 58 private static final int LOWER_LEFT = LOWER | LEFT; 59 private static final int LOWER_RIGHT = LOWER | RIGHT; 60 61 private final BandHost mHost; 62 private final StableIdProvider mStableIds; 63 private final SelectionPredicate mSelectionPredicate; 64 65 private final List<SelectionObserver> mOnSelectionChangedListeners = 66 new ArrayList<>(); 67 68 // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed 69 // by their y-offset. For example, if the first column of the view starts at an x-value of 5, 70 // mColumns.get(5) would return an array of positions in that column. Within that array, the 71 // value for key y is the adapter position for the item whose y-offset is y. 72 private final SparseArray<SparseIntArray> mColumns = new SparseArray<>(); 73 74 // List of limits along the x-axis (columns). 75 // This list is sorted from furthest left to furthest right. 76 private final List<Limits> mColumnBounds = new ArrayList<>(); 77 78 // List of limits along the y-axis (rows). Note that this list only contains items which 79 // have been in the viewport. 80 private final List<Limits> mRowBounds = new ArrayList<>(); 81 82 // The adapter positions which have been recorded so far. 83 private final SparseBooleanArray mKnownPositions = new SparseBooleanArray(); 84 85 // Array passed to registered OnSelectionChangedListeners. One array is created and reused 86 // throughout the lifetime of the object. 87 private final Set<String> mSelection = new HashSet<>(); 88 89 // The current pointer (in absolute positioning from the top of the view). 90 private Point mPointer = null; 91 92 // The bounds of the band selection. 93 private RelativePoint mRelativeOrigin; 94 private RelativePoint mRelativePointer; 95 96 private boolean mIsActive; 97 98 // Tracks where the band select originated from. This is used to determine where selections 99 // should expand from when Shift+click is used. 100 private int mPositionNearestOrigin = NOT_SET; 101 102 private final OnScrollListener mScrollListener; 103 104 GridModel( 105 BandHost host, 106 StableIdProvider stableIds, 107 SelectionPredicate selectionPredicate) { 108 109 mHost = host; 110 mStableIds = stableIds; 111 mSelectionPredicate = selectionPredicate; 112 113 mScrollListener = new OnScrollListener() { 114 @Override 115 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 116 GridModel.this.onScrolled(recyclerView, dx, dy); 117 } 118 }; 119 120 mHost.addOnScrollListener(mScrollListener); 121 } 122 123 /** 124 * Stops listening to the view's scrolls. Call this function before discarding a 125 * GridModel object to prevent memory leaks. 126 */ 127 void stopListening() { 128 mHost.removeOnScrollListener(mScrollListener); 129 } 130 131 /** 132 * Start a band select operation at the given point. 133 * @param relativeOrigin The origin of the band select operation, relative to the viewport. 134 * For example, if the view is scrolled to the bottom, the top-left of the viewport 135 * would have a relative origin of (0, 0), even though its absolute point has a higher 136 * y-value. 137 */ 138 void startCapturing(Point relativeOrigin) { 139 recordVisibleChildren(); 140 if (isEmpty()) { 141 // The selection band logic works only if there is at least one visible child. 142 return; 143 } 144 145 mIsActive = true; 146 mPointer = mHost.createAbsolutePoint(relativeOrigin); 147 mRelativeOrigin = new RelativePoint(mPointer); 148 mRelativePointer = new RelativePoint(mPointer); 149 computeCurrentSelection(); 150 notifySelectionChanged(); 151 } 152 153 /** 154 * Ends the band selection. 155 */ 156 void stopCapturing() { 157 mIsActive = false; 158 } 159 160 /** 161 * Resizes the selection by adjusting the pointer (i.e., the corner of the selection 162 * opposite the origin. 163 * @param relativePointer The pointer (opposite of the origin) of the band select operation, 164 * relative to the viewport. For example, if the view is scrolled to the bottom, the 165 * top-left of the viewport would have a relative origin of (0, 0), even though its 166 * absolute point has a higher y-value. 167 */ 168 @VisibleForTesting 169 void resizeSelection(Point relativePointer) { 170 mPointer = mHost.createAbsolutePoint(relativePointer); 171 updateModel(); 172 } 173 174 /** 175 * @return The adapter position for the item nearest the origin corresponding to the latest 176 * band select operation, or NOT_SET if the selection did not cover any items. 177 */ 178 int getPositionNearestOrigin() { 179 return mPositionNearestOrigin; 180 } 181 182 private void onScrolled(RecyclerView recyclerView, int dx, int dy) { 183 if (!mIsActive) { 184 return; 185 } 186 187 mPointer.x += dx; 188 mPointer.y += dy; 189 recordVisibleChildren(); 190 updateModel(); 191 } 192 193 /** 194 * Queries the view for all children and records their location metadata. 195 */ 196 private void recordVisibleChildren() { 197 for (int i = 0; i < mHost.getVisibleChildCount(); i++) { 198 int adapterPosition = mHost.getAdapterPositionAt(i); 199 // Sometimes the view is not attached, as we notify the multi selection manager 200 // synchronously, while views are attached asynchronously. As a result items which 201 // are in the adapter may not actually have a corresponding view (yet). 202 if (mHost.hasView(adapterPosition) && 203 mSelectionPredicate.canSetStateAtPosition(adapterPosition, true) && 204 !mKnownPositions.get(adapterPosition)) { 205 mKnownPositions.put(adapterPosition, true); 206 recordItemData(mHost.getAbsoluteRectForChildViewAt(i), adapterPosition); 207 } 208 } 209 } 210 211 /** 212 * Checks if there are any recorded children. 213 */ 214 private boolean isEmpty() { 215 return mColumnBounds.size() == 0 || mRowBounds.size() == 0; 216 } 217 218 /** 219 * Updates the limits lists and column map with the given item metadata. 220 * @param absoluteChildRect The absolute rectangle for the child view being processed. 221 * @param adapterPosition The position of the child view being processed. 222 */ 223 private void recordItemData(Rect absoluteChildRect, int adapterPosition) { 224 if (mColumnBounds.size() != mHost.getColumnCount()) { 225 // If not all x-limits have been recorded, record this one. 226 recordLimits( 227 mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right)); 228 } 229 230 recordLimits(mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom)); 231 232 SparseIntArray columnList = mColumns.get(absoluteChildRect.left); 233 if (columnList == null) { 234 columnList = new SparseIntArray(); 235 mColumns.put(absoluteChildRect.left, columnList); 236 } 237 columnList.put(absoluteChildRect.top, adapterPosition); 238 } 239 240 /** 241 * Ensures limits exists within the sorted list limitsList, and adds it to the list if it 242 * does not exist. 243 */ 244 private void recordLimits(List<Limits> limitsList, Limits limits) { 245 int index = Collections.binarySearch(limitsList, limits); 246 if (index < 0) { 247 limitsList.add(~index, limits); 248 } 249 } 250 251 /** 252 * Handles a moved pointer; this function determines whether the pointer movement resulted 253 * in a selection change and, if it has, notifies listeners of this change. 254 */ 255 private void updateModel() { 256 RelativePoint old = mRelativePointer; 257 mRelativePointer = new RelativePoint(mPointer); 258 if (old != null && mRelativePointer.equals(old)) { 259 return; 260 } 261 262 computeCurrentSelection(); 263 notifySelectionChanged(); 264 } 265 266 /** 267 * Computes the currently-selected items. 268 */ 269 private void computeCurrentSelection() { 270 if (areItemsCoveredByBand(mRelativePointer, mRelativeOrigin)) { 271 updateSelection(computeBounds()); 272 } else { 273 mSelection.clear(); 274 mPositionNearestOrigin = NOT_SET; 275 } 276 } 277 278 /** 279 * Notifies all listeners of a selection change. Note that this function simply passes 280 * mSelection, so computeCurrentSelection() should be called before this 281 * function. 282 */ 283 private void notifySelectionChanged() { 284 for (SelectionObserver listener : mOnSelectionChangedListeners) { 285 listener.onSelectionChanged(mSelection); 286 } 287 } 288 289 /** 290 * @param rect Rectangle including all covered items. 291 */ 292 private void updateSelection(Rect rect) { 293 int columnStart = 294 Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left)); 295 296 checkArgument(columnStart >= 0, "Rect doesn't intesect any known column."); 297 298 int columnEnd = columnStart; 299 300 for (int i = columnStart; i < mColumnBounds.size() 301 && mColumnBounds.get(i).lowerLimit <= rect.right; i++) { 302 columnEnd = i; 303 } 304 305 int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top)); 306 if (rowStart < 0) { 307 mPositionNearestOrigin = NOT_SET; 308 return; 309 } 310 311 int rowEnd = rowStart; 312 for (int i = rowStart; i < mRowBounds.size() 313 && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) { 314 rowEnd = i; 315 } 316 317 updateSelection(columnStart, columnEnd, rowStart, rowEnd); 318 } 319 320 /** 321 * Computes the selection given the previously-computed start- and end-indices for each 322 * row and column. 323 */ 324 private void updateSelection( 325 int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) { 326 327 if (BandSelectionHelper.DEBUG) { 328 Log.d(BandSelectionHelper.TAG, String.format( 329 "updateSelection: %d, %d, %d, %d", 330 columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex)); 331 } 332 333 mSelection.clear(); 334 for (int column = columnStartIndex; column <= columnEndIndex; column++) { 335 SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit); 336 for (int row = rowStartIndex; row <= rowEndIndex; row++) { 337 // The default return value for SparseIntArray.get is 0, which is a valid 338 // position. Use a sentry value to prevent erroneously selecting item 0. 339 final int rowKey = mRowBounds.get(row).lowerLimit; 340 int position = items.get(rowKey, NOT_SET); 341 if (position != NOT_SET) { 342 String id = mStableIds.getStableId(position); 343 if (id != null) { 344 // The adapter inserts items for UI layout purposes that aren't 345 // associated with files. Those will have a null model ID. 346 // Don't select them. 347 if (canSelect(id)) { 348 mSelection.add(id); 349 } 350 } 351 if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex, 352 row, rowStartIndex, rowEndIndex)) { 353 // If this is the position nearest the origin, record it now so that it 354 // can be returned by endSelection() later. 355 mPositionNearestOrigin = position; 356 } 357 } 358 } 359 } 360 } 361 362 private boolean canSelect(String id) { 363 return mSelectionPredicate.canSetStateForId(id, true); 364 } 365 366 /** 367 * @return Returns true if the position is the nearest to the origin, or, in the case of the 368 * lower-right corner, whether it is possible that the position is the nearest to the 369 * origin. See comment below for reasoning for this special case. 370 */ 371 private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex, 372 int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) { 373 int corner = computeCornerNearestOrigin(); 374 switch (corner) { 375 case UPPER_LEFT: 376 return columnIndex == columnStartIndex && rowIndex == rowStartIndex; 377 case UPPER_RIGHT: 378 return columnIndex == columnEndIndex && rowIndex == rowStartIndex; 379 case LOWER_LEFT: 380 return columnIndex == columnStartIndex && rowIndex == rowEndIndex; 381 case LOWER_RIGHT: 382 // Note that in some cases, the last row will not have as many items as there 383 // are columns (e.g., if there are 4 items and 3 columns, the second row will 384 // only have one item in the first column). This function is invoked for each 385 // position from left to right, so return true for any position in the bottom 386 // row and only the right-most position in the bottom row will be recorded. 387 return rowIndex == rowEndIndex; 388 default: 389 throw new RuntimeException("Invalid corner type."); 390 } 391 } 392 393 /** 394 * Listener for changes in which items have been band selected. 395 */ 396 public static abstract class SelectionObserver { 397 abstract void onSelectionChanged(Set<String> updatedSelection); 398 } 399 400 void addOnSelectionChangedListener(SelectionObserver listener) { 401 mOnSelectionChangedListeners.add(listener); 402 } 403 404 /** 405 * Called when {@link BandSelectionHelper} is finished with a GridModel. 406 */ 407 void onDestroy() { 408 mOnSelectionChangedListeners.clear(); 409 stopListening(); 410 } 411 412 /** 413 * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side 414 * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides 415 * of item columns and the top- and bottom sides of item rows so that it can be determined 416 * whether the pointer is located within the bounds of an item. 417 */ 418 private static class Limits implements Comparable<Limits> { 419 int lowerLimit; 420 int upperLimit; 421 422 Limits(int lowerLimit, int upperLimit) { 423 this.lowerLimit = lowerLimit; 424 this.upperLimit = upperLimit; 425 } 426 427 @Override 428 public int compareTo(Limits other) { 429 return lowerLimit - other.lowerLimit; 430 } 431 432 @Override 433 public int hashCode() { 434 return lowerLimit ^ upperLimit; 435 } 436 437 @Override 438 public boolean equals(Object other) { 439 if (!(other instanceof Limits)) { 440 return false; 441 } 442 443 return ((Limits) other).lowerLimit == lowerLimit && 444 ((Limits) other).upperLimit == upperLimit; 445 } 446 447 @Override 448 public String toString() { 449 return "(" + lowerLimit + ", " + upperLimit + ")"; 450 } 451 } 452 453 /** 454 * The location of a coordinate relative to items. This class represents a general area of the 455 * view as it relates to band selection rather than an explicit point. For example, two 456 * different points within an item are considered to have the same "location" because band 457 * selection originating within the item would select the same items no matter which point 458 * was used. Same goes for points between items as well as those at the very beginning or end 459 * of the view. 460 * 461 * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the 462 * advantage of tying the value to the Limits of items along that axis. This allows easy 463 * selection of items within those Limits as opposed to a search through every item to see if a 464 * given coordinate value falls within those Limits. 465 */ 466 private static class RelativeCoordinate 467 implements Comparable<RelativeCoordinate> { 468 /** 469 * Location describing points after the last known item. 470 */ 471 static final int AFTER_LAST_ITEM = 0; 472 473 /** 474 * Location describing points before the first known item. 475 */ 476 static final int BEFORE_FIRST_ITEM = 1; 477 478 /** 479 * Location describing points between two items. 480 */ 481 static final int BETWEEN_TWO_ITEMS = 2; 482 483 /** 484 * Location describing points within the limits of one item. 485 */ 486 static final int WITHIN_LIMITS = 3; 487 488 /** 489 * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM, 490 * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS. 491 */ 492 final int type; 493 494 /** 495 * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type == 496 * BETWEEN_TWO_ITEMS. 497 */ 498 Limits limitsBeforeCoordinate; 499 500 /** 501 * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS. 502 */ 503 Limits limitsAfterCoordinate; 504 505 // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM. 506 Limits mFirstKnownItem; 507 // Limits of the last known item; only populated when type == AFTER_LAST_ITEM. 508 Limits mLastKnownItem; 509 510 /** 511 * @param limitsList The sorted limits list for the coordinate type. If this 512 * CoordinateLocation is an x-value, mXLimitsList should be passed; otherwise, 513 * mYLimitsList should be pased. 514 * @param value The coordinate value. 515 */ 516 RelativeCoordinate(List<Limits> limitsList, int value) { 517 int index = Collections.binarySearch(limitsList, new Limits(value, value)); 518 519 if (index >= 0) { 520 this.type = WITHIN_LIMITS; 521 this.limitsBeforeCoordinate = limitsList.get(index); 522 } else if (~index == 0) { 523 this.type = BEFORE_FIRST_ITEM; 524 this.mFirstKnownItem = limitsList.get(0); 525 } else if (~index == limitsList.size()) { 526 Limits lastLimits = limitsList.get(limitsList.size() - 1); 527 if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) { 528 this.type = WITHIN_LIMITS; 529 this.limitsBeforeCoordinate = lastLimits; 530 } else { 531 this.type = AFTER_LAST_ITEM; 532 this.mLastKnownItem = lastLimits; 533 } 534 } else { 535 Limits limitsBeforeIndex = limitsList.get(~index - 1); 536 if (limitsBeforeIndex.lowerLimit <= value 537 && value <= limitsBeforeIndex.upperLimit) { 538 this.type = WITHIN_LIMITS; 539 this.limitsBeforeCoordinate = limitsList.get(~index - 1); 540 } else { 541 this.type = BETWEEN_TWO_ITEMS; 542 this.limitsBeforeCoordinate = limitsList.get(~index - 1); 543 this.limitsAfterCoordinate = limitsList.get(~index); 544 } 545 } 546 } 547 548 int toComparisonValue() { 549 if (type == BEFORE_FIRST_ITEM) { 550 return mFirstKnownItem.lowerLimit - 1; 551 } else if (type == AFTER_LAST_ITEM) { 552 return mLastKnownItem.upperLimit + 1; 553 } else if (type == BETWEEN_TWO_ITEMS) { 554 return limitsBeforeCoordinate.upperLimit + 1; 555 } else { 556 return limitsBeforeCoordinate.lowerLimit; 557 } 558 } 559 560 @Override 561 public int hashCode() { 562 return mFirstKnownItem.lowerLimit 563 ^ mLastKnownItem.upperLimit 564 ^ limitsBeforeCoordinate.upperLimit 565 ^ limitsBeforeCoordinate.lowerLimit; 566 } 567 568 @Override 569 public boolean equals(Object other) { 570 if (!(other instanceof RelativeCoordinate)) { 571 return false; 572 } 573 574 RelativeCoordinate otherCoordinate = (RelativeCoordinate) other; 575 return toComparisonValue() == otherCoordinate.toComparisonValue(); 576 } 577 578 @Override 579 public int compareTo(RelativeCoordinate other) { 580 return toComparisonValue() - other.toComparisonValue(); 581 } 582 } 583 584 /** 585 * The location of a point relative to the Limits of nearby items; consists of both an x- and 586 * y-RelativeCoordinateLocation. 587 */ 588 private class RelativePoint { 589 final RelativeCoordinate xLocation; 590 final RelativeCoordinate yLocation; 591 592 RelativePoint(Point point) { 593 this.xLocation = new RelativeCoordinate(mColumnBounds, point.x); 594 this.yLocation = new RelativeCoordinate(mRowBounds, point.y); 595 } 596 597 @Override 598 public int hashCode() { 599 return xLocation.toComparisonValue() 600 ^ yLocation.toComparisonValue(); 601 } 602 603 @Override 604 public boolean equals(Object other) { 605 if (!(other instanceof RelativePoint)) { 606 return false; 607 } 608 609 RelativePoint otherPoint = (RelativePoint) other; 610 return xLocation.equals(otherPoint.xLocation) && yLocation.equals(otherPoint.yLocation); 611 } 612 } 613 614 /** 615 * Generates a rectangle which contains the items selected by the pointer and origin. 616 * @return The rectangle, or null if no items were selected. 617 */ 618 private Rect computeBounds() { 619 Rect rect = new Rect(); 620 rect.left = getCoordinateValue( 621 min(mRelativeOrigin.xLocation, mRelativePointer.xLocation), 622 mColumnBounds, 623 true); 624 rect.right = getCoordinateValue( 625 max(mRelativeOrigin.xLocation, mRelativePointer.xLocation), 626 mColumnBounds, 627 false); 628 rect.top = getCoordinateValue( 629 min(mRelativeOrigin.yLocation, mRelativePointer.yLocation), 630 mRowBounds, 631 true); 632 rect.bottom = getCoordinateValue( 633 max(mRelativeOrigin.yLocation, mRelativePointer.yLocation), 634 mRowBounds, 635 false); 636 return rect; 637 } 638 639 /** 640 * Computes the corner of the selection nearest the origin. 641 * @return 642 */ 643 private int computeCornerNearestOrigin() { 644 int cornerValue = 0; 645 646 if (mRelativeOrigin.yLocation == 647 min(mRelativeOrigin.yLocation, mRelativePointer.yLocation)) { 648 cornerValue |= UPPER; 649 } else { 650 cornerValue |= LOWER; 651 } 652 653 if (mRelativeOrigin.xLocation == 654 min(mRelativeOrigin.xLocation, mRelativePointer.xLocation)) { 655 cornerValue |= LEFT; 656 } else { 657 cornerValue |= RIGHT; 658 } 659 660 return cornerValue; 661 } 662 663 private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) { 664 return first.compareTo(second) < 0 ? first : second; 665 } 666 667 private RelativeCoordinate max(RelativeCoordinate first, RelativeCoordinate second) { 668 return first.compareTo(second) > 0 ? first : second; 669 } 670 671 /** 672 * @return The absolute coordinate (i.e., the x- or y-value) of the given relative 673 * coordinate. 674 */ 675 private int getCoordinateValue( 676 RelativeCoordinate coordinate, List<Limits> limitsList, boolean isStartOfRange) { 677 678 switch (coordinate.type) { 679 case RelativeCoordinate.BEFORE_FIRST_ITEM: 680 return limitsList.get(0).lowerLimit; 681 case RelativeCoordinate.AFTER_LAST_ITEM: 682 return limitsList.get(limitsList.size() - 1).upperLimit; 683 case RelativeCoordinate.BETWEEN_TWO_ITEMS: 684 if (isStartOfRange) { 685 return coordinate.limitsAfterCoordinate.lowerLimit; 686 } else { 687 return coordinate.limitsBeforeCoordinate.upperLimit; 688 } 689 case RelativeCoordinate.WITHIN_LIMITS: 690 return coordinate.limitsBeforeCoordinate.lowerLimit; 691 } 692 693 throw new RuntimeException("Invalid coordinate value."); 694 } 695 696 private boolean areItemsCoveredByBand( 697 RelativePoint first, RelativePoint second) { 698 699 return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) && 700 doesCoordinateLocationCoverItems(first.yLocation, second.yLocation); 701 } 702 703 private boolean doesCoordinateLocationCoverItems( 704 RelativeCoordinate pointerCoordinate, RelativeCoordinate originCoordinate) { 705 706 if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM && 707 originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) { 708 return false; 709 } 710 711 if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM && 712 originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) { 713 return false; 714 } 715 716 if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS && 717 originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS && 718 pointerCoordinate.limitsBeforeCoordinate.equals( 719 originCoordinate.limitsBeforeCoordinate) && 720 pointerCoordinate.limitsAfterCoordinate.equals( 721 originCoordinate.limitsAfterCoordinate)) { 722 return false; 723 } 724 725 return true; 726 } 727 }