【ItemTouchHelper】微信发布朋友圈的图片移动和删除效果实现以及ItemTouchHelper原理浅析 您所在的位置:网站首页 微信朋友圈图片小 【ItemTouchHelper】微信发布朋友圈的图片移动和删除效果实现以及ItemTouchHelper原理浅析

【ItemTouchHelper】微信发布朋友圈的图片移动和删除效果实现以及ItemTouchHelper原理浅析

2024-02-06 01:24| 来源: 网络整理| 查看: 265

2b813be5a2904b33366913c6851c6520 概述

想要实现类似朋友圈发布的图片拖动的功能,涉及到了复杂的移动判断逻辑。幸运的是Google已经帮我们提供了一个ItemTouchHelper,可以帮助我们实现该复杂的功能。

本文将会以以下几步展开,其中会解析某些使用到的API的原理。

ItemTouchHelper是什么 移动功能的实现 选中后放大 松手后缩小 删除功能的实现 修改移动事件触发的阈值 限制最后一个“+”不能移动 ItemTouchHelper是什么

官方的解释

This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView. It works with a RecyclerView and a Callback class, which configures what type of interactions are enabled and also receives events when user performs these actions.

简单的翻译一下就是

这是一个支持滑动删除和拖拽的工具类,配合RecyclerView使用,里面的Callback类来配置支持哪种交互类型,然后获取用户的交互事件进行处理。

他怎么做到的呢

我可以在源码中看到ItemTouchHelper是继承于RecyclerView.ItemDecoration的

然后看看它里面有一个这样的一个函数 ItemTouchHelper#setupCallbacks()

private void setupCallbacks() { ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext()); mSlop = vc.getScaledTouchSlop(); //把自己作为一个ItemDecoration设置给RecyclerView mRecyclerView.addItemDecoration(this); //处理onTouchEvent和拦截TouchEvent mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); mRecyclerView.addOnChildAttachStateChangeListener(this); startGestureDetection(); }

然后看RecyclerView#onDraw(Canvas c)

