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.core.util.Preconditions.checkArgument;
20 
21 import android.view.MotionEvent;
22 import android.view.View;
23 
24 import androidx.recyclerview.widget.GridLayoutManager;
25 import androidx.recyclerview.widget.LinearLayoutManager;
26 import androidx.recyclerview.widget.RecyclerView;
27 
28 import org.jspecify.annotations.NonNull;
29 
30 /**
31  * Provides a means of controlling when and where band selection can be initiated.
32  *
33  * <p>
34  * Two default implementations are provided: {@link EmptyArea}, and {@link NonDraggableArea}.
35  *
36  * @see SelectionTracker.Builder#withBandPredicate(BandPredicate)
37  */
38 public abstract class BandPredicate {
39 
40     /**
41      * @return true if band selection can be initiated in response to the {@link MotionEvent}.
42      */
canInitiate(@onNull MotionEvent e)43     public abstract boolean canInitiate(@NonNull MotionEvent e);
44 
45     @SuppressWarnings("WeakerAccess") /* synthetic access */
hasSupportedLayoutManager(@onNull RecyclerView recyclerView)46     static boolean hasSupportedLayoutManager(@NonNull RecyclerView recyclerView) {
47         RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
48         return lm instanceof GridLayoutManager
49                 || lm instanceof LinearLayoutManager;
50     }
51 
52     /**
53      * A BandPredicate that allows initiation of band selection only in areas of RecyclerView
54      * that map to {@link RecyclerView#NO_POSITION}. In most cases, this will be the empty areas
55      * between views.
56      *
57      * <p>
58      * Use this implementation to permit band selection only in empty areas
59      * surrounding view items. But be advised that if there is no empy area around
60      * view items, band selection cannot be initiated.
61      */
62     public static final class EmptyArea extends BandPredicate {
63 
64         private final RecyclerView mRecyclerView;
65 
66         /**
67          * @param recyclerView the owner RecyclerView
68          */
EmptyArea(@onNull RecyclerView recyclerView)69         public EmptyArea(@NonNull RecyclerView recyclerView) {
70             checkArgument(recyclerView != null);
71 
72             mRecyclerView = recyclerView;
73         }
74 
75         @Override
canInitiate(@onNull MotionEvent e)76         public boolean canInitiate(@NonNull MotionEvent e) {
77             if (!hasSupportedLayoutManager(mRecyclerView)
78                     || mRecyclerView.hasPendingAdapterUpdates()) {
79                 return false;
80             }
81 
82             View itemView = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
83             int position = itemView != null
84                     ? mRecyclerView.getChildAdapterPosition(itemView)
85                     : RecyclerView.NO_POSITION;
86 
87             return position == RecyclerView.NO_POSITION;
88         }
89     }
90 
91     /**
92      * A BandPredicate that allows initiation of band selection in any area that is not
93      * draggable as determined by consulting
94      * {@link ItemDetailsLookup.ItemDetails#inDragRegion(MotionEvent)}. By default empty
95      * areas (those with a position that maps to {@link RecyclerView#NO_POSITION}
96      * are considered non-draggable.
97      *
98      * <p>
99      * Use this implementation in order to permit band selection in
100      * otherwise empty areas of a View. This is useful especially in
101      * list layouts where there is no empty space surrounding the list items,
102      * and individual list items may contain extra white space (like
103      * in a list of varying length words).
104      *
105      * @see ItemDetailsLookup.ItemDetails#inDragRegion(MotionEvent)
106      */
107     public static final class NonDraggableArea extends BandPredicate {
108 
109         private final RecyclerView mRecyclerView;
110         private final ItemDetailsLookup<?> mDetailsLookup;
111 
112         /**
113          * Creates a new instance.
114          *
115          * @param recyclerView  the owner RecyclerView
116          * @param detailsLookup provides access to item details.
117          */
NonDraggableArea( @onNull RecyclerView recyclerView, @NonNull ItemDetailsLookup<?> detailsLookup)118         public NonDraggableArea(
119                 @NonNull RecyclerView recyclerView, @NonNull ItemDetailsLookup<?> detailsLookup) {
120 
121             checkArgument(recyclerView != null);
122             checkArgument(detailsLookup != null);
123 
124             mRecyclerView = recyclerView;
125             mDetailsLookup = detailsLookup;
126         }
127 
128         @Override
canInitiate(@onNull MotionEvent e)129         public boolean canInitiate(@NonNull MotionEvent e) {
130             if (!hasSupportedLayoutManager(mRecyclerView)
131                     || mRecyclerView.hasPendingAdapterUpdates()) {
132                 return false;
133             }
134 
135             ItemDetailsLookup.ItemDetails<?> details =
136                     mDetailsLookup.getItemDetails(e);
137             return (details == null) || !details.inDragRegion(e);
138         }
139     }
140 }
141