仿微信侧滑删除
先上效果图:
代码实现如下:
recyclerview添加如下ItemTouchListener
class ItemTouchLis : RecyclerView.OnItemTouchListener { private var targetView: ViewGroup? = null//当前操作的view private var lastView: ViewGroup? = null//上一次操作的view private val animTime = 200L//动画时间 private var isDragging = false//是否处于拖拽状态 private var downPoint = PointF(0f, 0f)//按下的点,用于判断位移距离 private var downScrollX = 0//按下时view的已滑动距离 private val offsetDistance = ViewConfiguration.get(MyApp.instance).scaledTouchSlop//最小触发侧滑的距离 private var secondItemWidth = 0//由于第二项要伸缩,固定取第一次获取的宽度 private var fixedOffset = 0f//未读和删除两项总宽度之和,初始化同上 private var isVerticalScroll = false//recyclerview上下滑动 private var isExpanedWhenActionDown = false private var isTouchOriginalArea = false//是否触发原来item的点击事件(展开状态下,直接关闭侧滑菜单,不触发点击事件 private var isTouchConfirm = false//判断点击区域位置 private var downTime = 0L//按下的时间(用于判断是否触发点击事件 private var isExpanded = false//删除按钮是否展开状态 override fun onInterceptTouchEvent(rv: RecyclerView?, ev: MotionEvent?): Boolean { if (ev == null || rv == null) return false when (ev.action) { MotionEvent.ACTION_DOWN -> { downTime = System.currentTimeMillis() isVerticalScroll = false isDragging = false isTouchConfirm = false if (isTouchOriginalArea) { lastView = null isTouchOriginalArea = false } isExpanedWhenActionDown = isExpanded downPoint.set(ev.x, ev.y) lastView = targetView targetView = rv.findChildViewUnder(ev.x, ev.y) as ViewGroup? val targetView = targetView ?: return false downScrollX = targetView.scrollX val childCount = targetView.childCount if (fixedOffset == 0f) { if (childCount > 1) { (1 until childCount).forEach { val child = targetView.getChildAt(it) if (it == 2 && secondItemWidth == 0) { secondItemWidth = child.width } val lp = child.layoutParams as ViewGroup.MarginLayoutParams fixedOffset += child.width + lp.leftMargin - child.paddingRight } } } //恢复上一个view状态 if (lastView != null && lastView != targetView) { resetView(lastView!!) lastView = null isExpanded = false isExpanedWhenActionDown = false //判断当前view点击状态 } else if (lastView == targetView && targetView.scrollX != 0) { val w = targetView.width val pre = targetView.getChildAt(1) var firstItemWidth = pre.width - pre.paddingRight //点击删除,确认删除之前状态 if (targetView.scrollX + ev.x > w + firstItemWidth) { if (!isExpanded) { isTouchConfirm = true switch(targetView, true) return true } //点击item显示区域 } else if (targetView.scrollX + ev.x < w) { resetView(targetView) isTouchOriginalArea = true return true } } } MotionEvent.ACTION_MOVE -> { //判断是否可拖拽 drag(ev) } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { if(isTouchOriginalArea){ return true } var needResetTarget = true val targetView = targetView ?: return false if (targetView.scrollX != 0) { val w = targetView.width //触发点击事件 if (System.currentTimeMillis() - downTime < ViewConfiguration.getTapTimeout() && !isDragging) { val pre = targetView.getChildAt(1) val firstItemWidth = pre.width - pre.paddingRight //点击删除区域 if (targetView.scrollX + ev.x > w + firstItemWidth) { //判断是否确认删除状态 if (isExpanded) { if (!isExpanedWhenActionDown) { needResetTarget = false } } else { switch(targetView, true) return true } //点击非item区域 } else if (targetView.scrollX + ev.x > w) { //判断是否删除状态 if (isExpanded) { if (!isExpanedWhenActionDown) { needResetTarget = false } } else { resetView(targetView) needResetTarget = false } } } } if (needResetTarget) { resetView(targetView, isExpanded) } } else -> { } } return isDragging || isTouchOriginalArea || isTouchConfirm } override fun onTouchEvent(rv: RecyclerView?, event: MotionEvent?) { if (rv == null || event == null || isTouchOriginalArea || isTouchConfirm) { return } val targetView = targetView ?: return when (event.action) { MotionEvent.ACTION_MOVE -> { if (isExpanded) { return } var distance = downPoint.x - event.x + downScrollX if (distance < 0f) { distance = 0f } var rate = distance / fixedOffset val childCount = targetView.childCount if (childCount > 2) { (2 until childCount).forEach { val pre = targetView.getChildAt(it - 1) val translationX = (pre.width - pre.paddingRight) * rate val view = targetView.getChildAt(it) view.translationX = translationX } } targetView.scrollTo(distance.toInt(), 0) } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { var needResetTarget = true if (targetView.scrollX != 0) { val w = targetView.width //触发点击事件 if (System.currentTimeMillis() - downTime < ViewConfiguration.getTapTimeout() && !isDragging) { val pre = targetView.getChildAt(1) val firstItemWidth = pre.width - pre.paddingRight //点击删除区域 if (targetView.scrollX + event.x > w + firstItemWidth) { //判断是否确认删除状态 if (isExpanded) { if (!isExpanedWhenActionDown) { needResetTarget = false } } else { switch(targetView, true) return } //点击非item区域 } else if (targetView.scrollX + event.x > w) { //判断是否删除状态 if (isExpanded) { if (!isExpanedWhenActionDown) { needResetTarget = false } } else { resetView(targetView) needResetTarget = false } } } } if (needResetTarget) { resetView(targetView, isExpanded) } } else -> { } } } private fun drag(ev: MotionEvent) { if (isVerticalScroll) { return } val x = Math.abs(downPoint.x - ev.x) val y = Math.abs(downPoint.y - ev.y) if (y > offsetDistance && y > x) { isVerticalScroll = true return } if (x > offsetDistance) { isDragging = true } } private fun resetView(viewGroup: ViewGroup, toZero: Boolean = true) { log("resetView---toZero=$toZero") val count = viewGroup.childCount if (count <= 2) { viewGroup.scrollTo(0, 0) return } if (toZero) { scrollToZero(viewGroup) } else { if (viewGroup.scrollX > fixedOffset / 2) { scrollToMax(viewGroup) } else { scrollToZero(viewGroup) } } } private fun switch(vg: ViewGroup, expand: Boolean) { log("switch") isExpanded = true if (vg.scrollX != fixedOffset.toInt()) { vg.scrollTo(fixedOffset.toInt(), 0) } val pre = vg.getChildAt(1) val firstItemWidth = pre.width - pre.paddingRight val view = vg.getChildAt(2) if (view is TextView) { view.text = vg.context.string(R.string.confirm_delete) } val anim = ObjectAnimator.ofFloat(view, "translationX", view.translationX, 0f).setDuration(animTime) anim.addUpdateListener { val wd = (1 - view.translationX / firstItemWidth) * firstItemWidth val lp = view.layoutParams lp.width = wd.toInt() + secondItemWidth view.layoutParams = lp } anim.start() } fun scrollToZero(viewGroup: ViewGroup) { log("scrollToZero") isExpanded = false if (viewGroup.scrollX != 0) { val anim = ObjectAnimator.ofInt(viewGroup, "scrollX", viewGroup.scrollX, 0).setDuration(animTime) anim.interpolator = DecelerateInterpolator() anim.addListener(object : Animator.AnimatorListener { override fun onAnimationRepeat(animation: Animator?) { } override fun onAnimationEnd(animation: Animator?) { val view = viewGroup.getChildAt(2) if (view is TextView) { view.text = view.context.string(R.string.delete) } val lp = view.layoutParams if (view.translationX != 0f) { view.translationX = 0f } if (lp.width != secondItemWidth) { lp.width = secondItemWidth view.layoutParams = lp } } override fun onAnimationCancel(animation: Animator?) { } override fun onAnimationStart(animation: Animator?) { } }) anim.start() } } private fun scrollToMax(viewGroup: ViewGroup) { log("scrollToMax") val anim = ObjectAnimator.ofInt(viewGroup, "scrollX", viewGroup.scrollX, fixedOffset.toInt()).setDuration(animTime) anim.interpolator = OvershootInterpolator(3f) val view = viewGroup.getChildAt(2) val pre = viewGroup.getChildAt(1) val preW = pre.width - pre.paddingRight anim.addUpdateListener { var rate = viewGroup.scrollX / fixedOffset view.translationX = rate * preW } anim.start() } override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { } }
然后再Item布局外用如下FrameLayout嵌套,完全和frameLayout相同,只是改了第一个之后的子view的位置(只做了两个item的,多个item暂未适配(之后会考虑改为一个通用控件)
class SwipeDeleteLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) { private val screenWidth = context.screenWidth override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { (0 until childCount).forEach { val child = getChildAt(it) if (it == 0) { child.layout(0, 0, context.screenWidth, bottom - top) } else { child.layout(screenWidth, 0, child.measuredWidth + screenWidth, bottom - top) } } } }
用到了属性动画,用的时候记得自行处理一下生命周期的问题
布局文件如下
<?xml version="1.0" encoding="utf-8"?> <SwipeDeleteLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="@dimen/dimen_100"> <your_layout />
<android.support.text.emoji.widget.EmojiTextView android:id="@+id/read_status" android:layout_width="@dimen/dimen_416" android:layout_height="match_parent" android:background="@color/color_CECECE" android:gravity="center" android:paddingRight="@dimen/dimen_300" android:textColor="@color/color_FFFFFF" android:textStyle="bold" /> <android.support.text.emoji.widget.EmojiTextView android:id="@+id/delete" android:layout_width="@dimen/dimen_382" android:layout_height="match_parent" android:background="@color/color_FF0458" android:gravity="center" android:paddingRight="@dimen/dimen_300" android:text="@string/delete" android:textColor="@color/color_FFFFFF" android:textStyle="bold" /></SwipeDeleteLayout>