I'm currently trying to implement the new recyclerview-selection
APIs from Android Support Library 28.0.0-alpha1, and am running into some issues. My goal is to have a RecyclerView
, with the ability to select multiple rows, show a Contextual Action Bar, and perform actions on them, such as "delete" or "share"
I'll try and furnish enough code to give a good idea of what's going on, but I can always respond with more if necessary.
In my Fragment
which contains the RecyclerView
I'm concerned with, I am initiating a SelectionTracker
, and setting it on my RecyclerView.Adapter
, like so:
private void buildRecyclerView() {
sheetsAdapter = new SheetsAdapter(getContext(), this, sheets);
gridManager = new GridLayoutManager(getContext(), getResources().getInteger(R.integer.grid_span_count));
ItemOffsetDecoration itemDecoration = new ItemOffsetDecoration(getContext(), R.dimen.item_offset);
sheetsRecycler.addItemDecoration(itemDecoration);
sheetsRecycler.setLayoutManager(gridManager);
sheetsRecycler.setAdapter(sheetsAdapter);
sheetsRecycler.setHasFixedSize(true);
SelectionTracker selectionTracker = new SelectionTracker.Builder<>("sheet_selection",
sheetsRecycler,
new StableIdKeyProvider(sheetsRecycler),
new SheetDetailsLookup(sheetsRecycler),
StorageStrategy.createLongStorage())
.withOnContextClickListener(this)
.build();
sheetsAdapter.setSelectionTracker(selectionTracker);
}
This Fragment
also implements OnContextClickListener
, in order to listen for long-clicks on the items in my RecyclerView
:
@Override
public boolean onContextClick(@NonNull MotionEvent e) {
if (actionMode != null) {
return false;
}
// Start the CAB using the ActionMode.Callback defined below
if (getActivity() != null) {
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(actionModeCallback);
}
return true;
}
And it should show my CAB, like this:
private ActionMode.Callback actionModeCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.sheets_cab_menu, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.delete:
Toast.makeText(getContext(), R.string.sheets_delete, Toast.LENGTH_SHORT).show();
mode.finish();
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
}
};
My SheetDetailsLookup
looks like this:
public class SheetDetailsLookup extends ItemDetailsLookup<Long> {
private RecyclerView recyclerView;
SheetDetailsLookup(RecyclerView recyclerView) {
super();
this.recyclerView = recyclerView;
}
@Nullable
@Override
public ItemDetails<Long> getItemDetails(@NonNull MotionEvent e) {
View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
if (view != null) {
RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(view);
if (holder instanceof SheetsAdapter.SheetViewHolder) {
return ((SheetsAdapter.SheetViewHolder) holder).getItemDetails();
}
}
return null;
}
}
And in my SheetViewHolder
, I update the view to show that it has been selected:
if (selectionTracker.isSelected(sheet.uid)) {
layout.setBackgroundResource(R.color.md_grey_700);
} else {
layout.setBackgroundResource(android.R.color.transparent);
}
As well as:
public SheetItemDetails getItemDetails() {
return new SheetItemDetails(getAdapterPosition(), mSheets.get(getAdapterPosition()).uid);
}
Where SheetItemDetails
is simply:
public class SheetItemDetails extends ItemDetailsLookup.ItemDetails<Long> {
private int position;
private Long key;
SheetItemDetails(int position, Long key) {
this.position = position;
this.key = key;
}
@Override
public int getPosition() {
return position;
}
@Nullable
@Override
public Long getSelectionKey() {
return key;
}
}
I've implemented all of the things mentioned in the API specification, but am now running into troubles. My CAB doesn't show up when I select an item... and the app usually crashes. Crashes occur whenever I try to "back out" of selections,and then long-click to start another selection, with this stack trace:
java.lang.IllegalStateException
at android.support.v4.util.Preconditions.checkState(Preconditions.java:130)
at android.support.v4.util.Preconditions.checkState(Preconditions.java:142)
at androidx.recyclerview.selection.GestureSelectionHelper.start(GestureSelectionHelper.java:76)
at androidx.recyclerview.selection.SelectionTracker$Builder$4.run(SelectionTracker.java:742)
at androidx.recyclerview.selection.TouchInputHandler.onLongPress(TouchInputHandler.java:136)
at androidx.recyclerview.selection.GestureRouter.onLongPress(GestureRouter.java:95)
at android.view.GestureDetector.dispatchLongPress(GestureDetector.java:779)
at android.view.GestureDetector.access$200(GestureDetector.java:40)
at android.view.GestureDetector$GestureHandler.handleMessage(GestureDetector.java:293)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:823)
Also, I've now lost the ability to "short-click" on one of my items, to launch a detail view... Which I had working just fine until now.
What have I done wrong?
I recently started looking at this library and got stuck on the same exception. The problem occurs when the SelectionTracker
attempts to get an id from your custom RecyclerView.Adapter
subclass. To fix the problem, first call setHasStableIds(true)
in its constructor. Then override getItemId()
to return an id for the given position parameter.