一、目标

拖拽完成文件夹收藏排序。
RecyclerView使用ItemTouchHelper实现拖拽排序

通过拖拽右侧拖拽按钮,实现文件夹收藏排序。

二、体验地址

神马笔记最新版本:【神马笔记 版本1.3.0.apk

三、功能设计

收藏文件夹时,总是添加到第一个位置,显示在列表最上方。

当收藏的文件夹数量增加时,需要对收藏进行排序,方便查找。

常见的几种排序方式:

  1. 手动排序
  2. 名称
  3. ……

这里,我们使用手动排序方式。

四、准备工作

1. ItemTouchHelper

ItemTouchHelper有2个功能:

  1. 拖拽——调整列表项顺序
  2. 侧向滑动——删除列表项
public ItemTouchHelper(Callback callback); 

public void attachToRecyclerView(@Nullable RecyclerView recyclerView); 

public void startDrag(ViewHolder viewHolder); 
public void startSwipe(ViewHolder viewHolder); 

ItemTouchHelper提供给外部调用的接口主要有以上4个。

  1. 创建ItemTouchHelper对象
  2. 附加到RecyclerView
  3. 开始拖拽
  4. 开始侧向滑动

ItemTouchHelper的行为由Callback对象来决定,使用ItemTouchHelper的关键在于实现Callback接口。

2. ItemTouchHelper.Callback

Callback的需要实现的接口,分为4个类别。

  • 必须实现的抽象方法
// 必须通过makeMovementFlags返回值,上、下、左、右四个方向
// SimpleCallback提供了该方法的一个实现版本
public abstract int getMovementFlags(RecyclerView recyclerView,
                                     ViewHolder viewHolder);

// 发生拖拽事件
public abstract boolean onMove(RecyclerView recyclerView,
                               ViewHolder viewHolder, ViewHolder target);

// 发生侧滑事件
public abstract void onSwiped(ViewHolder viewHolder, int direction);
  • 拖拽行为接口
// 是否支持长按触发拖拽
public boolean isLongPressDragEnabled() {
    return true;
}

// 是否可以放置到target位置
public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
                           ViewHolder target) {
    return true;
}

// 触发移动的阈值
public float getMoveThreshold(ViewHolder viewHolder) {
    return .5f;
}
  • 侧滑行为接口
// Item是否支持侧向滑动
public boolean isItemViewSwipeEnabled() {
    return true;
}

// 侧滑事件的滑动距离触发值
public float getSwipeThreshold(ViewHolder viewHolder) {
    return .5f;
}

// 侧滑事件的速度触发值
public float getSwipeEscapeVelocity(float defaultValue) {
    return defaultValue;
}

// ???
public float getSwipeVelocityThreshold(float defaultValue) {
    return defaultValue;
}
  • UI相关接口(包括拖拽及侧滑)
// 选中目标Item时
public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
    if (viewHolder != null) {
        sUICallback.onSelected(viewHolder.itemView);
    }
}

// 动作完成时
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
    sUICallback.clearView(viewHolder.itemView);
}

// ItemDecoration#onDraw
public void onChildDraw(Canvas c, RecyclerView recyclerView,
                        ViewHolder viewHolder,
                        float dX, float dY, int actionState, boolean isCurrentlyActive) {
    sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
                       isCurrentlyActive);
}

// ItemDecoration#onDrawOver
public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
                            ViewHolder viewHolder,
                            float dX, float dY, 
                            int actionState, boolean isCurrentlyActive) {
    sUICallback.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
                           isCurrentlyActive);
}

3. ItemTouchUIUtil

拖拽及侧滑发生时,控制Item的UI。

interface ItemTouchUIUtil {
    void onDraw(Canvas c, RecyclerView recyclerView, View view,
                float dX, float dY, int actionState, boolean isCurrentlyActive);

    void onDrawOver(Canvas c, RecyclerView recyclerView, View view,
                    float dX, float dY, int actionState, boolean isCurrentlyActive);

    void clearView(View view);

    void onSelected(View view);
}

4. ItemTouchUIUtilImpl.BaseImpl

BaseImple在事件发生时,通过设置translationX及translationY移动View的位置。

static class BaseImpl implements ItemTouchUIUtil {

    @Override
    public void clearView(View view) {
        view.setTranslationX(0f);
        view.setTranslationY(0f);
    }

    @Override
    public void onSelected(View view) {

    }

    @Override
    public void onDraw(Canvas c, RecyclerView recyclerView, View view,
                       float dX, float dY, int actionState, boolean isCurrentlyActive) {
        view.setTranslationX(dX);
        view.setTranslationY(dY);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView recyclerView,
                           View view, float dX, float dY, int actionState, boolean isCurrentlyActive) {

    }
}

5. ItemTouchUIUtilImpl.Api21Impl

Api21ImplBaseImpl基础上设置View的elevation实现了阴影效果。

在RecyclerView最大elevation的基础上**+1**。

static class Api21Impl extends BaseImpl {
    @Override
    public void onDraw(Canvas c, RecyclerView recyclerView, View view,
                       float dX, float dY, int actionState, boolean isCurrentlyActive) {
        if (isCurrentlyActive) {
            Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
            if (originalElevation == null) {
                originalElevation = ViewCompat.getElevation(view);
                float newElevation = 1f + findMaxElevation(recyclerView, view);
                ViewCompat.setElevation(view, newElevation);
                view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
            }
        }
        super.onDraw(c, recyclerView, view, dX, dY, actionState, isCurrentlyActive);
    }

    private float findMaxElevation(RecyclerView recyclerView, View itemView) {
        final int childCount = recyclerView.getChildCount();
        float max = 0;
        for (int i = 0; i < childCount; i++) {
            final View child = recyclerView.getChildAt(i);
            if (child == itemView) {
                continue;
            }
            final float elevation = ViewCompat.getElevation(child);
            if (elevation > max) {
                max = elevation;
            }
        }
        return max;
    }

