fix(base): 修复Loading弹窗管理和超时机制
- 添加了Loading超时自动关闭功能,默认15秒超时 - 实现了Activity销毁时只关闭自己的Loading,防止误关其他页面 - 优化了Loading重复显示时的节流处理,避免ANR问题 - 在BaseActivity中增加了showLoading方法的超时参数支持 - 为图片上传等长时间操作添加了60秒超时兜底机制 - 增加了相关日志便于调试Loading的显示和关闭流程
This commit is contained in:
@@ -5,6 +5,7 @@ import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
@@ -37,6 +38,7 @@ import kotlin.getValue
|
||||
open class BaseActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
const val TAG = "BaseActivity"
|
||||
/** 日期时间格式化器,复用避免每秒重复创建 */
|
||||
private val DATE_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy年MM月dd日 EEEE***HH:mm:ss", Locale.CHINA)
|
||||
@@ -71,7 +73,8 @@ open class BaseActivity : AppCompatActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
dismissLoading()
|
||||
// 只关闭自己弹出的 Loading,防止误关其他 Activity 的 Loading
|
||||
Loading.dismissIfOwnedBy(this)
|
||||
super.onDestroy()
|
||||
ActivityManager.removeActivity(this)
|
||||
}
|
||||
@@ -150,22 +153,25 @@ open class BaseActivity : AppCompatActivity() {
|
||||
handler.removeCallbacks(updateTask)
|
||||
}
|
||||
|
||||
fun showLoading(msg: String = "加载中……") {
|
||||
fun showLoading(msg: String = "加载中……", timeoutMs: Long = Loading.DEFAULT_TIMEOUT_MS) {
|
||||
handler.post {
|
||||
Loading.show(
|
||||
context = this,
|
||||
message = msg
|
||||
message = msg,
|
||||
timeoutMs = timeoutMs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissLoading() {
|
||||
Log.d(TAG, "dismissLoading, ${this.javaClass.simpleName}")
|
||||
handler.post {
|
||||
Loading.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
fun delayDismissLoading() {
|
||||
Log.d(TAG, "delayDismissLoading, ${this.javaClass.simpleName}")
|
||||
handler.postDelayed({
|
||||
Loading.dismiss()
|
||||
}, 500)
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
package com.shuwei.dish.match.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import com.shuwei.dish.match.R
|
||||
import com.shuwei.dish.match.databinding.DialogLoadingBinding
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object Loading {
|
||||
|
||||
/** 默认超时兜底时长(毫秒):业务方未传值时使用 */
|
||||
const val DEFAULT_TIMEOUT_MS = 15_000L
|
||||
|
||||
private var dialog: LoadingDialog? = null
|
||||
/** 当前持有 Loading 的 context,用于隔离 Activity 销毁时的误关 */
|
||||
private var owner: Context? = null
|
||||
private val timeoutHandler = Handler(Looper.getMainLooper())
|
||||
private val timeoutRunnable = Runnable { dismiss() }
|
||||
|
||||
fun isShowing(): Boolean {
|
||||
return dialog != null && dialog!!.isShowing
|
||||
@@ -22,30 +33,71 @@ object Loading {
|
||||
dialog!!.updateContent(content)
|
||||
}
|
||||
|
||||
fun show(context: Context, message: String = "加载中……", onDismiss: () -> Unit = {}) {
|
||||
/**
|
||||
* 显示 Loading 弹窗
|
||||
*
|
||||
* @param context 上下文
|
||||
* @param message 提示文案
|
||||
* @param timeoutMs 超时兜底时长(毫秒):到期自动 dismiss;传 0 关闭兜底,由业务方自己负责
|
||||
* @param onDismiss 关闭回调
|
||||
*/
|
||||
fun show(
|
||||
context: Context,
|
||||
message: String = "加载中……",
|
||||
timeoutMs: Long = DEFAULT_TIMEOUT_MS,
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
try {
|
||||
// 节流:已显示中只更新文案 + 重置超时,不重新 attach 窗口
|
||||
// 避免反复触发 FocusEvent 在主线程繁忙时引发 ANR
|
||||
if (dialog?.isShowing == true) {
|
||||
dialog?.dismiss()
|
||||
}
|
||||
if (dialog == null) {
|
||||
dialog = LoadingDialog(context = context, message = message, onDismiss = onDismiss)
|
||||
dialog?.updateContent(message)
|
||||
owner = context // 同一弹窗最后一次 show 决定 owner
|
||||
resetTimeout(timeoutMs)
|
||||
return
|
||||
}
|
||||
dialog = LoadingDialog(context = context, message = message, onDismiss = onDismiss)
|
||||
owner = context
|
||||
dialog?.show()
|
||||
resetTimeout(timeoutMs)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
timeoutHandler.removeCallbacks(timeoutRunnable)
|
||||
try {
|
||||
dialog?.let {
|
||||
if (it.isShowing) {
|
||||
it.dismiss()
|
||||
}
|
||||
}
|
||||
dialog = null
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
dialog = null
|
||||
owner = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activity 销毁专用:只关闭自己持有的 Loading,防止跨 Activity 误关
|
||||
* 单例 Loading 在多 Activity 共享时,若每个 Activity 的 onDestroy 都无脑 dismiss,
|
||||
* 会误关后启动 Activity 弹出的 Loading;由本方法做 owner 校验后才允许关闭
|
||||
*/
|
||||
fun dismissIfOwnedBy(context: Context) {
|
||||
if (owner === context) dismiss()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置超时兜底定时器
|
||||
* @param timeoutMs <=0 表示不安排定时器(关闭兜底)
|
||||
*/
|
||||
private fun resetTimeout(timeoutMs: Long) {
|
||||
timeoutHandler.removeCallbacks(timeoutRunnable)
|
||||
if (timeoutMs > 0) {
|
||||
timeoutHandler.postDelayed(timeoutRunnable, timeoutMs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,8 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Log.d(TAG, "onResume, dinnerType=$dinnerType, isVisible=${isVisible}, isHidden=${isHidden}")
|
||||
if (isHidden) return // 过滤 Activity 回前台时隐藏 tab 的无效触发
|
||||
runCatching {
|
||||
pageNo = 1
|
||||
getDishList(currentFoodName)
|
||||
@@ -226,16 +228,19 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
|
||||
val localList = if (pageNo == 1) pendingLocalList else null
|
||||
netViewModel.searchFoodListWithCallback(
|
||||
param = param,
|
||||
onLoading = { activity.showLoading() }
|
||||
onLoading = { activity.showLoading(timeoutMs = 35_000L) }
|
||||
) { result ->
|
||||
when (result) {
|
||||
is UiState.Success -> {
|
||||
if (isAdded.not()) return@searchFoodListWithCallback
|
||||
Log.d(TAG, "getDishList, UiState.Success")
|
||||
activity.delayDismissLoading()
|
||||
finishRefresh()
|
||||
loadAndMergeDishList(result.data, localList)
|
||||
}
|
||||
is UiState.Error -> {
|
||||
if (isAdded.not()) return@searchFoodListWithCallback
|
||||
Log.d(TAG, "getDishList, UiState.Error")
|
||||
toast(result.msg)
|
||||
finishRefresh()
|
||||
activity.delayDismissLoading()
|
||||
|
||||
@@ -334,7 +334,9 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
||||
private fun upload() {
|
||||
lifecycleScope.launch {
|
||||
val totalFileCount = vectorList.count { it.imageFile != null }
|
||||
showWaitingDialog("图片上传中0/$totalFileCount")
|
||||
// 上传场景单批可能耗时较长(含图片传输 + 网络往返),用 60s 超时兜底
|
||||
// 每次 onProgress 都会重新调用 showWaitingDialog,会重置定时器,正常进度不会触发兜底
|
||||
showWaitingDialog("图片上传中0/$totalFileCount", timeoutMs = 60_000L)
|
||||
val params = mutableMapOf<String, RequestBody>()
|
||||
params["goodsId"] = checkedItem!!.goodsId.toRequestBody()
|
||||
params["goodsName"] = checkedItem!!.goodsName!!.toRequestBody()
|
||||
@@ -358,7 +360,7 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
||||
log("uploadImage返回:result=${result.toJsonString()}")
|
||||
if (result is UiState.Success) result.data else null
|
||||
}, onProgress = { count, batch, idList ->
|
||||
showWaitingDialog("图片上传中$count/$totalFileCount")
|
||||
showWaitingDialog("图片上传中$count/$totalFileCount", timeoutMs = 60_000L)
|
||||
val foodList = batch.mapIndexed { index, it ->
|
||||
val name = checkedItem!!.goodsName + "&&" + checkedItem!!.rawMaterialsType
|
||||
Food(
|
||||
@@ -507,12 +509,14 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
|
||||
currentActivity.dismissLoading()
|
||||
}
|
||||
|
||||
private fun showWaitingDialog(msg: String) {
|
||||
if (Loading.isShowing()) {
|
||||
Loading.updateContent(msg)
|
||||
return
|
||||
}
|
||||
currentActivity.showLoading(msg)
|
||||
/**
|
||||
* 显示等待弹窗
|
||||
* @param msg 提示文案
|
||||
* @param timeoutMs 超时兜底时长(毫秒):到期自动关闭,传 0 关闭兜底
|
||||
* Loading 内部已做节流,重复调用只会更新文案 + 重置超时定时器
|
||||
*/
|
||||
private fun showWaitingDialog(msg: String, timeoutMs: Long = Loading.DEFAULT_TIMEOUT_MS) {
|
||||
currentActivity.showLoading(msg, timeoutMs)
|
||||
}
|
||||
|
||||
private lateinit var previewView: PreviewView
|
||||
|
||||
Reference in New Issue
Block a user