Range.java revision 60dadaeed4f5cee272b575dfde6c02e3506a2fa0
1/* 2 * Copyright 2017 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 17package androidx.recyclerview.selection; 18 19import static androidx.core.util.Preconditions.checkArgument; 20import static androidx.recyclerview.selection.Shared.DEBUG; 21import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; 22 23import android.util.Log; 24 25import androidx.annotation.IntDef; 26import androidx.annotation.NonNull; 27 28import java.lang.annotation.Retention; 29import java.lang.annotation.RetentionPolicy; 30 31/** 32 * Class providing support for managing range selections. 33 */ 34final class Range { 35 36 static final int TYPE_PRIMARY = 0; 37 38 /** 39 * "Provisional" selection represents a overlay on the primary selection. A provisional 40 * selection maybe be eventually added to the primary selection, or it may be abandoned. 41 * 42 * <p> 43 * E.g. BandSelectionHelper creates a provisional selection while a user is actively 44 * selecting items with a band. GestureSelectionHelper creates a provisional selection 45 * while a user is active selecting via gesture. 46 * 47 * <p> 48 * Provisionally selected items are considered to be selected in 49 * {@link Selection#contains(String)} and related methods. A provisional may be abandoned or 50 * merged into the promary selection. 51 * 52 * <p> 53 * A provisional selection may intersect with the primary selection, however clearing the 54 * provisional selection will not affect the primary selection where the two may intersect. 55 */ 56 static final int TYPE_PROVISIONAL = 1; 57 @IntDef({ 58 TYPE_PRIMARY, 59 TYPE_PROVISIONAL 60 }) 61 @Retention(RetentionPolicy.SOURCE) 62 @interface RangeType {} 63 64 private static final String TAG = "Range"; 65 66 private final Callbacks mCallbacks; 67 private final int mBegin; 68 private int mEnd = NO_POSITION; 69 70 /** 71 * Creates a new range anchored at {@code position}. 72 * 73 * @param position 74 * @param callbacks 75 */ 76 Range(int position, @NonNull Callbacks callbacks) { 77 mBegin = position; 78 mCallbacks = callbacks; 79 if (DEBUG) Log.d(TAG, "Creating new Range anchored @ " + position); 80 } 81 82 void extendRange(int position, @RangeType int type) { 83 checkArgument(position != NO_POSITION, "Position cannot be NO_POSITION."); 84 85 if (mEnd == NO_POSITION || mEnd == mBegin) { 86 // Reset mEnd so it can be established in establishRange. 87 mEnd = NO_POSITION; 88 establishRange(position, type); 89 } else { 90 reviseRange(position, type); 91 } 92 } 93 94 private void establishRange(int position, @RangeType int type) { 95 checkArgument(mEnd == NO_POSITION, "End has already been set."); 96 97 mEnd = position; 98 99 if (position > mBegin) { 100 if (DEBUG) log(type, "Establishing initial range at @ " + position); 101 updateRange(mBegin + 1, position, true, type); 102 } else if (position < mBegin) { 103 if (DEBUG) log(type, "Establishing initial range at @ " + position); 104 updateRange(position, mBegin - 1, true, type); 105 } 106 } 107 108 private void reviseRange(int position, @RangeType int type) { 109 checkArgument(mEnd != NO_POSITION, "End must already be set."); 110 checkArgument(mBegin != mEnd, "Beging and end point to same position."); 111 112 if (position == mEnd) { 113 if (DEBUG) log(type, "Ignoring no-op revision for range @ " + position); 114 } 115 116 if (mEnd > mBegin) { 117 reviseAscending(position, type); 118 } else if (mEnd < mBegin) { 119 reviseDescending(position, type); 120 } 121 // the "else" case is covered by checkState at beginning of method. 122 123 mEnd = position; 124 } 125 126 /** 127 * Updates an existing ascending selection. 128 */ 129 private void reviseAscending(int position, @RangeType int type) { 130 if (DEBUG) log(type, "*ascending* Revising range @ " + position); 131 132 if (position < mEnd) { 133 if (position < mBegin) { 134 updateRange(mBegin + 1, mEnd, false, type); 135 updateRange(position, mBegin - 1, true, type); 136 } else { 137 updateRange(position + 1, mEnd, false, type); 138 } 139 } else if (position > mEnd) { // Extending the range... 140 updateRange(mEnd + 1, position, true, type); 141 } 142 } 143 144 private void reviseDescending(int position, @RangeType int type) { 145 if (DEBUG) log(type, "*descending* Revising range @ " + position); 146 147 if (position > mEnd) { 148 if (position > mBegin) { 149 updateRange(mEnd, mBegin - 1, false, type); 150 updateRange(mBegin + 1, position, true, type); 151 } else { 152 updateRange(mEnd, position - 1, false, type); 153 } 154 } else if (position < mEnd) { // Extending the range... 155 updateRange(position, mEnd - 1, true, type); 156 } 157 } 158 159 /** 160 * Try to set selection state for all elements in range. Not that callbacks can cancel 161 * selection of specific items, so some or even all items may not reflect the desired state 162 * after the update is complete. 163 * 164 * @param begin Adapter position for range start (inclusive). 165 * @param end Adapter position for range end (inclusive). 166 * @param selected New selection state. 167 */ 168 private void updateRange( 169 int begin, int end, boolean selected, @RangeType int type) { 170 mCallbacks.updateForRange(begin, end, selected, type); 171 } 172 173 @Override 174 public String toString() { 175 return "Range{begin=" + mBegin + ", end=" + mEnd + "}"; 176 } 177 178 private void log(@RangeType int type, String message) { 179 String opType = type == TYPE_PRIMARY ? "PRIMARY" : "PROVISIONAL"; 180 Log.d(TAG, String.valueOf(this) + ": " + message + " (" + opType + ")"); 181 } 182 183 /* 184 * @see {@link DefaultSelectionTracker#updateForRange(int, int , boolean, int)}. 185 */ 186 abstract static class Callbacks { 187 abstract void updateForRange( 188 int begin, int end, boolean selected, @RangeType int type); 189 } 190} 191