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