Drag and Swipe in RecyclerView. Part 2: Drag and Drop Controllers, Grid, and Custom Animations
- Transfer
- Tutorial
In the first part, we looked at ItemTouchHelper and the implementation of ItemTouchHelper.Callback , which adds basic drag & drop and swipe-to-dismiss functions in RecyclerView
. In this article, we will continue what was done in the previous one, adding support for the location of elements in the form of a grid, drag controllers list item selection and user swipe animation ( Eng. A swipe).
Drag and Drop Controllers
When creating a list that supports drag & drop , you usually implement the ability to drag items by touch. This contributes to the comprehensibility and usability of the list in “edit mode”, and also recommended by material guidelines. Adding drag-and-drop controllers to our example is fabulously easy.
First update the layout
item ( item_main.xml ).
<?xml version="1.0" encoding="utf-8"?><FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/item"android:layout_width="match_parent"android:layout_height="?listPreferredItemHeight"android:clickable="true"android:focusable="true"android:foreground="?selectableItemBackground"><TextViewandroid:id="@+id/text"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:layout_marginLeft="16dp"android:textAppearance="?android:attr/textAppearanceMedium" /><ImageViewandroid:id="@+id/handle"android:layout_width="?listPreferredItemHeight"android:layout_height="match_parent"android:layout_gravity="center_vertical|right"android:scaleType="center"android:src="@drawable/ic_reorder_grey_500_24dp" /></FrameLayout>
The image used for the drag and drop controller can be found in the Material Design icons and added to the project using the convenient plug- in icon generator in Android Studio.
As briefly mentioned in the last article, you can use ItemTouchHelper.startDrag(ViewHolder)
to programmatically start dragging. So, all we need to do is update ViewHolder
by adding a drag controller, and set up a simple touch handler that will call startDrag()
.
We need an interface to send the event through the chain:
publicinterfaceOnStartDragListener{
/**
* Called when a view is requesting a start of a drag.
*
* @param viewHolder The holder of the view to drag.
*/voidonStartDrag(RecyclerView.ViewHolder viewHolder);
}
Then determine ImageView
for the drag controller in ItemViewHolder
:
publicfinal ImageView handleView;
publicItemViewHolder(View itemView){
super(itemView);
// ...
handleView = (ImageView) itemView.findViewById(R.id.handle);
}
and update RecyclerListAdapter
:
privatefinal OnStartDragListener mDragStartListener;
publicRecyclerListAdapter(OnStartDragListener dragStartListener){
mDragStartListener = dragStartListener;
// ...
}
@OverridepublicvoidonBindViewHolder(final ItemViewHolder holder,
int position){
// ...
holder.handleView.setOnTouchListener(new OnTouchListener() {
@OverridepublicbooleanonTouch(View v, MotionEvent event){
if (MotionEventCompat.getActionMasked(event) ==
MotionEvent.ACTION_DOWN) {
mDragStartListener.onStartDrag(holder);
}
returnfalse;
}
});
}
RecyclerListAdapter
should now look something like this .
All that is left to do is add OnStartDragListener
to the snippet:
publicclassRecyclerListFragmentextendsFragmentimplementsOnStartDragListener{
// ...@OverridepublicvoidonViewCreated(View view, Bundle icicle){
super.onViewCreated(view, icicle);
RecyclerListAdapter a = new RecyclerListAdapter(this);
// ...
}
@OverridepublicvoidonStartDrag(RecyclerView.ViewHolder viewHolder){
mItemTouchHelper.startDrag(viewHolder);
}
}
RecyclerListFragment
should now look like this . Now, when you start the application, you can start dragging by touching the controller.
Select item list
Now in our example, there is no visual indication of an item that is being dragged. Obviously, this should not be so, but it is easy to fix. With the help ItemTouchHelper
you can use the standard effects of highlighting an element On Lollipop and later versions of Android, the backlight “blurs” over the element as it interacts with it; in earlier versions, the element simply changes its color to darkened.
To implement this in our example, just add the background (the property background
) in the root FrameLayout
element item_main.xml or install it in the constructor RecyclerListAdapter.ItemViewHolder . It will look something like this:
It looks cool, but you might want to control even more. One way to do this is to allow the ViewHolder
item state to be processed. For this, it ItemTouchHelper.Callback
provides two more methods:
onSelectedChanged(ViewHolder, int)
called whenever the state of an item changes to drag ( ACTION_STATE_DRAG ) or swipe ( ACTION_STATE_SWIPE ). This is the ideal place to change the state of theview
component to active .clearView(RecyclerView, ViewHolder)
called when the dragview
and drop component is completed, as well as when the swipe is completed ( ACTION_STATE_IDLE ). It usually restores the original state of yourview
component.
And now let's just put it all together.
First create an interface that will be implemented ViewHolders
:
/**
* Notifies a View Holder of relevant callbacks from
* {@link ItemTouchHelper.Callback}.
*/publicinterfaceItemTouchHelperViewHolder{
/**
* Called when the {@link ItemTouchHelper} first registers an
* item as being moved or swiped.
* Implementations should update the item view to indicate
* it's active state.
*/voidonItemSelected();
/**
* Called when the {@link ItemTouchHelper} has completed the
* move or swipe, and the active item state should be cleared.
*/voidonItemClear();
}
Then SimpleItemTouchHelperCallback
implement the appropriate methods:
@OverridepublicvoidonSelectedChanged(RecyclerView.ViewHolder viewHolder,
int actionState){
// We only want the active itemif (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
if (viewHolder instanceof ItemTouchHelperViewHolder) {
ItemTouchHelperViewHolder itemViewHolder =
(ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemSelected();
}
}
super.onSelectedChanged(viewHolder, actionState);
}
@OverridepublicvoidclearView(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder){
super.clearView(recyclerView, viewHolder);
if (viewHolder instanceof ItemTouchHelperViewHolder) {
ItemTouchHelperViewHolder itemViewHolder =
(ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemClear();
}
}
Now it remains only to RecyclerListAdapter.ItemViewHolder
implement ItemTouchHelperViewHolder
:
publicclassItemViewHolderextendsRecyclerView.ViewHolderimplementsItemTouchHelperViewHolder{
// ...@OverridepublicvoidonItemSelected(){
itemView.setBackgroundColor(Color.LTGRAY);
}
@OverridepublicvoidonItemClear(){
itemView.setBackgroundColor(0);
}
}
In this example, we simply add a gray background while the item is active, and then delete it. If your ItemTouchHelper
adapter is closely related, you can easily drop this setting and switch the state of the view
component directly to ItemTouchHelper.Callback
.
Nets
If you now try to use it GridLayoutManager
, you will see that it is not working properly. The cause and solution of the problem are simple: we must inform ours ItemTouchHelper
that we want to support dragging elements left and right. Earlier SimpleItemTouchHelperCallback
we already indicated:
@OverridepublicintgetMovementFlags(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder){
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
The only change needed to support grids is to add the appropriate flags:
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN |
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
However, swipe-to-dismiss is not a very natural behavior for elements in the form of a grid, so it is swipeFlags
most reasonable to zero:
@OverridepublicintgetMovementFlags(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder){
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN |
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
int swipeFlags = 0;
return makeMovementFlags(dragFlags, swipeFlags);
}
To see the working example GridLayoutManager
, see RecyclerGridFragment . Here is what it looks like at startup:
Custom swipe animations
ItemTouchHelper.Callback
provides a really convenient way to fully control the animation while dragging or swiping. Since ItemTouchHelper
this is RecyclerView.ItemDecoration , we can intervene in the view
component drawing process in a similar way. In the next part, we will examine this question in more detail, but for now let's look at a simple example of overriding the default swipe animation to show linear extinction.
@OverridepublicvoidonChildDraw(Canvas c, RecyclerView recyclerView,
ViewHolder viewHolder, float dX, float dY,
int actionState, boolean isCurrentlyActive){
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float width = (float) viewHolder.itemView.getWidth();
float alpha = 1.0f - Math.abs(dX) / width;
viewHolder.itemView.setAlpha(alpha);
viewHolder.itemView.setTranslationX(dX);
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY,
actionState, isCurrentlyActive);
}
}
Parameters dX
and dY
is the current shift relative to the selected view
component, where:
- -1.0f - this is a full swipe from right to left (from
ItemTouchHelper.END
toItemTouchHelper.START
) - 1.0f - this is a full swipe from left to right (from
ItemTouchHelper.START
toItemTouchHelper.END
)
It is important to call super
for anyone actionState
that you do not handle in order for the default animation to start.
In the next part, we will look at an example in which we will control the drawing of the element at the moment of dragging.
Conclusion
In fact, setting up an ItemTouchHelper is quite fun. In order not to increase the volume of this article, I have divided it into several.
Source
View all the code for this series of articles in the Android-ItemTouchHelper-Demo GitHub repository . This article covers commits from ef8f149 to d164fba .