package com.beancount.mobile.accessibility import android.content.Context import android.graphics.PixelFormat import android.os.Handler import android.os.Looper import android.util.Log import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.WindowManager import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.TextView import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit /** * 浮窗提示(plan.md「3.8 浮窗账单提示」)。 * * 参考 AutoAccounting 的 FloatingTip + RepeatToast: * - 轻量浮窗(非全屏),滑入动画,自动消失 * - 三种布局:顶部 / 左侧 / 右侧 * - 倒计时进度环 * - 重复账单提示(RepeatToast) * * 比 FloatingBillView 更轻:仅展示提示,不交互。 * 需 SYSTEM_ALERT_WINDOW 权限(由 Config Plugin 注册)。 */ class FloatingTip( private val context: Context, private val message: String, private val position: TipPosition = TipPosition.TOP, private val durationMs: Long = 3000L, ) { companion object { private const val TAG = "FloatingTip" private const val ANIM_DURATION = 300L } enum class TipPosition { TOP, LEFT, RIGHT } private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager private var view: View? = null private val handler = Handler(Looper.getMainLooper()) /** 显示浮窗提示。 */ fun show() { try { val layoutParams = WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT, ).apply { gravity = when (position) { TipPosition.TOP -> Gravity.TOP or Gravity.CENTER_HORIZONTAL TipPosition.LEFT -> Gravity.LEFT or Gravity.CENTER_VERTICAL TipPosition.RIGHT -> Gravity.RIGHT or Gravity.CENTER_VERTICAL } y = if (position == TipPosition.TOP) 100 else 0 x = if (position != TipPosition.TOP) 50 else 0 } val container = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL setBackgroundColor(0xF0333333.toInt()) setPadding(32, 16, 32, 16) } val text = TextView(context).apply { text = message textSize = 13f setTextColor(0xFFFFFFFF.toInt()) } // 倒计时进度条 val progress = ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal) progress.max = 100 progress.progress = 100 progress.layoutParams = LinearLayout.LayoutParams(200, 8).apply { setMargins(16, 0, 0, 0) } container.addView(text) container.addView(progress) view = container windowManager.addView(view, layoutParams) Log.d(TAG, "FloatingTip 显示: $message") // 倒计时动画(300ms 内 progress 从 100 到 0) val steps = 30 val stepMs = durationMs / steps var currentStep = 0 handler.post(object : Runnable { override fun run() { currentStep++ progress.progress = 100 - (currentStep * 100 / steps) if (currentStep < steps) { handler.postDelayed(this, stepMs) } } }) handler.postDelayed({ dismiss() }, durationMs) } catch (e: Exception) { Log.e(TAG, "FloatingTip 显示失败: ${e.message}") } } /** 关闭浮窗。 */ fun dismiss() { handler.removeCallbacksAndMessages(null) try { view?.let { windowManager.removeView(it) } } catch (_: Exception) {} view = null } } /** * 重复账单提示(plan.md「3.8」+ AutoAccounting RepeatToast)。 * 当检测到重复账单时,轻量提示用户(不弹浮窗,用系统 Toast 风格)。 */ class RepeatToast(private val context: Context, private val message: String) { fun show() { val tip = FloatingTip(context, "⚠ $message", FloatingTip.TipPosition.TOP, 2000L) tip.show() Log.d("RepeatToast", "重复提示: $message") } }