From 87949a07ee6ddf752f74f5421d7a57a369e80189 Mon Sep 17 00:00:00 2001 From: PM Date: Thu, 29 Oct 2026 12:50:51 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(toast):=20=E6=96=B0=E5=A2=9E=E5=8F=AF?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=9A=84=E9=98=9F=E5=88=97=E5=8C=96=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=20Toast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CustomToastView 使用独立 HandlerThread 串行展示,避免连续 toast 互相覆盖 - 支持文字大小/颜色、背景色、圆角、显示时长、边距等配置 - 新增 layout_custom_toast.xml 布局 --- .../stardust/autojs/util/CustomToastView.kt | 229 ++++++++++++++++++ .../main/res/layout/layout_custom_toast.xml | 20 ++ 2 files changed, 249 insertions(+) create mode 100644 autojs/src/main/java/com/stardust/autojs/util/CustomToastView.kt create mode 100644 autojs/src/main/res/layout/layout_custom_toast.xml diff --git a/autojs/src/main/java/com/stardust/autojs/util/CustomToastView.kt b/autojs/src/main/java/com/stardust/autojs/util/CustomToastView.kt new file mode 100644 index 000000000..04a96c013 --- /dev/null +++ b/autojs/src/main/java/com/stardust/autojs/util/CustomToastView.kt @@ -0,0 +1,229 @@ +package com.stardust.autojs.util + +import android.content.Context +import android.graphics.BitmapFactory +import android.graphics.Color +import android.graphics.PixelFormat +import android.graphics.drawable.GradientDrawable +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.util.Base64 +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import android.widget.ImageView +import android.widget.TextView +import androidx.core.graphics.toColorInt +import com.stardust.autojs.R +import com.stardust.autojs.runtime.api.Console + +class CustomToastView( + private val context: Context, + private val console: Console? = null +) { + private val toastViews = mutableListOf() + private val toastHandler by lazy { + val thread = HandlerThread("ToastThread") + thread.start() + Handler(thread.looper) + } + private val mainHandler = Handler(Looper.getMainLooper()) + private val toastQueue = mutableListOf() + private var isShowing = false + + private data class ToastTask( + val msg: String, + val config: Config + ) + + data class Config( + var log: Boolean = false, + var duration: Long = 2000L, + var gravity: Int = Gravity.BOTTOM, + var xOffset: Int = 0, + var yOffset: Int = 150, + var alpha: Float = 1.0f, + var bg: Int = "#DD333333".toColorInt(), + var textColor: Int = Color.WHITE, + var textSize: Float = 16f, + var radius: Float = 8f, + var padding: Int = 8, + var icon: String? = null, + var override: Boolean = false + ) + + private var def = Config() + + private fun parseConfig(config: Any?, target: Config) { + if (config == null) return + try { + val map = config as? Map<*, *> + map?.let { configMap -> + configMap["log"]?.let { target.log = it as Boolean } + configMap["duration"]?.let { target.duration = (it as Number).toLong() } + configMap["gravity"]?.let { target.gravity = (it as Number).toInt() } + configMap["xOffset"]?.let { target.xOffset = (it as Number).toInt() } + configMap["yOffset"]?.let { target.yOffset = (it as Number).toInt() } + configMap["alpha"]?.let { target.alpha = (it as Number).toFloat() } + configMap["bg"]?.let { target.bg = (it as Number).toInt() } + configMap["textColor"]?.let { target.textColor = (it as Number).toInt() } + configMap["textSize"]?.let { target.textSize = (it as Number).toFloat() } + configMap["radius"]?.let { target.radius = (it as Number).toFloat() } + configMap["padding"]?.let { target.padding = (it as Number).toInt() } + configMap["icon"]?.let { + target.icon = when (it) { + is String -> it.replace(Regex("^data:image/\\w+;base64,"), "") + else -> it.toString() + } + } + configMap["override"]?.let { target.override = it as Boolean } + } + } catch (e: Exception) {} + } + + fun init(config: Any?) { + if (config != null) { + parseConfig(config, def) + } + } + + fun show(msg: String) { + showInternal(msg, def) + } + + fun show(msg: String, config: Any? = null) { + val cfg = def.copy() + if (config != null) { + parseConfig(config, cfg) + } + if (cfg.override) { + dismissAll() + showInternal(msg, cfg) + } else { + toastQueue.add(ToastTask(msg, cfg)) + if (!isShowing) { + showNext() + } + } + } + + private fun showNext() { + if (toastQueue.isEmpty()) { + isShowing = false + return + } + isShowing = true + val task = toastQueue.removeAt(0) + showInternal(task.msg, task.config) + } + + private fun showInternal(msg: String, config: Config) { + if (config.log) { + console?.log(msg) + } + showFull( + msg, + config.duration, + config.gravity, + config.xOffset, + config.yOffset, + config.alpha, + config.bg, + config.textColor, + config.textSize, + config.radius, + config.padding, + config.icon + ) + } + + private fun showFull( + msg: String, + duration: Long, + _gravity: Int, + xOffset: Int, + yOffset: Int, + alpha: Float, + bg: Int, + textColor: Int, + textSize: Float, + radius: Float, + padding: Int, + icon: String? + ) { + if (toastViews.size > 20) { + dismissAll() + } + val view = LayoutInflater.from(context).inflate(R.layout.layout_custom_toast, null, false) + val tv = view.findViewById(R.id.tv_toast_message) + tv.text = msg + tv.setTextColor(textColor) + tv.textSize = textSize + + val iv = view.findViewById(R.id.iv_toast_icon) + if (!icon.isNullOrEmpty()) { + val data = Base64.decode(icon, Base64.DEFAULT) + iv.setImageBitmap(BitmapFactory.decodeByteArray(data, 0, data.size)) + iv.visibility = View.VISIBLE + } else { + iv.visibility = View.GONE + } + + val drawable = GradientDrawable().apply { + setColor(bg) + cornerRadius = dipToPx(radius.toInt()).toFloat() + } + view.background = drawable + view.setPadding(dipToPx(padding), dipToPx(padding / 2), dipToPx(padding), dipToPx(padding / 2)) + view.alpha = alpha + + val layoutParams = WindowManager.LayoutParams().apply { + type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + format = PixelFormat.TRANSLUCENT + width = WindowManager.LayoutParams.WRAP_CONTENT + height = WindowManager.LayoutParams.WRAP_CONTENT + gravity = _gravity + x = xOffset + y = yOffset + } + + mainHandler.post { + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + wm.addView(view, layoutParams) + toastViews.add(view) + } + toastViews.add(view) + toastHandler.postDelayed({ + dismissView(view) + }, duration) + } + + private fun dismissView(view: View) { + mainHandler.post { + try { + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + wm.removeView(view) + } catch (e: Exception) {} + toastViews.remove(view) + showNext() + } + } + + fun dismissAll() { + toastHandler.removeCallbacksAndMessages(null) + toastViews.forEach { view -> + try { + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + wm.removeView(view) + } catch (e: Exception) {} + } + toastViews.clear() + toastQueue.clear() + isShowing = false + } + + private fun dipToPx(dip: Int) = (dip * context.resources.displayMetrics.density).toInt() +} \ No newline at end of file diff --git a/autojs/src/main/res/layout/layout_custom_toast.xml b/autojs/src/main/res/layout/layout_custom_toast.xml new file mode 100644 index 000000000..810c2e0d0 --- /dev/null +++ b/autojs/src/main/res/layout/layout_custom_toast.xml @@ -0,0 +1,20 @@ + + + + + From effa90e1b154ab8f8f1b0516c956688a57c9ec38 Mon Sep 17 00:00:00 2001 From: PM Date: Mon, 9 Nov 2026 23:42:37 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(toast):=20=E7=9F=AD=E6=97=B6=E9=95=BF?= =?UTF-8?q?=20toastx=20=E4=BB=8E=E7=9C=9F=E6=AD=A3=E4=B8=8A=E5=B1=8F?= =?UTF-8?q?=E5=90=8E=E5=BC=80=E5=A7=8B=E8=AE=A1=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原来在 showFull() 里直接 toastHandler.postDelayed(dismiss, duration), 而 wm.addView 通过 mainHandler.post 异步排到主线程。主线程排队与加窗口的 几十到几百毫秒被算进 duration,500ms 这类短时长实际可见时间明显偏短。 把 postDelayed 移进 mainHandler.post、放在 addView 之后,计时从真正上屏 开始;pending dismiss 仍在 toastHandler 上,dismissAll() 可正常取消。 顺带删掉重复调用一次的 toastViews.add(view)(原本导致 list 只增不减、 第 11 个 toast 就误触发 dismissAll 清掉队列)。 --- .../java/com/stardust/autojs/util/CustomToastView.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/autojs/src/main/java/com/stardust/autojs/util/CustomToastView.kt b/autojs/src/main/java/com/stardust/autojs/util/CustomToastView.kt index 04a96c013..705329a0c 100644 --- a/autojs/src/main/java/com/stardust/autojs/util/CustomToastView.kt +++ b/autojs/src/main/java/com/stardust/autojs/util/CustomToastView.kt @@ -194,11 +194,13 @@ class CustomToastView( val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager wm.addView(view, layoutParams) toastViews.add(view) + // 计时从真正 addView 之后开始。原先在 showFull 中直接 postDelayed, + // 会把主线程排队与加窗口的时间算进 duration,短时长(如 500ms) + // 会被削掉几百毫秒。放在这里仍可被 dismissAll() 正常取消。 + toastHandler.postDelayed({ + dismissView(view) + }, duration) } - toastViews.add(view) - toastHandler.postDelayed({ - dismissView(view) - }, duration) } private fun dismissView(view: View) { From 0ac8f2acbc7078daa8a93d6d17f3d63e2ace6d01 Mon Sep 17 00:00:00 2001 From: PM Date: Thu, 12 Nov 2026 16:54:26 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(toast):=20=E5=9C=A8=20ScriptRuntime=20?= =?UTF-8?q?=E6=9A=B4=E9=9C=B2=20toastX=20=E8=84=9A=E6=9C=AC=E5=8F=98?= =?UTF-8?q?=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CustomToastView 类本身不足以让脚本使用 toastX:__globals__.js 里 global.toastX = runtime.toastX 需要 ScriptRuntime 上有对应字段。 原本地基线 2d08665f 里这段存在于别处,rebase 到纯上游 4c396b21 后丢失,导致 toastX 为 undefined。补齐 import、@ScriptVariable 字段 与构造函数初始化,使该 PR 单独合并也能正常工作。 --- .../java/com/stardust/autojs/runtime/ScriptRuntime.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java b/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java index 1f5b7b950..297c11af7 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java +++ b/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java @@ -32,6 +32,7 @@ import com.stardust.autojs.runtime.exception.ScriptEnvironmentException; import com.stardust.autojs.runtime.exception.ScriptException; import com.stardust.autojs.runtime.exception.ScriptInterruptedException; +import com.stardust.autojs.util.CustomToastView; import com.stardust.lang.ThreadCompat; import com.stardust.util.ScreenMetrics; import com.stardust.util.SdkVersionUtil; @@ -68,6 +69,10 @@ public abstract class ScriptRuntime { @ScriptVariable public final Dialogs dialogs; + /** 队列化自定义 Toast,供 global.toastX 使用(暴露给脚本的入口)。 */ + @ScriptVariable + public CustomToastView toastX; + @ScriptVariable public Events events; @@ -133,6 +138,7 @@ protected ScriptRuntime(ScriptRuntimeV2.Builder builder) { floaty = new Floaty(uiHandler, ui, this); files = new Files(this); media = new Media(context, this); + toastX = new CustomToastView(context, console); } public abstract void init();