关联用户滑动效果

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

@@ -5,6 +5,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.defenseapplication.databinding.ItemLinkedUserBinding
import com.example.defenseapplication.view.SlideLayout
data class LinkedUser(
val userId: String,
@@ -22,6 +23,11 @@ class LinkedUserAdapter(
) : RecyclerView.Adapter<LinkedUserAdapter.LinkedUserViewHolder>() {
private val items = mutableListOf<LinkedUser>()
private var _openSlideLayout: SlideLayout? = null
/** 供外部(如 Activity访问当前展开的 SlideLayout */
val openSlideLayout: SlideLayout?
get() = _openSlideLayout
fun submitList(userList: List<LinkedUser>) {
items.clear()
@@ -36,12 +42,6 @@ class LinkedUserAdapter(
}
}
fun closeSwipe(position: Int) {
if (position in items.indices) {
notifyItemChanged(position)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LinkedUserViewHolder {
val binding = ItemLinkedUserBinding.inflate(
LayoutInflater.from(parent.context),
@@ -52,24 +52,68 @@ class LinkedUserAdapter(
}
override fun onBindViewHolder(holder: LinkedUserViewHolder, position: Int) {
val item = items[position]
holder.bind(item)
holder.bind(items[position], position)
// 每次 bind 都设置监听器RecyclerView 复用视图时也会调用
val slideLayout = holder.itemView as SlideLayout
slideLayout.setOnSlideChangeListener(object : SlideLayout.OnSlideChangeListener {
override fun onMenuOpen(slideLayout: SlideLayout) {
val prev = _openSlideLayout
if (prev != null && prev != slideLayout) {
prev.closeMenu()
}
_openSlideLayout = slideLayout
}
override fun onMenuClose(slideLayout: SlideLayout) {
if (_openSlideLayout == slideLayout) {
_openSlideLayout = null
}
}
override fun onClick(slideLayout: SlideLayout) {
val prev = _openSlideLayout
if (prev != null && prev != slideLayout) {
prev.closeMenu()
}
}
})
holder.binding.llContent.setOnClickListener {
val pos = holder.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
onItemClick(items[pos], pos)
}
}
holder.binding.tvDeleteAction.setOnClickListener {
val pos = holder.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
onDeleteClick(items[pos], pos)
}
}
holder.binding.tvUnlinkAction.setOnClickListener {
val pos = holder.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
onDeleteClick(items[pos], pos)
}
}
}
override fun getItemCount(): Int = items.size
inner class LinkedUserViewHolder(
private val binding: ItemLinkedUserBinding
val binding: ItemLinkedUserBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LinkedUser, position: Int) {
binding.tvPhone.text = "手机号码:${item.phone}"
binding.tvIdCard.text = "身份证号:${item.idCard}"
binding.llContent.translationX = 0f
fun bind(item: LinkedUser) {
binding.tvUserName.text = item.name
binding.tvTime.text = item.time
binding.tvPhone.text = "手机号码:${item.phone}"
binding.tvIdCard.text = "身份证号:${item.idCard}"
// MODE_SETTINGS → 显示"解除关联"MODE_PERSONAL → 显示"删除"
if (isUnlinkMode) {
binding.tvUnlinkAction.visibility = View.VISIBLE
binding.tvDeleteAction.visibility = View.GONE
@@ -78,27 +122,6 @@ class LinkedUserAdapter(
binding.tvDeleteAction.visibility = View.VISIBLE
binding.tvDeleteAction.text = deleteText
}
binding.llContent.setOnClickListener {
if (binding.llContent.translationX < 0f) {
binding.llContent.animate()
.translationX(0f)
.setDuration(200)
.start()
} else {
onItemClick(item, position)
}
}
binding.tvDeleteAction.setOnClickListener {
onDeleteClick(item, position)
}
binding.tvUnlinkAction.setOnClickListener {
onDeleteClick(item, position)
}
}
fun getContentLayout(): View = binding.llContent
}
}

View File

@@ -18,7 +18,7 @@ import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.recyclerview.widget.RecyclerView
class AddLinkedUserActivity : AppCompatActivity() {
private lateinit var binding: AddLinkedUserActivityBinding
private val selectedPhoneSet = mutableSetOf<String>()
@@ -37,9 +37,16 @@ class AddLinkedUserActivity : AppCompatActivity() {
initRecyclerView()
initListener()
setupSwipeRefresh()
fetchFamilyMembers()
}
private fun setupSwipeRefresh() {
binding.swipeRefresh.setOnRefreshListener {
fetchFamilyMembers()
}
}
private fun initRecyclerView() {
binding.rvAddLinkedUser.layoutManager = LinearLayoutManager(this)
binding.rvAddLinkedUser.adapter =
@@ -52,13 +59,31 @@ class AddLinkedUserActivity : AppCompatActivity() {
selectedUserIdMap.remove(phone)
}
}
// 设置上拉加载更多
binding.rvAddLinkedUser.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val totalItemCount = layoutManager.itemCount
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
if (totalItemCount > 0 && lastVisibleItem >= totalItemCount - 3) {
// 滑到底部时自动刷新(加载更多)
if (!binding.swipeRefresh.isRefreshing) {
fetchFamilyMembers()
}
}
}
})
}
private fun fetchFamilyMembers() {
binding.swipeRefresh.isRefreshing = true
lifecycleScope.launch {
try {
val response = RetrofitNetwork.getNetworkService().getFamilyMembers()
withContext(Dispatchers.Main) {
binding.swipeRefresh.isRefreshing = false
if (response.code == 200) {
if (response.data != null && response.data.isNotEmpty()) {
updateFamilyMemberList(response.data)
@@ -72,6 +97,7 @@ class AddLinkedUserActivity : AppCompatActivity() {
} catch (e: Exception) {
android.util.Log.e("AddLinkedUserActivity", "网络请求异常", e)
withContext(Dispatchers.Main) {
binding.swipeRefresh.isRefreshing = false
Toast.makeText(this@AddLinkedUserActivity, "网络请求失败: ${e.message}", Toast.LENGTH_SHORT).show()
}
}

View File

@@ -4,12 +4,10 @@ import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.defenseapplication.R
@@ -93,120 +91,26 @@ class LinkedUserActivity : AppCompatActivity() {
binding.rvLinkedUser.layoutManager = LinearLayoutManager(this)
binding.rvLinkedUser.adapter = adapter
// 根据模式只显示一个按钮,滑动露出完整按钮
val swipeThresholdPx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, -100f, resources.displayMetrics
)
val heldPositions = mutableMapOf<Int, Boolean>()
binding.swipeRefresh.setOnRefreshListener {
fetchUserList()
}
var currentlyHeldPosition: Int = -1
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
override fun onChildDraw(
c: android.graphics.Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
if (viewHolder is LinkedUserAdapter.LinkedUserViewHolder) {
val contentLayout = viewHolder.getContentLayout()
val position = viewHolder.adapterPosition
// 如果当前滑动的位置和已展开的位置不同,先收起已展开的项
if (isCurrentlyActive && currentlyHeldPosition != -1 && currentlyHeldPosition != position) {
val oldViewHolder = recyclerView.findViewHolderForAdapterPosition(currentlyHeldPosition)
if (oldViewHolder is LinkedUserAdapter.LinkedUserViewHolder) {
oldViewHolder.getContentLayout().animate()
.translationX(0f)
.setDuration(200)
.start()
}
heldPositions.remove(currentlyHeldPosition)
currentlyHeldPosition = -1
}
if (isCurrentlyActive) {
val isHeld = heldPositions[position]
if (isHeld == true) {
if (dX > 0) {
// 已展开时向右滑动 → 平滑收回
val newX = swipeThresholdPx + dX
// 不能超过原位
contentLayout.translationX = if (newX > 0) 0f else newX
} else {
// 保持展开或继续左滑(限制不超过阈值)
contentLayout.translationX = swipeThresholdPx
}
} else {
// 未展开,正常滑动限制(左滑不超过阈值,右滑不超过原位)
val swipeDistance = when {
dX < swipeThresholdPx -> swipeThresholdPx
dX > 0 -> 0f
else -> dX
}
contentLayout.translationX = swipeDistance
}
} else {
val isHeld = heldPositions[position]
if (isHeld == true) {
contentLayout.translationX = swipeThresholdPx
}
binding.rvLinkedUser.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val totalItemCount = layoutManager.itemCount
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
if (totalItemCount > 0 && lastVisibleItem >= totalItemCount - 3) {
if (!binding.swipeRefresh.isRefreshing) {
fetchUserList()
}
}
}
/*
- 滑动超过一半后松手:列表项 保持在滑动位置,显示按钮
- 滑动未超过一半松手:列表项 恢复原位
- 已展开状态下向右拖动归位或拖动超过一半松手:恢复原位
* */
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
if (viewHolder is LinkedUserAdapter.LinkedUserViewHolder) {
val position = viewHolder.adapterPosition
val contentLayout = viewHolder.getContentLayout()
// 判断滑动距离是否超过一半阈值(注意都是负数,所以绝对值比较)
if (Math.abs(contentLayout.translationX) > Math.abs(swipeThresholdPx / 2)) {
heldPositions[position] = true
currentlyHeldPosition = position
contentLayout.translationX = swipeThresholdPx
} else {
heldPositions.remove(position)
if (currentlyHeldPosition == position) {
currentlyHeldPosition = -1
}
contentLayout.animate()
.translationX(0f)
.setDuration(300)
.start()
}
}
}
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float = 0.05f
})
itemTouchHelper.attachToRecyclerView(binding.rvLinkedUser)
}
private var currentSwipePosition: Int = -1
private var currentSwipeHeld: Boolean = false
private fun showUnlinkDialog(user: LinkedUser, position: Int) {
currentSwipePosition = position
currentSwipeHeld = true
val dialog = CustomDialogFragmentText()
if (mode == MODE_SETTINGS) {
@@ -223,7 +127,10 @@ class LinkedUserActivity : AppCompatActivity() {
deleteDeviceUser(user, position)
}
dialog.setOnDismissListener {
resetSwipeState()
// Dialog 关闭后,如果之前打开了菜单则关闭它
if (adapter.openSlideLayout != null) {
adapter.openSlideLayout?.closeMenu()
}
}
dialog.show(supportFragmentManager, "UnlinkDialog")
}
@@ -232,7 +139,6 @@ class LinkedUserActivity : AppCompatActivity() {
val deviceId = DeviceIdEvent.getDeviceId()
if (deviceId.isNullOrEmpty() || user.userId.isEmpty()) {
Toast.makeText(this, "参数错误", Toast.LENGTH_SHORT).show()
resetSwipeState()
return
}
@@ -251,39 +157,22 @@ class LinkedUserActivity : AppCompatActivity() {
} else {
Toast.makeText(this@LinkedUserActivity, response.msg, Toast.LENGTH_SHORT).show()
}
resetSwipeState()
}
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(this@LinkedUserActivity, "网络请求失败", Toast.LENGTH_SHORT).show()
resetSwipeState()
}
}
}
}
private fun resetSwipeState() {
if (currentSwipePosition >= 0 && currentSwipeHeld) {
binding.rvLinkedUser.post {
val viewHolder = binding.rvLinkedUser.findViewHolderForAdapterPosition(currentSwipePosition)
if (viewHolder is LinkedUserAdapter.LinkedUserViewHolder) {
viewHolder.getContentLayout().animate()
.translationX(0f)
.setDuration(300)
.start()
}
currentSwipePosition = -1
currentSwipeHeld = false
}
}
}
private fun initData() {
fetchUserList()
}
private fun fetchUserList() {
binding.swipeRefresh.isRefreshing = true
lifecycleScope.launch {
try {
val response = if (mode == MODE_SETTINGS) {
@@ -297,6 +186,7 @@ class LinkedUserActivity : AppCompatActivity() {
RetrofitNetwork.getNetworkService().getFamilyMembers()
}
withContext(Dispatchers.Main) {
binding.swipeRefresh.isRefreshing = false
if (response.code == 200 && response.data != null) {
if (mode == MODE_SETTINGS) {
updateDeviceUserList(response.data as List<DeviceUserBean>)
@@ -310,6 +200,7 @@ class LinkedUserActivity : AppCompatActivity() {
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
binding.swipeRefresh.isRefreshing = false
Toast.makeText(this@LinkedUserActivity, "网络请求失败", Toast.LENGTH_SHORT).show()
}
}

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
}