Compare commits

..

2 Commits

Author SHA1 Message Date
fd84f18e4d 关联用户列表优化 2026-05-25 14:04:53 +08:00
fe025c8a69 关联用户滑动效果 2026-05-23 15:39:39 +08:00
10 changed files with 353 additions and 291 deletions

View File

@@ -0,0 +1,48 @@
kotlin version: 2.2.10
error message: java.nio.file.AccessDeniedException: C:\Users\Administrator\AppData\Local\kotlin\daemon\kotlin-daemon-client-tsmarker12650102232292672793.tmp
error message: Daemon compilation failed: Could not connect to Kotlin compile daemon
java.lang.RuntimeException: Could not connect to Kotlin compile daemon
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:214)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:68)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:64)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:61)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:102)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:61)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:58)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:176)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
at org.gradle.internal.Factories$1.create(Factories.java:30)
at org.gradle.internal.work.DefaultWorkerLeaseService.lambda$withLocksAcquired$0(DefaultWorkerLeaseService.java:269)
at org.gradle.internal.work.ResourceLockStatistics$1.measure(ResourceLockStatistics.java:42)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocksAcquired(DefaultWorkerLeaseService.java:267)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:259)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)

View File

@@ -0,0 +1,48 @@
kotlin version: 2.2.10
error message: java.nio.file.AccessDeniedException: C:\Users\Administrator\AppData\Local\kotlin\daemon\kotlin-daemon-client-tsmarker2077405252922174295.tmp
error message: Daemon compilation failed: Could not connect to Kotlin compile daemon
java.lang.RuntimeException: Could not connect to Kotlin compile daemon
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:214)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:68)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:64)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:61)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:102)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:61)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:58)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:176)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
at org.gradle.internal.Factories$1.create(Factories.java:30)
at org.gradle.internal.work.DefaultWorkerLeaseService.lambda$withLocksAcquired$0(DefaultWorkerLeaseService.java:269)
at org.gradle.internal.work.ResourceLockStatistics$1.measure(ResourceLockStatistics.java:42)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocksAcquired(DefaultWorkerLeaseService.java:267)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:259)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)

View File

@@ -102,5 +102,6 @@ dependencies {
implementation("androidx.camera:camera-view:1.0.0-alpha25") implementation("androidx.camera:camera-view:1.0.0-alpha25")
//上拉刷新下拉加载 //上拉刷新下拉加载
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("com.yanzhenjie.recyclerview:x:1.3.2")
} }

View File

