From 1d54d6aa2a52484d3578742105941e20d8d47df4 Mon Sep 17 00:00:00 2001 From: maxinyu Date: Fri, 10 Jul 2026 10:42:28 +0800 Subject: [PATCH] =?UTF-8?q?logd=E5=8A=A0=E8=BD=BD=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .kotlin/errors/errors-1783643668109.log | 48 +++ .kotlin/errors/errors-1783645418169.log | 48 +++ .kotlin/errors/errors-1783645640346.log | 48 +++ .../defense_device/home/HomeMenuFragment.kt | 61 +++- .../ckkj/defense_device/view/LoadingDialog.kt | 22 +- .../defense_device/view/PullRefreshLayout.kt | 288 ++++++++++++++++++ app/src/main/res/layout/dialog_loading.xml | 2 +- .../main/res/layout/home_menu_fragment.xml | 6 +- .../main/res/layout/layout_refresh_header.xml | 24 ++ app/src/main/res/mipmap-xxxhdpi/setting.png | Bin 0 -> 8796 bytes app/src/main/res/values-zh/strings.xml | 7 + app/src/main/res/values/strings.xml | 7 + 12 files changed, 548 insertions(+), 13 deletions(-) create mode 100644 .kotlin/errors/errors-1783643668109.log create mode 100644 .kotlin/errors/errors-1783645418169.log create mode 100644 .kotlin/errors/errors-1783645640346.log create mode 100644 app/src/main/java/com/ckkj/defense_device/view/PullRefreshLayout.kt create mode 100644 app/src/main/res/layout/layout_refresh_header.xml create mode 100644 app/src/main/res/mipmap-xxxhdpi/setting.png diff --git a/.kotlin/errors/errors-1783643668109.log b/.kotlin/errors/errors-1783643668109.log new file mode 100644 index 0000000..2af1222 --- /dev/null +++ b/.kotlin/errors/errors-1783643668109.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-tsmarker11134052769962658558.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-1783645418169.log b/.kotlin/errors/errors-1783645418169.log new file mode 100644 index 0000000..43b12a2 --- /dev/null +++ b/.kotlin/errors/errors-1783645418169.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-tsmarker4236714666597388016.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-1783645640346.log b/.kotlin/errors/errors-1783645640346.log new file mode 100644 index 0000000..db5a68a --- /dev/null +++ b/.kotlin/errors/errors-1783645640346.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-tsmarker12754528945444950713.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/ckkj/defense_device/home/HomeMenuFragment.kt b/app/src/main/java/com/ckkj/defense_device/home/HomeMenuFragment.kt index c8faf51..f45183d 100644 --- a/app/src/main/java/com/ckkj/defense_device/home/HomeMenuFragment.kt +++ b/app/src/main/java/com/ckkj/defense_device/home/HomeMenuFragment.kt @@ -39,6 +39,7 @@ import com.ckkj.defense_device.utils.RetrofitNetwork import com.ckkj.defense_device.utils.UserinfoUtil import com.ckkj.defense_device.view.DefenseDialogs import com.ckkj.defense_device.view.LoadingDialog +import com.ckkj.defense_device.view.PullRefreshLayout import com.ckkj.defense_device.view.showDefenseDialog import com.gyf.immersionbar.ImmersionBar import com.maning.mlkitscanner.scan.MNScanManager @@ -62,6 +63,9 @@ class HomeMenuFragment : Fragment() { private val pageSize = 10 private var hasMore = true private var isLoading = false + private var loadingDialog: LoadingDialog? = null + private var lastNoMoreToastTime: Long = 0L + private val noMoreToastInterval: Long = 1500L private val requestPermissionsLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() @@ -162,7 +166,7 @@ class HomeMenuFragment : Fragment() { } private fun fetchInviterInfo(inviteCode: String) { - val loadingDialog = LoadingDialog(requireActivity()).apply { show() } + val loadingDialog = LoadingDialog(requireActivity()).setMessage(this.getString(R.string.loading)).apply { show() } CoroutineScope(Dispatchers.IO).launch { try { val response = RetrofitNetwork.getNetworkService() @@ -297,9 +301,11 @@ class HomeMenuFragment : Fragment() { ) } - binding.swipeRefreshLayout.setOnRefreshListener { - refreshDeviceList() - } + binding.swipeRefreshLayout.setOnRefreshListener(object : PullRefreshLayout.OnRefreshListener { + override fun onRefresh() { + refreshDeviceList() + } + }) binding.rvDeviceList.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -307,9 +313,24 @@ class HomeMenuFragment : Fragment() { val layoutManager = recyclerView.layoutManager as GridLayoutManager val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() val totalItemCount = layoutManager.itemCount + val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() + + if (firstVisibleItemPosition == 0) { + val firstChild = layoutManager.findViewByPosition(0) + firstChild?.let { child -> + val top = child.top + if (top >= 0) { + val offset = (top.toFloat() / dpToPx(80f)).coerceIn(0f, 1f) + binding.gradientLayout.translationY = -offset * dpToPx(20f) + } + } + } if (!isLoading && hasMore && lastVisibleItemPosition >= totalItemCount - 1 && dy > 0) { loadMoreDevices() + } else if (!hasMore && !isLoading && lastVisibleItemPosition >= totalItemCount - 1 + && dy > 0 && totalItemCount > 0) { + showNoMoreToast() } } }) @@ -425,7 +446,7 @@ class HomeMenuFragment : Fragment() { canViewDevice = userInfo?.role == "appadmin" && familyBean.role != "member" updateAdminUI() updateFamilyDisplayText(familyName) - fetchDeviceList() + fetchDeviceList(showLoading = true) FamilySwitchEvent.notifyFamilySwitched(familyName) } else { Toast.makeText(requireContext(), response.msg, Toast.LENGTH_SHORT).show() @@ -439,15 +460,20 @@ class HomeMenuFragment : Fragment() { } } - private fun fetchDeviceList(page: Int = 1, isLoadMore: Boolean = false) { + private fun fetchDeviceList(page: Int = 1, isLoadMore: Boolean = false, showLoading: Boolean = false) { if (!canViewDevice || isLoading) return isLoading = true + loadingDialog = if (showLoading) LoadingDialog(requireActivity()).setMessage("加载中...").apply { show() } else null + CoroutineScope(Dispatchers.IO).launch { try { val response = RetrofitNetwork.getNetworkService().getDeviceList() withContext(Dispatchers.Main) { + if (!isAdded) return@withContext + loadingDialog?.dismiss() + loadingDialog = null Log.d("DeviceList", "code = ${response.code}, data = ${response.data}") if (response.code == 200 && response.data != null) { val newDevices = response.data @@ -470,25 +496,36 @@ class HomeMenuFragment : Fragment() { } } isLoading = false + binding.swipeRefreshLayout.finishRefresh() } } catch (e: Exception) { withContext(Dispatchers.Main) { + if (!isAdded) return@withContext + loadingDialog?.dismiss() + loadingDialog = null noNetwork() Toast.makeText(requireContext(), R.string.network_error, Toast.LENGTH_SHORT).show() isLoading = false if (isLoadMore) { hasMore = false } + binding.swipeRefreshLayout.finishRefresh() } } } } + private fun showNoMoreToast() { + val now = System.currentTimeMillis() + if (now - lastNoMoreToastTime < noMoreToastInterval) return + lastNoMoreToastTime = now + ToastUtils.showToast(requireContext(), getString(R.string.no_more_data)) + } + private fun refreshDeviceList() { currentPage = 1 hasMore = true fetchDeviceList(currentPage, false) - binding.swipeRefreshLayout.isRefreshing = false } private fun loadMoreDevices() { @@ -566,6 +603,14 @@ class HomeMenuFragment : Fragment() { ).toInt() } + private fun dpToPx(dp: Float): Float { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp, + resources.displayMetrics + ) + } + private class GridHorizontalSpaceItemDecoration( private val horizontalSpace: Int ) : RecyclerView.ItemDecoration() { @@ -588,6 +633,8 @@ class HomeMenuFragment : Fragment() { override fun onDestroyView() { super.onDestroyView() + loadingDialog?.dismiss() + loadingDialog = null binding.rvFamilyList.adapter = null binding.rvDeviceList.adapter = null MessageStatusManager.removeListener(unreadCountChangeListener) diff --git a/app/src/main/java/com/ckkj/defense_device/view/LoadingDialog.kt b/app/src/main/java/com/ckkj/defense_device/view/LoadingDialog.kt index 4436af5..d100512 100644 --- a/app/src/main/java/com/ckkj/defense_device/view/LoadingDialog.kt +++ b/app/src/main/java/com/ckkj/defense_device/view/LoadingDialog.kt @@ -4,6 +4,8 @@ import android.app.Dialog import android.content.Context import android.graphics.Color import android.graphics.drawable.ColorDrawable +import android.os.Handler +import android.os.Looper import android.view.Gravity import android.view.LayoutInflater import android.view.WindowManager @@ -13,6 +15,13 @@ class LoadingDialog(context: Context) { private val dialog: Dialog = Dialog(context) private val binding: DialogLoadingBinding + private val handler = Handler(Looper.getMainLooper()) + private var showTime: Long = 0 + private val dismissRunnable = Runnable { + if (dialog.isShowing) { + dialog.dismiss() + } + } init { binding = DialogLoadingBinding.inflate(LayoutInflater.from(context)) @@ -35,14 +44,23 @@ class LoadingDialog(context: Context) { } fun show() { + handler.removeCallbacks(dismissRunnable) + showTime = System.currentTimeMillis() if (!dialog.isShowing) { dialog.show() } } fun dismiss() { - if (dialog.isShowing) { - dialog.dismiss() + handler.removeCallbacks(dismissRunnable) + val elapsed = System.currentTimeMillis() - showTime + val MIN_SHOW_TIME = 1000L + if (elapsed >= MIN_SHOW_TIME) { + if (dialog.isShowing) { + dialog.dismiss() + } + } else { + handler.postDelayed(dismissRunnable, MIN_SHOW_TIME - elapsed) } } diff --git a/app/src/main/java/com/ckkj/defense_device/view/PullRefreshLayout.kt b/app/src/main/java/com/ckkj/defense_device/view/PullRefreshLayout.kt new file mode 100644 index 0000000..311fe25 --- /dev/null +++ b/app/src/main/java/com/ckkj/defense_device/view/PullRefreshLayout.kt @@ -0,0 +1,288 @@ +package com.ckkj.defense_device.view + +import android.animation.ValueAnimator +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import android.view.ViewGroup +import android.view.animation.LinearInterpolator +import android.widget.ImageView +import android.widget.TextView +import com.ckkj.defense_device.R + +/** + * 自定义下拉刷新控件 + * 仿网易云/抖音风格:下拉时列表跟随向下移动,顶部出现 Logo + 文字下拉头。 + * + * 使用方式:直接包裹 RecyclerView 等可滚动子 View,调用 [setOnRefreshListener] 监听刷新。 + */ +class PullRefreshLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ViewGroup(context, attrs, defStyleAttr) { + + private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop + + /** 触发刷新的下拉阈值(Header 的高度) */ + private val headerHeight: Int = dp(120) + + /** 触发刷新的最大下拉距离(Header 高度的 1.5 倍) */ + private val maxPullDistance: Int = (headerHeight * 1.5f).toInt() + + private var headerView: View + private val ivRefreshIcon: ImageView + private val tvHint: TextView + + /** 子内容 View(通常是 RecyclerView) */ + private var contentView: View? = null + + private var downY = 0f + private var isDragging = false + + /** 当前下拉偏移量(>= 0) */ + private var currentOffset = 0f + + /** 是否处于刷新状态 */ + private var isRefreshing = false + set(value) { + if (field == value) return + field = value + if (value) { + startLoadingAnimation() + } else { + stopLoadingAnimation() + } + } + + private var refreshListener: OnRefreshListener? = null + private var resetAnimator: ValueAnimator? = null + + init { + clipToPadding = false + // 添加 Header 视图 + headerView = LayoutInflater.from(context).inflate(R.layout.layout_refresh_header, this, false) + addView(headerView) + ivRefreshIcon = headerView.findViewById(R.id.ivRefreshIcon) + tvHint = headerView.findViewById(R.id.tvRefreshHint) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + + // 测量 Header + val headerHeightSpec = MeasureSpec.makeMeasureSpec(headerHeight, MeasureSpec.EXACTLY) + headerView.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + headerHeightSpec + ) + + // 测量内容 View(仅第一个非 Header 子 View) + for (i in 0 until childCount) { + val child = getChildAt(i) + if (child === headerView) continue + child.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) + ) + contentView = child + } + setMeasuredDimension(width, height) + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + // Header 布局在容器顶部之上(初始隐藏,下拉时滑入) + headerView.layout( + 0, + -headerHeight + currentOffset.toInt(), + width, + currentOffset.toInt() + ) + + // 内容 View 跟随偏移 + contentView?.layout( + 0, + currentOffset.toInt(), + width, + height + currentOffset.toInt() + ) + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + if (isRefreshing) return false + + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> { + downY = ev.y + isDragging = false + cancelResetAnimator() + } + MotionEvent.ACTION_MOVE -> { + val dy = ev.y - downY + if (dy > touchSlop && canPullDown()) { + isDragging = true + return true + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + isDragging = false + } + } + return false + } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + if (isRefreshing) return false + + when (ev.actionMasked) { + MotionEvent.ACTION_MOVE -> { + val dy = ev.y - downY + if (dy > 0) { + if (!isDragging) { + if (dy > touchSlop) { + isDragging = true + } else { + return true + } + } + // 添加阻尼效果:越往下拉越吃力 + val damped = computePullOffset(dy) + currentOffset = damped.coerceAtMost(maxPullDistance.toFloat()) + updateHeaderState() + requestLayout() + return true + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if (currentOffset >= headerHeight * 0.9f) { + // 触发刷新 + triggerRefresh() + } else { + // 回弹 + resetToIdle() + } + isDragging = false + return true + } + } + return isDragging + } + + /** + * 添加阻尼效果,让下拉越深越慢,模拟弹性手感 + */ + private fun computePullOffset(rawDy: Float): Float { + // 原始距离的 0.5 倍为基本偏移,超过阈值后逐渐衰减 + val resistance = 1.8f + return (rawDy / resistance).coerceAtMost(maxPullDistance.toFloat()) + } + + /** + * 判断是否可以从当前位置下拉(列表已经滚动到顶部) + */ + private fun canPullDown(): Boolean { + val content = contentView ?: return false + return !content.canScrollVertically(-1) + } + + /** + * 根据下拉距离更新 Header 的视觉状态(旋转角度、提示文字) + */ + private fun updateHeaderState() { + val ratio = (currentOffset / headerHeight).coerceIn(0f, 1f) + + // 刷新图标根据下拉比例旋转 0~360 度 + ivRefreshIcon.rotation = ratio * 360f + + // 文字提示 + tvHint.text = when { + currentOffset >= headerHeight * 0.9f -> context.getString(R.string.refresh_hint_release) + else -> context.getString(R.string.refresh_hint_pull) + } + } + + private fun triggerRefresh() { + // 平滑动画到 header 高度 + val animator = ValueAnimator.ofFloat(currentOffset, headerHeight.toFloat()) + animator.duration = 200L + animator.interpolator = LinearInterpolator() + animator.addUpdateListener { + currentOffset = it.animatedValue as Float + requestLayout() + } + animator.start() + resetAnimator = animator + + isRefreshing = true + tvHint.text = context.getString(R.string.refresh_hint_refreshing) + refreshListener?.onRefresh() + } + + /** + * 结束刷新并回弹 + */ + fun finishRefresh() { + if (!isRefreshing) return + isRefreshing = false + resetToIdle() + } + + private fun resetToIdle() { + cancelResetAnimator() + val animator = ValueAnimator.ofFloat(currentOffset, 0f) + animator.duration = 300L + animator.interpolator = LinearInterpolator() + animator.addUpdateListener { + currentOffset = it.animatedValue as Float + if (!isRefreshing) { + // 刷新结束后,重置图标旋转角度 + ivRefreshIcon.rotation = 0f + tvHint.text = context.getString(R.string.refresh_hint_pull) + } + requestLayout() + } + animator.start() + resetAnimator = animator + } + + private fun cancelResetAnimator() { + resetAnimator?.cancel() + resetAnimator = null + } + + private fun startLoadingAnimation() { + // 持续旋转动画 + ivRefreshIcon.animate() + .rotation(ivRefreshIcon.rotation + 360f) + .setDuration(800L) + .setInterpolator(LinearInterpolator()) + .withEndAction { + if (isRefreshing) startLoadingAnimation() + } + .start() + } + + private fun stopLoadingAnimation() { + ivRefreshIcon.animate().cancel() + ivRefreshIcon.rotation = 0f + tvHint.text = context.getString(R.string.refresh_hint_pull) + } + + fun setOnRefreshListener(listener: OnRefreshListener) { + this.refreshListener = listener + } + + interface OnRefreshListener { + fun onRefresh() + } + + private fun dp(value: Int): Int { + val density = resources.displayMetrics.density + return (value * density).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_loading.xml b/app/src/main/res/layout/dialog_loading.xml index cd687d7..9bf44e0 100644 --- a/app/src/main/res/layout/dialog_loading.xml +++ b/app/src/main/res/layout/dialog_loading.xml @@ -20,7 +20,7 @@ android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="@string/uploading" - android:textColor="@color/black" + android:textColor="@color/green" android:textSize="16sp"/> \ No newline at end of file diff --git a/app/src/main/res/layout/home_menu_fragment.xml b/app/src/main/res/layout/home_menu_fragment.xml index 6d8326f..e0e13e5 100644 --- a/app/src/main/res/layout/home_menu_fragment.xml +++ b/app/src/main/res/layout/home_menu_fragment.xml @@ -134,7 +134,7 @@ - - + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-xxxhdpi/setting.png b/app/src/main/res/mipmap-xxxhdpi/setting.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a5a1a6a70363b639d4e34693f9126adc5b1c42 GIT binary patch literal 8796 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注销 确定注销该用户吗? + + 没有更多了 + 正在刷新… + 下拉刷新 + 松开刷新 + 正在刷新… + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 378aa45..fbe81c2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -519,4 +519,11 @@ Current Account Cancel Confirm Cancel Account User Account + + + No more data + Refreshing… + Pull down to refresh + Release to refresh + Refreshing… \ No newline at end of file