【ItemTouchHelper】微信发布朋友圈的图片移动和删除效果实现以及ItemTouchHelper原理浅析 | 您所在的位置:网站首页 › 微信朋友圈图片小 › 【ItemTouchHelper】微信发布朋友圈的图片移动和删除效果实现以及ItemTouchHelper原理浅析 |
想要实现类似朋友圈发布的图片拖动的功能,涉及到了复杂的移动判断逻辑。幸运的是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)好了 这样我们就可以实现图片的移动了,但是还不完整 效果如下 当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 } } } }效果如下 接下来还差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 } } } }效果如下 接下来就是要判断拖拽的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 } }最后删除的效果如下 移动和删除都完成了,但是会发现拖拽的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 }可以了 最后效果如下 我们在修改移动阈值的时候 查看源码发现canDropOver()如果返回flase的话 则不会被当成可替换的目标 好了很开心 简简单单一行代码 override fun canDropOver(recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { //这里判断逻辑简单起见 我就直接判断文案是否为“+”了 实际上可以判断viewholder的类型或其他方法 return adapter.list[target.adapterPosition] != "+" }”+“的确不能被越过了,但是我们发现 虽然不能被越过 但是它还是能被拖拽的。。。。 怎么办呢,记不记得之前介绍的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] != "+" }运行一下 发现终于不能被拖动了。。 最终效果好了 虽然其中一些细节和微信的还会有一些差异 但是大体上差不多 如果想进一步了解拖动view是怎么被选中的以及一些列的事件传递 可以参考这篇文章 juejin.cn/post/684490… |
今日新闻 |
推荐新闻 |
专题文章 |
CopyRight 2018-2019 实验室设备网 版权所有 |