(Android)手把手教你实现-菜单拖拽排序、删除、添加(仿 IOS 应用删除效果)

425 阅读5分钟

现在的 APP 很多都有功能区菜单或者 Tab 分类栏,而且它的类别都很多,这就需要用户自己编辑自己常用的菜单或者 Tab 放在常用的位置上。一般它们都会含有拖拽排序、删除、添加的功能,同时在执行这些操作的时候还会伴有一些动画

这篇文章就来介绍一下这个功能的实现


一、效果展示

1662028379310.gif

二、功能分析

  1. 区位

    包含上下两个区位,上面的是展示已经显示的分类,下面的展示未显示的分类

  2. 模式

    分为编辑和展示模式。编辑模式下,上面的区域显示删除按钮同时展示抖动动画,提示用户可以对这个区域进行删除操作和拖动排序;下面区域展示添加按钮,提示用户可以将分类添加到展示区域

  3. 拖动

    上面的区域长按某个分类可以实现对该分类的拖动排序,同时可以限制哪些分类的位置不可编辑

  4. 删除、添加

    编辑模式下,点击上区域中每个分类的❌,可以将分类删除,放到下面未显示区域。点击下区域中每个分类的➕,可以将分类添加到显示区域

三、实现过程

  1. 首先对于两个部分的菜单列表,使用两个 RecyclerView 实现,这一点相信都没啥问题

  2. 编辑模式的抖动动画

    对于动画,之前想着是放在外部统一处理控制,但是后来发现这样做了之后,当拖动或者删除、添加 Item 后刷新列表动画会被取消,这显然不太合理。

//不是太不合理的做法
/*** 控制 RecycleView 动画*/
private void startRecycleViewAnimation(boolean startAnim) {
  for (int i = 0; i < mAdapter.getItems().size(); i++) {
      //遍历每个ViewHodler
      ListLoadMoreAdapter.ListLoadMoreVH viewHolder = (ListLoadMoreAdapter.ListLoadMoreVH) getBinding().rvLoadMore.findViewHolderForAdapterPosition(i);
      if (viewHolder == null) return;
      //拿到每个人 Item 的根布局
      View view = viewHolder.binding.getRoot();
      Animation animation =  view.getAnimation();
      if (startAnim) {//开启动画
          if (animation == null) {
              float height = view.getHeight() / 2f;
              float width = view.getWidth() / 2f;
              animation = animation(width,height);
              view.startAnimation(animation);
          }else{
              animation.start();
          }
      } else {//关闭动画
          if (animation != null) {
              animation.cancel();
          }
      }
  }
}

/*** 创建一个动画*/
private Animation animation(float centerX,float centerY){
  RotateAnimation rotateAnimation = new RotateAnimation(-2, 2, centerX, centerY);
  rotateAnimation.setRepeatMode(Animation.REVERSE);
  rotateAnimation.setRepeatCount(-1);
  rotateAnimation.setDuration(100);
  return rotateAnimation;
}

合理的做法是直接在 Adapter 的 onBindViewHolder 中直接对单个 View 做动画处理的判断

public class HomeChannelAdapter extends RecyclerView.Adapter<HomeChannelAdapter.HomeChannelViewHolder>{
  ...
 
  @Override
  public void onBindViewHolder(@NonNull HomeChannelViewHolder holder, int position) {
      holder.bind(mData.get(position), mIsAddOptional, mPopup, position);
  }
  ...
  public static class HomeChannelViewHolder extends RecyclerView.ViewHolder {
  
      public void bind(TitleTypeBean item, boolean isAddOptional, HomeChannelPopup popup, int position) {
          startRecycleViewAnimation(popup.getIsEditMode().get(), isAddOptional);
      }
      
      /*** 控制 RecycleView 动画*/
      private void startRecycleViewAnimation(boolean isEdit, boolean isAdd) {
          if (isAdd) return;
          if (mItem != null) {//如果当前的 item 是不可操作的就不显示动画
              int channelId = mItem.getChannelId();
              if (mPopup.mNoOptChannels.contains(channelId)) {
                  return;
              }
          }
          View view = mBinding.getRoot();
          Animation animation = view.getAnimation();
          if (isEdit) {
              if (animation == null) {
                  animation = animation();
                  view.startAnimation(animation);
              } else {
                  animation.cancel();
                  animation.start();
              }
          } else {
              if (animation != null) {
                  animation.cancel();
              }
          }

      }

      /*** 创建一个动画*/
      private Animation animation() {
          RotateAnimation rotateAnimation = new RotateAnimation(-2, 2, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
          rotateAnimation.setRepeatMode(Animation.REVERSE);
          rotateAnimation.setRepeatCount(-1);
          rotateAnimation.setDuration(150);
          return rotateAnimation;
      }
  }
}

代码很简单,应该不需要过多的解释

  1. 拖拽排序