    @Override
    public void clearView(View view) {
        final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);
        if (tag != null && tag instanceof Float) {
            ViewCompat.setElevation(view, (Float) tag);
        }
        view.setTag(R.id.item_touch_helper_previous_elevation, null);
        super.clearView(view);
    }
}

五、组合起来

1. 触发拖拽

// dragBtn按下时即触发拖拽
dragBtn.setOnTouchListener(this::onDragTouch);

// 必须返回false,ItemTouchHelper会接管
boolean onDragTouch(View view, MotionEvent event) {
    parent.requestDrag(this);

    return false;
}

// 调用ItemTouchHelper#startDrag开始拖拽
void requestDrag(RecyclerView.ViewHolder viewHolder) {
    itemTouchHelper.startDrag(viewHolder);
    dividerDecoration.setDragViewHolder(viewHolder);
}

2. SimpleDragCallback

SimpleDragCallback禁用了侧滑功能,为拖拽量身定做。

  • 禁用侧滑功能——isItemViewSwipeEnabled返回false
  • 禁用长按触发拖拽功能——isLongPressDragEnabled返回false
  • 实现onSwiped方法为空方法
  • 将elevation值在增加12,使阴影效果更加明显
public abstract class SimpleDragCallback extends ItemTouchHelper.SimpleCallback {

    protected float elevation;

    public SimpleDragCallback(int dragDirs, int swipeDirs) {
        super(dragDirs, swipeDirs);

        this.elevation = 12.f;
    }

    @Override
    public boolean isLongPressDragEnabled() {
        return false;
    }

    @Override
    public boolean isItemViewSwipeEnabled() {
        return false;
    }

    @Override
    public float getMoveThreshold(RecyclerView.ViewHolder viewHolder) {
        return super.getMoveThreshold(viewHolder);
    }

    @CallSuper
    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {

        // order attention
        {
            View view = viewHolder.itemView;

            final Object tag = view.getTag(R.id.anc_item_drag_previous_elevation);
            if (tag != null && tag instanceof Float) {
                ViewCompat.setElevation(view, (Float) tag);
            }

            view.setTag(R.id.anc_item_drag_previous_elevation, null);
        }

        {
            super.clearView(recyclerView, viewHolder);
        }

    }

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        return super.getMovementFlags(recyclerView, viewHolder);
    }

    @Override
    public boolean canDropOver(RecyclerView recyclerView, RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) {
        return super.canDropOver(recyclerView, current, target);
    }

    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView,
                            RecyclerView.ViewHolder viewHolder, float dX, float dY,
                            int actionState, boolean isCurrentlyActive) {

        // order attention
        {
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
        }

        if (isCurrentlyActive) {
            View view = viewHolder.itemView;

            Object originalElevation = view.getTag(R.id.anc_item_drag_previous_elevation);
            if (originalElevation == null) {
                originalElevation = ViewCompat.getElevation(view);

                float newElevation = elevation + findMaxElevation(recyclerView, view);
                ViewCompat.setElevation(view, newElevation);

                view.setTag(R.id.anc_item_drag_previous_elevation, originalElevation);
            }
        }

    }

    @Override
    public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
        super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
    }

    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        super.onSelectedChanged(viewHolder, actionState);
    }

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {

    }

    private float findMaxElevation(RecyclerView recyclerView, View itemView) {
        final int childCount = recyclerView.getChildCount();
        float max = 0;
        for (int i = 0; i < childCount; i++) {
            final View child = recyclerView.getChildAt(i);
            if (child == itemView) {
                continue;
            }
            final float elevation = ViewCompat.getElevation(child);
            if (elevation > max) {
                max = elevation;
            }
        }
        return max;
    }
}

3. ItemDragCallback

继承自SimpleDragCallback,实现收藏相关的功能。

  • 实现onMove接口,完成文件夹收藏排序
  • 重载canDropOver方法,限定只在文件夹收藏间移动
  • 重载clearView方法,拖拽完成后,保存数据
private static class ItemDragCallback extends SimpleDragCallback {

    HashMap<Class, BiConsumer<RecyclerView.ViewHolder, RecyclerView.ViewHolder>> actionMap;

    EntranceFragment parent;

    public ItemDragCallback(EntranceFragment f) {
        super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);

        this.elevation = 12.f;

        this.parent = f;

        this.actionMap = new HashMap<>();
        actionMap.put(FavoriteViewHolder.class, (viewHolder, target) -> {
            FavoriteEntity from = ((FavoriteViewHolder)viewHolder).getItem();
            FavoriteEntity to = ((FavoriteViewHolder)target).getItem();

            FavoriteEntity ds = FavoriteEntity.obtain();
            ds.swap(from, to);
        });
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {

        {
            BiConsumer consumer = actionMap.get(viewHolder.getClass());
            if (consumer != null) {
                consumer.accept(viewHolder, target);
            }
        }

        {
            int from = viewHolder.getAdapterPosition();
            int to = target.getAdapterPosition();

            recyclerView.getAdapter().notifyItemMoved(from, to);
        }

        return true;
    }

    @Override
    public boolean canDropOver(RecyclerView recyclerView,
                               RecyclerView.ViewHolder current,
                               RecyclerView.ViewHolder target) {
        return (current.getClass() == target.getClass());
    }

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);

        parent.dividerDecoration.setDragViewHolder(null);

        {
            FavoriteEntity ds = FavoriteEntity.obtain();
            ds.save();
        }
    }
}

六、Finally

~仙人有待乘黄鹤~海客无心随白鸥~

分类:

技术点:

相关文章: