Implementing Grid Drag-and-Drop in Android RecyclerView Using ItemTouchHelper
The article explains how to implement a performant, draggable grid in Android by using RecyclerView with ItemTouchHelper, detailing event interception, view translation via matrix or translation properties, efficient data swapping, custom item decorations for spacing, and handling drawing order for older Android versions.
In the development of Android applications, grid layouts are popular because they provide a convenient way for users to interact with app menus. Traditional implementations such as GridView , GridLayout , TableLayout and others have become less necessary since RecyclerView offers higher flexibility and extensibility. Most layout effects that used to require separate View components can now be achieved directly with RecyclerView .
The article focuses on three core problems when implementing a draggable grid:
Event handling
Image translation
Data swapping
Event handling – Android’s ItemTouchHelper provides a convenient way to intercept touch events for RecyclerView . The following interface shows the methods that need to be implemented:
public interface OnItemTouchListener {
// Whether RecyclerView should intercept the event
boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
// Handle the event after interception
void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
// Listen for requests to disallow interception
void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
}Image translation – Unlike GridView , which moves the child View itself, RecyclerView keeps the child view static and moves the visual representation using a Matrix (or by modifying translationX / translationY ). This avoids the need for complex view hierarchy adjustments.
Data updating – RecyclerView can swap items without a full requestLayout , which improves performance. The typical swap implementation looks like this:
@Override
public boolean onItemMove(int fromPosition, int toPosition) {
Collections.swap(mDataList, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
return true;
}After a drag operation, the view’s translation and elevation must be cleared:
@Override
public void clearView(View view) {
if (Build.VERSION.SDK_INT >= 21) {
Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);
if (tag instanceof Float) {
ViewCompat.setElevation(view, (Float) tag);
}
view.setTag(R.id.item_touch_helper_previous_elevation, null);
}
view.setTranslationX(0f);
view.setTranslationY(0f);
}The article also explains how to adjust child drawing order for older Android versions (e.g., Android 4.4) using RecyclerView.ChildDrawingOrderCallback and View#getChildDrawingOrder :
private void addChildDrawingOrderCallback() {
if (Build.VERSION.SDK_INT >= 21) {
return; // Lollipop uses elevation
}
if (mChildDrawingOrderCallback == null) {
mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
@Override
public int onGetChildDrawingOrder(int childCount, int i) {
if (mOverdrawChild == null) {
return i;
}
int childPosition = mOverdrawChildPosition;
if (childPosition == -1) {
childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
mOverdrawChildPosition = childPosition;
}
if (i == childCount - 1) {
// Show the dragged view on top
return childPosition;
}
// Shift other views forward
return i < childPosition ? i : i + 1;
}
};
}
mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback);
}For visual spacing, a custom ItemDecoration is used to calculate offsets based on the grid’s column count:
public class SimpleItemDecoration extends RecyclerView.ItemDecoration {
public int delta;
public SimpleItemDecoration(int padding) { delta = padding; }
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
RecyclerView.Adapter adapter = parent.getAdapter();
int viewType = adapter.getItemViewType(position);
if (viewType == Bean.TYPE_GROUP) return;
GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
int cols = layoutManager.getSpanCount();
int current = layoutManager.getSpanSizeLookup().getSpanIndex(position, cols);
int currentCol = current % cols;
int bottomPadding = delta / 2;
if (currentCol == 0) {
outRect.left = 0;
outRect.right = delta / 4;
outRect.bottom = bottomPadding;
} else if (currentCol == cols - 1) {
outRect.left = delta / 4;
outRect.right = 0;
outRect.bottom = bottomPadding;
} else {
outRect.left = delta / 4;
outRect.right = delta / 4;
outRect.bottom = bottomPadding;
}
}
}The main drag‑and‑drop logic is encapsulated in a GridItemTouchCallback that extends ItemTouchHelper.Callback :
public class GridItemTouchCallback extends ItemTouchHelper.Callback {
private final ItemTouchCallback mItemTouchCallback;
public GridItemTouchCallback(ItemTouchCallback itemTouchCallback) {
mItemTouchCallback = itemTouchCallback;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (viewHolder.getItemViewType() == Bean.TYPE_GROUP) {
return 0; // non‑draggable group header
}
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
return makeMovementFlags(dragFlags, 0);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return mItemTouchCallback.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mItemTouchCallback.onItemRemove(viewHolder.getAdapterPosition());
}
@Override
public void onChildDraw(@NonNull Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
@Override
public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
Log.d("GridItemTouch", "dx=" + dX + ", dy=" + dY);
super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}The activity that ties everything together creates the RecyclerView , sets a GridLayoutManager , adds the SimpleItemDecoration , and attaches the ItemTouchHelper :
public class GridDragActivity extends Activity {
RecyclerView mRecyclerView;
RecyclerViewAdapter mAdapter;
private GridLayoutManager mLinearLayoutManager;
private final int spanCount = 5;
private final int padding = 10;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.drag_activity);
mRecyclerView = findViewById(R.id.recyclerView);
mLinearLayoutManager = new GridLayoutManager(this, spanCount, LinearLayoutManager.VERTICAL, false);
mAdapter = new RecyclerViewAdapter();
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(mLinearLayoutManager);
mRecyclerView.addItemDecoration(new SimpleItemDecoration(padding));
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new GridItemTouchCallback(mAdapter));
itemTouchHelper.attachToRecyclerView(mRecyclerView);
// load data (split a source bitmap into a grid of bitmaps) and update the adapter
}
}Finally, the article summarizes that using RecyclerView with ItemTouchHelper provides a powerful, efficient way to achieve grid drag‑and‑drop, with fine‑grained control over event handling, drawing order, and partial UI updates.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.