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 com.example.androidx.widget.selection.fancy;
18 
19 import static androidx.core.util.Preconditions.checkArgument;
20 
21 import android.content.Context;
22 import android.net.Uri;
23 import android.view.ViewGroup;
24 
25 import androidx.core.util.Predicate;
26 import androidx.recyclerview.selection.ItemKeyProvider;
27 import androidx.recyclerview.selection.SelectionTracker;
28 import androidx.recyclerview.widget.RecyclerView;
29 
30 import com.example.androidx.Cheeses;
31 
32 import org.jspecify.annotations.NonNull;
33 import org.jspecify.annotations.Nullable;
34 
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.List;
38 
39 final class DemoAdapter extends RecyclerView.Adapter<DemoHolder> {
40 
41     public static final int TYPE_HEADER = 1;
42     public static final int TYPE_ITEM = 2;
43 
44     private final KeyProvider mKeyProvider;
45     private final Context mContext;
46     // Our list of thingies. Our DemoHolder subclasses extract display
47     // values directly from the Uri, so we only need this simple list.
48     // The list also contains entries for alphabetical section headers.
49     private final List<Uri> mCheeses = new ArrayList<>();
50     private boolean mSmallItemLayout;
51     private boolean mAllCheesesEnabled;
52 
53     // This default implementation must be replaced
54     // with a real implementation in #bindSelectionHelper.
55     private Predicate<Uri> mIsSelectedTest = new Predicate<Uri>() {
56         @Override
57         public boolean test(Uri key) {
58             throw new IllegalStateException(
59                     "Adapter must be initialized with SelectionTracker");
60         }
61     };
62 
DemoAdapter(Context context)63     DemoAdapter(Context context) {
64         mContext = context;
65         mKeyProvider = new KeyProvider(mCheeses);
66 
67         // In the fancy edition of selection support we supply access to stable
68         // ids using content URI. Since we can map between position and selection key
69         // at-will we get band selection and range support.
70         setHasStableIds(false);
71     }
72 
getItemKeyProvider()73     ItemKeyProvider<Uri> getItemKeyProvider() {
74         return mKeyProvider;
75     }
76 
77     // Glue together SelectionTracker and the adapter.
bindSelectionTracker(final SelectionTracker<Uri> tracker)78     public void bindSelectionTracker(final SelectionTracker<Uri> tracker) {
79         checkArgument(tracker != null);
80         mIsSelectedTest = new Predicate<Uri>() {
81             @Override
82             public boolean test(Uri key) {
83                 return tracker.isSelected(key);
84             }
85         };
86     }
87 
88     @Override
getItemCount()89     public int getItemCount() {
90         return mCheeses.size();
91     }
92 
93     @Override
getItemId(int position)94     public long getItemId(int position) {
95         return position;
96     }
97 
98     @Override
onBindViewHolder(@onNull DemoHolder holder, int position)99     public void onBindViewHolder(@NonNull DemoHolder holder, int position) {
100         Uri uri = mKeyProvider.getKey(position);
101         holder.update(uri);
102         if (holder instanceof DemoItemHolder) {
103             DemoItemHolder itemHolder = (DemoItemHolder) holder;
104             itemHolder.setSelected(mIsSelectedTest.test(uri));
105             itemHolder.setSmallLayoutMode(mSmallItemLayout);
106         }
107     }
108 
109     @Override
getItemViewType(int position)110     public int getItemViewType(int position) {
111         Uri uri = mKeyProvider.getKey(position);
112         if (Uris.isGroup(uri)) {
113             return TYPE_HEADER;
114         } else if (Uris.isCheese(uri)) {
115             return TYPE_ITEM;
116         }
117 
118         throw new RuntimeException("Unknown view type a position " + position);
119     }
120 
121     @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)122     public DemoHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
123         switch (viewType) {
124             case TYPE_HEADER:
125                 return new DemoHeaderHolder(mContext, parent);
126             case TYPE_ITEM:
127                 return new DemoItemHolder(mContext, parent);
128         }
129         throw new RuntimeException("Unsupported view type" + viewType);
130     }
131 
132     // Creates a list of cheese Uris and section header Uris.
populateCheeses(int maxItemsPerGroup)133     private void populateCheeses(int maxItemsPerGroup) {
134         String group = "-";  // any ol' value other than 'a' will do the trick here.
135         int itemsInGroup = 0;
136 
137         for (String cheese : Cheeses.sCheeseStrings) {
138             String leadingChar = Character.toString(cheese.toLowerCase().charAt(0));
139 
140             // When we find a new leading character insert an artificial
141             // cheese header
142             if (!leadingChar.equals(group)) {
143                 group = leadingChar;
144                 itemsInGroup = 0;
145                 mCheeses.add(Uris.forGroup(group));
146             }
147             if (++itemsInGroup <= maxItemsPerGroup) {
148                 mCheeses.add(Uris.forCheese(group, cheese));
149             }
150         }
151     }
152 
removeItem(Uri key)153     public boolean removeItem(Uri key) {
154         int position = mKeyProvider.getPosition(key);
155         if (position == RecyclerView.NO_POSITION) {
156             return false;
157         }
158 
159         Uri removed = mCheeses.remove(position);
160         notifyItemRemoved(position);
161         return removed != null;
162     }
163 
enableSmallItemLayout(boolean enabled)164     void enableSmallItemLayout(boolean enabled) {
165         mSmallItemLayout = enabled;
166     }
167 
enableAllCheeses(boolean enabled)168     void enableAllCheeses(boolean enabled) {
169         mAllCheesesEnabled = enabled;
170     }
171 
smallItemLayoutEnabled()172     boolean smallItemLayoutEnabled() {
173         return mSmallItemLayout;
174     }
175 
allCheesesEnabled()176     boolean allCheesesEnabled() {
177         return mAllCheesesEnabled;
178     }
179 
180 
181 
refresh()182     void refresh() {
183         mCheeses.clear();
184         populateCheeses(mAllCheesesEnabled ? Integer.MAX_VALUE : 5);
185         notifyDataSetChanged();
186     }
187 
188     /**
189      * When ever possible provide the selection library with a
190      * "SCOPED_MAPPED" ItemKeyProvider. This enables the selection
191      * library to provide ChromeOS friendly features such as mouse-driven
192      * band selection.
193      *
194      * Background: SCOPED_MAPPED providers allow the library to access
195      * an item's key or position independently of how the data is
196      * represented in the RecyclerView. This is useful in that it
197      * allows the library to operate on items that are not currently laid
198      * out in RecyclerView.
199      */
200     static final class KeyProvider extends ItemKeyProvider<Uri> {
201 
202         private final List<Uri> mData;
203 
KeyProvider(List<Uri> data)204         KeyProvider(List<Uri> data) {
205             // Advise the world we can supply ids/position for any item at any time,
206             // not just when visible in RecyclerView.
207             // This enables fancy stuff especially helpful to users with pointy
208             // devices like Chromebooks, or tablets with touch pads
209             super(SCOPE_MAPPED);
210             mData = data;
211         }
212 
213         @Override
getKey(int position)214         public @Nullable Uri getKey(int position) {
215             return mData.get(position);
216         }
217 
218         @Override
getPosition(@onNull Uri key)219         public int getPosition(@NonNull Uri key) {
220             int position = Collections.binarySearch(mData, key);
221             return position >= 0 ? position : RecyclerView.NO_POSITION;
222         }
223     }
224 }
225