@@ -15,10 +15,8 @@ data class LinkedUser(
) )
class LinkedUserAdapter( class LinkedUserAdapter(
private val deleteText: String,
private val isUnlinkMode: Boolean, private val isUnlinkMode: Boolean,
private val onItemClick: (LinkedUser, Int) -> Unit, private val onItemClick: (LinkedUser, Int) -> Unit
private val onDeleteClick: (LinkedUser, Int) -> Unit
) : RecyclerView.Adapter<LinkedUserAdapter.LinkedUserViewHolder>() { ) : RecyclerView.Adapter<LinkedUserAdapter.LinkedUserViewHolder>() {
private val items = mutableListOf<LinkedUser>() private val items = mutableListOf<LinkedUser>()
@@ -36,12 +34,6 @@ class LinkedUserAdapter(
} }
} }
fun closeSwipe(position: Int) {
if (position in items.indices) {
notifyItemChanged(position)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LinkedUserViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LinkedUserViewHolder {
val binding = ItemLinkedUserBinding.inflate( val binding = ItemLinkedUserBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
@@ -52,53 +44,24 @@ class LinkedUserAdapter(
} }
override fun onBindViewHolder(holder: LinkedUserViewHolder, position: Int) { override fun onBindViewHolder(holder: LinkedUserViewHolder, position: Int) {
holder.bind(items[position], position) holder.bind(items[position], position)
} }
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
inner class LinkedUserViewHolder( inner class LinkedUserViewHolder(
private val binding: ItemLinkedUserBinding val binding: ItemLinkedUserBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: LinkedUser, position: Int) { fun bind(item: LinkedUser, position: Int) {
binding.tvPhone.text = "手机号码:${item.phone}"
binding.tvIdCard.text = "身份证号:${item.idCard}"
binding.llContent.translationX = 0f
binding.tvUserName.text = item.name binding.tvUserName.text = item.name
binding.tvTime.text = item.time binding.tvTime.text = item.time
binding.tvPhone.text = "手机号码:${item.phone}"
binding.tvIdCard.text = "身份证号:${item.idCard}"
// MODE_SETTINGS → 显示"解除关联"MODE_PERSONAL → 显示"删除" binding.root.setOnClickListener {
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.llContent.setOnClickListener {
if (binding.llContent.translationX < 0f) {
binding.llContent.animate()
.translationX(0f)
.setDuration(200)
.start()
} else {
onItemClick(item, position) 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import androidx.recyclerview.widget.RecyclerView
class AddLinkedUserActivity : AppCompatActivity() { class AddLinkedUserActivity : AppCompatActivity() {
private lateinit var binding: AddLinkedUserActivityBinding private lateinit var binding: AddLinkedUserActivityBinding
private val selectedPhoneSet = mutableSetOf<String>() private val selectedPhoneSet = mutableSetOf<String>()
@@ -37,9 +37,16 @@ class AddLinkedUserActivity : AppCompatActivity() {
initRecyclerView() initRecyclerView()
initListener() initListener()
setupSwipeRefresh()
fetchFamilyMembers() fetchFamilyMembers()
} }
private fun setupSwipeRefresh() {
binding.swipeRefresh.setOnRefreshListener {
fetchFamilyMembers()
}
}
private fun initRecyclerView() { private fun initRecyclerView() {
binding.rvAddLinkedUser.layoutManager = LinearLayoutManager(this) binding.rvAddLinkedUser.layoutManager = LinearLayoutManager(this)
binding.rvAddLinkedUser.adapter = binding.rvAddLinkedUser.adapter =
@@ -52,13 +59,31 @@ class AddLinkedUserActivity : AppCompatActivity() {
selectedUserIdMap.remove(phone) 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() { private fun fetchFamilyMembers() {
binding.swipeRefresh.isRefreshing = true
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val response = RetrofitNetwork.getNetworkService().getFamilyMembers() val response = RetrofitNetwork.getNetworkService().getFamilyMembers()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.swipeRefresh.isRefreshing = false
if (response.code == 200) { if (response.code == 200) {
if (response.data != null && response.data.isNotEmpty()) { if (response.data != null && response.data.isNotEmpty()) {
updateFamilyMemberList(response.data) updateFamilyMemberList(response.data)
@@ -72,6 +97,7 @@ class AddLinkedUserActivity : AppCompatActivity() {
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("AddLinkedUserActivity", "网络请求异常", e) android.util.Log.e("AddLinkedUserActivity", "网络请求异常", e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.swipeRefresh.isRefreshing = false
Toast.makeText(this@AddLinkedUserActivity, "网络请求失败: ${e.message}", Toast.LENGTH_SHORT).show() Toast.makeText(this@AddLinkedUserActivity, "网络请求失败: ${e.message}", Toast.LENGTH_SHORT).show()
} }
} }

View File

@@ -4,12 +4,11 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.example.defenseapplication.R import com.example.defenseapplication.R
@@ -24,10 +23,12 @@ import com.example.defenseapplication.utils.DeviceUserUpdateEvent
import com.example.defenseapplication.utils.RetrofitNetwork import com.example.defenseapplication.utils.RetrofitNetwork
import com.example.defenseapplication.view.CustomDialogFragmentText import com.example.defenseapplication.view.CustomDialogFragmentText
import com.gyf.immersionbar.ImmersionBar import com.gyf.immersionbar.ImmersionBar
import com.yanzhenjie.recyclerview.SwipeMenuItem
import com.yanzhenjie.recyclerview.SwipeRecyclerView
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
//关联用户页面
class LinkedUserActivity : AppCompatActivity() { class LinkedUserActivity : AppCompatActivity() {
private lateinit var binding: LinkedUserActivityBinding private lateinit var binding: LinkedUserActivityBinding
@@ -37,8 +38,6 @@ class LinkedUserActivity : AppCompatActivity() {
private var mode: Int = MODE_SETTINGS private var mode: Int = MODE_SETTINGS
companion object { companion object {
const val REQUEST_CODE_SEARCH = 1001
const val EXTRA_SEARCH_RESULT = "search_result"
const val EXTRA_MODE = "extra_mode" const val EXTRA_MODE = "extra_mode"
const val MODE_SETTINGS = 0 const val MODE_SETTINGS = 0
const val MODE_PERSONAL = 1 const val MODE_PERSONAL = 1
@@ -75,139 +74,57 @@ class LinkedUserActivity : AppCompatActivity() {
} }
private fun initView() { 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( adapter = LinkedUserAdapter(
deleteText = deleteText, isUnlinkMode = mode == MODE_SETTINGS,
isUnlinkMode = isUnlinkMode,
onItemClick = { user, position -> onItemClick = { user, position ->
val intent = Intent(this, UserDetailsActivity::class.java).apply { val intent = Intent(this, UserDetailsActivity::class.java).apply {
putExtra("userId", user.userId) 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) startActivity(intent)
},
onDeleteClick = { user, position ->
showUnlinkDialog(user, position)
} }
) )
binding.rvLinkedUser.layoutManager = LinearLayoutManager(this)
binding.rvLinkedUser.adapter = adapter
// 根据模式只显示一个按钮,滑动露出完整按钮 (binding.rvLinkedUser as RecyclerView).layoutManager = LinearLayoutManager(this)
val swipeThresholdPx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, -100f, resources.displayMetrics
)
val heldPositions = mutableMapOf<Int, Boolean>()
var currentlyHeldPosition: Int = -1 binding.swipeRefresh.setOnRefreshListener {
fetchUserList()
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( binding.rvLinkedUser.setSwipeMenuCreator { leftMenu, rightMenu, position ->
c: android.graphics.Canvas, if (mode == MODE_SETTINGS) {
recyclerView: RecyclerView, val unlinkItem = SwipeMenuItem(this@LinkedUserActivity).apply {
viewHolder: RecyclerView.ViewHolder, setText(getString(R.string.unlink))
dX: Float, setBackgroundColor(getColor(R.color.red))
dY: Float, setTextColor(getColor(R.color.white))
actionState: Int, width = 200
isCurrentlyActive: Boolean height = ViewGroup.LayoutParams.MATCH_PARENT
) {
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) rightMenu.addMenuItem(unlinkItem)
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 { } else {
// 保持展开或继续左滑(限制不超过阈值) val deleteItem = SwipeMenuItem(this@LinkedUserActivity).apply {
contentLayout.translationX = swipeThresholdPx setText(getString(R.string.delete))
} setBackgroundColor(getColor(R.color.red))
} else { setTextColor(getColor(R.color.white))
// 未展开,正常滑动限制(左滑不超过阈值,右滑不超过原位) width = 200
val swipeDistance = when { height = ViewGroup.LayoutParams.MATCH_PARENT
dX < swipeThresholdPx -> swipeThresholdPx
dX > 0 -> 0f
else -> dX
}
contentLayout.translationX = swipeDistance
}
} else {
val isHeld = heldPositions[position]
if (isHeld == true) {
contentLayout.translationX = swipeThresholdPx
}
} }
rightMenu.addMenuItem(deleteItem)
} }
} }
/* binding.rvLinkedUser.setOnItemMenuClickListener { _, position ->
- 滑动超过一半后松手:列表项 保持在滑动位置,显示按钮 // 直接关闭菜单(使用 RecyclerView 的方法)
- 滑动未超过一半松手:列表项 恢复原位 binding.rvLinkedUser.smoothCloseMenu() // 或 closeMenu()
- 已展开状态下向右拖动归位或拖动超过一半松手:恢复原位 val user = userList[position]
* */ showDeleteDialog(user, position)
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 (binding.rvLinkedUser as RecyclerView).adapter = adapter
})
itemTouchHelper.attachToRecyclerView(binding.rvLinkedUser)
} }
private var currentSwipePosition: Int = -1 private fun showDeleteDialog(user: LinkedUser, position: Int) {
private var currentSwipeHeld: Boolean = false
private fun showUnlinkDialog(user: LinkedUser, position: Int) {
currentSwipePosition = position
currentSwipeHeld = true
val dialog = CustomDialogFragmentText() val dialog = CustomDialogFragmentText()
if (mode == MODE_SETTINGS) { if (mode == MODE_SETTINGS) {
dialog.setTitle(getString(R.string.tip)) dialog.setTitle(getString(R.string.tip))
@@ -217,22 +134,16 @@ class LinkedUserActivity : AppCompatActivity() {
dialog.setMessage(getString(R.string.confirm_delete_user)) dialog.setMessage(getString(R.string.confirm_delete_user))
} }
dialog.setShowInput(false) dialog.setShowInput(false)
dialog.setPositiveText(getString(R.string.confirm))
dialog.setNegativeText(getString(R.string.cancel))
dialog.setOnConfirmListener { dialog.setOnConfirmListener {
deleteDeviceUser(user, position) deleteDeviceUser(user, position)
} }
dialog.setOnDismissListener { dialog.show(supportFragmentManager, "DeleteDialog")
resetSwipeState()
}
dialog.show(supportFragmentManager, "UnlinkDialog")
} }
private fun deleteDeviceUser(user: LinkedUser, position: Int) { private fun deleteDeviceUser(user: LinkedUser, position: Int) {
val deviceId = DeviceIdEvent.getDeviceId() val deviceId = DeviceIdEvent.getDeviceId()
if (deviceId.isNullOrEmpty() || user.userId.isEmpty()) { if (deviceId.isNullOrEmpty() || user.userId.isEmpty()) {
Toast.makeText(this, "参数错误", Toast.LENGTH_SHORT).show() Toast.makeText(this, "参数错误", Toast.LENGTH_SHORT).show()
resetSwipeState()
return return
} }
@@ -246,57 +157,45 @@ class LinkedUserActivity : AppCompatActivity() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (response.code == 200) { if (response.code == 200) {
userList.removeAt(position) userList.removeAt(position)
adapter.removeItem(position) adapter.notifyItemRemoved(position) // 通知删除动画
adapter.notifyItemRangeChanged(position, userList.size - position) // 更新后续位置索引
Toast.makeText(this@LinkedUserActivity, "删除成功", Toast.LENGTH_SHORT).show() Toast.makeText(this@LinkedUserActivity, "删除成功", Toast.LENGTH_SHORT).show()
} else { } else {
Toast.makeText(this@LinkedUserActivity, response.msg, Toast.LENGTH_SHORT).show() Toast.makeText(this@LinkedUserActivity, response.msg, Toast.LENGTH_SHORT).show()
} }
resetSwipeState()
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(this@LinkedUserActivity, "网络请求失败", Toast.LENGTH_SHORT).show() 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() { private fun initData() {
fetchUserList() fetchUserList()
} }
private fun fetchUserList() { private fun fetchUserList() {
binding.swipeRefresh.isRefreshing = true
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val response = if (mode == MODE_SETTINGS) {
val deviceId = DeviceIdEvent.getDeviceId() val deviceId = DeviceIdEvent.getDeviceId()
if (deviceId.isNullOrEmpty()) { if (deviceId.isNullOrEmpty()) {
Toast.makeText(this@LinkedUserActivity, "设备 ID 为空", Toast.LENGTH_SHORT).show() Toast.makeText(this@LinkedUserActivity, "设备 ID 为空", Toast.LENGTH_SHORT).show()
withContext(Dispatchers.Main) {
binding.swipeRefresh.isRefreshing = false
}
return@launch return@launch
} }
val response = if (mode == MODE_SETTINGS) {
RetrofitNetwork.getNetworkService().getDeviceUserList(deviceId) RetrofitNetwork.getNetworkService().getDeviceUserList(deviceId)
} else { } else {
RetrofitNetwork.getNetworkService().getFamilyMembers() RetrofitNetwork.getNetworkService().getFamilyMembers()
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.swipeRefresh.isRefreshing = false
if (response.code == 200 && response.data != null) { if (response.code == 200 && response.data != null) {
if (mode == MODE_SETTINGS) { if (mode == MODE_SETTINGS) {
updateDeviceUserList(response.data as List<DeviceUserBean>) updateDeviceUserList(response.data as List<DeviceUserBean>)
@@ -310,6 +209,7 @@ class LinkedUserActivity : AppCompatActivity() {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.swipeRefresh.isRefreshing = false
Toast.makeText(this@LinkedUserActivity, "网络请求失败", Toast.LENGTH_SHORT).show() Toast.makeText(this@LinkedUserActivity, "网络请求失败", Toast.LENGTH_SHORT).show()
} }
} }
@@ -398,18 +298,5 @@ class LinkedUserActivity : AppCompatActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) 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

@@ -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

@@ -68,14 +68,19 @@
</LinearLayout> </LinearLayout>
<!-- 列表区域 --> <!-- 列表区域 -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvAddLinkedUser" android:id="@+id/rvAddLinkedUser"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:layout_weight="1"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="16dp"/> android:paddingBottom="16dp"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- 底部按钮 --> <!-- 底部按钮 -->
<LinearLayout <LinearLayout

View File

@@ -1,43 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 背景按钮层:删除 + 解除关联 两个按钮 -->
<LinearLayout
android:id="@+id/llActions"
android:layout_width="match_parent"
android:layout_height="120dp"
android:gravity="end"
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: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_width="match_parent"
android:layout_height="120dp" android:layout_height="120dp"
android:background="@color/white" android:background="@color/white"
@@ -81,6 +43,5 @@
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:textColor="@color/gray" android:textColor="@color/gray"
android:textSize="14sp" /> android:textSize="14sp" />
</LinearLayout>
</FrameLayout> </LinearLayout>

View File

@@ -73,14 +73,19 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<!-- 列表区域 --> <!-- 列表区域 -->
<androidx.recyclerview.widget.RecyclerView <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/rvLinkedUser" android:id="@+id/swipeRefresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" 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:paddingHorizontal="16dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="16dp"/> android:paddingBottom="16dp"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- 底部按钮 --> <!-- 底部按钮 -->
<LinearLayout <LinearLayout