diff --git a/app/src/main/java/com/shuwei/dish/match/base/BaseActivity.kt b/app/src/main/java/com/shuwei/dish/match/base/BaseActivity.kt index 522ee70..5ede779 100644 --- a/app/src/main/java/com/shuwei/dish/match/base/BaseActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/base/BaseActivity.kt @@ -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) diff --git a/app/src/main/java/com/shuwei/dish/match/dialog/LoadingDialog.kt b/app/src/main/java/com/shuwei/dish/match/dialog/LoadingDialog.kt index f893ce8..e22cb4a 100644 --- a/app/src/main/java/com/shuwei/dish/match/dialog/LoadingDialog.kt +++ b/app/src/main/java/com/shuwei/dish/match/dialog/LoadingDialog.kt @@ -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) } } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt index 7e2d01e..a10c10a 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/FoodListFragment.kt @@ -145,6 +145,8 @@ class FoodListFragment : BaseFragment() { 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() { 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() diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt index e8cb506..244485f 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/VectorCollectionFragment.kt @@ -334,7 +334,9 @@ class VectorCollectionFragment : BaseFragment() 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() params["goodsId"] = checkedItem!!.goodsId.toRequestBody() params["goodsName"] = checkedItem!!.goodsName!!.toRequestBody() @@ -358,7 +360,7 @@ class VectorCollectionFragment : BaseFragment() 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() 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