关联用户滑动效果

This commit is contained in:
2026-05-23 15:39:39 +08:00
parent 3cd21f8fca
commit fe025c8a69
9 changed files with 401 additions and 179 deletions

View File

@@ -0,0 +1,176 @@
package com.example.defenseapplication.view
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewParent
import android.widget.FrameLayout
import android.widget.OverScroller
class SlideLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val scroller = OverScroller(context)
private var downX: Float = 0f
private var downY: Float = 0f
private var lastX: Float = 0f
private var isHorizontalDragging: Boolean = false
private var listener: OnSlideChangeListener? = null
private var shouldMenuBeOpen: Boolean = false
/** 缓存菜单宽度,避免 RecyclerView 复用时 measuredWidth 读取不到 */
private var cachedMenuWidth: Int = 0
interface OnSlideChangeListener {
fun onMenuOpen(slideLayout: SlideLayout)
fun onMenuClose(slideLayout: SlideLayout)
fun onClick(slideLayout: SlideLayout)
}
fun setOnSlideChangeListener(listener: OnSlideChangeListener) {
this.listener = listener
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// 缓存菜单宽度,在 onLayout 和 onTouchEvent 中使用
cachedMenuWidth = if (childCount >= 2) getChildAt(0).measuredWidth else 0
}
private fun getMenuWidth(): Int {
// 优先使用缓存值RecyclerView 复用时 measuredWidth 可能尚未更新
val w = cachedMenuWidth
return if (w > 0) w else {
if (childCount >= 2) {
val measured = getChildAt(0).measuredWidth
if (measured > 0) measured else getChildAt(0).width
} else 0
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (childCount < 2) return
val contentWidth = right - left
val menu = getChildAt(0)
val menuW = getMenuWidth()
// 内容左移 scrollX → 菜单右边缘逐渐露出
// 菜单左边界 = 内容右边缘 - scrollX
menu.layout(contentWidth - scrollX, top, contentWidth - scrollX + menuW, bottom)
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
downX = ev.x
downY = ev.y
lastX = ev.x
isHorizontalDragging = false
}
MotionEvent.ACTION_MOVE -> {
val dx = Math.abs(ev.x - downX)
val dy = Math.abs(ev.y - downY)
if (dx > dy && dx > 10f) {
isHorizontalDragging = true
disallowInterceptChain(true)
return true
}
}
}
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
lastX = event.x
disallowInterceptChain(true)
return true
}
MotionEvent.ACTION_MOVE -> {
val dx = lastX - event.x
lastX = event.x
if (Math.abs(event.x - downX) > 10f) {
isHorizontalDragging = true
}
if (isHorizontalDragging) {
val menuW = getMenuWidth()
var newScrollX = scrollX + dx.toInt()
if (newScrollX < 0) newScrollX = 0
if (newScrollX > menuW) newScrollX = menuW
scrollTo(newScrollX, 0)
}
}
MotionEvent.ACTION_UP -> {
val wasDragging = isHorizontalDragging
isHorizontalDragging = false
disallowInterceptChain(false)
if (!wasDragging) {
listener?.onClick(this)
} else {
val menuW = getMenuWidth()
if (scrollX > menuW / 2) {
openMenu()
} else {
closeMenu()
}
}
}
MotionEvent.ACTION_CANCEL -> {
isHorizontalDragging = false
disallowInterceptChain(false)
// 弹窗等外部中断:按当前菜单是否露出状态决定动画关闭或保持关闭
if (scrollX > 0) {
closeMenu()
}
}
}
return true
}
private fun disallowInterceptChain(disallow: Boolean) {
var parent: ViewParent? = parent
while (parent != null) {
parent.requestDisallowInterceptTouchEvent(disallow)
parent = parent.parent
}
}
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
postInvalidateOnAnimation()
}
}
fun openMenu() {
val menuW = getMenuWidth()
if (shouldMenuBeOpen && scrollX == menuW) return
shouldMenuBeOpen = true
scroller.startScroll(scrollX, 0, menuW - scrollX, 0, 300)
postInvalidateOnAnimation()
listener?.onMenuOpen(this)
}
fun closeMenu() {
if (!shouldMenuBeOpen && scrollX == 0) return
shouldMenuBeOpen = false
scroller.startScroll(scrollX, 0, -scrollX, 0, 300)
postInvalidateOnAnimation()
listener?.onMenuClose(this)
}
fun isMenuOpened(): Boolean = shouldMenuBeOpen
}