关联用户滑动效果

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

@@ -5,6 +5,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.example.defenseapplication.databinding.ItemLinkedUserBinding import com.example.defenseapplication.databinding.ItemLinkedUserBinding
import com.example.defenseapplication.view.SlideLayout
data class LinkedUser( data class LinkedUser(
val userId: String, val userId: String,
@@ -22,6 +23,11 @@ class LinkedUserAdapter(
) : RecyclerView.Adapter<LinkedUserAdapter.LinkedUserViewHolder>() { ) : RecyclerView.Adapter<LinkedUserAdapter.LinkedUserViewHolder>() {
private val items = mutableListOf<LinkedUser>() private val items = mutableListOf<LinkedUser>()
private var _openSlideLayout: SlideLayout? = null
/** 供外部(如 Activity访问当前展开的 SlideLayout */
val openSlideLayout: SlideLayout?
get() = _openSlideLayout
fun submitList(userList: List<LinkedUser>) { fun submitList(userList: List<LinkedUser>) {
items.clear() 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 { 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,24 +52,68 @@ class LinkedUserAdapter(
} }
override fun onBindViewHolder(holder: LinkedUserViewHolder, position: Int) { 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 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) {
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 → 显示"删除"
if (isUnlinkMode) { if (isUnlinkMode) {
binding.tvUnlinkAction.visibility = View.VISIBLE binding.tvUnlinkAction.visibility = View.VISIBLE
binding.tvDeleteAction.visibility = View.GONE binding.tvDeleteAction.visibility = View.GONE
@@ -78,27 +122,6 @@ class LinkedUserAdapter(
binding.tvDeleteAction.visibility = View.VISIBLE binding.tvDeleteAction.visibility = View.VISIBLE
binding.tvDeleteAction.text = deleteText 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.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,10 @@ 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.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
@@ -93,120 +91,26 @@ class LinkedUserActivity : AppCompatActivity() {
binding.rvLinkedUser.layoutManager = LinearLayoutManager(this) binding.rvLinkedUser.layoutManager = LinearLayoutManager(this)
binding.rvLinkedUser.adapter = adapter binding.rvLinkedUser.adapter = adapter
// 根据模式只显示一个按钮,滑动露出完整按钮 binding.swipeRefresh.setOnRefreshListener {
val swipeThresholdPx = TypedValue.applyDimension( fetchUserList()
TypedValue.COMPLEX_UNIT_DIP, -100f, resources.displayMetrics
)
val heldPositions = mutableMapOf<Int, Boolean>()
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( binding.rvLinkedUser.addOnScrollListener(object : RecyclerView.OnScrollListener() {
c: android.graphics.Canvas, override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
recyclerView: RecyclerView, super.onScrolled(recyclerView, dx, dy)
viewHolder: RecyclerView.ViewHolder, val layoutManager = recyclerView.layoutManager as LinearLayoutManager
dX: Float, val totalItemCount = layoutManager.itemCount
dY: Float, val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
actionState: Int, if (totalItemCount > 0 && lastVisibleItem >= totalItemCount - 3) {
isCurrentlyActive: Boolean if (!binding.swipeRefresh.isRefreshing) {
) { fetchUserList()
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
} }
} }
} }
}
/*
- 滑动超过一半后松手:列表项 保持在滑动位置,显示按钮
- 滑动未超过一半松手:列表项 恢复原位
- 已展开状态下向右拖动归位或拖动超过一半松手:恢复原位
* */
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) { 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) {
@@ -223,7 +127,10 @@ class LinkedUserActivity : AppCompatActivity() {
deleteDeviceUser(user, position) deleteDeviceUser(user, position)
} }
dialog.setOnDismissListener { dialog.setOnDismissListener {
resetSwipeState() // Dialog 关闭后,如果之前打开了菜单则关闭它
if (adapter.openSlideLayout != null) {
adapter.openSlideLayout?.closeMenu()
}
} }
dialog.show(supportFragmentManager, "UnlinkDialog") dialog.show(supportFragmentManager, "UnlinkDialog")
} }
@@ -232,7 +139,6 @@ class LinkedUserActivity : AppCompatActivity() {
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
} }
@@ -251,39 +157,22 @@ class LinkedUserActivity : AppCompatActivity() {
} 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 response = if (mode == MODE_SETTINGS) {
@@ -297,6 +186,7 @@ class LinkedUserActivity : AppCompatActivity() {
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 +200,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()
} }
} }

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
}

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,5 +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" <com.example.defenseapplication.view.SlideLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@@ -7,9 +7,9 @@
<!-- 背景按钮层:删除 + 解除关联 两个按钮 --> <!-- 背景按钮层:删除 + 解除关联 两个按钮 -->
<LinearLayout <LinearLayout
android:id="@+id/llActions" android:id="@+id/llActions"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="120dp" android:layout_height="120dp"
android:gravity="end" android:gravity="start|center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
@@ -83,4 +83,4 @@
android:textSize="14sp" /> android:textSize="14sp" />
</LinearLayout> </LinearLayout>
</FrameLayout> </com.example.defenseapplication.view.SlideLayout>

View File

@@ -73,14 +73,19 @@
</LinearLayout> </LinearLayout>
</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/rvLinkedUser" android:id="@+id/rvLinkedUser"
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