关联用户列表优化

This commit is contained in:
2026-05-25 14:04:53 +08:00
parent fe025c8a69
commit fd84f18e4d
7 changed files with 206 additions and 366 deletions

View File

@@ -5,7 +5,6 @@ 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,
@@ -16,18 +15,11 @@ data class LinkedUser(
)
class LinkedUserAdapter(
private val deleteText: String,
private val isUnlinkMode: Boolean,
private val onItemClick: (LinkedUser, Int) -> Unit,
private val onDeleteClick: (LinkedUser, Int) -> Unit
private val onItemClick: (LinkedUser, Int) -> Unit
) : 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()
@@ -52,54 +44,7 @@ class LinkedUserAdapter(
}
override fun onBindViewHolder(holder: LinkedUserViewHolder, position: Int) {
val item = items[position]
holder.bind(item)
// 每次 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)
}
}
holder.bind(items[position], position)
}
override fun getItemCount(): Int = items.size
@@ -108,19 +53,14 @@ class LinkedUserAdapter(
val binding: ItemLinkedUserBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LinkedUser) {
fun bind(item: LinkedUser, position: Int) {
binding.tvUserName.text = item.name
binding.tvTime.text = item.time
binding.tvPhone.text = "手机号码:${item.phone}"
binding.tvIdCard.text = "身份证号:${item.idCard}"
if (isUnlinkMode) {
binding.tvUnlinkAction.visibility = View.VISIBLE
binding.tvDeleteAction.visibility = View.GONE
} else {
binding.tvUnlinkAction.visibility = View.GONE
binding.tvDeleteAction.visibility = View.VISIBLE
binding.tvDeleteAction.text = deleteText
binding.root.setOnClickListener {
onItemClick(item, position)
}
}
}

View File

@@ -5,6 +5,7 @@ import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
@@ -22,10 +23,12 @@ import com.example.defenseapplication.utils.DeviceUserUpdateEvent
import com.example.defenseapplication.utils.RetrofitNetwork
import com.example.defenseapplication.view.CustomDialogFragmentText
import com.gyf.immersionbar.ImmersionBar
import com.yanzhenjie.recyclerview.SwipeMenuItem
import com.yanzhenjie.recyclerview.SwipeRecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
//关联用户页面
class LinkedUserActivity : AppCompatActivity() {
private lateinit var binding: LinkedUserActivityBinding
@@ -35,8 +38,6 @@ class LinkedUserActivity : AppCompatActivity() {
private var mode: Int = MODE_SETTINGS
companion object {
const val REQUEST_CODE_SEARCH = 1001
const val EXTRA_SEARCH_RESULT = "search_result"
const val EXTRA_MODE = "extra_mode"
const val MODE_SETTINGS = 0
const val MODE_PERSONAL = 1
@@ -73,45 +74,57 @@ class LinkedUserActivity : AppCompatActivity() {
}
private fun initView() {
val deleteText = if (mode == MODE_SETTINGS) getString(R.string.unlink) else getString(R.string.delete)
val isUnlinkMode = mode == MODE_SETTINGS
adapter = LinkedUserAdapter(
deleteText = deleteText,
isUnlinkMode = isUnlinkMode,
isUnlinkMode = mode == MODE_SETTINGS,
onItemClick = { user, position ->
val intent = Intent(this, UserDetailsActivity::class.java).apply {
putExtra("userId", user.userId)
putExtra("deleteText", if (mode == MODE_SETTINGS) getString(R.string.unlink) else getString(R.string.delete))
putExtra("isUnlinkMode", mode == MODE_SETTINGS)
}
startActivity(intent)
},
onDeleteClick = { user, position ->
showUnlinkDialog(user, position)
}
)
binding.rvLinkedUser.layoutManager = LinearLayoutManager(this)
binding.rvLinkedUser.adapter = adapter
(binding.rvLinkedUser as RecyclerView).layoutManager = LinearLayoutManager(this)
binding.swipeRefresh.setOnRefreshListener {
fetchUserList()
}
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()
}
binding.rvLinkedUser.setSwipeMenuCreator { leftMenu, rightMenu, position ->
if (mode == MODE_SETTINGS) {
val unlinkItem = SwipeMenuItem(this@LinkedUserActivity).apply {
setText(getString(R.string.unlink))
setBackgroundColor(getColor(R.color.red))
setTextColor(getColor(R.color.white))
width = 200
height = ViewGroup.LayoutParams.MATCH_PARENT
}
rightMenu.addMenuItem(unlinkItem)
} else {
val deleteItem = SwipeMenuItem(this@LinkedUserActivity).apply {
setText(getString(R.string.delete))
setBackgroundColor(getColor(R.color.red))
setTextColor(getColor(R.color.white))
width = 200
height = ViewGroup.LayoutParams.MATCH_PARENT
}
rightMenu.addMenuItem(deleteItem)
}
})
}
binding.rvLinkedUser.setOnItemMenuClickListener { _, position ->
// 直接关闭菜单(使用 RecyclerView 的方法)
binding.rvLinkedUser.smoothCloseMenu() // 或 closeMenu()
val user = userList[position]
showDeleteDialog(user, position)
}
(binding.rvLinkedUser as RecyclerView).adapter = adapter
}
private fun showUnlinkDialog(user: LinkedUser, position: Int) {
private fun showDeleteDialog(user: LinkedUser, position: Int) {
val dialog = CustomDialogFragmentText()
if (mode == MODE_SETTINGS) {
dialog.setTitle(getString(R.string.tip))
@@ -121,18 +134,10 @@ class LinkedUserActivity : AppCompatActivity() {
dialog.setMessage(getString(R.string.confirm_delete_user))
}
dialog.setShowInput(false)
dialog.setPositiveText(getString(R.string.confirm))
dialog.setNegativeText(getString(R.string.cancel))
dialog.setOnConfirmListener {
deleteDeviceUser(user, position)
}
dialog.setOnDismissListener {
// Dialog 关闭后,如果之前打开了菜单则关闭它
if (adapter.openSlideLayout != null) {
adapter.openSlideLayout?.closeMenu()
}
}
dialog.show(supportFragmentManager, "UnlinkDialog")
dialog.show(supportFragmentManager, "DeleteDialog")
}
private fun deleteDeviceUser(user: LinkedUser, position: Int) {
@@ -152,7 +157,8 @@ class LinkedUserActivity : AppCompatActivity() {
withContext(Dispatchers.Main) {
if (response.code == 200) {
userList.removeAt(position)
adapter.removeItem(position)
adapter.notifyItemRemoved(position) // 通知删除动画
adapter.notifyItemRangeChanged(position, userList.size - position) // 更新后续位置索引
Toast.makeText(this@LinkedUserActivity, "删除成功", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@LinkedUserActivity, response.msg, Toast.LENGTH_SHORT).show()
@@ -175,12 +181,15 @@ class LinkedUserActivity : AppCompatActivity() {
binding.swipeRefresh.isRefreshing = true
lifecycleScope.launch {
try {
val response = if (mode == MODE_SETTINGS) {
val deviceId = DeviceIdEvent.getDeviceId()
if (deviceId.isNullOrEmpty()) {
Toast.makeText(this@LinkedUserActivity, "设备 ID 为空", Toast.LENGTH_SHORT).show()
return@launch
val deviceId = DeviceIdEvent.getDeviceId()
if (deviceId.isNullOrEmpty()) {
Toast.makeText(this@LinkedUserActivity, "设备 ID 为空", Toast.LENGTH_SHORT).show()
withContext(Dispatchers.Main) {
binding.swipeRefresh.isRefreshing = false
}
return@launch
}
val response = if (mode == MODE_SETTINGS) {
RetrofitNetwork.getNetworkService().getDeviceUserList(deviceId)
} else {
RetrofitNetwork.getNetworkService().getFamilyMembers()
@@ -289,18 +298,5 @@ class LinkedUserActivity : AppCompatActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_SEARCH && resultCode == RESULT_OK) {
data?.getStringExtra(EXTRA_SEARCH_RESULT)?.let { result ->
val newUser = LinkedUser(
userId = "",
name = result,
phone = "13800000000",
idCard = "110101199001011234",
time = "2026-05-10 ${android.text.format.DateFormat.format("HH:mm:ss", System.currentTimeMillis())}"
)
userList.add(0, newUser)
adapter.submitList(userList)
}
}
}
}
}

View File

@@ -1,176 +0,0 @@
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
}

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/bg_gray"
tools:context=".liveVideo.LinkedUserActivity">
<!-- 标题栏 -->
<LinearLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:background="@color/white">
<ImageView
android:id="@+id/ivBack"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/back" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/linked_user"
android:textColor="@color/black"
android:textSize="16dp"
android:textStyle="bold" />
</LinearLayout>
<!-- 搜索区域 -->
<LinearLayout
android:id="@+id/searchLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp"
android:layout_marginTop="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/bg_user_manage"
android:gravity="center_vertical"
android:paddingHorizontal="16dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@drawable/search"
android:layout_marginRight="8dp"/>
<EditText
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@null"
android:hint="@string/search_preset_text"
android:textColorHint="@color/gray"
android:textColor="@color/black"
android:textSize="14sp"
android:inputType="text"
android:imeOptions="actionSearch"
android:singleLine="true"/>
</LinearLayout>
</LinearLayout>
<!-- 列表区域 -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.yanzhenjie.recyclerview.SwipeRecyclerView
android:id="@+id/rvLinkedUser"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:paddingTop="8dp"
android:paddingBottom="16dp"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- 底部按钮 -->
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingBottom="60dp">
<android.widget.Button
android:id="@+id/btnAddLinkedUser"
android:layout_width="match_parent"
android:layout_height="40dp"
android:text="@string/add_linked_user"
android:textColor="@color/white"
android:textSize="14dp"
android:background="@drawable/rounded_blue_bg" />
<!-- 悬浮加号按钮 -->
<ImageView
android:visibility="gone"
android:layout_gravity="end"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/add_img"
android:id="@+id/fabAdd"/>
</LinearLayout>
</LinearLayout>

View File

@@ -1,86 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<com.example.defenseapplication.view.SlideLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="120dp"
android:background="@color/white"
android:orientation="vertical"
android:padding="16dp">
<!-- 背景按钮层:删除 + 解除关联 两个按钮 -->
<LinearLayout
android:id="@+id/llActions"
android:layout_width="wrap_content"
android:layout_height="120dp"
android:gravity="start|center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvDeleteAction"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="@color/red"
android:gravity="center"
android:text="@string/delete"
android:textColor="@color/white"
android:id="@+id/tvUserName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvUnlinkAction"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="@color/red"
android:gravity="center"
android:text="@string/unlink"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 前景内容层:随 swipe 移动 -->
<LinearLayout
android:id="@+id/llContent"
android:layout_width="match_parent"
android:layout_height="120dp"
android:background="@color/white"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvUserName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/gray"
android:textSize="12sp" />
</LinearLayout>
<TextView
android:id="@+id/tvPhone"
android:id="@+id/tvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/gray"
android:textSize="14sp" />
<TextView
android:id="@+id/tvIdCard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/gray"
android:textSize="14sp" />
android:textSize="12sp" />
</LinearLayout>
</com.example.defenseapplication.view.SlideLayout>
<TextView
android:id="@+id/tvPhone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/gray"
android:textSize="14sp" />
<TextView
android:id="@+id/tvIdCard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/gray"
android:textSize="14sp" />
</LinearLayout>

View File

@@ -78,7 +78,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
<com.yanzhenjie.recyclerview.SwipeRecyclerView
android:id="@+id/rvLinkedUser"
android:layout_width="match_parent"
android:layout_height="match_parent"