From fe025c8a692ed6d6ab090c721e13972f3aee90f2 Mon Sep 17 00:00:00 2001 From: maxinyu Date: Sat, 23 May 2026 15:39:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B3=E8=81=94=E7=94=A8=E6=88=B7=E6=BB=91?= =?UTF-8?q?=E5=8A=A8=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .kotlin/errors/errors-1779517395470.log | 48 +++++ .kotlin/errors/errors-1779517396396.log | 48 +++++ .../adapter/LinkedUserAdapter.kt | 91 +++++---- .../liveVideo/AddLinkedUserActivity.kt | 28 ++- .../liveVideo/LinkedUserActivity.kt | 147 ++------------- .../defenseapplication/view/SlideLayout.kt | 176 ++++++++++++++++++ .../res/layout/add_linked_user_activity.xml | 17 +- app/src/main/res/layout/item_linked_user.xml | 8 +- .../main/res/layout/linked_user_activity.xml | 17 +- 9 files changed, 401 insertions(+), 179 deletions(-) create mode 100644 .kotlin/errors/errors-1779517395470.log create mode 100644 .kotlin/errors/errors-1779517396396.log create mode 100644 app/src/main/java/com/example/defenseapplication/view/SlideLayout.kt diff --git a/.kotlin/errors/errors-1779517395470.log b/.kotlin/errors/errors-1779517395470.log new file mode 100644 index 0000000..0a6d54d --- /dev/null +++ b/.kotlin/errors/errors-1779517395470.log @@ -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) + + diff --git a/.kotlin/errors/errors-1779517396396.log b/.kotlin/errors/errors-1779517396396.log new file mode 100644 index 0000000..732808a --- /dev/null +++ b/.kotlin/errors/errors-1779517396396.log @@ -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) + + diff --git a/app/src/main/java/com/example/defenseapplication/adapter/LinkedUserAdapter.kt b/app/src/main/java/com/example/defenseapplication/adapter/LinkedUserAdapter.kt index 7b992ce..18990b2 100644 --- a/app/src/main/java/com/example/defenseapplication/adapter/LinkedUserAdapter.kt +++ b/app/src/main/java/com/example/defenseapplication/adapter/LinkedUserAdapter.kt @@ -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() { private val items = mutableListOf() + private var _openSlideLayout: SlideLayout? = null + + /** 供外部(如 Activity)访问当前展开的 SlideLayout */ + val openSlideLayout: SlideLayout? + get() = _openSlideLayout fun submitList(userList: List) { 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 } } diff --git a/app/src/main/java/com/example/defenseapplication/liveVideo/AddLinkedUserActivity.kt b/app/src/main/java/com/example/defenseapplication/liveVideo/AddLinkedUserActivity.kt index 5ce64ce..ef3f5cd 100644 --- a/app/src/main/java/com/example/defenseapplication/liveVideo/AddLinkedUserActivity.kt +++ b/app/src/main/java/com/example/defenseapplication/liveVideo/AddLinkedUserActivity.kt @@ -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() @@ -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() } } diff --git a/app/src/main/java/com/example/defenseapplication/liveVideo/LinkedUserActivity.kt b/app/src/main/java/com/example/defenseapplication/liveVideo/LinkedUserActivity.kt index 5448b2e..33dc062 100644 --- a/app/src/main/java/com/example/defenseapplication/liveVideo/LinkedUserActivity.kt +++ b/app/src/main/java/com/example/defenseapplication/liveVideo/LinkedUserActivity.kt @@ -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() + 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) @@ -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() } } diff --git a/app/src/main/java/com/example/defenseapplication/view/SlideLayout.kt b/app/src/main/java/com/example/defenseapplication/view/SlideLayout.kt new file mode 100644 index 0000000..fdca350 --- /dev/null +++ b/app/src/main/java/com/example/defenseapplication/view/SlideLayout.kt @@ -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 +} diff --git a/app/src/main/res/layout/add_linked_user_activity.xml b/app/src/main/res/layout/add_linked_user_activity.xml index c32e020..732e08f 100644 --- a/app/src/main/res/layout/add_linked_user_activity.xml +++ b/app/src/main/res/layout/add_linked_user_activity.xml @@ -68,14 +68,19 @@ - + android:layout_weight="1"> + + - @@ -7,9 +7,9 @@ - + diff --git a/app/src/main/res/layout/linked_user_activity.xml b/app/src/main/res/layout/linked_user_activity.xml index c400d26..ebf8636 100644 --- a/app/src/main/res/layout/linked_user_activity.xml +++ b/app/src/main/res/layout/linked_user_activity.xml @@ -73,14 +73,19 @@ - + android:layout_weight="1"> + +