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 static androidx.annotation.RestrictTo.Scope.LIBRARY; 20 21 import android.view.MotionEvent; 22 23 import androidx.annotation.RestrictTo; 24 import androidx.recyclerview.widget.RecyclerView; 25 26 import org.jspecify.annotations.NonNull; 27 import org.jspecify.annotations.Nullable; 28 29 /** 30 * The Selection library calls {@link #getItemDetails(MotionEvent)} when it needs 31 * access to information about the area and/or {@link ItemDetails} under a {@link MotionEvent}. 32 * Your implementation must negotiate 33 * {@link RecyclerView.ViewHolder ViewHolder} lookup with the 34 * corresponding RecyclerView instance, and the subsequent conversion of the ViewHolder 35 * instance to an {@link ItemDetails} instance. 36 * 37 * <p> 38 * <b>Example</b> 39 * <pre> 40 * final class MyDetailsLookup extends ItemDetailsLookup<Uri> { 41 * 42 * private final RecyclerView mRecyclerView; 43 * 44 * MyDetailsLookup(RecyclerView recyclerView) { 45 * mRecyclerView = recyclerView; 46 * } 47 * 48 * public @Nullable ItemDetails<Uri> getItemDetails(@NonNull MotionEvent e) { 49 * View view = mRecyclerView.findChildViewUnder(e.getX(), e.getY()); 50 * if (view != null) { 51 * ViewHolder holder = mRecyclerView.getChildViewHolder(view); 52 * if (holder instanceof MyHolder) { 53 * return ((MyHolder) holder).getItemDetails(); 54 * } 55 * } 56 * return null; 57 * } 58 *} 59 * </pre> 60 * 61 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types. 62 */ 63 public abstract class ItemDetailsLookup<K> { 64 65 /** 66 * @return true if there is an item at the event coordinates. 67 */ overItem(@onNull MotionEvent e)68 final boolean overItem(@NonNull MotionEvent e) { 69 return getItemPosition(e) != RecyclerView.NO_POSITION; 70 } 71 72 /** 73 * @return true if there is an item w/ a stable ID at the event coordinates. 74 */ 75 @RestrictTo(LIBRARY) overItemWithSelectionKey(@onNull MotionEvent e)76 protected boolean overItemWithSelectionKey(@NonNull MotionEvent e) { 77 return overItem(e) && hasSelectionKey(getItemDetails(e)); 78 } 79 80 /** 81 * @return true if the event coordinates are in an area of the item 82 * that can result in dragging the item. List items frequently have a white 83 * area that is not draggable allowing band selection to be initiated 84 * in that area. 85 */ inItemDragRegion(@onNull MotionEvent e)86 final boolean inItemDragRegion(@NonNull MotionEvent e) { 87 return overItem(e) && getItemDetails(e).inDragRegion(e); 88 } 89 90 /** 91 * @return true if the event coordinates are in a "selection hot spot" 92 * region of an item. Contact in these regions result in immediate 93 * selection, even when there is no existing selection. 94 */ inItemSelectRegion(@onNull MotionEvent e)95 final boolean inItemSelectRegion(@NonNull MotionEvent e) { 96 return overItem(e) && getItemDetails(e).inSelectionHotspot(e); 97 } 98 99 /** 100 * @return the adapter position of the item at the event coordinates. 101 */ getItemPosition(@onNull MotionEvent e)102 final int getItemPosition(@NonNull MotionEvent e) { 103 ItemDetails<?> item = getItemDetails(e); 104 return item != null 105 ? item.getPosition() 106 : RecyclerView.NO_POSITION; 107 } 108 hasSelectionKey(@ullable ItemDetails<?> item)109 private static boolean hasSelectionKey(@Nullable ItemDetails<?> item) { 110 return item != null && item.getSelectionKey() != null; 111 } 112 113 /** 114 * @return the ItemDetails for the item under the event, or null. 115 */ getItemDetails(@onNull MotionEvent e)116 public abstract @Nullable ItemDetails<K> getItemDetails(@NonNull MotionEvent e); 117 118 /** 119 * An ItemDetails implementation provides the selection library with access to information 120 * about a specific RecyclerView item. This class is a key component in controling 121 * the behaviors of the selection library in the context of a specific activity. 122 * 123 * <p> 124 * <b>Selection Hotspot</b> 125 * 126 * <p> 127 * This is an optional feature identifying an area within a view that 128 * is single-tap to select. Ordinarily a single tap on an item when there is no 129 * existing selection will result in that item being activated. If the tap 130 * occurs within the "selection hotspot" the item will instead be selected. 131 * 132 * <p> 133 * See {@link OnItemActivatedListener} for details on handling item activation. 134 * 135 * <p> 136 * <b>Drag Region</b> 137 * 138 * <p> 139 * The selection library provides support for mouse driven band selection. The "lasso" 140 * typically associated with mouse selection can be started only in an empty 141 * area of the RecyclerView (an area where the item position == RecyclerView#NO_POSITION, 142 * or where RecyclerView#findChildViewUnder returns null). But in many instances 143 * the item views presented by RecyclerView will contain areas that may be perceived 144 * by the user as being empty. The user may expect to be able to initiate band 145 * selection in these empty areas. 146 * 147 * <p> 148 * The "drag region" concept exists in large part to accommodate this user expectation. 149 * Drag region is the content in an item view that the user doesn't otherwise 150 * perceive to be empty or part of the background of recycler view. 151 * 152 * Take for example a traditional single column layout where 153 * the view layout width is "match_parent": 154 * <pre> 155 * ------------------------------------------------------- 156 * | [icon] A string label. ...empty space... | 157 * ------------------------------------------------------- 158 * < --- drag region --> < --treated as background--> 159 *</pre> 160 * 161 * <p> 162 * Further more, within a drag region, a mouse click and drag will immediately 163 * initiate drag and drop (if supported by your configuration). 164 * 165 * <p> 166 * As user expectations around touch and mouse input differ substantially, 167 * "drag region" has no effect on handling of touch input. 168 * 169 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types. 170 */ 171 public abstract static class ItemDetails<K> { 172 173 /** 174 * Returns the adapter position of the item. See 175 * {@link RecyclerView.ViewHolder#getAbsoluteAdapterPosition() ViewHolder 176 * .getAbsoluteAdapterPosition} 177 * 178 * @return the position of an item. 179 */ getPosition()180 public abstract int getPosition(); 181 182 /** 183 * @return true if the item has a selection key. 184 */ hasSelectionKey()185 public boolean hasSelectionKey() { 186 return getSelectionKey() != null; 187 } 188 189 /** 190 * @return the selection key of an item. 191 */ getSelectionKey()192 public abstract @Nullable K getSelectionKey(); 193 194 /** 195 * Areas are often included in a view that behave similar to checkboxes, such 196 * as the icon to the left of an email message. "selection 197 * hotspot" provides a mechanism to identify such regions, and for the 198 * library to directly translate taps in these regions into a change 199 * in selection state. 200 * 201 * @return true if the event is in an area of the item that should be 202 * directly interpreted as a user wishing to select the item. This 203 * is useful for checkboxes and other UI affordances focused on enabling 204 * selection. 205 */ inSelectionHotspot(@onNull MotionEvent e)206 public boolean inSelectionHotspot(@NonNull MotionEvent e) { 207 return false; 208 } 209 210 /** 211 * "Item Drag Region" identifies areas of an item that are not considered when the library 212 * evaluates whether or not to initiate band-selection for mouse input. The drag region 213 * will usually correspond to an area of an item that represents user visible content. 214 * Mouse driven band selection operations are only ever initiated in non-drag-regions. 215 * This is a consideration as many layouts may not include empty space between 216 * RecyclerView items where band selection can be initiated. 217 * 218 * <p> 219 * For example. You may present a single column list of contact names in a 220 * RecyclerView instance in which the individual view items expand to fill all 221 * available space. 222 * But within the expanded view item after the contact name there may be empty space that a 223 * user would reasonably expect to initiate band selection. When a MotionEvent occurs 224 * in such an area, you should return identify this as NOT in a drag region. 225 * 226 * <p> 227 * Further more, within a drag region, a mouse click and drag will immediately 228 * initiate drag and drop (if supported by your configuration). 229 * 230 * @return true if the item is in an area of the item that can result in dragging 231 * the item. List items frequently have a white area that is not draggable allowing 232 * mouse driven band selection to be initiated in that area. 233 */ inDragRegion(@onNull MotionEvent e)234 public boolean inDragRegion(@NonNull MotionEvent e) { 235 return false; 236 } 237 238 @Override equals(@ullable Object obj)239 public boolean equals(@Nullable Object obj) { 240 return (obj instanceof ItemDetails) 241 && isEqualTo((ItemDetails<?>) obj); 242 } 243 isEqualTo(@onNull ItemDetails<?> other)244 private boolean isEqualTo(@NonNull ItemDetails<?> other) { 245 K key = getSelectionKey(); 246 boolean sameKeys = false; 247 if (key == null) { 248 sameKeys = other.getSelectionKey() == null; 249 } else { 250 sameKeys = key.equals(other.getSelectionKey()); 251 } 252 return sameKeys && this.getPosition() == other.getPosition(); 253 } 254 255 @Override hashCode()256 public int hashCode() { 257 return getPosition() >>> 8; 258 } 259 } 260 } 261