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 0000000..f7a5a1a
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/setting.png differ
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 059a0fe..cbc3f3e 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -512,4 +512,11 @@
注销
确定注销该用户吗?
+
+ 没有更多了
+ 正在刷新…
+ 下拉刷新
+ 松开刷新
+ 正在刷新…
+
\ 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