    关于拖拽,SDK 里提供给我们 ItemTouchHelper 和 ItemTouchHelper.Callback 这两个类。按文档解释来看 ItemTouchHelper 就是帮我们来处理 RecyclerView 的拖拽、滑动、删除的。ItemTouchHelper.Callback 是ItemTouchHelper 和 我们应用之间的契约类,用来控制每一个 ViewHolder 上的触摸行为是否能够有效,以及当我们进行这些操作时收到的回调

//系统源码
public abstract static class Callback {
  
/**
 * 简单的来说就是返回一个标识位,用来控制哪些方向上可以有触摸操作
 * Should return a composite flag which defines the enabled move directions in each state
 * (idle, swiping, dragging).
 * <p>
 * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int,
 * int)}
 * or {@link #makeFlag(int, int)}.
 * <p>
 * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next
 * 8 bits are for SWIPE state and third 8 bits are for DRAG state.
 * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in
 * {@link ItemTouchHelper}.
 * <p>
 * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to
 * swipe by swiping RIGHT, you can return:
 * <pre>
 *      makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT);
 * </pre>
 * This means, allow right movement while IDLE and allow right and left movement while
 * swiping.
 *
 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached.
 * @param viewHolder   The ViewHolder for which the movement information is necessary.
 * @return flags specifying which movements are allowed on this ViewHolder.
 * @see #makeMovementFlags(int, int)
 * @see #makeFlag(int, int)
 */
public abstract int getMovementFlags(@NonNull RecyclerView recyclerView,
        @NonNull ViewHolder viewHolder);
       

/**
 * 简单来说就是当我们对 ViewHolder 进行操作时的一个回调方法,返回当前操作的 ViewHolder 和 目标 ViewHolder
 * Called when ItemTouchHelper wants to move the dragged item from its old position to
 * the new position.
 * <p>
 * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved
 * to the adapter position of {@code target} ViewHolder
 * ({@link ViewHolder#getAdapterPosition()
 * ViewHolder#getAdapterPosition()}).
 * <p>
 * If you don't support drag & drop, this method will never be called.
 *
 * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to.
 * @param viewHolder   The ViewHolder which is being dragged by the user.
 * @param target       The ViewHolder over which the currently active item is being
 *                     dragged.
 * @return True if the {@code viewHolder} has been moved to the adapter position of
 * {@code target}.
 * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int)
 */
public abstract boolean onMove(@NonNull RecyclerView recyclerView,
        @NonNull ViewHolder viewHolder, @NonNull ViewHolder target);

}

有了上面的两个方法我们便可以轻而易举的实现上面的拖拽排序及固定位置不可操作的功能。

话不多说直接上代码

/**
 * 拖动帮助类
 */
private class MyItemTouchHelperCallBack extends ItemTouchHelper.Callback {

    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        int dragFlag = 0;
        if (viewHolder instanceof HomeChannelAdapter.HomeChannelViewHolder) {
            TitleTypeBean bean = ((HomeChannelAdapter.HomeChannelViewHolder) viewHolder).getItem();
            if (bean == null || !mNoOptChannels.contains(bean.getChannelId())) {//只有是可操作的 item 才可以拖拽
                dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
            }
        }
        return makeMovementFlags(dragFlag, 0);
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        if (target instanceof HomeChannelAdapter.HomeChannelViewHolder) {
            TitleTypeBean titleTypeBean = ((HomeChannelAdapter.HomeChannelViewHolder) target).getItem();
            if (titleTypeBean != null && mNoOptChannels.contains(titleTypeBean.getChannelId())) {//目标 viewHolder 不能是不可操作对象
                return true;
            }
        }
        //得到当拖拽的viewHolder的Position
        int fromPosition = viewHolder.getAdapterPosition();
        //拿到当前拖拽到的item的viewHolder
        int toPosition = target.getAdapterPosition();
        if (fromPosition >= mShowAdapter.getData().size() || toPosition >= mShowAdapter.getData().size()) {//做好边界判断处理
            return true;
        }
        //交换位置
        mShowAdapter.notifyItemMoved(fromPosition, toPosition);
        if (fromPosition < toPosition) {
            for (int i = fromPosition; i < toPosition; i++) {
                Collections.swap(mShowAdapter.getData(), i, i + 1);
            }
        } else {
            for (int i = fromPosition; i > toPosition; i--) {
                Collections.swap(mShowAdapter.getData(), i, i - 1);
            }
        }
        return true;
    }

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

    }
}

然后将 ItemTouchHelper 和 RecyclerView 绑定在一起

/**
 * 处理是否拖拽帮助类到 RecyclerView
 *
 * @param attach
 */
private void attachRecyclerTouchHelper(boolean attach) {
    if (itemTouchHelper == null) {
        itemTouchHelper = new ItemTouchHelper(new MyItemTouchHelperCallBack());
    }
    if (attach) {
        itemTouchHelper.attachToRecyclerView(binding.rvChannelSelect);
    } else {
        itemTouchHelper.attachToRecyclerView(null);
    }
}
  1. 添加、删除

    添加删除就直接对数组元素进行增删操作,然后刷新列表即可

至此,整个功能就完整的开发完了

作者:openJK
欢迎分享,分享请标注来源juejin.cn/post/713866…
欢迎指正、欢迎点赞