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