// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.browser_ui.widget.dragreorder;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import org.chromium.base.ObserverList;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.browser_ui.widget.R;
import java.util.Collections;
import java.util.List;
/**
* Adapter for a {@link RecyclerView} that manages drag-reorderable lists.
*
* @param <T> The type of item that inhabits this adapter's list
*/
public abstract class DragReorderableListAdapter<T> extends RecyclerView.Adapter<ViewHolder> {
protected final Context mContext;
// keep track of the list and list managers
protected ItemTouchHelper mItemTouchHelper;
private ItemTouchHelper.Callback mTouchHelperCallback;
protected List<T> mElements;
protected RecyclerView mRecyclerView;
// keep track of how this item looks
private final int mDraggedBackgroundColor;
private final float mDraggedElevation;
protected DragStateDelegate mDragStateDelegate;
private int mStart;
private ObserverList<DragListener> mListeners = new ObserverList<>();
/** A callback for touch actions on drag-reorderable lists. */
private class DragTouchCallback extends ItemTouchHelper.Callback {
// The view that is being dragged now; null means no view is being dragged now;
private @Nullable ViewHolder mBeingDragged;
@Override
public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
// this method may be called multiple times until the view is dropped
// ensure there is only one bookmark being dragged
if ((mBeingDragged == viewHolder || mBeingDragged == null)
&& isActivelyDraggable(viewHolder)) {
return makeMovementFlags(
ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0 /* swipe flags */);
}
return makeMovementFlags(0, 0);
}
@Override
public boolean onMove(RecyclerView recyclerView, ViewHolder current, ViewHolder target) {
int from = current.getAdapterPosition();
int to = target.getAdapterPosition();
if (from == to) return false;
Collections.swap(mElements, from, to);
notifyItemMoved(from, to);
return true;
}
@Override
public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
// similar to getMovementFlags, this method may be called multiple times
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && mBeingDragged != viewHolder) {
mBeingDragged = viewHolder;
mStart = viewHolder.getAdapterPosition();
onDragStateChange(true);
updateVisualState(true, viewHolder);
}
}
@Override
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
// no need to commit change if recycler view is not attached to window, e.g.:
// dragging is terminated by destroying activity
if (viewHolder.getAdapterPosition() != mStart && recyclerView.isAttachedToWindow()) {
// Commit the position change for the dragged item when it's dropped and
// recyclerView has finished layout computing
recyclerView.post(() -> setOrder(mElements));
}
// the row has been dropped, even though it is possible at same row
mBeingDragged = null;
onDragStateChange(false);
updateVisualState(false, viewHolder);
}
@Override
public boolean isLongPressDragEnabled() {
return mDragStateDelegate.getDragActive();
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(ViewHolder viewHolder, int direction) {
// no-op
}
@Override
public boolean canDropOver(
RecyclerView recyclerView, ViewHolder current, ViewHolder target) {
boolean currentDraggable = isPassivelyDraggable(current);
boolean targetDraggable = isPassivelyDraggable(target);
return currentDraggable && targetDraggable;
}
/**
* Update the visual state of this row.
*
* @param dragged Whether this row is currently being dragged.
* @param viewHolder The DraggableRowViewHolder that is holding this row's content.
*/
private void updateVisualState(boolean dragged, ViewHolder viewHolder) {
DragUtils.createViewDragAnimation(
dragged,
viewHolder.itemView,
mDraggedBackgroundColor,
mDraggedElevation)
.start();
}
}
/** Listens to drag actions in a drag-reorderable list. */
public interface DragListener {
/**
* Called when drag starts or ends.
*
* @param drag True iff drag is currently on.
*/
void onDragStateChange(boolean drag);
}
/**
* Construct a DragReorderableListAdapter.
*
* @param context The context for that this DragReorderableListAdapter occupies.
*/
public DragReorderableListAdapter(Context context) {
mContext = context;
Resources resources = context.getResources();
// Set the alpha to 90% when dragging which is 230/255
mDraggedBackgroundColor =
ColorUtils.setAlphaComponent(
ChromeColors.getSurfaceColor(mContext, R.dimen.default_elevation_1),
resources.getInteger(R.integer.list_item_dragged_alpha));
mDraggedElevation = resources.getDimension(R.dimen.list_item_dragged_elevation);
}
@Override
public int getItemCount() {
return mElements.size();
}
protected T getItemByPosition(int position) {
return mElements.get(position);
}
/** Enables drag & drop interaction on the RecyclerView that this adapter is attached to. */
public void enableDrag() {
if (mItemTouchHelper == null) {
mTouchHelperCallback = new DragTouchCallback();
mItemTouchHelper = new ItemTouchHelper(mTouchHelperCallback);
}
mItemTouchHelper.attachToRecyclerView(mRecyclerView);
}
/** Disables drag & drop interaction. */
public void disableDrag() {
if (mItemTouchHelper != null) mItemTouchHelper.attachToRecyclerView(null);
}
/**
* Sets the order of the items in the drag-reorderable list.
*
* @param order The new order for the items.
*/
protected abstract void setOrder(List<T> order);
/**
* Returns true iff a drag can start on viewHolder.
*
* @param viewHolder The view holder of interest.
* @return True iff a drag can start on viewHolder.
*/
protected abstract boolean isActivelyDraggable(ViewHolder viewHolder);
/**
* Returns true iff another item can be dragged over viewHolder.
*
* @param viewHolder The view holder of interest.
* @return True iff other items can be dragged over viewHolder.
*/
protected abstract boolean isPassivelyDraggable(ViewHolder viewHolder);
/**
* Get the item inside of a view holder.
*
* @param holder The view holder of interest.
* @return The item contained by holder.
*/
protected T getItemByHolder(ViewHolder holder) {
return getItemByPosition(mRecyclerView.getChildLayoutPosition(holder.itemView));
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
mRecyclerView = recyclerView;
}
@Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
mRecyclerView = null;
}
/**
* Set the drag state delegate for this adapter.
* It is expected that the drag state delegate will be set through calling this method.
*
* @param delegate The drag state delegate for this adapter.
*/
protected void setDragStateDelegate(DragStateDelegate delegate) {
mDragStateDelegate = delegate;
}
/** @param l The drag listener to be added. */
public void addDragListener(DragListener l) {
mListeners.addObserver(l);
}
/** @param l The drag listener to be added. */
public void removeDragListener(DragListener l) {
mListeners.removeObserver(l);
}
/**
* Called when drag state changes (drag starts / ends), and notifies all listeners.
*
* @param drag True iff drag is currently on.
*/
private void onDragStateChange(boolean drag) {
for (DragListener l : mListeners) {
l.onDragStateChange(drag);
}
}
/**
* Simulate a drag. All items that are involved in the drag must be visible (no scrolling).
*
* @param start The index of the ViewHolder that you want to drag.
* @param end The index this ViewHolder should be dragged to and dropped at.
*/
public void simulateDragForTests(int start, int end) {
ViewHolder viewHolder = mRecyclerView.findViewHolderForAdapterPosition(start);
mItemTouchHelper.startDrag(viewHolder);
int increment = start < end ? 1 : -1;
int i = start;
while (i != end) {
i += increment;
mTouchHelperCallback.onMove(
mRecyclerView, viewHolder, mRecyclerView.findViewHolderForAdapterPosition(i));
}
mTouchHelperCallback.onSelectedChanged(viewHolder, ItemTouchHelper.ACTION_STATE_IDLE);
mTouchHelperCallback.clearView(mRecyclerView, viewHolder);
}
}