realm-recyclerview-lite
realm-recyclerview-lite copied to clipboard
A RecyclerView for Realm data which supports swiping, drag-and-drop, fast scrolling, and multi-select
realm-recyclerview-lite
realm-recyclerview-lite is an implementation of a RecyclerView which supports Realm data.
It is intended to be a slim library which fulfills the functionality requirements I have some of my open source Android apps, such as Minerva; no more, no less. I decided to release it as a library in case others had similar use cases to mine. Take a look at the table of contents for a better idea of what features are offered.
If you just want to know what's new, the changelog is here.
Please be sure to take a moment to look at the Origin section. You'll find a link to Thorben Primke's realm-recyclerview library, which this library is a heavily reworked derivative of.
And yes...at this point is isn't really "lite" anymore 😉.
Table of Contents
- Installation
- Basic Usage
- Swiping
-
Drag and Drop
- Long Click as the Drag Trigger
- Advanced Swiping and Drag and Drop
- Multi-Select
-
Fast Scrolling
- Handle State Notifications
- Fast Scroller Customization
- Padding
allprojects {
repositories {
jcenter()
}
}
And then add this to your app's build.gradle
file:
dependencies {
compile ('com.bkromhout:realm-recyclerview-lite:{latest version}@aar') {
transitive = true
}
}
Please note that at this time, realm-recyclerview-lite has been tested and is verified to work with Realm 2.0.2. Don't be afraid to try a newer version of Realm, just be sure to open an issue if you run into problems.
realm-recyclerview-lite is compatible with Android API Levels >= 11.
## Basic Usage Start by adding a `RealmRecyclerView` to your layout: ```xml<com.bkromhout.rrvl.RealmRecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Next, create an adapter class which extends RealmRecyclerViewAdapter
.
Here's a slimmed down version of the sample app's ItemAdapter
class:
public class ItemAdapter extends RealmRecyclerViewAdapter<Item, ItemAdapter.ItemVH> {
public ItemAdapter(Context context, RealmResults<Item> realmResults) {
super(context, realmResults);
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
return realmResults.get(position).uniqueId;
}
@Override
public ItemVH onCreateViewHolder(ViewGroup parent, int viewType) {
return new ItemVH(inflater.inflate(R.layout.item_card, parent, false));
}
@Override
public void onBindViewHolder(final ItemVH holder, int position) {
Item item = realmResults.get(position);
holder.name.setText(item.name);
}
class ItemVH extends RecyclerView.ViewHolder {
@Bind(R.id.content)
RelativeLayout content;
@Bind(R.id.drag_handle)
ImageView dragHandle;
@Bind(R.id.name)
TextView name;
@Bind(R.id.delete_button)
ImageButton delete;
public ItemVH(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
}
Any model class you wish to use display using a RealmRecyclerViewAdapter
must implement the UIDModel
interface.
Here's a slimmed down version of the sample app's Item
class:
public class Item extends RealmObject implements UIDModel {
public String name;
@Index
public long position;
@PrimaryKey
public long uniqueId;
public Item() {
}
public Item(String name) {
this.name = name;
this.position = nextPos;
nextPos += GAP;
this.uniqueId = nextUniqueId.getAndIncrement();
}
@Override
public Object getUID() {
// Use our uniqueId field as the UID.
return uniqueId;
}
}
The UIDModel.getUID
method allows a RealmRecyclerViewAdapter
to obtain some value unique to each instance of a model class in order to support predictive animations.
If you're planning to return anything more than the value of a primary key field, as is shown here, I recommend that you read the JavaDoc in the UIDModel
interface class.
Finally, you only need to call RealmRecyclerView.setAdapter
to set your adapter.
A couple more points of note:
-
RealmRecyclerView
supportsLinearLayoutManager
only -
RealmRecyclerView
is not actually aRecyclerView
subclass, it's aFrameLayout
. If you need access to the realRecyclerView
orLinearLayoutManager
instances for some reason, you can use thegetRecyclerView
andgetLayoutManager
methods - When you're done using an adapter (such as when an Activity or Fragment is being destroyed), be sure to call its
close
method to prevent any possible Realm instance leaks
- In our layout, with the
swipe
attribute:
<com.bkromhout.rrvl.RealmRecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:swipe="true"/>
- Or programmatically, with the
RealmRecyclerView.setSwipe
method:
recyclerView.setSwipe(true);
Next, we need to override the onSwiped
method in our adapter, as well as add a single line to our onBindViewHolder
method.
These are the relevant parts from the sample app's ItemAdapter
class:
@Override
public void onBindViewHolder(final ItemVH holder, int position) {
Item item = realmResults.get(position);
holder.name.setText(item.name);
// We set the unique ID as the tag on a view so that we will be able to get it
// in the onSwiped() method.
holder.content.setTag(item.uniqueId);
}
@Override
public void onSwiped(RecyclerView.ViewHolder swiped, int direction) {
// Get the unique ID of the item.
long swipedId = (long) ((ItemVH) swiped).content.getTag()
// Remove the item from Realm.
try (Realm realm = Realm.getDefaultInstance()) {
Item item = realm.where(Item.class).equalTo("uniqueId", swipedId).findFirst();
realm.beginTransaction();
item.deleteFromRealm();
realm.commitTransaction();
}
}
## Drag and Drop
Drag and drop can be a tricky feature to implement in the first place since your data model usually must have some field which keeps track of a position. Combine this with Realm's auto-updating nature, and you can quickly get lost in a sea of troubles. Luckily, I've done most of the work for you 😉.
Keep in mind as you read through these steps that my preferred implementation choices may not line up exactly with yours; I've tried to keep this in mind to allow you maximum flexibility.
First, we need to enable drag and drop functionality. This can be done in two ways:
- In our layout, with the
dragAndDrop
attribute:
<com.bkromhout.rrvl.RealmRecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:dragAndDrop="true"/>
- Or programmatically, with the
RealmRecyclerView.setDragAndDrop
method:
recyclerView.setDragAndDrop(true);
Next, some work needs to be done in our adapter. For drag and drop to work, we need to override the onMove
method. We also need to add a bit more to our overridden onBindViewHolder
method so that our items' drag handle views actually initiate drags when touched.
These are the relevant parts from the sample app's ItemAdapter
class:
@Override
public void onBindViewHolder(final ItemVH holder, int position) {
Item item = realmResults.get(position);
holder.name.setText(item.name);
// We set the unique ID as the tag on a view so that we will be able to get it
// in the onMove() method.
holder.content.setTag(item.uniqueId);
// Grabbing the drag handle should trigger a drag.
holder.dragHandle.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN)
startDragging(holder);
return false;
}
});
}
@Override
public boolean onMove(RecyclerView.ViewHolder dragging, RecyclerView.ViewHolder target) {
// Get positions of items in adapter.
int draggingPos = dragging.getAdapterPosition();
int targetPos = target.getAdapterPosition();
// Get the unique IDs of the items from the tag that we set in onBindViewHolder().
long draggingId = (long) ((ItemVH) dragging).content.getTag();
long targetId = (long) ((ItemVH) target).content.getTag();
// Move the item up or down. The methods in ItemDragHelper will calculate and
// assign a new position value for the item whose uniqueId == draggingId.
if (draggingPos > targetPos) ItemDragHelper.moveItemToBefore(draggingId, targetId);
else ItemDragHelper.moveItemToAfter(draggingId, targetId);
return true;
}
Note how we've ensured that the onMove
method will have access to the value in each Item
's uniqueId
field by storing that value as the tag of the content
view in the view holder.
Please take a moment to look at some of the sample application's classes if you haven't done so yet. Especially relevant here are the Item
model class and the ItemDragHelper
class.
It's vitally important that you understand at least the concept of how the methods in ItemDragHelper
work because you will need to implement something similar. If you're lucky then you may be able to adapt mine for your use. I won't detail it here, but if you want to read a bit more about my ordering scheme, you can read this.
You'll notice that we must handle two cases in the onMove
method:
- An item has been moved up
- An item has been moved down
You should also notice that nowhere in this code, be it the onMove
method above or the methods in ItemDragHelper
, do we call any of the notify*Changed
methods. This is intended, because RealmRecyclerViewAdapter
handles making the correct calls for you when it detects the changes you've made to your data (it relies on a RealmChangeListener
to get these notifications, and if you wish to see how it decides which of the notify*Changed
methods to call, take a look at the RealmRecyclerViewAdapter
class).
While that's great, we also have the option automatically initiating a drag when an item is long clicked; and for some use cases that's really all we need.
To enable this functionality, we can either:
- Add the
longClickTriggersDrag
attribute to our layout:
<com.bkromhout.rrvl.RealmRecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:dragAndDrop="true"
app:longClickTriggersDrag="true"/>
- Or call the
setLongClickTriggersDrag
method on ourRealmRecyclerView
:
recyclerView.setLongClickTriggersDrag(true);
Once enabled, long clicking an item will automatically initiate a drag without you needing to call startDragging
. You can still call it yourself in response to some other interaction if you'd like though.
Note that you do still have to implement the onMove
method.
Similar to onSwiped
and onMove
, these correspond directly to the methods available in the ItemTouchHelper.Callback
Class provided by the Android support libraries.
The SelectionChangeListener
interface will allow an implementing class to be notified when the selection set of an adapter changes.
Additionally, there are void saveInstanceState(Bundle out)
and void restoreInstanceState(Bundle in)
methods which will save and restore the currently selected positions.
You may make use of these how you wish. Here are some things to note, tips, etc:
- The methods which change the set of selected items all call the appropriate
notify*Changed()
methods for you, but they do not actually modify the state of your views. In your overriddenonBindViewHolder
method, you should make a call toisSelected(position);
to check and see if the item at that position is currently selected. -
clearSelections
is automatically called if the adapter is notified by Realm that the data has changed. So you cannot, for example, maintain a set of selected items and do drag-and-drop (At some point I hope to re-work multi-select to remove this limitation) - The
List
returned bygetSelectedRealmObjects
is not managed by Realm - All of these methods are well-documented, any questions which remain should be answered by referring to their JavaDoc.
There are a few different attributes which are associated with the fast scrolling feature:
<com.bkromhout.rrvl.RealmRecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fastScroll="true"
app:autoHideFastScrollHandle="true"
app:handleAutoHideDelay="1000"
app:useFastScrollBubble="true"/>
Here's what the attributes (and their associated methods on RealmRecyclerView
) do
Attribute/Method | Method | Description |
---|---|---|
fastScroll |
setFastScroll |
Turns the fast scroller on. The default vertical scrollbar will be used if it's off. |
autoHideFastScrollHandle |
setAutoHideFastScrollHandle |
For if you want the fast scroller's handle to auto-hide after a delay instead of staying visible. False by default. |
handleAutoHideDelay |
setHandleAutoHideDelay |
For if you want the handle's auto-hide delay to be something other than the default 2000 milliseconds. |
useFastScrollBubble |
setUseFastScrollBubble |
For if you want the fast scroller to display a bubble next to the handle while using it to scroll. False by default. |
Other than the last one, these attributes are all you need to set if you want to have fast scrolling functionality.
To have the fast scroller show a bubble (akin to the stock Android Contacts app), you need to both set that last one to true
as well as have some class implement the BubbleTextProvider
interface, which defines one method, getFastScrollBubbleText
. That method provides the position of the item in the adapter and expects the text which should be shown in the bubble in return.
Our sample application's ItemAdapter
class implements this method like so:
@Override
public String getFastScrollBubbleText(int position) {
return String.valueOf(realmResults.get(position).name.charAt(0));
}
And then in the MainActivity
class, we pass the adapter to the RealmRecyclerView
both as the adapter (of course) and as the bubble text provider at the end of onCreate
, like this:
recyclerView.setAdapter(adapter);
recyclerView.setBubbleTextProvider((ItemAdapter) adapter);
That's all there is to it! Note that while I chose to have the adapter implement the getFastScrollBubbleText
method in my example, you could have some other object implement it if you so choose. Just remember that all you're given to work with is a position, so that object would need to have a copy of the same RealmResults
that your adapter has in the first place.
A good example of this is when you have a FloatingActionButton
on the screen with your RealmRecyclerView
. While you can create a "behavior" which will cause the FloatingActionButton
to automatically show/hide itself as you scroll the RealmRecyclerView
up/down, that behavior class won't pick up on the scrolling done with the fast scroller, only normal scrolling 😞.
For my use case, I wanted the FloatingActionButton
to hide itself when I grabbed the fast scroll handle, so I created the FastScrollHandleStateListener
interface. Here's a snippet showing how that can be done:
@Override
public void onHandleStateChanged(FastScrollerHandleState newState) {
switch (newState) {
case VISIBLE:
case HIDDEN:
case RELEASED:
break;
case PRESSED:
// Hide the FloatingActionButton.
fab.hide();
break;
}
}
While I've only used the PRESSED
state to do something here, you can see that there are a total of four states which you'll be notified of.
Note that the VISIBLE
and HIDDEN
states are only triggered if you have auto-hide on, and they're triggered after the show/hide animation completes.
- In
colors.xml
:
<color name="rrvl_handle_inactive_color">#757575</color>
<color name="rrvl_handle_active_color">#FF4081</color>
<color name="rrvl_bubble_color">#FF4081</color>
<color name="rrvl_bubble_text_color">@android:color/white</color>
- In
dimens.xml
:
<dimen name="rrvl_bubble_text_size">48sp</dimen>
<dimen name="rrvl_handle_margin_end">8dp</dimen>
<dimen name="rrvl_handle_padding_start">8dp</dimen>
## Padding
Since `RealmRecyclerView` is technically a `FrameLayout`, setting its padding directly affects both its underlying `RecyclerView` and the position of the fast scroller.
To counter this, RealmRecyclerView
has a handful of attributes and a couple of methods which let you set padding values to just the underlying RecyclerView
.
The following attributes can be used on a RealmRecyclerView
in XML:
-
rvPadding
-
rvPaddingStart
-
rvPaddingTop
-
rvPaddingEnd
-
rvPaddingBottom
The following methods on RealmRecyclerView
can also be used:
-
setPadding(int padding)
-
setPadding(int start, int top, int end, int bottom)
Things to keep in mind:
- These all default to 0
- Values' units match the frameworks'. That is, the attributes accept any dimension value, while the methods interpret the number as pixels
- The more specific attributes' values will override the value set for
rvPadding
- If the device's API level is below 17, start and end values will be treated as left and right values (respectively)