关联用户滑动效果
This commit is contained in:
48
.kotlin/errors/errors-1779517395470.log
Normal file
48
.kotlin/errors/errors-1779517395470.log
Normal 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)
|
||||
|
||||
|
||||
48
.kotlin/errors/errors-1779517396396.log
Normal file
48
.kotlin/errors/errors-1779517396396.log
Normal 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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -68,14 +68,19 @@
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 列表区域 -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvAddLinkedUser"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="16dp"/>
|
||||
android:layout_weight="1">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvAddLinkedUser"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="16dp"/>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<LinearLayout
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
@@ -7,9 +7,9 @@
|
||||
<!-- 背景按钮层:删除 + 解除关联 两个按钮 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/llActions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="120dp"
|
||||
android:gravity="end"
|
||||
android:gravity="start|center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
@@ -83,4 +83,4 @@
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
</com.example.defenseapplication.view.SlideLayout>
|
||||
|
||||
@@ -73,14 +73,19 @@
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<!-- 列表区域 -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvLinkedUser"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="16dp"/>
|
||||
android:layout_weight="1">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user