@Override public void onDraw(Canvas c) { super.onDraw(c); final int count = mItemDecorations.size(); //调用ItemDecoration的onDraw for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDraw(c, this, mState); } }

然后看 OnItemTouchListener怎么处理onTouchEvent的 OnItemTouchListener#onTouchEvent()

````` 省略一些别的代码 switch (action) { case MotionEvent.ACTION_MOVE: { // Find the index of the active pointer and fetch its position if (activePointerIndex >= 0) { //设置dxdy updateDxDy(event, mSelectedFlags, activePointerIndex); //判断是否需要移动 里面回回调onMove moveIfNecessary(viewHolder); //这个主要处理拖动到边缘后移动recyclerview的 不是本期重点 mRecyclerView.removeCallbacks(mScrollRunnable); mScrollRunnable.run(); //重绘recyclerview mRecyclerView.invalidate(); } break; } `````

recycerview里面item的移动流程

接收到move事件后 RecyclerView.invalidate() ->RecyclerView.onDraw()->ItemTouchHelper.onDraw()->ItemTouchHelper.onChildDraw() 最后在onChildDraw()将对应的Child进行移动。

对于view的选中逻辑这里就不展开讲了

moveIfNecessary(viewHolder);这里是判断是否要移动item的 这里面的逻辑下面再介绍

移动功能的实现 ItemTouchHelper( object : ItemTouchHelper.Callback() { //判断是否可侧滑和拖拽 override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { //我们可以上下左右拖拽 所以把UP,DOWN,LEFT,RIGHT都或一下,因为我们不支持滑动 所以第二个参数返回传0 return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0) } //item被拖拽到可移动的位置时会回调 override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { //得到item原来的position val fromPosition = viewHolder.adapterPosition //得到目标position val toPosition = target.adapterPosition if (fromPosition == toPosition) return false val list = adapter.list //将list的fromPosition移动到toPosition 移动方法类似冒泡 var form = fromPosition for (i in IntProgression.fromClosedRange( fromPosition, toPosition, toPosition.compareTo(fromPosition) )) { val temp = list[form] list[form] = list[i] list[i] = temp form = i } adapter.notifyItemMoved(fromPosition, toPosition) //如果已经被移动到目的地了 返回true return true } //item被滑动到可以删除时会回调 override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { } } ).attachToRecyclerView(recyclerView)

好了 这样我们就可以实现图片的移动了,但是还不完整

效果如下

5c6dbbc2fdd556e8bebe203838105136 选中后放大 松手后缩小

当item的选中状态发生变化时,会回调ItemTouchHelper.Callback#onSelectedChanged()

我们可以在接收到状态改变的回调后改变大小

//重写callback的onSelectedChanged override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { when (actionState) { //选中后回调 ACTION_STATE_DRAG -> { //保存当前拖拽的view draggingView = viewHolder draggingView?.itemView?.apply { scaleX = 1.1f scaleY = 1.1f } } //松手后回调 ACTION_STATE_IDLE -> { draggingView?.let { draggingView!!.itemView.apply { scaleX = 1f scaleY = 1f } draggingView = null } } } }

效果如下

145d97181542c2776217c19b1df46727

接下来还差item的删除

删除功能的实现

首先我们要在用户选中后显示删除区域,用户松手后隐藏删除区域

首先我们先定义两个动画,动画效果可以自定义

val showAnimation by lazy { TranslateAnimation( 0f, 0f, deleteView.height.toFloat(), 0f ).apply { fillAfter = true duration = 200 setAnimationListener(object : Animation.AnimationListener { override fun onAnimationStart(animation: Animation?) { deleteView.visibility = View.VISIBLE } override fun onAnimationEnd(animation: Animation?) { } override fun onAnimationRepeat(animation: Animation?) { } }) } } val hideAnimation by lazy { TranslateAnimation( 0f, 0f, 0f, deleteView.height.toFloat() ).apply { fillAfter = true duration = 200 setAnimationListener(object : Animation.AnimationListener { override fun onAnimationStart(animation: Animation?) { } override fun onAnimationEnd(animation: Animation?) { deleteView.visibility = View.INVISIBLE } override fun onAnimationRepeat(animation: Animation?) { } }) } }

然后在onSelectedChanged()里面开启动画就可以了

//重写callback的onSelectedChanged override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { when (actionState) { //选中后回调 ACTION_STATE_DRAG -> { //显示删除区域 deleteView.startAnimation(showAnimation) //保存当前拖拽的view draggingView = viewHolder draggingView?.itemView?.apply { scaleX = 1.1f scaleY = 1.1f } } //松手后回调 ACTION_STATE_IDLE -> { //隐藏删除区域 deleteView.startAnimation(hideAnimation) draggingView?.let { draggingView!!.itemView.apply { scaleX = 1f scaleY = 1f } draggingView = null } } } }

效果如下

010c968e39dbef5c5aa72684199e5203

接下来就是要判断拖拽的item是不是到了删除区域。根据之前所说的我们知道 当recyclerview的item被移动的时候 会调用ItemTouchHelper.onChildDraw() 里面也会调用Callback的onChildDraw()

我们可以重写该方法,然后根据移动的xy来判断item是否移动到删除区域,然后进行下一步的处理(记得要调用一下父类的onChildDraw 里面有移动的逻辑 ),代码如下

//item被移动的时候回调 override fun onChildDraw( c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean ) { super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) val deleteViewLocations = IntArray(2) deleteView.getLocationInWindow(deleteViewLocations) //删除区域的顶部 val deleteViewTop = deleteViewLocations[1] val dragViewLocations = IntArray(2) viewHolder.itemView.getLocationInWindow(dragViewLocations) //拖拽区域的底部 itemView的高度要*1.1是因为我们view被放大了所以我们高度也要一起变大 val dragViewBottom = dragViewLocations[1] + viewHolder.itemView.height * 1.1f //判断拖拽区域的底部是否越过删除区域的顶部 保存是否在删除区域 当用户松手的时候我们可以用这个变量判断是否在删除区域松手 if (dragViewBottom > deleteViewTop) { isDelete = true deleteView.alpha = .5f } else { isDelete = false deleteView.alpha = 1f } }

然后我们判断onSelectedChanged()回调的状态是ACTION_STATE_IDLE的话,判断isDelete的状态 如果是true的话则代表是删除操作

改一下onSelectedChanged()的代码

//松手后回调 ACTION_STATE_IDLE -> { //隐藏删除区域 deleteView.startAnimation(hideAnimation) draggingView?.let { //是否在删除区域松手 if (isDelete) { //这里需要把拖拽的view gone掉 不然还会显示在UI上 draggingView!!.itemView.visibility = View.GONE val list = adapter.list list.removeAt(draggingView!!.adapterPosition) adapter.notifyItemRemoved(draggingView!!.adapterPosition) } else { draggingView!!.itemView.apply { scaleX = 1f scaleY = 1f } } draggingView = null } }

最后删除的效果如下

73a5e34e5f24cd9bbc12d393a6a5f271 修改移动事件触发的阈值 08868c7788069a18d7cb994252a9f1b8

移动和删除都完成了,但是会发现拖拽的item需要越过替换目标的时候才会触发onMove的回调,有没有办法让这个回调提前一点呢,比如移动到75%的时候就触发。

顺着Callback.onMove往上找,看有没有提前调用onMove,最后发现是在ItemtouchHelper#moveIfNecessary()里面被调用的,源码如下

//上面介绍了 这个函数会在控件被拖动的时候onTouchEvent里面调用 void moveIfNecessary(ViewHolder viewHolder) { if (mRecyclerView.isLayoutRequested()) { return; } if (mActionState != ACTION_STATE_DRAG) { return; } //0 获取认为是移动事件的阈值 默认为0.5可以重写来修改 final float threshold = mCallback.getMoveThreshold(viewHolder); final int x = (int) (mSelectedStartX + mDx); final int y = (int) (mSelectedStartY + mDy); //如果view被拖动的距离没有超过阈值 则return if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold && Math.abs(x - viewHolder.itemView.getLeft()) < viewHolder.itemView.getWidth() * threshold) { return; } //1 List swapTargets = findSwapTargets(viewHolder); if (swapTargets.size() == 0) { return; } //2 ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y); if (target == null) { mSwapTargets.clear(); mDistances.clear(); return; } final int toPosition = target.getAdapterPosition(); final int fromPosition = viewHolder.getAdapterPosition(); //找到target后调用callback的onMove if (mCallback.onMove(mRecyclerView, viewHolder, target)) { // keep target visible mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, target, toPosition, x, y); } } //该函数是寻找与拖动控件覆盖的view 会将return的结果以覆盖区域大小从大到小排序 private List findSwapTargets(ViewHolder viewHolder) { if (mSwapTargets == null) { mSwapTargets = new ArrayList(); mDistances = new ArrayList(); } else { mSwapTargets.clear(); mDistances.clear(); } final int margin = mCallback.getBoundingBoxMargin(); final int left = Math.round(mSelectedStartX + mDx) - margin; final int top = Math.round(mSelectedStartY + mDy) - margin; final int right = left + viewHolder.itemView.getWidth() + 2 * margin; final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin; final int centerX = (left + right) / 2; final int centerY = (top + bottom) / 2; final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); final int childCount = lm.getChildCount(); //遍历RecyclerView里面的所有view for (int i = 0; i < childCount; i++) { View other = lm.getChildAt(i); if (other == viewHolder.itemView) { continue; //myself! } //判断这个view是否被拖动view覆盖了 if (other.getBottom() < top || other.getTop() > bottom || other.getRight() < left || other.getLeft() > right) { continue; } final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other); //调用canDropOver来确定这个被覆盖的view是否能被替换(我们可以利用canDropOver这个回调来限制不能被移动的view) if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) { // find the index to add final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2); final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2); final int dist = dx * dx + dy * dy; int pos = 0; final int cnt = mSwapTargets.size(); //寻找插入顺序,大的排前面 for (int j = 0; j < cnt; j++) { if (dist > mDistances.get(j)) { pos++; } else { break; } } //添加到返回结果里面 mSwapTargets.add(pos, otherVh); mDistances.add(pos, dist); } } return mSwapTargets; } //该函数是从被覆盖的view(dropTargets)里面中选取一个可以被拖动view替换的ViewHolder,然后将其返回 public ViewHolder chooseDropTarget(@NonNull ViewHolder selected, @NonNull List dropTargets, int curX, int curY) { int right = curX + selected.itemView.getWidth(); int bottom = curY + selected.itemView.getHeight(); ViewHolder winner = null; int winnerScore = -1; //curX是拖动view的当前x轴的位置 -去left 的到的就是x轴偏移量 final int dx = curX - selected.itemView.getLeft(); //curY是拖动view的当前Y轴的位置 -去top 的到的就是y轴偏移量 final int dy = curY - selected.itemView.getTop(); final int targetsSize = dropTargets.size(); //遍历dropTargets 寻找最合适的替换目标 赋值给winner for (int i = 0; i < targetsSize; i++) { final ViewHolder target = dropTargets.get(i); if (dx > 0) {//向右移 int diff = target.itemView.getRight() - right; //diff小于0 代表着拖动的view右边界已经越过了覆盖view的右边界 if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) { //score的作用是为了寻找一个越界程度最大的target 那个gerget就是最终的winner final int score = Math.abs(diff); if (score > winnerScore) { winnerScore = score; winner = target; } } } if (dx < 0) {//向左移 int diff = target.itemView.getLeft() - curX; //diff大于0 代表着拖动的view左边界已经越过了覆盖view的左边界 if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) { final int score = Math.abs(diff); if (score > winnerScore) { winnerScore = score; winner = target; } } } if (dy < 0) {//向上移 int diff = target.itemView.getTop() - curY; //diff大于0 代表着拖动的view上边界已经越过了覆盖view的上边界 if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) { final int score = Math.abs(diff); if (score > winnerScore) { winnerScore = score; winner = target; } } } if (dy > 0) {//向下移 int diff = target.itemView.getBottom() - bottom; //diff小于0 代表着拖动的view下边界已经越过了覆盖view的下边界 if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) { final int score = Math.abs(diff); if (score > winnerScore) { winnerScore = score; winner = target; } } } } return winner; }

看了源码就很清晰了

控件拖动的时候会触发ItemtouchHelper#moveIfNecessary()

moveIfNecessary()里面

判断控件拖动的距离是否超过设定的阈值 0通过后,获取所有与拖动控件叠加的view 获取到的view不为空的话,选取一个最适合的view返回 如果2能选取到合适的view的话 则将替换的view作为参数调用onMove

根据源码我们发现 我们可以重写第2步,chooseDropTarget(因为之前是要整个越过才作为符合条件的view,不符合我们的预期)选取我们想要的targetview进行返回

重写代码如下,复制父类的代码 然后增加一个阈值 修改修改里面的diff

override fun chooseDropTarget( selected: RecyclerView.ViewHolder, dropTargets: MutableList, curX: Int, curY: Int ): RecyclerView.ViewHolder? { val right = curX + selected.itemView.width val bottom = curY + selected.itemView.height var winner: RecyclerView.ViewHolder? = null var winnerScore = -1f val dx = curX - selected.itemView.left val dy = curY - selected.itemView.top //这里是0.75f 这个值不能小于getMoveThreshold()返回的阈值 如果想要小于的话,可以重写getMoveThreshold 返回一个最小值就好了 val onMoveThreshold = 0.75f dropTargets.forEach { target-> val ignoreWidth = target.itemView.width * (1 - onMoveThreshold) val ignoreHeight = target.itemView.height * (1 - onMoveThreshold) if (dx > 0) { val diff = target.itemView.right - right - ignoreWidth if (diff < 0 && target.itemView.right > selected.itemView.right) { val score = Math.abs(diff) if (score > winnerScore) { winnerScore = score winner = target } } } if (dx < 0) { val diff = target.itemView.left - curX + ignoreWidth if (diff > 0 && target.itemView.left < selected.itemView.left) { val score = Math.abs(diff) if (score > winnerScore) { winnerScore = score winner = target } } } if (dy < 0) { val diff = target.itemView.top - curY + ignoreHeight if (diff > 0 && target.itemView.top < selected.itemView.top) { val score = Math.abs(diff) if (score > winnerScore) { winnerScore = score winner = target } } } if (dy > 0) { val diff = target.itemView.bottom - bottom - ignoreHeight if (diff < 0 && target.itemView.bottom > selected.itemView.bottom) { val score = Math.abs(diff) if (score > winnerScore) { winnerScore = score winner = target } } } } return winner }

可以了 最后效果如下

530df457f54530aa93557f7af9017ad5 限制最后一个“+”不能移动

我们在修改移动阈值的时候 查看源码发现canDropOver()如果返回flase的话 则不会被当成可替换的目标

好了很开心 简简单单一行代码

override fun canDropOver(recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { //这里判断逻辑简单起见 我就直接判断文案是否为“+”了 实际上可以判断viewholder的类型或其他方法 return adapter.list[target.adapterPosition] != "+" } 7d76b98cbc3ad870da77ab05a08d031e

”+“的确不能被越过了,但是我们发现 虽然不能被越过 但是它还是能被拖拽的。。。。

怎么办呢,记不记得之前介绍的getMovementFlags这个函数,这个函数是来判断viewholder支持的交互类型的

那么很好 我们在里面判断 viewhoder是“+”的话 返回不可移动就可以了

最终代码如下

//判断是否可侧滑和拖拽 override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { return if (canMove(viewHolder)) { makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0) } else { makeMovementFlags(0, 0) } } override fun canDropOver(recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { return canMove(target) } private fun canMove(holder: RecyclerView.ViewHolder): Boolean { return adapter.list[holder.adapterPosition] != "+" }

运行一下 发现终于不能被拖动了。。

最终效果 2b813be5a2904b33366913c6851c6520

好了 虽然其中一些细节和微信的还会有一些差异 但是大体上差不多

如果想进一步了解拖动view是怎么被选中的以及一些列的事件传递 可以参考这篇文章

juejin.cn/post/684490…



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有