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