177 lines
5.8 KiB
Kotlin
177 lines
5.8 KiB
Kotlin
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
|
||
}
|