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 17 package androidx.recyclerview.selection; 18 19 import org.jspecify.annotations.NonNull; 20 import org.jspecify.annotations.Nullable; 21 22 import java.util.Iterator; 23 import java.util.LinkedHashMap; 24 import java.util.LinkedHashSet; 25 import java.util.Map; 26 import java.util.Set; 27 28 /** 29 * Object representing a "primary" selection and a "provisional" selection. 30 * 31 * <p> 32 * This class tracks selected items by managing two sets: 33 * 34 * <p> 35 * <b>Primary Selection</b> 36 * 37 * <p> 38 * Primary selection consists of items selected by a user. This represents the selection 39 * "at rest", as the selection does not contains items that are in a "provisional" selected 40 * state created by way of an ongoing gesture or band operation. 41 * 42 * <p> 43 * <b>Provisional Selection</b> 44 * 45 * <p> 46 * Provisional selections are selections which are interim in nature. 47 * 48 * <p> 49 * Provisional selection exists to address issues where a transitory selection might 50 * momentarily intersect with a previously established selection resulting in a some 51 * or all of the established selection being erased. Such situations may arise 52 * when band selection is being performed in "additive" mode (e.g. SHIFT or CTRL is pressed 53 * on the keyboard prior to mouse down), or when there's an active gesture selection 54 * (which can be initiated by long pressing an unselected item while there is an 55 * existing selection). 56 * 57 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types. 58 * @see MutableSelection 59 */ 60 public class Selection<K> implements Iterable<K> { 61 62 // NOTE: Not currently private as DefaultSelectionTracker directly manipulates values. 63 final Set<K> mSelection; 64 final Set<K> mProvisionalSelection; 65 Selection()66 Selection() { 67 mSelection = new LinkedHashSet<>(); 68 mProvisionalSelection = new LinkedHashSet<>(); 69 } 70 71 /** 72 * Used by {@link StorageStrategy} when restoring selection. 73 */ Selection(@onNull Set<K> selection)74 Selection(@NonNull Set<K> selection) { 75 mSelection = selection; 76 mProvisionalSelection = new LinkedHashSet<>(); 77 } 78 79 /** 80 * @return true if the position is currently selected. 81 */ contains(@ullable K key)82 public boolean contains(@Nullable K key) { 83 return mSelection.contains(key) || mProvisionalSelection.contains(key); 84 } 85 86 /** 87 * Returns an {@link Iterator} that iterators over the selection, *excluding* 88 * any provisional selection. 89 * 90 * {@inheritDoc} 91 */ 92 @Override iterator()93 public @NonNull Iterator<K> iterator() { 94 return mSelection.iterator(); 95 } 96 97 /** 98 * @return size of the selection including both final and provisional selected items. 99 */ size()100 public int size() { 101 return mSelection.size() + mProvisionalSelection.size(); 102 } 103 104 /** 105 * @return true if the selection is empty. 106 */ isEmpty()107 public boolean isEmpty() { 108 return mSelection.isEmpty() && mProvisionalSelection.isEmpty(); 109 } 110 111 /** 112 * Sets the provisional selection, which is a temporary selection that can be saved, 113 * canceled, or adjusted at a later time. When a new provision selection is applied, the old 114 * one (if it exists) is abandoned. 115 * 116 * @return Map of ids added or removed. Added ids have a value of true, removed are false. 117 */ setProvisionalSelection(@onNull Set<K> newSelection)118 Map<K, Boolean> setProvisionalSelection(@NonNull Set<K> newSelection) { 119 Map<K, Boolean> delta = new LinkedHashMap<>(); 120 121 for (K key : mProvisionalSelection) { 122 // Mark each item that used to be in the provisional selection 123 // but is not in the new provisional selection. 124 if (!newSelection.contains(key) && !mSelection.contains(key)) { 125 delta.put(key, false); 126 } 127 } 128 129 for (K key : mSelection) { 130 // Mark each item that in the selection but is not in the new 131 // provisional selection. 132 if (!newSelection.contains(key)) { 133 delta.put(key, false); 134 } 135 } 136 137 for (K key : newSelection) { 138 // Mark each item that was not previously in the selection but is in the new 139 // provisional selection. 140 if (!mSelection.contains(key) && !mProvisionalSelection.contains(key)) { 141 delta.put(key, true); 142 } 143 } 144 145 // Now, iterate through the changes and actually add/remove them to/from the current 146 // selection. This could not be done in the previous loops because changing the size of 147 // the selection mid-iteration changes iteration order erroneously. 148 for (Map.Entry<K, Boolean> entry : delta.entrySet()) { 149 K key = entry.getKey(); 150 if (entry.getValue()) { 151 mProvisionalSelection.add(key); 152 } else { 153 mProvisionalSelection.remove(key); 154 } 155 } 156 157 return delta; 158 } 159 160 /** 161 * Saves the existing provisional selection. Once the provisional selection is saved, 162 * subsequent provisional selections which are different from this existing one cannot 163 * cause items in this existing provisional selection to become deselected. 164 */ mergeProvisionalSelection()165 void mergeProvisionalSelection() { 166 mSelection.addAll(mProvisionalSelection); 167 mProvisionalSelection.clear(); 168 } 169 170 /** 171 * Abandons the existing provisional selection so that all items provisionally selected are 172 * now deselected. 173 */ clearProvisionalSelection()174 void clearProvisionalSelection() { 175 mProvisionalSelection.clear(); 176 } 177 178 /** 179 * Adds a new item to the primary selection. 180 * 181 * @return true if the operation resulted in a modification to the selection. 182 */ add(@onNull K key)183 boolean add(@NonNull K key) { 184 return mSelection.add(key); 185 } 186 187 /** 188 * Removes an item from the primary selection. 189 * 190 * @return true if the operation resulted in a modification to the selection. 191 */ remove(@onNull K key)192 boolean remove(@NonNull K key) { 193 return mSelection.remove(key); 194 } 195 196 /** 197 * Clears the primary selection. The provisional selection, if any, is unaffected. 198 */ clear()199 void clear() { 200 mSelection.clear(); 201 } 202 203 /** 204 * Clones primary and provisional selection from supplied {@link Selection}. 205 * Does not copy active range data. 206 */ copyFrom(@onNull Selection<K> source)207 void copyFrom(@NonNull Selection<K> source) { 208 mSelection.clear(); 209 mSelection.addAll(source.mSelection); 210 211 mProvisionalSelection.clear(); 212 mProvisionalSelection.addAll(source.mProvisionalSelection); 213 } 214 215 @Override toString()216 public String toString() { 217 if (size() <= 0) { 218 return "size=0, items=[]"; 219 } 220 221 StringBuilder buffer = new StringBuilder(size() * 28); 222 buffer.append("Selection{") 223 .append("primary{size=" + mSelection.size()) 224 .append(", entries=" + mSelection) 225 .append("}, provisional{size=" + mProvisionalSelection.size()) 226 .append(", entries=" + mProvisionalSelection) 227 .append("}}"); 228 return buffer.toString(); 229 } 230 231 @Override hashCode()232 public int hashCode() { 233 return mSelection.hashCode() ^ mProvisionalSelection.hashCode(); 234 } 235 236 @Override equals(Object other)237 public boolean equals(Object other) { 238 return (this == other) 239 || (other instanceof Selection && isEqualTo((Selection<?>) other)); 240 } 241 isEqualTo(Selection<?> other)242 private boolean isEqualTo(Selection<?> other) { 243 return mSelection.equals(other.mSelection) 244 && mProvisionalSelection.equals(other.mProvisionalSelection); 245 } 246 } 247