package com.margelo.nitro.progressandmodal import android.app.Activity import android.view.Gravity import android.view.View import android.view.ViewGroup import android.util.TypedValue import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import android.graphics.drawable.GradientDrawable import android.view.animation.AccelerateDecelerateInterpolator import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.graphics.toColorInt import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import android.os.Handler import android.os.Looper class PAMToastManager(private val getActivity: () -> Activity?) { private val toastContainerTag = 9997 private val handler = Handler(Looper.getMainLooper()) // Instance-level state (không dùng companion/static để tránh state leak) private val toastQueue: MutableList = mutableListOf() private val toastTimers: MutableMap = mutableMapOf() private var toastContainer: FrameLayout? = null fun configure() { getActivity()?.let { activity -> activity.runOnUiThread { ensureToastContainer(activity) } } } fun showToast(params: PAM_ToastParams) { val activity = getActivity() ?: return activity.runOnUiThread { ensureToastContainer(activity) val durationMs = params.duration?.toInt() ?: 2000 val toastView = buildToastView(activity, params) val stackOffset = ViewUtils.dpToPx(activity, 8f) * toastQueue.size toastView.alpha = 0f toastView.translationY = -stackOffset toastContainer?.addView( toastView, FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { gravity = Gravity.BOTTOM setMargins( ViewUtils.dpToPx(activity, 16f).toInt(), ViewUtils.dpToPx(activity, 8f).toInt(), ViewUtils.dpToPx(activity, 16f).toInt(), ViewUtils.dpToPx(activity, 8f).toInt() ) } ) toastQueue.add(toastView) updateToastPositions(activity) toastView.animate() .alpha(1f) .setDuration(250) .setInterpolator(AccelerateDecelerateInterpolator()) .start() val hideRunnable = Runnable { hideToast(toastView, activity) } toastTimers[toastView] = hideRunnable handler.postDelayed(hideRunnable, durationMs.toLong()) } } private fun hideToast(toast: View, activity: Activity) { if (!toastQueue.contains(toast)) return toastTimers.remove(toast)?.let { handler.removeCallbacks(it) } toast.animate() .alpha(0f) .setDuration(200) .withEndAction { toastContainer?.removeView(toast) toastQueue.remove(toast) updateToastPositions(activity) } .start() } private fun ensureToastContainer(activity: Activity) { // Nếu container vẫn attach vào window, chỉ refresh padding val existing = toastContainer if (existing != null && existing.parent != null) { applyCurrentInsetsPadding(activity, existing) return } val decor = activity.window.decorView as ViewGroup // Nếu container đã được add vào decor nhưng reference bị mất decor.findViewWithTag(toastContainerTag)?.let { toastContainer = it applyCurrentInsetsPadding(activity, it) return } val container = FrameLayout(activity).apply { tag = toastContainerTag layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) isClickable = false isFocusable = false clipToPadding = false clipChildren = false } ViewCompat.setOnApplyWindowInsetsListener(container) { v, insets -> val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom val sysBottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom val bottomInset = maxOf(imeBottom, sysBottom) v.setPadding( v.paddingLeft, v.paddingTop, v.paddingRight, bottomInset + ViewUtils.dpToPx(activity, 24f).toInt() ) updateToastPositions(activity) insets } applyCurrentInsetsPadding(activity, container) decor.addView(container) toastContainer = container } private fun applyCurrentInsetsPadding(activity: Activity, container: FrameLayout) { val navBarHeight = getNavigationBarHeight(activity) val rootInsets = ViewCompat.getRootWindowInsets(container) val imeBottom = rootInsets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0 val sysBottom = rootInsets?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom ?: navBarHeight val bottomInset = maxOf(imeBottom, sysBottom) container.setPadding( container.paddingLeft, container.paddingTop, container.paddingRight, bottomInset + ViewUtils.dpToPx(activity, 24f).toInt() ) } private fun updateToastPositions(activity: Activity) { while (toastQueue.size > 3) { toastQueue.firstOrNull()?.let { oldest -> toastTimers.remove(oldest)?.let { handler.removeCallbacks(it) } toastContainer?.removeView(oldest) toastQueue.remove(oldest) } } toastQueue.forEachIndexed { index, view -> val displayIndex = toastQueue.size - 1 - index val stackOffset = ViewUtils.dpToPx(activity, 8f) * displayIndex val scale = maxOf(0.94f, 1.0f - 0.03f * displayIndex) view.translationY = -stackOffset view.scaleX = scale view.scaleY = scale view.translationZ = ViewUtils.dpToPx(activity, 6f) + (toastQueue.size - 1 - displayIndex) * 0.1f } toastQueue.lastOrNull()?.bringToFront() } private fun buildToastView(activity: Activity, params: PAM_ToastParams): View { val container = LinearLayoutCompat(activity).apply { orientation = LinearLayoutCompat.HORIZONTAL gravity = Gravity.CENTER_VERTICAL background = GradientDrawable().apply { cornerRadius = ViewUtils.dpToPx(activity, 12f) setColor(params.backgroundColor.toColorInt()) } elevation = ViewUtils.dpToPx(activity, 8f) setPadding( ViewUtils.dpToPx(activity, 14f).toInt(), ViewUtils.dpToPx(activity, 12f).toInt(), ViewUtils.dpToPx(activity, 14f).toInt(), ViewUtils.dpToPx(activity, 12f).toInt() ) layoutParams = LinearLayoutCompat.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) } params.image?.let { source -> val iv = ImageView(activity) val size = ViewUtils.dpToPx(activity, params.imageSize?.toFloat() ?: 24f).toInt() ViewUtils.loadImageBitmap(source.uri, iv) val lp = LinearLayoutCompat.LayoutParams(size, size) lp.marginEnd = ViewUtils.dpToPx(activity, 8f).toInt() iv.layoutParams = lp container.addView(iv) } val tv = TextView(activity).apply { text = params.message setTextColor(params.textColor.toColorInt()) setTextSize(TypedValue.COMPLEX_UNIT_SP, (params.fontSize ?: 14).toFloat()) gravity = Gravity.CENTER_VERTICAL layoutParams = LinearLayoutCompat.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) } container.addView(tv) return container } private fun getNavigationBarHeight(activity: Activity): Int { val resId = activity.resources.getIdentifier("navigation_bar_height", "dimen", "android") return if (resId > 0) activity.resources.getDimensionPixelSize(resId) else 0 } }