logd加载动画优化
This commit is contained in:
48
.kotlin/errors/errors-1783643668109.log
Normal file
48
.kotlin/errors/errors-1783643668109.log
Normal file
@@ -0,0 +1,48 @@
|
||||
kotlin version: 2.2.10
|
||||
error message: java.nio.file.AccessDeniedException: C:\Users\Administrator\AppData\Local\kotlin\daemon\kotlin-daemon-client-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)
|
||||
|
||||
|
||||
48
.kotlin/errors/errors-1783645418169.log
Normal file
48
.kotlin/errors/errors-1783645418169.log
Normal file
@@ -0,0 +1,48 @@
|
||||
kotlin version: 2.2.10
|
||||
error message: java.nio.file.AccessDeniedException: C:\Users\Administrator\AppData\Local\kotlin\daemon\kotlin-daemon-client-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)
|
||||
|
||||
|
||||
48
.kotlin/errors/errors-1783645640346.log
Normal file
48
.kotlin/errors/errors-1783645640346.log
Normal file
@@ -0,0 +1,48 @@
|
||||
kotlin version: 2.2.10
|
||||
error message: java.nio.file.AccessDeniedException: C:\Users\Administrator\AppData\Local\kotlin\daemon\kotlin-daemon-client-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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -134,7 +134,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
<com.ckkj.defense_device.view.PullRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginLeft="12dp"
|
||||
@@ -152,11 +152,11 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:clipToPadding="false"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:overScrollMode="never"
|
||||
android:paddingBottom="16dp"
|
||||
android:background="@android:color/transparent"
|
||||
tools:listitem="@layout/item_device" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</com.ckkj.defense_device.view.PullRefreshLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutNoPermission"
|
||||
|
||||
24
app/src/main/res/layout/layout_refresh_header.xml
Normal file
24
app/src/main/res/layout/layout_refresh_header.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingBottom="20dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivRefreshIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@mipmap/setting" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvRefreshHint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/refresh_hint_pull"
|
||||
android:textColor="@color/green"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
BIN
app/src/main/res/mipmap-xxxhdpi/setting.png
Normal file
BIN
app/src/main/res/mipmap-xxxhdpi/setting.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
@@ -512,4 +512,11 @@
|
||||
<string name="cancel_account_btn">注销</string>
|
||||
<string name="confirm_cancel_account_user_account">确定注销该用户吗?</string>
|
||||
|
||||
<!-- HomeMenuFragment 列表刷新 -->
|
||||
<string name="no_more_data">没有更多了</string>
|
||||
<string name="refreshing">正在刷新…</string>
|
||||
<string name="refresh_hint_pull">下拉刷新</string>
|
||||
<string name="refresh_hint_release">松开刷新</string>
|
||||
<string name="refresh_hint_refreshing">正在刷新…</string>
|
||||
|
||||
</resources>
|
||||
@@ -519,4 +519,11 @@
|
||||
<string name="current_account">Current Account</string>
|
||||
<string name="cancel_account_btn">Cancel</string>
|
||||
<string name="confirm_cancel_account_user_account">Confirm Cancel Account User Account</string>
|
||||
|
||||
<!-- Home Menu refresh / load more -->
|
||||
<string name="no_more_data">No more data</string>
|
||||
<string name="refreshing">Refreshing…</string>
|
||||
<string name="refresh_hint_pull">Pull down to refresh</string>
|
||||
<string name="refresh_hint_release">Release to refresh</string>
|
||||
<string name="refresh_hint_refreshing">Refreshing…</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user