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