ui 用户界面
ui 用户界面
ui 是 ScriptX 里这套脚本界面模块。它的定位不是“只给你一个弹窗”,而是直接给你一套可在脚本里创建页面、加载 XML、查找控件、绑定事件、更新列表、绘制画布和扩展自定义控件的运行时 UI 能力。
如果你第一次接触这套模块,先把它理解成 4 层:
ui.layout(...)/ui.inflate(...)负责把 XML 变成界面。ui.someId、ui.findView(...)、view.attr(...)负责找控件和改控件。view.click(fn)、view.on("item_click", fn)、ui.emitter.on(...)负责事件。ui.Widget、ui.registerWidget(...)负责扩展自己的标签和控件。
先记住这 14 条
ui.layout(xml)会直接把当前脚本 UI 页的根视图设置掉,适合“整页界面”。ui.inflate(xml, parent?, attach?)只负责创建视图,不会自动替换当前页面。ui.layoutFile(file)读取的是本地 XML 文件文本,不是 Androidres/layout资源名。ui.findById("xxx")/ui.findView("xxx")找的是你在 XML 里写的id="xxx",不是必须写成@id/xxx。- 只要根页面里有
id,很多时候你可以直接写ui.title、ui.list、ui.submitBtn这种形式访问。 view.text()是双态方法: 不传参数时是读取,传参数时是设置。view.click()不传函数时会执行点击;view.click(fn)传函数时会注册点击事件。- 列表
list/grid的数据源既能直接传数组,也能挂一个自定义 adapter 对象。 viewpager的每个直接子节点就是一页,不需要再单独写 adapter。canvas视图的draw事件会自动持续重绘,适合做动态画面、简单仪表盘、波形、标注层。- 这套 XML 不是 Android 官方 XML 原样照搬,支持一批脚本侧简写属性,比如
padding="12"、text="..."、bg="#222"、src="..."。 - 尺寸默认按
dp解释;只有写sp/px才会按对应单位处理。 - 很多事件支持通过
event.consumed = true或event.returnValue = true阻止继续处理,像返回键、长按、触摸、按键这些尤其重要。 - 当前
ui.useAndroidResources()固定返回false,也就是这套模块目前不是走 Android 原生资源编译布局那条路。
ui.layout(xml)
作用
把一段 XML 布局直接设置为当前脚本页面内容。
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
xml | string | XML | number | XML 字符串、E4X XML、布局资源 id | 不能为空;根节点必须能成功解析成一个 View |
返回值
返回根视图对应的脚本代理对象,也就是 UiViewProxy。
行为特点
- 会确保当前脚本有可用的 UI Activity;没有时会尝试自动拉起。
- 根视图会被设置为当前页面
contentView。 - 根视图默认会强制按整页布局处理,宽高通常会走
match_parent。
失败行为
典型失败包括:
- XML 为空:
ui.inflate(xml) requires a non-empty xml layout
- 没有根节点:
ui.inflate(xml) requires a root element
- 标签不支持:
Unsupported ui tag: <xxx>
示例:最小整页布局
ui.layout(
<vertical padding="16" bg="#111111">
<text id="title" text="ScriptX UI" textColor="#ffffff" textSize="18sp" />
<button id="helloBtn" text="点我" marginTop="12" />
</vertical>
);
ui.helloBtn.click(function () {
toast("hello");
});
示例:直接拿返回值
const root = ui.layout(
<frame>
<text id="status" text="准备中" gravity="center" />
</frame>
);
root.status.text("已加载");
ui.layoutFile(file)
作用
从本地文件读取 XML 文本,并把它设置成当前脚本页面。
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
file | string | 本地文件路径 | 必须是非空字符串;源码里会直接 File(file).readText() |
返回值
返回根视图代理对象。
适合场景
- 页面 XML 很长,不想把整段写在脚本里。
- 想把布局和业务逻辑拆开。
- 做多页面或可复用布局模板。
示例
const root = ui.layoutFile(files.join(getProjectDir(), "layouts/main.xml"));
root.title.text("从文件加载的页面");
ui.inflate(xml, parent?, attach?)
作用
把 XML 布局解析成一个视图,但不自动替换当前页面。
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
xml | string | XML | number | XML 字符串、E4X XML、布局资源 id | 必填 |
parent | View | UiViewProxy | ViewGroup | 目标父容器 | 可选;主要影响布局参数推导 |
attach | boolean | true / false | 可选,默认 false;是否在 inflate 后自动加到 parent 里 |
返回值
返回新建视图的代理对象。
你什么时候该用它
- 想动态插入一小块子视图。
- 想给列表项、卡片、局部面板生成一段模板。
- 想先构造视图,再决定要不要
ui.setContentView(...)。
示例:先 inflate 再手动塞进去
ui.layout(
<vertical id="root" padding="16">
<vertical id="container" />
</vertical>
);
const block = ui.inflate(
<card id="infoCard" padding="12" marginTop="8">
<text id="name" text="动态内容" />
</card>,
ui.container,
false
);
ui.container.addView(block);
示例:直接 attach
ui.layout(<vertical id="root" />);
ui.inflate(
<text text="我已经直接 attach 到 root 里了" marginTop="12" />,
ui.root,
true
);
ui.__inflate__(contextIgnored, xml, parent?, attach?)
作用
兼容入口。当前实现里第一个参数并不会真的当成外部 context 使用,真正解析时仍然使用模块内部的主题化上下文。
参数顺序
| 位置 | 含义 |
|---|---|
| 第 1 个参数 | 兼容占位 |
| 第 2 个参数 | xml |
| 第 3 个参数 | parent |
| 第 4 个参数 | attach |
返回值
返回新建视图代理对象。
建议
除非你在兼容旧脚本,否则直接用 ui.inflate(...) 就够了。新文档和新示例都建议按 ui.inflate(xml, parent, attach) 写。
ui.registerWidget(name, widget)
作用
注册自定义控件标签。注册后,你就可以在 XML 里直接写这个标签名。
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
name | string | 非空字符串 | 标签名,区分大小写时建议统一使用小写或驼峰 |
widget | function | class | 可构造函数 | 必须能被当成构造器调用 |
返回值
true
失败行为
name为空会抛:
ui.registerWidget(name, widget) requires a non-empty name
widget不是函数 / 类会抛:
ui.registerWidget(name, widget) requires a function/class
示例:注册一个最简单的自定义控件
function InfoBox() {
ui.Widget.call(this);
}
InfoBox.prototype = Object.create(ui.Widget.prototype);
InfoBox.prototype.constructor = InfoBox;
InfoBox.prototype.render = function () {
return (
<vertical padding="12" bg="#1e1e1e">
<text id="title" text={this.title || "默认标题"} textColor="#ffffff" />
<text id="desc" text={this.desc || ""} textColor="#bbbbbb" marginTop="6" />
</vertical>
);
};
ui.registerWidget("info-box", InfoBox);
ui.layout(
<vertical padding="16">
<info-box title="自定义卡片" desc="这是 Widget 渲染出来的内容" />
</vertical>
);
ui.setContentView(view)
作用
把一个已经创建好的视图设置成当前页面内容。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
view | View | UiViewProxy | 必须能解包成 Android View |
返回值
true
失败行为
如果参数不是有效 View,会抛:
ui.setContentView(view) requires a View
示例
const root = ui.inflate(
<vertical padding="16">
<text id="msg" text="先 inflate,再切换到整页" />
</vertical>
);
ui.setContentView(root);
ui.findById(idName)
作用
按字符串 id 查找当前页面里的控件。
参数
| 参数 | 类型 | 可填值 |
|---|---|---|
idName | string | 你在 XML 里写的 id 名字 |
返回值
找到时返回控件代理对象,找不到返回 null。
说明
- 这里传的是名字,比如
"title"、"submitBtn"。 - 不需要写
@id/title。 - 内部查找的是脚本 UI 自己维护的命名 id,不是只能靠 Android 资源表。
示例
const titleView = ui.findById("title");
if (titleView) {
titleView.text("查找成功");
}
ui.findView(idName)
作用
和 ui.findById(idName) 一样,是同一类入口的别名写法。
返回值
找到时返回控件代理对象,找不到返回 null。
示例
ui.findView("status")?.text("done");
ui.findByStringId(root, idName)
作用
从指定根视图开始,按字符串 id 查找子控件。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
root | View | UiViewProxy | 查找起点 |
idName | string | 子控件 id 名称 |
返回值
找到时返回控件代理对象,找不到返回 null。
什么时候要用它
- 页面里有多个同类区域,想限定在其中一个区块里查。
- 自定义控件内部有局部同名节点,不想全局扫。
示例
const card = ui.findView("profileCard");
const name = ui.findByStringId(card, "name");
name?.text("局部查找");
ui.isUiThread()
作用
判断当前代码是不是跑在主线程。
返回值
boolean
示例
log(`当前是否主线程: ${ui.isUiThread()}`);
ui.run(action)
作用
把一个函数切到 UI 线程同步执行。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
action | function | 必填 |
返回值
返回回调函数本身的执行结果。
失败行为
如果没传函数,会抛:
ui.run(action) requires a function
适合场景
- 你当前在工作线程里。
- 你想确保一批视图操作同步完成。
- 你需要拿到视图读取结果再继续算逻辑。
示例
threads.start(function () {
const value = ui.run(function () {
ui.title.text("由工作线程切回 UI 线程更新");
return ui.title.text();
});
log(value);
});
ui.post(action, delay?)
作用
把一个函数投递到 UI 线程异步执行,可选延迟。
参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
action | function | 无 | 必填 |
delay | number | 0 | 毫秒;小于 0 会按 0 处理 |
返回值
true
失败行为
如果第一个参数不是函数,会抛:
ui.post(action[, delay]) requires a function
示例:延迟修改界面
ui.post(function () {
ui.status.text("2 秒后更新");
}, 2000);
ui.statusBarColor(color)
作用
修改当前脚本页面状态栏颜色。
参数
| 参数 | 类型 | 可填值 | 说明 |
|---|---|---|---|
color | number | string | 颜色整数、#RRGGBB、#AARRGGBB 等 | 能被模块内部解析成颜色即可 |
返回值
| 场景 | 返回值 |
|---|---|
| 颜色解析成功并设置成功 | true |
| 颜色无法解析 | false |
示例
ui.statusBarColor("#121212");
ui.statusBarColor("#ff2d55");
ui.finish()
作用
关闭当前脚本 UI 页面。
返回值
true
说明
- 它做的是
activity.finish()这一类页面结束动作。 - 只是关掉 UI 页,不等于强制停止整段脚本。
示例
ui.closeBtn.click(function () {
ui.finish();
});
ui.useAndroidResources()
作用
查询当前 UI 模块是否走 Android 原生资源布局模式。
返回值
当前固定返回 false。
怎么理解
这表示当前这套脚本 UI 主要是“运行时 XML 解析器 + 自定义属性系统”这一套,而不是直接把脚本写成原生 res/layout/*.xml 开发模式。
ui.emitter
作用
页面级事件总线。它管的是 Activity / 页面生命周期相关事件,不是某个具体按钮、列表项、输入框的点击。
当前支持的方法
| 方法 | 作用 |
|---|---|
ui.emitter.on(event, listener) | 追加监听 |
ui.emitter.addListener(event, listener) | 同 on |
ui.emitter.prependListener(event, listener) | 头部插入监听 |
ui.emitter.once(event, listener) | 只触发一次 |
ui.emitter.prependOnceListener(event, listener) | 头部插入且只触发一次 |
ui.emitter.removeListener(event, listener) / off(...) | 移除指定监听 |
ui.emitter.removeAllListeners(event?) | 移除一个事件或全部事件 |
ui.emitter.emit(event, ...args) | 手动触发事件 |
ui.emitter.listenerCount(event) | 监听器数量 |
ui.emitter.listeners(event) | 监听器数组 |
ui.emitter.eventNames() | 当前已注册的事件名列表 |
页面级事件名
| 事件名 | 何时触发 | 参数 |
|---|---|---|
restart | Activity 重建时 | 无 |
new_intent | 收到新 Intent 时 | (intent) |
activity_result | 子 Activity 返回时 | (requestCode, resultCode, data) |
request_permission_result | 权限请求返回时 | (requestCode, permissions, grantResults) |
create_options_menu | 创建菜单时 | (menu) |
options_item_selected | 菜单项被点击时 | (event, item) |
key_down | 页面级按键按下 | (keyCode, event, simpleEvent) |
key_up | 页面级按键抬起 | (keyCode, event) |
generic_motion_event | 通用运动事件 | (motionEvent, simpleEvent) |
back_pressed | 按返回键 | (simpleEvent) |
save_instance_state | 保存页面状态 | (bundle) |
restore_instance_state | 恢复页面状态 | (bundle) |
destroy | 页面销毁 | 无 |
关于可拦截事件
像下面这些事件,如果你想阻止默认行为,通常要在回调里设置:
event.consumed = true;
或者:
event.returnValue = true;
源码里很多地方都是按这两个条件判断“是否已消费”。
示例:拦截返回键
ui.emitter.on("back_pressed", function (event) {
toast("先别退出");
event.consumed = true;
});
示例:处理菜单项点击
ui.emitter.on("options_item_selected", function (event, item) {
log(item);
event.consumed = true;
});
ui.R
作用
按资源类型读取当前应用资源 id。
用法结构
ui.R 不是单个数字,而是分层对象:
ui.R.drawable.xxx
ui.R.layout.xxx
ui.R.id.xxx
ui.R.string.xxx
返回值
找到资源时返回资源 id 整数,找不到返回 0。
示例
const iconId = ui.R.drawable.ic_launcher;
log(iconId);
说明
- 它内部是按
resources.getIdentifier(name, type, packageName)查的。 - 更适合拿现成资源 id 去喂给
ui.inflate(...)或其它需要原生资源 id 的地方。
ui.Widget
作用
自定义控件基类。你自己写的 widget 一般都以它为原型基础。
内部已经给你的能力
| 成员 | 作用 |
|---|---|
renderInternal() | 内部调用 render();你通常重写 render() 即可 |
defineAttr(...) | 定义自定义属性 |
hasAttr(name) | 判断某属性有没有注册 |
primeAttr(name, value) | 在视图真正创建前先把属性值暂存到实例 |
setAttr(view, name, value, setterBridge) | 自定义设置属性 |
getAttr(view, name, getterBridge) | 自定义读取属性 |
notifyViewCreated(view) | 视图创建后回调到 onViewCreated(view) |
notifyAfterInflation(view) | 所有子元素 inflate 结束后回调到 onFinishInflation(view) |
最常见写法
function ColorLabel() {
ui.Widget.call(this);
this.defineAttr("label");
this.defineAttr("accent");
}
ColorLabel.prototype = Object.create(ui.Widget.prototype);
ColorLabel.prototype.constructor = ColorLabel;
ColorLabel.prototype.render = function () {
return (
<text
id="labelView"
text={this.label || "默认文案"}
textColor={this.accent || "#ffffff"}
textSize="16sp"
/>
);
};
ui.registerWidget("color-label", ColorLabel);
defineAttr(...) 怎么理解
最常见有 3 类写法:
1. 直接定义一个普通属性
this.defineAttr("title");
效果是:
- XML 写
<my-widget title="Hello" /> - 这个值会先落到实例
this.title - 你在
render()里就能直接用
2. 绑定别名字段
this.defineAttr("title", "headerText");
效果是:
- XML 仍然写
title - 实例里真正存到
this.headerText
3. 自定义设置逻辑
this.defineAttr("accent", function (view, attrName, value, setterBridge) {
this.accent = value;
setterBridge(view, "textColor", value);
});
这样你既能保存自己的属性值,又能顺手把原生视图属性一起改掉。
ui.imageCache.clearMemory()
作用
清理 UI 图像缓存的内存层接口。
返回值
当前固定返回 true。
说明
从源码看,这一层目前更像“预留接口”,不是复杂的缓存统计对象。
ui.imageCache.clearDiskCache()
作用
清理 UI 图像缓存的磁盘层接口。
返回值
当前固定返回 true。
view.text()
作用
针对文本控件的快捷读写方法。
适用控件
主要适用于 TextView 体系控件,比如:
textbuttoninputedittexttextinputedittext
调用方式
| 写法 | 含义 |
|---|---|
view.text() | 读取文本 |
view.text(value) | 设置文本 |
返回值
| 写法 | 返回值 |
|---|---|
view.text() | string |
view.text(value) | 当前视图代理对象本身,支持链式 |
示例
const title = ui.title.text();
ui.title.text(title + " - 已更新");
view.click()
作用
执行点击,或者注册点击事件。
调用方式
| 写法 | 含义 |
|---|---|
view.click() | 直接调用底层 performClick() |
view.click(fn) | 绑定点击监听 |
返回值
| 写法 | 返回值 |
|---|---|
view.click() | boolean,表示点击动作是否成功 |
view.click(fn) | 当前视图代理对象 |
示例:主动触发点击
ui.submitBtn.click();
示例:绑定点击监听
ui.submitBtn.click(function () {
ui.status.text("已点击");
});
view.longClick()
作用
执行长按,或者注册长按事件。
调用方式
| 写法 | 含义 |
|---|---|
view.longClick() | 主动执行长按 |
view.longClick(fn) | 绑定长按事件 |
返回值
| 写法 | 返回值 |
|---|---|
view.longClick() | boolean |
view.longClick(fn) | 当前视图代理对象 |
示例
ui.item.longClick(function () {
toast("长按了");
});
view.on(event, listener)
作用
给控件注册通用事件监听。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
event | string | 事件名 |
listener | function | 监听函数 |
返回值
当前视图代理对象。
失败行为
如果第二个参数不是函数,会抛:
view.on(event, listener) requires a function
常用事件名
| 事件名 | 别名 | 触发对象 / 参数 |
|---|---|---|
click | 无 | (view) |
long_click | longClick | (view) |
check | 无 | (isChecked) |
checked_change | checkedChange | (isChecked) |
item_bind | itemBind | (itemView, itemHolder) |
item_data_bind | itemDataBind | (itemView, itemHolder) |
item_click | itemClick | (item, position, itemView, listView) |
item_long_click | itemLongClick | (event, item, position, itemView, listView) |
page_selected | pageSelected | (position, selectedPageView) |
draw | onDraw | (canvasScope) |
key | 无 | (keyCode, keyEvent, simpleEvent) |
key_down | 无 | (keyCode, keyEvent, simpleEvent) |
key_up | 无 | (keyCode, keyEvent, simpleEvent) |
touch | 无 | (motionEvent, simpleEvent) |
touch_down | 无 | (motionEvent, simpleEvent) |
touch_move | 无 | (motionEvent, simpleEvent) |
touch_up | 无 | (motionEvent, simpleEvent) |
示例:普通点击
ui.saveBtn.on("click", function (view) {
log(view);
});
示例:列表项点击
ui.list.on("item_click", function (item, position, itemView, listView) {
toast(`点击了第 ${position} 项: ${item.title}`);
});
示例:触摸拦截
ui.panel.on("touch", function (motionEvent, event) {
if (motionEvent.getActionMasked() === motionEvent.ACTION_MOVE) {
event.consumed = true;
}
});
view.setOnTouchListener(listenerOrNull)
作用
直接设置 Android 风格的触摸监听。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
listenerOrNull | function | View.OnTouchListener | null | 传 null 可取消 |
回调参数
如果传的是脚本函数,收到参数为:
function (view, motionEvent) {}
返回值
当前视图代理对象。
回调返回值
脚本函数返回值会被转成布尔语义:
true、非 0 数字、字符串"true"会视为已消费。- 其它值视为未消费。
示例
ui.dragArea.setOnTouchListener(function (view, event) {
log(event.getActionMasked());
return false;
});
view.setOnCheckedChangeListener(listenerOrNull)
作用
给 RadioGroup 或 CompoundButton 体系控件直接设置勾选变化监听。
适用控件
checkboxswitchmaterialswitchradioradiogroup
RadioGroup 回调参数
function (groupView, checkedId) {}
CompoundButton 回调参数
function (buttonView, isChecked) {}
返回值
当前视图代理对象。
示例
ui.enableSwitch.setOnCheckedChangeListener(function (view, checked) {
ui.status.text(checked ? "已开启" : "已关闭");
});
view.setSource(value)
作用
给图片控件设置图片来源,本质等价于设置 src 属性。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
value | string | 本地路径、网络地址、颜色值、资源引用等 |
返回值
当前视图代理对象。
支持来源
| 值类型 | 示例 |
|---|---|
| 本地文件路径 | "/sdcard/Pictures/a.png" |
| 网络图片 | "https://example.com/a.png" |
| 资源引用 | "@drawable/ic_launcher" |
| 主题引用 | "?attr/colorPrimary" |
| 颜色值 | "#ff0000" |
示例
ui.avatar.setSource("/sdcard/Pictures/avatar.png");
ui.banner.setSource("https://example.com/banner.jpg");
view.attr(name)
作用
读取某个属性当前值。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
name | string | 属性名,不能为空 |
返回值
返回属性当前值。类型取决于属性本身,可能是数字、字符串、布尔、Drawable、列表等。
失败行为
如果属性名为空,会抛:
view.attr(name[, value]) requires a non-empty attribute name
示例
log(ui.title.attr("textColor"));
log(ui.progress.attr("progress"));
view.attr(name, value)
作用
动态设置某个属性。
返回值
当前视图代理对象。
最常用的属性名
这一批都是源码里已经明确支持的:
| 分类 | 常见属性 |
|---|---|
| 文本 | text、hint、textColor、hintColor、textSize、size、textStyle、singleLine、maxLines、ellipsize、inputType |
| 图片 | src、scaleType、tint、borderWidth、borderColor、radius、circle |
| 布局 | layout_weight、weight、layout_gravity、gravity、padding*、margin* |
| 视图状态 | visibility、alpha、selected、enabled、clickable |
| 变换 | rotation、rotationX、rotationY、scaleX、scaleY、translationX、translationY |
| 语义 | contentDescription |
| Toolbar | title、subtitle |
| WebView | url |
| Spinner / Pager | entries、titles |
示例
ui.title.attr("textColor", "#ffffff");
ui.title.attr("gravity", "center");
ui.avatar.attr("scaleType", "centerCrop");
ui.card.attr("padding", "12 16");
view.findView(nameOrId) / view.findById(nameOrId) / view.findViewById(nameOrId)
作用
从当前控件内部继续查找子控件。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
nameOrId | string | number | 传字符串时按脚本 id 名查;传数字时按 Android 原生 id 查 |
返回值
找到时返回子控件代理对象,找不到返回 null。
示例
const nameText = ui.profileCard.findView("name");
nameText?.text("局部 findView");
view.text / view.hint / view.checked 等内建属性
除了方法式写法,这套 UI 还支持很多直接赋值的属性读写。常见的有:
| 属性 | 适用控件 | 读 / 写 |
|---|---|---|
text | TextView 系 | 可读可写 |
hint | TextView / TextInputLayout | 可读可写 |
checked | CompoundButton | 可读可写 |
currentItem | viewpager | 可读可写 |
titles | viewpager | 可读可写 |
spanCount | grid | 可读可写 |
progress | ProgressBar | 可读可写 |
max | SeekBar / ProgressBar | 可读可写 |
min | SeekBar | 可读可写 |
secondaryProgress | ProgressBar | 可读可写 |
visibility | 全部视图 | 可读可写 |
alpha | 全部视图 | 可读可写 |
selected | 全部视图 | 可读可写 |
enabled | 全部视图 | 可读可写 |
clickable | 全部视图 | 可读可写 |
示例
ui.title.text = "直接赋值";
ui.enableSwitch.checked = true;
ui.pager.currentItem = 1;
ui.grid.spanCount = 2;
ui.panel.visibility = 8;
list.setDataSource(data, observeAutomatically?)
作用
给 list / grid 设置数据源。
参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
data | array | list | any | 无 | 最常见是数组 |
observeAutomatically | boolean | false | 会原样传给自定义 adapter 的 setDataSource |
返回值
当前列表视图代理对象。
数据源支持情况
| 数据类型 | 行为 |
|---|---|
| JS 数组 | 逐项渲染 |
Java List | 逐项渲染 |
| Java / JS 数组 | 逐项渲染 |
| 普通单值 | 会被包成只有 1 项的列表 |
null | 空列表 |
没写 item 模板时会怎样
如果 <list> / <grid> 里没有写子模板,默认会把每项 toString() 后显示成一行文本。
示例:最常用数组数据源
ui.layout(
<list id="userList">
<horizontal padding="12">
<text id="name" text="{{item.name}}" />
<text id="age" text=" / {{item.age}}" marginLeft="8" />
</horizontal>
</list>
);
ui.userList.setDataSource([
{ name: "张三", age: 18 },
{ name: "李四", age: 20 }
]);
list.getDataSource()
作用
读取当前保存的原始数据源对象。
返回值
返回你上次传给 setDataSource(...) 的原始值。
list.setDataSourceAdapter(adapter)
作用
设置自定义数据适配器桥接对象。
适配器建议实现的方法
| 方法 | 是否建议实现 | 作用 |
|---|---|---|
setDataSource(data, observeAutomatically) | 强烈建议 | 数据源更新时同步内部状态 |
getItemCount(data) | 强烈建议 | 返回总数 |
getItem(data, index) | 强烈建议 | 返回某一项数据 |
返回值
当前列表视图代理对象。
示例
const adapter = {
data: [],
setDataSource(data) {
this.data = data || [];
},
getItemCount() {
return this.data.length;
},
getItem(data, index) {
return this.data[index];
}
};
ui.userList.setDataSourceAdapter(adapter);
ui.userList.setDataSource([{ name: "A" }, { name: "B" }]);
list.getDataSourceAdapter()
作用
读取当前挂上的自定义 adapter 对象。
返回值
返回原始 adapter。
list.notifyDataSetChanged()
作用
通知列表整体刷新。
返回值
当前列表视图代理对象。
什么时候用它
这个方法适合“我改了很多项,懒得一项一项通知”的场景,比如:
- 整个数组被重新排序
- 做了一轮筛选,结果集完全变了
- 批量修改了很多项,不值得逐个
notifyItemChanged(...)
和 list.adapter.notifyDataSetChanged() 的区别
两者最终目的相同,都是整表刷新:
list.notifyDataSetChanged()更像直接从列表对象上调,写法短一些。list.adapter.notifyDataSetChanged()更适合你已经在统一走adapter这一套更新接口时继续链式写。
示例
const users = [
{ name: "张三", role: "管理员" },
{ name: "李四", role: "访客" }
];
ui.userList.setDataSource(users);
users.reverse();
ui.userList.notifyDataSetChanged();
list.adapter.notifyDataSetChanged()
作用
通过桥接对象刷新整个列表。
返回值
桥接对象本身,支持继续链式调用。
什么时候优先用它
如果你在同一段逻辑里连续调用多种 adapter 更新方法,比如先插入、再修改、最后读数量,那么统一写成 ui.list.adapter.xxx(...) 会更工整。
示例
ui.userList.adapter
.notifyDataSetChanged()
.notifyItemChanged(0);
上面这种链式写法语法上是允许的,但实际业务里不要为了链式而链式。真正需要局部更新时,优先只发局部通知,不要先整表刷新再局部刷新。
list.adapter.notifyItemInserted(position)
作用
通知某一项插入。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
position | number | 插入位置;内部会至少修正为 0 |
返回值
桥接对象本身。
典型场景
你先往数据源里 push(...) 或 splice(...) 插入了一项,然后希望列表只补渲染这一条,而不是整表重绘。
示例
const tasks = [
{ title: "早间巡检", done: false },
{ title: "拉取日志", done: true }
];
ui.taskList.setDataSource(tasks);
function addTask(title) {
const item = { title, done: false };
tasks.push(item);
ui.taskList.adapter.notifyItemInserted(tasks.length - 1);
}
list.adapter.notifyItemRangeInserted(positionStart, itemCount)
作用
通知一段范围插入。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
positionStart | number | 起始位置 |
itemCount | number | 插入了多少项 |
返回值
桥接对象本身。
适合什么场景
适合一次性追加多条数据,比如:
- 分页加载下一页
- 批量导入任务
- 搜索时从服务端补回一组结果
示例
const nextPage = [
{ title: "任务 4" },
{ title: "任务 5" },
{ title: "任务 6" }
];
const start = tasks.length;
Array.prototype.push.apply(tasks, nextPage);
ui.taskList.adapter.notifyItemRangeInserted(start, nextPage.length);
list.adapter.notifyItemRemoved(position)
作用
通知某一项删除。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
position | number | 被删除项原来的位置 |
返回值
桥接对象本身。
使用顺序一定要注意
正确顺序通常是:
- 先改你的数据源
- 再通知列表哪一项被删了
不要只调用 notifyItemRemoved(...) 却不改数组,否则界面和真实数据会对不上。
示例
function removeTask(index) {
if (index < 0 || index >= tasks.length) {
return;
}
tasks.splice(index, 1);
ui.taskList.adapter.notifyItemRemoved(index);
}
list.adapter.notifyItemRangeRemoved(positionStart, itemCount)
作用
通知一段范围删除。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
positionStart | number | 起始位置 |
itemCount | number | 删除数量 |
返回值
桥接对象本身。
示例
function removeFirstTwo() {
if (tasks.length < 2) {
return;
}
tasks.splice(0, 2);
ui.taskList.adapter.notifyItemRangeRemoved(0, 2);
}
list.adapter.notifyItemChanged(position)
作用
通知某一项内容变化。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
position | number | 被修改项的位置 |
返回值
桥接对象本身。
什么时候用
适合“这一项还在原位,只是内容变了”的情况,比如:
- 勾选状态切换
- 标题被改名
- 下载进度变化
- 状态文字从“等待中”变成“已完成”
示例
function toggleDone(index) {
const item = tasks[index];
if (!item) {
return;
}
item.done = !item.done;
ui.taskList.adapter.notifyItemChanged(index);
}
list.adapter.notifyItemRangeChanged(positionStart, itemCount)
作用
通知一段范围内容变化。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
positionStart | number | 起始位置 |
itemCount | number | 受影响数量 |
返回值
桥接对象本身。
示例
function markTopThreeAsHot() {
const count = Math.min(3, tasks.length);
for (let i = 0; i < count; i += 1) {
tasks[i].hot = true;
}
ui.taskList.adapter.notifyItemRangeChanged(0, count);
}
list.adapter.notifyItemMoved(fromPosition, toPosition)
作用
通知某一项移动。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
fromPosition | number | 原位置 |
toPosition | number | 新位置 |
返回值
桥接对象本身。
典型场景
适合上移、下移、置顶这类“数据还是这一组,只是顺序变了”的操作。
示例
function moveToTop(index) {
if (index <= 0 || index >= tasks.length) {
return;
}
const picked = tasks.splice(index, 1)[0];
tasks.unshift(picked);
ui.taskList.adapter.notifyItemMoved(index, 0);
}
list.adapter.getItemCount()
作用
读取当前桥接层看到的项目数量。
返回值
number
说明
它读的是当前列表适配层可见的数量。对于绝大多数普通数组数据源来说,它通常会和 ui.list.getDataSource().length 一样;如果你用了自定义 adapter,那么最终数量取决于 adapter 自己怎么实现。
示例:手动桥接刷新
const users = [
{ name: "A" },
{ name: "B" }
];
ui.userList.setDataSource(users);
users.push({ name: "C" });
ui.userList.adapter.notifyItemInserted(2);
grid.spanCount
作用
设置或读取网格列数。
可填值
- 任意数字都可以传。
- 内部最终会至少修正为
1。
示例
ui.grid.spanCount = 3;
pager.setCurrentItem(index)
作用
切换 viewpager 当前页。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
index | number | 会被限制在 0 ~ 最后一页 |
返回值
| 场景 | 返回值 |
|---|---|
| 切换成功 | 当前 pager 代理对象 |
| 没有页面或页码没变 | false |
pager.getCurrentItem()
作用
读取当前页索引。
返回值
number
补充说明
- 第一页是
0 - 第二页是
1 - 这套下标和 JS 数组下标一致
示例
const current = ui.mainPager.getCurrentItem();
ui.pageInfo.text = `当前在第 ${current + 1} 页`;
pager.setTitles(value)
作用
设置 viewpager 页面标题。
支持值
| 类型 | 行为 |
|---|---|
string | 只有一个标题 |
array | 按数组顺序设置 |
List / Array | 按顺序转换 |
| 其它 | 转字符串后当单项标题 |
示例
ui.pager.setTitles(["首页", "日志", "设置"]);
这个标题通常和谁一起用
最常见的组合是:
tabs + viewpager- 页头文字 + 上一步 / 下一步按钮
- 左右切换时同步显示“当前步骤名”
pager.getTitles()
作用
读取当前页标题数组。
返回值
string[]
示例
const titles = ui.pager.getTitles();
log(titles.join(" / "));
pager.on("page_selected", listener)
page_selected 是 viewpager 最常用的事件之一。虽然它不是单独的成员方法,但在实际页面里非常常用,所以这里单独拎出来讲。
回调参数
function (position, selectedPageView) {}
| 参数 | 说明 |
|---|---|
position | 当前被选中的页索引,从 0 开始 |
selectedPageView | 当前真正显示出来的页面控件代理 |
什么时候触发
- 用户手动左右切页
- 你调用
pager.setCurrentItem(index),并且页码确实发生变化时
不会触发的情况
viewpager还没有任何页面- 传入的页码和当前页相同
示例
ui.mainPager.on("page_selected", function (position) {
ui.pageTitle.text = ui.mainPager.getTitles()[position] || `第 ${position + 1} 页`;
});
toolbar.setupWithDrawer(drawerLayout)
作用
把 toolbar 和 drawer 关联起来,通常用于抽屉页左上角导航按钮。
参数
| 参数 | 类型 |
|---|---|
drawerLayout | DrawerLayout | UiViewProxy |
返回值
当前 toolbar 代理对象。
示例
ui.layout(
<drawer id="drawerRoot">
<vertical>
<toolbar id="toolbar" title="示例" />
</vertical>
<vertical layout_gravity="start" w="280">
<text text="抽屉内容" />
</vertical>
</drawer>
);
ui.toolbar.setupWithDrawer(ui.drawerRoot);
consoleView.setLogLevel(level)
作用
设置内嵌控制台视图的日志级别过滤。
可填值
只能是下面这些之一,不区分大小写:
| 值 | 含义 |
|---|---|
E | 只看错误 |
W | 看警告和错误 |
I | 看信息、警告、错误 |
D | 看调试及以上 |
V | 全部,默认 |
传其它值时会回退成 V。
返回值
当前控制台视图代理对象。
consoleView.getLogLevel()
作用
读取当前日志级别。
返回值
"E" | "W" | "I" | "D" | "V"
consoleView.clear()
作用
清空当前控制台视图文本缓冲。
返回值
当前控制台视图代理对象。
consoleView.appendLine(line)
作用
手动往内嵌控制台里追加一行文本。
参数
| 参数 | 类型 |
|---|---|
line | string |
返回值
当前控制台视图代理对象。
consoleView.setConsole(consoleBinding)
作用
设置这个控件绑定的 console 对象引用。
返回值
当前控制台视图代理对象。
consoleView.getConsole()
作用
读取当前绑定的 console 对象。
consoleView.setInputEnabled(enabled)
作用
控制这个控件是否允许获取焦点、点击、长按等输入能力。
参数
| 参数 | 类型 | 可填值 |
|---|---|---|
enabled | boolean | number | string | true/false、1/0、"true" 等 |
返回值
当前控制台视图代理对象。
consoleView.isInputEnabled()
作用
读取内嵌控制台是否允许输入 / 焦点相关能力。
返回值
boolean
支持的 XML 标签
下面这些标签是当前源码里已经明确支持的常用集合。
基础容器
| 标签 | 说明 |
|---|---|
vertical | 纵向 LinearLayout |
horizontal | 横向 LinearLayout |
linear | 普通 LinearLayout,方向可再配 orientation |
frame | FrameLayout |
relative | RelativeLayout |
coordinatorlayout | CoordinatorLayout |
appbar | AppBarLayout |
drawer | DrawerLayout |
scroll / scrollview | ScrollView |
view | 纯 View |
文本与输入
| 标签 | 说明 |
|---|---|
text | 文本 |
button | 按钮 |
input / edittext | 输入框 |
textinputedittext | Material 风格输入框 |
textinputlayout | Material 输入容器 |
checkbox | 复选框 |
radio | 单选按钮 |
radiogroup | 单选组 |
switch | 开关 |
materialswitch | Material 开关 |
图片与进度
| 标签 | 说明 |
|---|---|
img / image / imageview | 图片 |
progressbar | 进度条 |
seekbar | 拖动条 |
spinner | 下拉选择 |
timepicker | 时间选择 |
datepicker | 日期选择 |
Material / 导航 / 复杂控件
| 标签 | 说明 |
|---|---|
toolbar | 工具栏 |
materialbutton | Material 按钮 |
card | Material 卡片 |
fab | 悬浮按钮 |
tabs / tab | TabLayout |
chipgroup | 标签组 |
chip | 标签项 |
bottomnavigationview | 底部导航 |
navigationview | 抽屉导航视图 |
materialbuttontogglegroup | 按钮切换组 |
高级视图
| 标签 | 说明 |
|---|---|
viewpager | 分页容器 |
canvas | 自绘视图 |
webview | 网页视图 |
list | 列表 |
grid | 网格 |
console / globalconsole | 内嵌日志视图 |
常用尺寸写法
宽高属性
常见宽高属性包括:
whlayout_widthlayout_height
可填值
| 值 | 含义 |
|---|---|
* | match_parent |
match_parent | MATCH_PARENT |
fill_parent | MATCH_PARENT |
auto | wrap_content |
wrap_content | WRAP_CONTENT |
12 | 默认按 12dp |
12dp / 12dip | 按 dp |
14sp | 按 sp |
100px | 按 px |
示例
<text w="*" h="auto" />
<image w="64" h="64" />
<text textSize="14sp" />
常用属性值说明
textColor
适用控件
TextView 体系控件,包括:
textbuttoninputedittexttextinputedittext
可填值
走统一颜色解析,常见可写:
"#ffffff""#ffcc00"0xffff0000
示例
<text text="白字" textColor="#ffffff" />
hintColor / textColorHint
适用控件
TextView 体系输入控件。
说明
hintColor和textColorHint在当前实现里都能用。- 它们最终都会改提示文字颜色。
示例
<input hint="请输入账号" hintColor="#888888" />
textSize / size
适用控件
TextView 体系控件。
可填值
| 值 | 含义 |
|---|---|
16 | 默认按 16dp 解析 |
16sp | 推荐写法,真正文本大小更稳 |
18px | 直接按像素 |
说明
源码内部最终走的是像素设置,所以你传 sp / px / dp 的区别都会真实生效。
建议
文本大小优先写 sp,别偷懒只写裸数字。
typeface
适用控件
TextView 体系控件。
当前支持值
| 值 | 说明 |
|---|---|
sans / sans_serif / sans-serif / normal | 无衬线 |
serif | 衬线 |
monospace / mono | 等宽 |
| 其它 | 回退成默认字体 |
示例
<text text="代码风格" typeface="monospace" />
inputType
当前支持值
| 值 | 说明 |
|---|---|
number | 纯数字 |
numberDecimal / decimal | 小数数字 |
numberSigned / signed | 带符号数字 |
phone | 电话输入 |
password | 密码文本 |
textMultiline / multiline | 多行文本 |
| 其它任意值 | 回退成普通文本 |
示例
<input inputType="number" />
<input inputType="password" />
<input inputType="multiline" />
password
适用控件
EditText
可填值
布尔语义:
truefalse10"true"
作用
这是兼容属性。传 true 时会直接把输入类型切成密码模式。
示例
<input password="true" />
numeric
适用控件
EditText
作用
兼容属性。传 true 时会把输入类型切成数字模式。
phoneNumber
适用控件
EditText
作用
兼容属性。传 true 时会把输入类型切成电话输入模式。
digit / digits
适用控件
EditText
作用
限制可输入字符集合。
示例
<input digits="0123456789.-" />
textStyle
当前支持值
| 值 | 说明 |
|---|---|
bold | 粗体 |
italic | 斜体 |
bold italic | 粗斜体 |
| 其它 | 普通 |
singleLine
适用控件
TextViewChipGroup
可填值
布尔语义。
含义
- 对文本控件来说:是否限制成单行。
- 对
ChipGroup来说:是否单行排列。
lines
适用控件
TextView
含义
设置固定行数。
maxLines
适用控件
TextView
含义
设置最大行数。
ems
适用控件
TextView
含义
按字宽设置大致宽度,适合输入框做“预计能容纳多少字符”的场景。
ellipsize
当前支持值
| 值 | 说明 |
|---|---|
start | 开头省略 |
middle | 中间省略 |
marquee | 跑马灯 |
| 其它 | 结尾省略,也就是 end |
orientation
适用控件
LinearLayout
当前支持值
| 值 | 说明 |
|---|---|
horizontal | 横向排列 |
| 其它任意值 | 纵向排列 |
示例
<linear orientation="horizontal">
<text text="A" />
<text text="B" />
</linear>
visibility
当前支持值
| 值 | 说明 |
|---|---|
visible | 可见 |
invisible | 占位但不可见 |
gone | 完全不占位 |
| 其它 | 回退成 visible |
gravity
适用控件
LinearLayoutTextView
含义
- 对
LinearLayout:控制子项整体摆放方向。 - 对
TextView:控制文字在控件内部的对齐。
当前支持 token
这些 token 可以用空格、逗号或 | 组合:
centerleftrighttopbottomstartendcenter_verticalcenter_horizontalfillfill_verticalfill_horizontalclip_verticalclip_horizontal
示例
<text gravity="center" />
<text gravity="end|center_vertical" />
align / layout_gravity
适用场景
控制当前控件在父容器里的摆放位置。
当前会生效的布局参数类型
LinearLayout.LayoutParamsFrameLayout.LayoutParamsCoordinatorLayout.LayoutParamsDrawerLayout.LayoutParams
说明
align和layout_gravity在当前实现里都走同一套重力解析逻辑。- 如果父布局根本不吃
gravity,那你写了也不会有视觉效果。
textAlignment
适用控件
普通 View 都能设,但最常见还是文本控件。
当前支持值
| 值 | 说明 |
|---|---|
inherit | 继承 |
gravity | 跟随 gravity |
center | 居中 |
textStart / start / viewStart / left / viewLeft | 靠起始边 |
textEnd / end / viewEnd / right / viewRight | 靠结束边 |
layout_weight / weight
适用控件
子控件位于 LinearLayout 内时。
可填值
数字或可转数字的字符串。
说明
- 只有父容器是
LinearLayout才吃这个属性。 - 宽高没有显式写时,源码会按
weight情况自动给你推导0或match_parent/wrap_content。
示例
<horizontal>
<text w="0" layout_weight="1" text="左边" />
<text w="0" layout_weight="2" text="右边更宽" />
</horizontal>
margin
适用控件
所有带 MarginLayoutParams 的视图。
可填格式
和 CSS 的 margin 很像:
| 写法 | 含义 |
|---|---|
12 | 四边都是 12dp |
12 8 | 上下 12dp,左右 8dp |
12 8 4 | 上 12dp,左右 8dp,下 4dp |
12 8 4 16 | 上右下左四边分别设置 |
分隔符可以是空格、逗号、分号。
marginLeft / marginTop / marginRight / marginBottom
作用
单独设置某一边外边距。
对应别名
layout_marginLeftlayout_marginToplayout_marginRightlayout_marginBottom
padding
作用
设置内边距。
可填格式
和 margin 一样,也支持 1 / 2 / 3 / 4 个值的写法。
paddingLeft / paddingTop / paddingRight / paddingBottom
作用
单独设置某一边内边距。
contentPadding
适用控件
优先给 MaterialCardView 使用。
说明
- 如果目标是
MaterialCardView,会走卡片内容内边距。 - 否则会退化成普通
padding的行为。
background / bg
作用
设置背景。
当前支持来源
| 写法 | 说明 |
|---|---|
#rrggbb / #aarrggbb | 纯色背景 |
@drawable/xxx | 应用资源 |
@android:drawable/xxx | Android 系统资源 |
?attr/colorPrimary | 主题属性 |
| 本地文件路径 | 会尝试读成位图背景 |
示例
<text bg="#1c1c1e" />
<image background="@drawable/shape_card_bg" />
foreground
作用
设置前景。
注意
只在 Android 6.0 及以上才会真正应用,因为源码里做了版本判断。
当前支持来源
- Drawable 资源
- 主题 drawable
- 颜色值
foregroundGravity
适用控件
FrameLayout
含义
控制前景 drawable 的摆放方式。
elevation
含义
设置视图海拔,常用于卡片、悬浮按钮、工具栏阴影。
可填值
支持 dp / sp / px / 裸数字。
alpha
含义
设置透明度。
建议取值
通常按 0 ~ 1 使用最符合预期。
rotation / rotationX / rotationY
作用
设置旋转角度。
说明
rotation是平面旋转。rotationX/rotationY带一点 3D 翻转感。
scaleX / scaleY
作用
设置缩放比例。
示例
<image scaleX="1.2" scaleY="1.2" />
translationX / translationY
作用
设置平移偏移量。
可填值
支持 dp / px / 裸数字。
transformPivotX / transformPivotY
作用
设置旋转或缩放的支点。
enabled / selected / clickable / focusable
含义
这一组都吃布尔语义:
enabled:是否启用selected:是否选中clickable:是否允许点击focusable:是否能拿焦点
minWidth / minHeight
含义
设置最小宽高。
可填值
支持 dp / px / 裸数字。
maxWidth / maxHeight
适用控件
当前源码里只对 TextView 生效。
contentDescription
作用
设置无障碍描述。
示例
<image src="@drawable/ic_search" contentDescription="搜索按钮" />
scaleType
当前支持值
| 值 | 说明 |
|---|---|
fitXY | 拉伸铺满 |
centerInside | 保持比例,完整放入 |
centerCrop | 保持比例,裁剪铺满 |
center | 居中不缩放 |
fitCenter | 居中等比适配 |
src
适用控件
ImageView
当前支持来源
| 写法 | 说明 |
|---|---|
http://... / https://... | 网络图片 |
@drawable/xxx | 本地资源 |
?attr/xxx | 主题资源 / 主题色 |
#rrggbb | 颜色块 |
| 本地文件路径 | 本地图片 |
说明
- 网络图会切到主线程用内部 image loader 加载。
- 资源找不到时不会抛一个很友好的业务错误,所以写路径和资源名时最好自己确认。
borderWidth
适用控件
ScriptUiImageView
作用
设置图片描边宽度。
borderColor
适用控件
ScriptUiImageView
作用
设置图片描边颜色。
radius
适用控件
ScriptUiImageView
作用
设置图片圆角半径。
circle
适用控件
ScriptUiImageView
作用
布尔语义。传 true 时会把图片裁成圆形。
说明
内部实现不是简单“画个圆遮罩”,而是直接调整 ShapeableImageView 的形状模型。
tint
适用控件
ImageView
作用
给图片加 tint。
autoLink
当前支持值
这些 token 可以组合:
allweb/url/urlsemail/emailsphone/phonesmap/maps/address/addresses
示例
<text autoLink="web email" text="官网 https://example.com / mail@test.com" />
title
适用控件
Toolbar
subtitle
适用控件
Toolbar
titleTextColor
适用控件
Toolbar
作用
单独设置标题颜色。
titleCentered
适用控件
MaterialToolbar
作用
布尔语义。是否把标题居中。
logo
适用控件
Toolbar
当前支持来源
和 background / src 类似,主要吃资源引用。
logoDescription
适用控件
Toolbar
作用
设置 Logo 的无障碍描述。
navigationIcon
适用控件
Toolbar
作用
设置左侧导航图标。
titleMargin
适用控件
Toolbar
作用
一口气把 titleMarginStart / End / Top / Bottom 都设成同一个值。
titleMarginStart / titleMarginEnd / titleMarginTop / titleMarginBottom
作用
分别精细控制 Toolbar 标题边距。
url
适用控件
WebView
作用
直接加载网址。
示例
<webview url="https://example.com" />
drawerElevation
适用控件
DrawerLayout
作用
设置抽屉阴影海拔。
cardCornerRadius
适用控件
MaterialCardView
作用
设置卡片圆角。
cardElevation
适用控件
MaterialCardView
作用
设置卡片阴影海拔。
cardBackgroundColor
适用控件
MaterialCardView
作用
设置卡片背景色。
strokeColor
适用控件
MaterialCardView
作用
设置卡片描边色。
strokeWidth
适用控件
MaterialCardView
作用
设置卡片描边宽度。
fabColor
适用控件
FloatingActionButton
作用
设置 FAB 背景 tint。
backgroundTint
适用控件
FloatingActionButtonMaterialButton- 其他支持背景 tint 的普通 View
作用
统一设置背景 tint。
tabMode
适用控件
TabLayout
当前支持值
| 值 | 说明 |
|---|---|
scrollable | 可横向滚动 |
| 其它任意值 | 固定模式 fixed |
singleSelection
适用控件
ChipGroup
作用
布尔语义。是否单选。
chipSpacing
适用控件
ChipGroup
作用
同时设置横向和纵向间距。
chipSpacingVertical
适用控件
ChipGroup
chipSpacingHorizontal
适用控件
ChipGroup
checkedIcon
适用控件
Chip
作用
设置选中图标。
checkedIconEnabled
适用控件
Chip
作用
布尔语义。是否显示选中图标。
checkedButton
适用控件
RadioGroup
可填值
通常写同组内某个 radio 的 id 名:
<radiogroup checkedButton="@id/optionA">
或者:
<radiogroup checkedButton="optionA">
说明
源码内部会自动把 @id/ / @+id/ 前缀去掉,再按脚本 UI 的命名 id 去找。
prompt
适用控件
Spinner
作用
设置 Spinner 提示文案。
spinnerMode
适用控件
Spinner
当前支持值
| 值 | 说明 |
|---|---|
dialog | 对话框模式 |
| 其它任意值 | 下拉模式 dropdown |
说明
这是创建控件时就会参与构造的模式字段,不只是普通后置属性。
entries
适用控件
Spinner
支持值
| 类型 | 行为 |
|---|---|
| 数组 | 每项转字符串后作为一项 |
List | 同上 |
Array | 同上 |
Scriptable | 按可枚举项转 |
| 字符串 | 按 ` |
示例
<spinner entries="调试|发布|混合" />
dropDownWidth
适用控件
AppCompatSpinner
可填值
和普通宽高一样,支持:
match_parentwrap_content*autodp/px/ 裸数字
dropDownHorizontalOffset
适用控件
AppCompatSpinner
dropDownVerticalOffset
适用控件
AppCompatSpinner
popupBackground
适用控件
AppCompatSpinner
作用
设置下拉弹层背景。
timePickerMode
适用控件
TimePicker
当前支持值
| 值 | 说明 |
|---|---|
spinner | 滚轮模式 |
| 其它任意值 | 默认系统模式 |
说明
这也是创建阶段参数。源码里如果写成 spinner,会切到专门的主题包装上下文去构造。
datePickerMode
适用控件
DatePicker
当前支持值
| 值 | 说明 |
|---|---|
spinner | 滚轮模式 |
| 其它任意值 | 默认系统模式 |
说明
和 timePickerMode 一样,这是构造阶段就生效的模式值。
boxBackgroundMode
适用控件
TextInputLayout
当前支持值
| 值 | 说明 |
|---|---|
none | 无框 |
filled / filledBox | 填充风格 |
| 其它任意值 | 轮廓风格 outline |
boxBackgroundColor
适用控件
TextInputLayout
boxStrokeColor
适用控件
TextInputLayout
helperText
适用控件
TextInputLayout
说明
空字符串会被当成 null,也就是清空辅助文字。
error
适用控件
TextInputLayout
说明
空字符串会被当成 null,也就是清空错误提示。
spinnersShown
适用控件
DatePicker
作用
布尔语义。是否显示滚轮选择部分。
calendarViewShown
适用控件
DatePicker
作用
布尔语义。是否显示日历面板。
firstDayOfWeek
适用控件
DatePicker
含义
设置一周从哪一天开始,直接传数字。
maxDate
适用控件
DatePicker
可填格式
当前源码按:
YYYY/MM/DD
解析。
示例
<datepicker maxDate="2026/12/31" />
minDate
适用控件
DatePicker
可填格式
同样是:
YYYY/MM/DD
layout_below / layout_above / layout_toLeftOf / layout_toRightOf
适用控件
父容器必须是 RelativeLayout。
作用
让当前控件相对另一个 id 进行摆放。
可填值
可直接写:
otherId@id/otherId@+id/otherId
layout_toStartOf / layout_toEndOf
适用控件
RelativeLayout
作用
按开始边 / 结束边相对布局。
layout_alignTop / layout_alignBottom / layout_alignLeft / layout_alignRight
适用控件
RelativeLayout
作用
让当前控件边缘和目标控件边缘对齐。
layout_alignStart / layout_alignEnd
适用控件
RelativeLayout
layout_alignParentTop / layout_alignParentBottom / layout_alignParentLeft / layout_alignParentRight
适用控件
RelativeLayout
作用
布尔语义。对齐父容器边缘。
layout_alignParentStart / layout_alignParentEnd
适用控件
RelativeLayout
layout_centerInParent / layout_centerHorizontal / layout_centerVertical
适用控件
RelativeLayout
作用
布尔语义。控制居中方式。
组合示例:TextInputLayout + textinputedittext
这个组合最适合做账号、密码、搜索框、表单输入。
示例
ui.layout(
<vertical padding="16" bg="#121212">
<textinputlayout
id="accountBox"
hint="账号"
boxBackgroundMode="outline"
helperText="请输入手机号或邮箱"
boxStrokeColor="#7dd3fc">
<textinputedittext
id="accountInput"
inputType="text"
textColor="#ffffff"
hintColor="#777777" />
</textinputlayout>
<textinputlayout
id="passwordBox"
hint="密码"
boxBackgroundMode="filled"
boxBackgroundColor="#1c1c1e"
marginTop="12">
<textinputedittext
id="passwordInput"
password="true"
textColor="#ffffff" />
</textinputlayout>
</vertical>
);
ui.passwordBox.error = "密码至少 8 位";
这段示例想说明什么
- 外层
textinputlayout负责框体、标题、辅助文案、错误文案。 - 内层
textinputedittext负责真正输入。 helperText和error都是写在外层,不是写在输入框本体上。
组合示例:spinner
spinner 更适合“选项不多、只选一个”的场景,比如模式切换、环境切换、筛选条件。
示例
ui.layout(
<vertical padding="16">
<spinner
id="modeSpinner"
entries="调试|发布|灰度"
prompt="请选择运行模式"
spinnerMode="dialog"
dropDownWidth="220"
dropDownVerticalOffset="8" />
</vertical>
);
如果你想动态改选项
ui.modeSpinner.attr("entries", ["开发", "测试", "正式"]);
组合示例:DatePicker + TimePicker
示例
ui.layout(
<vertical padding="16">
<datepicker
id="datePicker"
datePickerMode="spinner"
spinnersShown="true"
calendarViewShown="false"
firstDayOfWeek="1"
minDate="2026/01/01"
maxDate="2026/12/31" />
<timepicker
id="timePicker"
timePickerMode="spinner"
marginTop="12" />
</vertical>
);
这段里几个最容易搞混的点
datePickerMode/timePickerMode是“控件长什么样”的模式。spinnersShown/calendarViewShown是DatePicker内部子区域是否显示。minDate/maxDate格式当前按YYYY/MM/DD解析,不是横杠格式。
组合示例:Toolbar + DrawerLayout
示例
ui.layout(
<drawer id="drawerRoot">
<vertical>
<toolbar
id="toolbar"
title="项目文档"
subtitle="ScriptX UI"
titleTextColor="#ffffff"
background="#1d4ed8"
titleCentered="true"
navigationIcon="@android:drawable/ic_menu_sort_by_size" />
<vertical padding="16">
<text text="正文区域" />
</vertical>
</vertical>
<vertical
layout_gravity="start"
w="280"
bg="#111827"
padding="16">
<text text="抽屉菜单" textColor="#ffffff" />
</vertical>
</drawer>
);
ui.toolbar.setupWithDrawer(ui.drawerRoot);
什么时候要写 setupWithDrawer
只要你想让 Toolbar 和 DrawerLayout 真正联动,基本都要调这一句。只写 XML 不会自动给你绑上抽屉切换逻辑。
组合示例:ChipGroup
示例
ui.layout(
<vertical padding="16">
<chipgroup
id="tagGroup"
singleSelection="true"
chipSpacing="8">
<chip id="allChip" text="全部" checked="true" checkedIconEnabled="true" />
<chip id="debugChip" text="调试" checkedIconEnabled="true" />
<chip id="releaseChip" text="正式" checkedIconEnabled="true" />
</chipgroup>
</vertical>
);
这块怎么理解
singleSelection="true"表示整组只允许一个选中。chipSpacing是整组间距,不是某一个chip自己的 margin。checkedIconEnabled控制的是选中图标是否显示,不等于当前是否选中。
组合示例:Card + Image
示例
ui.layout(
<card
id="profileCard"
cardCornerRadius="18"
cardElevation="4"
cardBackgroundColor="#1c1c1e"
strokeColor="#2f3542"
strokeWidth="1"
margin="16">
<horizontal padding="16" gravity="center_vertical">
<image
id="avatar"
w="56"
h="56"
src="/sdcard/Pictures/avatar.png"
circle="true"
borderWidth="2"
borderColor="#7dd3fc" />
<vertical marginLeft="12">
<text text="道无涯i" textColor="#ffffff" textSize="18sp" />
<text text="ScriptX 用户" textColor="#9ca3af" marginTop="4" />
</vertical>
</horizontal>
</card>
);
TextInputLayout 专题
TextInputLayout 在这套 UI 里更像“输入框外壳”,真正输入发生在里面那层 textinputedittext 或普通 input 上。
你应该怎么理解它
- 外层负责:标题、边框、填充框样式、辅助说明、错误提示。
- 内层负责:真实输入、输入类型、光标焦点、密码模式等。
最常用的属性组合
| 目标 | 常用属性 |
|---|---|
| 轮廓输入框 | boxBackgroundMode="outline" |
| 填充输入框 | boxBackgroundMode="filled" |
| 辅助说明 | helperText="..." |
| 错误提示 | error="..." |
| 标题 / hint | 外层 hint="..." |
最容易搞混的点
helperText/error写在外层,不写在内层输入框。inputType/password/digits写在内层输入控件。- 空字符串错误提示会被清掉,因为源码里
error/helperText会把空串转成null。
运行时修改示例
ui.accountBox.helperText = "支持手机号 / 邮箱";
ui.accountBox.error = "";
ui.accountInput.text = "demo@example.com";
Spinner 专题
spinner 适合做“从几个固定项里单选一个”。在 ScriptX 这套 UI 里,它既能在 XML 里一次写死选项,也能后面脚本里动态改。
最常见的两种模式
| 模式 | 写法 | 说明 |
|---|---|---|
| 对话框选择 | spinnerMode="dialog" | 点开后更像弹出一个选择框 |
| 下拉选择 | 不写或写其他值 | 默认 dropdown |
最常用组合
| 目标 | 常用属性 |
|---|---|
| 给用户一个标题提示 | prompt="请选择..." |
| 直接在 XML 写静态选项 | `entries="A |
| 控制下拉层宽度 | dropDownWidth="220" |
| 调整下拉层偏移 | dropDownHorizontalOffset / dropDownVerticalOffset |
| 换下拉层背景 | popupBackground |
动态改选项最常见写法
ui.modeSpinner.attr("entries", ["开发", "测试", "正式"]);
一个要记住的小点
entries 在 Spinner 上和在 ViewPager 上不是一回事:
Spinner的entries是下拉选项。ViewPager的titles才是分页标题。
DatePicker / TimePicker 专题
这两个控件最重要的不是“能选日期时间”,而是你要先想清楚是要滚轮式,还是系统默认式。
模式相关属性
| 控件 | 属性 | 当前支持值 |
|---|---|---|
DatePicker | datePickerMode | spinner / 其它 |
TimePicker | timePickerMode | spinner / 其它 |
DatePicker 常用属性
| 属性 | 含义 |
|---|---|
spinnersShown | 是否显示滚轮部分 |
calendarViewShown | 是否显示日历面板 |
firstDayOfWeek | 一周起始日 |
minDate | 最小日期,格式 YYYY/MM/DD |
maxDate | 最大日期,格式 YYYY/MM/DD |
最容易踩坑的点
minDate/maxDate当前按斜杠格式解析,不是2026-12-31这种横杠格式。spinner模式是构造阶段生效的,不是后面赋值随便切。spinnersShown和calendarViewShown都是布尔语义。
Toolbar + DrawerLayout 专题
这组控件最常见的用途就是做“有标题栏、有抽屉”的脚本页面。
结构上怎么摆
最稳的结构通常是:
- 最外层
drawer - 第一块主内容区域,里面放
toolbar - 第二块抽屉区域,通常
layout_gravity="start"
属性上最常用的几项
| 属性 | 作用 |
|---|---|
title | 主标题 |
subtitle | 副标题 |
titleTextColor | 标题颜色 |
titleCentered | 是否居中 |
navigationIcon | 左侧导航图标 |
drawerElevation | 抽屉阴影海拔 |
最关键的一句
ui.toolbar.setupWithDrawer(ui.drawerRoot);
只写 XML 不会自动把 Toolbar 和 DrawerLayout 联起来,这句才是真正把它们接上。
ChipGroup 专题
ChipGroup 最适合做“标签式筛选”或者“模式切换”。
你可以怎么理解它
ChipGroup决定的是整组行为和布局。Chip决定的是单个标签项。
最常用的整组属性
| 属性 | 作用 |
|---|---|
singleSelection | 是否单选 |
chipSpacing | 横纵统一间距 |
chipSpacingHorizontal | 横向间距 |
chipSpacingVertical | 纵向间距 |
单个 Chip 常用属性
| 属性 | 作用 |
|---|---|
checked | 是否默认选中 |
checkedIcon | 选中图标 |
checkedIconEnabled | 是否显示选中图标 |
最容易误会的点
checkedIconEnabled不等于“当前是否选中”,它只是控制选中图标显不显示。chipSpacing是组属性,不是单个 chip 的 margin。
RadioGroup 专题
RadioGroup 的重点在“如何指定默认选中项”。
当前最推荐写法
<radiogroup id="modeGroup" checkedButton="optionA">
<radio id="optionA" text="方案 A" />
<radio id="optionB" text="方案 B" />
</radiogroup>
checkedButton 能怎么写
optionA@id/optionA@+id/optionA
为什么能这么写
源码内部会把 @id/ 和 @+id/ 前缀剥掉,再按脚本 UI 自己的命名 id 去找目标控件。
数据绑定写法 {{ ... }}
作用
在 XML 属性或文本里嵌入绑定表达式。
规则
- 如果整个属性值就是
{{ expr }},表达式结果会按原始值参与后续处理。 - 如果只是文本里嵌一部分,比如
"姓名: {{item.name}}",那结果会被转成字符串再拼接。
列表里可用的常见上下文
| 名称 | 说明 |
|---|---|
item | 当前项数据 |
this | 当前绑定上下文 |
item.xxx | 当前项字段 |
示例
<text text="{{item.name}}" />
<text text="年龄: {{item.age}}" />
canvas 视图怎么用
canvas 标签不是 canvas 画布 模块本身,但它内部回调拿到的 canvasScope 来自同一套绘制能力。
最小示例
ui.layout(
<canvas id="board" w="*" h="200" bg="#111111" />
);
const textPaint = canvas.newPaint();
textPaint.setARGB(255, 255, 255, 255);
textPaint.setTextSize(20);
ui.board.on("draw", function (canvas) {
canvas.drawColor("#111111");
canvas.drawText("Hello ScriptX", 24, 60, textPaint);
});
怎么理解这个回调
- 每次重绘时都会触发一次
draw。 - 绑定了
draw监听后,会自动开启持续重绘。 - 页面暂停时不会继续无意义地刷。
- 回调参数不是普通对象,而是
ScriptUiCanvasScope。 - 这意味着它能直接调用一批和 canvas 画布 一致的绘图方法。
画布能做什么
因为它底层拿的是 ScriptUiCanvasScope,所以思路上可以按 canvas 画布 那页去用,例如:
- 清屏、填色
- 画线、画矩形、画圆
- 画文本
- 做简单坐标轴、波形图、检测框、点位标注
canvasScope.getWidth() / canvasScope.width()
作用
读取当前 canvas 视图可绘制宽度,单位是像素。
返回值
number
示例
ui.board.on("draw", function (canvas) {
const w = canvas.width();
const h = canvas.height();
log(`canvas size = ${w} x ${h}`);
});
canvasScope.getHeight() / canvasScope.height()
作用
读取当前 canvas 视图可绘制高度,单位是像素。
返回值
number
canvasScope.drawRGB(r, g, b)
作用
用 RGB 通道直接填充整个画布。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
r | number | string | 红色通道,最终会夹到 0 ~ 255 |
g | number | string | 绿色通道,最终会夹到 0 ~ 255 |
b | number | string | 蓝色通道,最终会夹到 0 ~ 255 |
示例
ui.board.on("draw", function (canvas) {
canvas.drawRGB(18, 18, 18);
});
canvasScope.drawARGB(a, r, g, b)
作用
用 ARGB 通道直接填充整个画布。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
a | number | string | 透明度通道,最终会夹到 0 ~ 255 |
r | number | string | 红色通道 |
g | number | string | 绿色通道 |
b | number | string | 蓝色通道 |
canvasScope.drawColor(color)
作用
按颜色值填充整个画布。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
color | number | string | array | 走 ScriptX 统一颜色解析 |
可用颜色写法
"#ff0000""#aarrggbb"0xffff0000[255, 0, 0]
示例
ui.board.on("draw", function (canvas) {
canvas.drawColor("#101114");
});
canvasScope.drawColor(color, mode)
作用
按颜色填充整个画布,并带上混合模式。
第二个参数怎么传
当前最稳的是传真实的 canvas.PorterDuff.Mode 枚举对象,而不是字符串。
示例
ui.board.on("draw", function (canvas) {
canvas.drawColor("#00000000", canvas.PorterDuff.Mode.CLEAR);
});
canvasScope.drawPaint(paint)
作用
用一整支 Paint 对画布执行绘制。
参数
| 参数 | 类型 |
|---|---|
paint | Paint |
注意
如果不是 Paint 实例,源码会直接抛错;它不会自动把普通对象字面量转成画笔。
canvasScope.drawPoint(x, y, paint)
作用
画一个点。
示例
const p = canvas.newPaint();
p.setARGB(255, 255, 80, 80);
p.setStrokeWidth(8);
ui.board.on("draw", function (canvas) {
canvas.drawPoint(40, 40, p);
});
canvasScope.drawPoints(points, paint)
作用
按数字数组一次画多个点。
points 格式
[x1, y1, x2, y2, x3, y3]
当前接受的数组类型
- JS 数组
FloatArrayDoubleArrayIntArray
canvasScope.drawLine(startX, startY, stopX, stopY, paint)
作用
画一条线段。
示例
const p = canvas.newPaint();
p.setARGB(255, 0, 200, 255);
p.setStrokeWidth(4);
ui.board.on("draw", function (canvas) {
canvas.drawLine(20, 20, 180, 120, p);
});
canvasScope.drawLines(points, paint)
作用
按点对批量画线。
points 格式
[x1, y1, x2, y2, x3, y3, x4, y4]
它会按 (x1, y1) -> (x2, y2)、(x3, y3) -> (x4, y4) 这种方式解释。
canvasScope.drawRect(left, top, right, bottom, paint)
作用
按 4 个坐标画矩形。
canvasScope.drawRect(rect, paint)
作用
按 Rect / RectF / 坐标数组画矩形。
rect 可填值
| 写法 | 说明 |
|---|---|
Rect | 直接使用 |
RectF | 直接使用 |
[left, top, right, bottom] | 自动转成 RectF |
示例
const fill = canvas.newPaint();
fill.setARGB(255, 125, 211, 252);
fill.setStyle(canvas.Paint.Style.FILL);
ui.board.on("draw", function (canvas) {
canvas.drawRect(20, 20, 120, 90, fill);
canvas.drawRect([140, 20, 220, 90], fill);
});
canvasScope.drawOval(oval, paint)
作用
在包围盒里画椭圆。
oval 可填值
和 drawRect(rect, paint) 一样,支持:
RectRectF[left, top, right, bottom]
canvasScope.drawCircle(cx, cy, radius, paint)
作用
画圆。
canvasScope.drawArc(oval, startAngle, sweepAngle, useCenter, paint)
作用
画弧形或扇形。
参数说明
| 参数 | 说明 |
|---|---|
oval | 弧形所在包围盒 |
startAngle | 起始角度 |
sweepAngle | 扫过角度 |
useCenter | true 时连到圆心形成扇形 |
paint | Paint |
canvasScope.drawRoundRect(rect, rx, ry, paint)
作用
画圆角矩形。
参数说明
| 参数 | 说明 |
|---|---|
rect | Rect / RectF / [l, t, r, b] |
rx | x 方向圆角半径 |
ry | y 方向圆角半径 |
paint | Paint |
canvasScope.drawPath(path, paint)
作用
按 Path 路径绘制。
参数
| 参数 | 类型 |
|---|---|
path | Path |
paint | Paint |
示例
const path = canvas.newPath();
path.moveTo(20, 100);
path.lineTo(80, 30);
path.lineTo(140, 100);
const stroke = canvas.newPaint();
stroke.setARGB(255, 255, 255, 255);
stroke.setStyle(canvas.Paint.Style.STROKE);
stroke.setStrokeWidth(4);
ui.board.on("draw", function (canvas) {
canvas.drawPath(path, stroke);
});
canvasScope.drawBitmap(bitmap, left, top, paint?)
作用
把另一张位图画到当前 canvas 视图上。
bitmap 可填值
| 可填值 | 说明 |
|---|---|
Bitmap | 直接使用 |
Image | 会取内部位图 |
surface | 会取这个画布对象自己的底层位图 |
canvasScope.drawBitmap(bitmap, matrix, paint?)
作用
带 Matrix 变换地绘制位图。
参数
| 参数 | 类型 |
|---|---|
bitmap | Bitmap | Image | surface |
matrix | Matrix |
paint | Paint | null |
canvasScope.drawImage(...)
作用
drawBitmap(...) 的别名。
canvasScope.drawPicture(picture)
作用
绘制 Picture。
参数
| 参数 | 类型 |
|---|---|
picture | Picture |
canvasScope.drawText(text, x, y, paint)
作用
在指定位置画文字。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
text | any | 最终会转成字符串 |
x | number | 基线起点 x |
y | number | 基线 y |
paint | Paint | 文字样式 |
示例
const textPaint = canvas.newPaint();
textPaint.setARGB(255, 255, 255, 255);
textPaint.setTextSize(22);
ui.board.on("draw", function (canvas) {
canvas.drawText("FPS", 16, 28, textPaint);
});
canvasScope.drawTextOnPath(text, path, hOffset, vOffset, paint)
作用
沿路径绘制文字。
参数说明
| 参数 | 说明 |
|---|---|
text | 文本 |
path | Path |
hOffset | 沿路径偏移 |
vOffset | 垂直路径偏移 |
paint | Paint |
canvasScope.translate(dx, dy)
作用
平移画布坐标系。
canvasScope.scale(sx, sy)
作用
按当前原点缩放。
canvasScope.scale(sx, sy, px, py)
作用
按指定支点缩放。
canvasScope.rotate(degrees)
作用
按当前原点旋转。
canvasScope.rotate(degrees, px, py)
作用
按指定支点旋转。
canvasScope.skew(sx, sy)
作用
做斜切变换。
canvasScope.save()
作用
把当前画布状态压栈。
返回值
number
也就是一个 saveCount,后面可以传给 restoreToCount(...)。
canvasScope.restore()
作用
恢复最近一次 save() 前的状态。
canvasScope.restoreToCount(saveCount)
作用
直接恢复到指定栈层。
canvasScope.concat(matrix)
作用
把一个 Matrix 乘到当前画布变换上。
参数
| 参数 | 类型 |
|---|---|
matrix | Matrix |
canvas 视图完整示例:网格 + 文本 + 旋转指针
ui.layout(
<canvas id="board" w="*" h="220" bg="#0f1115" />
);
const gridPaint = canvas.newPaint();
gridPaint.setARGB(255, 55, 65, 81);
gridPaint.setStrokeWidth(2);
const textPaint = canvas.newPaint();
textPaint.setARGB(255, 255, 255, 255);
textPaint.setTextSize(20);
const needlePaint = canvas.newPaint();
needlePaint.setARGB(255, 125, 211, 252);
needlePaint.setStrokeWidth(6);
let degrees = 0;
ui.board.on("draw", function (canvasScope) {
const w = canvasScope.width();
const h = canvasScope.height();
canvasScope.drawColor("#0f1115");
for (let x = 0; x <= w; x += 24) {
canvasScope.drawLine(x, 0, x, h, gridPaint);
}
for (let y = 0; y <= h; y += 24) {
canvasScope.drawLine(0, y, w, y, gridPaint);
}
canvasScope.drawText(`angle=${degrees}`, 16, 28, textPaint);
const saveCount = canvasScope.save();
canvasScope.translate(w / 2, h / 2);
canvasScope.rotate(degrees);
canvasScope.drawLine(0, 0, 60, 0, needlePaint);
canvasScope.restoreToCount(saveCount);
degrees = (degrees + 2) % 360;
});
完整示例:列表 + 详情 + Toolbar + Drawer
ui.layout(
<drawer id="drawerRoot">
<vertical>
<toolbar id="toolbar" title="用户列表" />
<list id="userList" h="*" w="*">
<horizontal padding="12" gravity="center_vertical">
<text id="name" text="{{item.name}}" textSize="16sp" />
<text id="role" text="{{item.role}}" marginLeft="8" textColor="#999999" />
</horizontal>
</list>
</vertical>
<vertical layout_gravity="start" w="280" padding="16">
<text text="抽屉菜单" textSize="18sp" />
</vertical>
</drawer>
);
ui.toolbar.setupWithDrawer(ui.drawerRoot);
ui.userList.setDataSource([
{ name: "张三", role: "管理员" },
{ name: "李四", role: "访客" },
{ name: "王五", role: "开发" }
]);
ui.userList.on("item_click", function (item, position) {
dialogs.alert("点击结果", `第 ${position} 项: ${item.name} / ${item.role}`);
});
ui.emitter.on("back_pressed", function (event) {
toast("这里示范拦截返回键");
event.consumed = true;
});
完整示例:自定义 Widget
function ProfileCard() {
ui.Widget.call(this);
this.defineAttr("name");
this.defineAttr("desc");
this.defineAttr("accent");
}
ProfileCard.prototype = Object.create(ui.Widget.prototype);
ProfileCard.prototype.constructor = ProfileCard;
ProfileCard.prototype.render = function () {
return (
<card padding="16" cardCornerRadius="16" cardBackgroundColor="#1c1c1e">
<vertical>
<text id="nameView" text={this.name || "未命名"} textSize="18sp" textColor={this.accent || "#ffffff"} />
<text id="descView" text={this.desc || ""} marginTop="8" textColor="#b0b0b0" />
</vertical>
</card>
);
};
ui.registerWidget("profile-card", ProfileCard);
ui.layout(
<vertical padding="16">
<profile-card
id="card1"
name="ScriptX"
desc="这是一个自定义控件"
accent="#7dd3fc"
/>
</vertical>
);
页面示例:登录页
这个例子适合拿来做:
- 登录页
- 激活页
- 简单表单入口页
示例
ui.layout(
<vertical
id="root"
w="*"
h="*"
gravity="center_horizontal"
padding="24"
bg="#0f172a">
<text
text="ScriptX"
textSize="28sp"
textStyle="bold"
textColor="#ffffff"
marginTop="48" />
<text
text="登录到你的工作区"
textColor="#94a3b8"
marginTop="8"
marginBottom="24" />
<textinputlayout
id="accountBox"
w="*"
hint="账号"
boxBackgroundMode="outline"
boxStrokeColor="#38bdf8">
<textinputedittext
id="accountInput"
textColor="#ffffff"
hintColor="#64748b" />
</textinputlayout>
<textinputlayout
id="passwordBox"
w="*"
hint="密码"
boxBackgroundMode="outline"
boxStrokeColor="#38bdf8"
marginTop="12">
<textinputedittext
id="passwordInput"
password="true"
textColor="#ffffff"
hintColor="#64748b" />
</textinputlayout>
<button
id="loginBtn"
w="*"
text="登录"
marginTop="18"
bg="#2563eb"
textColor="#ffffff" />
<text
id="statusText"
text=""
textColor="#fbbf24"
marginTop="12" />
</vertical>
);
ui.loginBtn.click(function () {
const account = ui.accountInput.text().trim();
const password = ui.passwordInput.text().trim();
ui.accountBox.error = "";
ui.passwordBox.error = "";
ui.statusText.text = "";
if (!account) {
ui.accountBox.error = "请输入账号";
return;
}
if (!password) {
ui.passwordBox.error = "请输入密码";
return;
}
ui.statusText.text = "校验通过,准备登录...";
});
这个例子值得学的点
- 外层
textinputlayout专门负责错误提示。 - 输入校验逻辑直接绑在按钮点击里,小白最容易照着改。
statusText这种状态提示比直接弹 Toast 更适合表单页。
页面示例:设置页
这个例子适合做:
- 功能开关页
- 调试选项页
- 偏好设置页
示例
ui.layout(
<scroll>
<vertical padding="16" bg="#111827">
<text text="设置" textSize="22sp" textStyle="bold" textColor="#ffffff" />
<card
cardBackgroundColor="#1f2937"
cardCornerRadius="16"
strokeColor="#374151"
strokeWidth="1"
marginTop="16">
<vertical padding="14">
<horizontal gravity="center_vertical">
<vertical layout_weight="1" w="0">
<text text="启用调试模式" textColor="#ffffff" />
<text text="输出更多日志和诊断信息" textColor="#9ca3af" marginTop="4" />
</vertical>
<materialswitch id="debugSwitch" checked="true" />
</horizontal>
<horizontal gravity="center_vertical" marginTop="14">
<vertical layout_weight="1" w="0">
<text text="自动检查更新" textColor="#ffffff" />
<text text="启动时检查新版本" textColor="#9ca3af" marginTop="4" />
</vertical>
<materialswitch id="updateSwitch" />
</horizontal>
</vertical>
</card>
<text id="settingResult" text="" textColor="#38bdf8" marginTop="14" />
</vertical>
</scroll>
);
ui.debugSwitch.setOnCheckedChangeListener(function (view, checked) {
ui.settingResult.text = checked ? "已开启调试模式" : "已关闭调试模式";
});
ui.updateSwitch.setOnCheckedChangeListener(function (view, checked) {
ui.settingResult.text = checked ? "已开启自动更新检查" : "已关闭自动更新检查";
});
这个例子值得学的点
scroll + vertical是设置页最稳的基础结构。- 每一项设置通常都是“标题 + 描述 + 开关”。
- 用
layout_weight让左边说明区域自动吃剩余宽度,很适合这种行布局。
页面示例:搜索筛选页
这个例子适合做:
- 搜索结果页
- 规则筛选页
- 模式切换页
示例
ui.layout(
<vertical w="*" h="*" bg="#0b1220" padding="16">
<textinputlayout
id="keywordBox"
hint="输入关键字"
boxBackgroundMode="filled"
boxBackgroundColor="#172033">
<textinputedittext
id="keywordInput"
textColor="#ffffff"
hintColor="#6b7280" />
</textinputlayout>
<horizontal marginTop="12" gravity="center_vertical">
<spinner
id="typeSpinner"
w="0"
layout_weight="1"
entries="全部|类|方法|字段"
prompt="类型"
spinnerMode="dialog" />
<chipgroup
id="tagGroup"
layout_weight="1"
w="0"
singleSelection="true"
chipSpacing="8"
marginLeft="12">
<chip id="allTag" text="全部" checked="true" />
<chip id="hotTag" text="常用" />
<chip id="newTag" text="最近" />
</chipgroup>
</horizontal>
<button
id="searchBtn"
text="开始搜索"
marginTop="14"
bg="#2563eb"
textColor="#ffffff" />
<text id="searchResult" text="" textColor="#93c5fd" marginTop="12" />
</vertical>
);
ui.searchBtn.click(function () {
const keyword = ui.keywordInput.text().trim();
ui.searchResult.text = `关键字: ${keyword || "未填写"},已触发搜索`;
});
这个例子值得学的点
- 搜索区常常是“输入框 + 类型筛选 + 标签筛选”组合。
Spinner和ChipGroup很适合搭配使用,一个负责大类,一个负责快捷过滤。
页面示例:列表 + 详情切换页
这个例子适合做:
- 数据列表页
- 用户列表、规则列表、任务列表
- 左侧列表 + 右侧详情的简单版本
示例
const users = [
{ name: "张三", role: "管理员", desc: "负责权限和配置" },
{ name: "李四", role: "访客", desc: "只读访问" },
{ name: "王五", role: "开发", desc: "负责脚本维护" }
];
ui.layout(
<vertical w="*" h="*" bg="#111827">
<list id="userList" h="0" layout_weight="1">
<horizontal padding="14" gravity="center_vertical">
<vertical>
<text text="{{item.name}}" textColor="#ffffff" textSize="16sp" />
<text text="{{item.role}}" textColor="#9ca3af" marginTop="4" />
</vertical>
</horizontal>
</list>
<card
id="detailCard"
cardBackgroundColor="#1f2937"
cardCornerRadius="16"
margin="12"
padding="16">
<vertical>
<text id="detailName" text="请选择一项" textColor="#ffffff" textSize="18sp" />
<text id="detailRole" text="" textColor="#93c5fd" marginTop="6" />
<text id="detailDesc" text="" textColor="#cbd5e1" marginTop="10" />
</vertical>
</card>
</vertical>
);
ui.userList.setDataSource(users);
ui.userList.on("item_click", function (item) {
ui.detailName.text = item.name;
ui.detailRole.text = item.role;
ui.detailDesc.text = item.desc;
});
这个例子值得学的点
list用来承载选择行为。- 下方
detailCard用来承载当前选中项详情。 - 这是最容易迁移到真实项目里的列表页骨架。
页面示例:表单校验页
这个例子适合做:
- 提交任务
- 新建配置
- 创建用户
- 提交参数页
示例
ui.layout(
<scroll>
<vertical padding="16" bg="#0f172a">
<text text="创建任务" textSize="22sp" textStyle="bold" textColor="#ffffff" />
<textinputlayout id="nameBox" hint="任务名称" boxBackgroundMode="outline" marginTop="16">
<textinputedittext id="nameInput" textColor="#ffffff" />
</textinputlayout>
<textinputlayout id="portBox" hint="端口" boxBackgroundMode="outline" marginTop="12">
<textinputedittext id="portInput" inputType="number" textColor="#ffffff" />
</textinputlayout>
<spinner id="envSpinner" entries="开发|测试|正式" prompt="运行环境" marginTop="12" />
<button id="submitBtn" text="提交" marginTop="16" bg="#22c55e" textColor="#ffffff" />
<text id="submitResult" text="" textColor="#fbbf24" marginTop="12" />
</vertical>
</scroll>
);
ui.submitBtn.click(function () {
const name = ui.nameInput.text().trim();
const port = ui.portInput.text().trim();
ui.nameBox.error = "";
ui.portBox.error = "";
ui.submitResult.text = "";
if (!name) {
ui.nameBox.error = "任务名称不能为空";
return;
}
if (!port) {
ui.portBox.error = "端口不能为空";
return;
}
const portNum = Number(port);
if (!Number.isFinite(portNum) || portNum < 1 || portNum > 65535) {
ui.portBox.error = "端口范围必须在 1 ~ 65535";
return;
}
ui.submitResult.text = `已通过校验: ${name} / ${portNum}`;
});
这个例子值得学的点
- 表单校验最实用的方式不是都用对话框,而是直接把错误挂回对应输入框。
TextInputLayout.error很适合做字段级错误提示。Number.isFinite(...)这种基础校验逻辑配合inputType="number"就已经够解决很多页面需求。
页面示例:动态增删改列表页
这个例子适合做:
- 待办事项页
- 任务清单页
- 脚本队列页
- 可新增、可删除、可切换状态的数据列表
先理解这个页面的 3 个核心
tasks是真正的数据源,列表显示什么,以它为准。<list>里的子模板只负责“每一项长什么样”。- 新增、删除、修改时,要先改
tasks,再调用对应的notify...(...)。
示例
const tasks = [
{ title: "检查无障碍服务", status: "待处理", done: false },
{ title: "连接宿主进程", status: "进行中", done: false },
{ title: "导出日志样本", status: "已完成", done: true }
];
ui.layout(
<vertical w="*" h="*" bg="#0f172a">
<vertical padding="16">
<text text="任务清单" textSize="22sp" textStyle="bold" textColor="#ffffff" />
<horizontal marginTop="12" gravity="center_vertical">
<textinputlayout
id="taskInputBox"
w="0"
layout_weight="1"
hint="输入新任务标题"
boxBackgroundMode="outline">
<textinputedittext id="taskInput" textColor="#ffffff" hintColor="#64748b" />
</textinputlayout>
<button
id="addTaskBtn"
text="新增"
marginLeft="12"
bg="#2563eb"
textColor="#ffffff" />
</horizontal>
<text
id="taskSummary"
text=""
textColor="#93c5fd"
marginTop="10" />
</vertical>
<list id="taskList" h="0" layout_weight="1">
<card
marginLeft="16"
marginRight="16"
marginBottom="10"
cardBackgroundColor="#111827"
cardCornerRadius="14"
strokeColor="#1f2937"
strokeWidth="1">
<horizontal padding="14" gravity="center_vertical">
<vertical layout_weight="1" w="0">
<text text="{{item.title}}" textColor="#ffffff" textSize="16sp" />
<text text="{{item.status}}" textColor="{{item.done ? '#22c55e' : '#94a3b8'}}" marginTop="4" />
</vertical>
<text
text="{{item.done ? '已完成' : '未完成'}}"
textColor="{{item.done ? '#22c55e' : '#f59e0b'}}" />
</horizontal>
</card>
</list>
</vertical>
);
ui.taskList.setDataSource(tasks);
renderSummary();
ui.addTaskBtn.click(function () {
const title = ui.taskInput.text().trim();
ui.taskInputBox.error = "";
if (!title) {
ui.taskInputBox.error = "任务标题不能为空";
return;
}
tasks.push({
title,
status: "待处理",
done: false
});
ui.taskList.adapter.notifyItemInserted(tasks.length - 1);
ui.taskInput.text = "";
renderSummary();
});
ui.taskList.on("item_click", function (item, position) {
item.done = !item.done;
item.status = item.done ? "已完成" : "待处理";
ui.taskList.adapter.notifyItemChanged(position);
renderSummary();
});
ui.taskList.on("item_long_click", function (event, item, position) {
event.consumed = true;
dialogs.confirm("删除任务", `确定删除“${item.title}”吗?`).then(function (confirmed) {
if (!confirmed) {
return;
}
tasks.splice(position, 1);
ui.taskList.adapter.notifyItemRemoved(position);
renderSummary();
});
});
function renderSummary() {
const doneCount = tasks.filter(task => task.done).length;
ui.taskSummary.text = `共 ${tasks.length} 项,已完成 ${doneCount} 项,点击切换状态,长按删除`;
}
这个例子值得学的点
- 新增用
notifyItemInserted(...),删除用notifyItemRemoved(...),状态切换用notifyItemChanged(...),这就是最标准的一套局部刷新思路。 item_click回调拿到的是(item, position, itemView, listView),最常用的是前两个。item_long_click回调第一个参数是事件对象,不想让后续默认流程继续时,可以直接event.consumed = true。- 因为这个示例跑在
ui页面环境里,所以dialogs.confirm(...)这里用then(...)接结果更稳,不要默认把它当成普通脚本里的同步布尔返回值。 - 这种写法比每次都
notifyDataSetChanged()更像真正的列表页,也更方便你后面扩展拖动排序、分页加载。
页面示例:步骤向导页
这个例子适合做:
- 首次配置向导
- 多步骤表单
- 安装流程页
- 一步一步带用户完成设置的页面
这个页面要关注什么
- 页面本体放在
viewpager里。 - 步骤标题用
pager.setTitles([...])维护。 - 上一步 / 下一步按钮本质上就是在改
currentItem。 page_selected事件可以用来同步顶部提示文字。
示例
ui.layout(
<vertical w="*" h="*" bg="#111827">
<vertical padding="16">
<text id="wizardTitle" text="步骤 1 / 3:选择宿主" textColor="#ffffff" textSize="20sp" textStyle="bold" />
<text id="wizardDesc" text="先选择你准备调试的目标应用" textColor="#93c5fd" marginTop="6" />
</vertical>
<viewpager id="mainPager" h="0" layout_weight="1">
<scroll>
<vertical padding="16">
<text text="第一步:选择宿主应用" textColor="#ffffff" textSize="18sp" />
<spinner id="hostSpinner" entries="ScriptX|微信|抖音|测试宿主" prompt="选择应用" marginTop="12" />
<text text="这里通常放宿主选择、包名确认、版本检查。" textColor="#94a3b8" marginTop="12" />
</vertical>
</scroll>
<scroll>
<vertical padding="16">
<text text="第二步:设置运行参数" textColor="#ffffff" textSize="18sp" />
<checkbox id="enableLogCheck" text="启用详细日志" checked="true" textColor="#ffffff" marginTop="12" />
<checkbox id="enableHookCheck" text="启动后自动注入 Hook" textColor="#ffffff" marginTop="10" />
</vertical>
</scroll>
<scroll>
<vertical padding="16">
<text text="第三步:完成确认" textColor="#ffffff" textSize="18sp" />
<text id="confirmSummary" text="点击下方完成,生成本次配置。" textColor="#cbd5e1" marginTop="12" />
</vertical>
</scroll>
</viewpager>
<horizontal padding="16" gravity="center_vertical">
<button
id="prevBtn"
text="上一步"
w="0"
layout_weight="1"
enabled="false"
bg="#334155"
textColor="#ffffff" />
<button
id="nextBtn"
text="下一步"
w="0"
layout_weight="1"
marginLeft="12"
bg="#2563eb"
textColor="#ffffff" />
</horizontal>
</vertical>
);
const wizardTitles = ["选择宿主", "运行参数", "完成确认"];
ui.mainPager.setTitles(wizardTitles);
syncWizardHeader(0);
ui.mainPager.on("page_selected", function (position) {
syncWizardHeader(position);
});
ui.prevBtn.click(function () {
const current = ui.mainPager.getCurrentItem();
if (current > 0) {
ui.mainPager.setCurrentItem(current - 1);
}
});
ui.nextBtn.click(function () {
const current = ui.mainPager.getCurrentItem();
if (current < wizardTitles.length - 1) {
if (current === 1) {
ui.confirmSummary.text =
`宿主候选:${ui.hostSpinner.attr("entries").join(" / ")},详细日志:${ui.enableLogCheck.checked ? "开启" : "关闭"},自动 Hook:${ui.enableHookCheck.checked ? "开启" : "关闭"}`;
}
ui.mainPager.setCurrentItem(current + 1);
return;
}
dialogs.alert("完成", "向导流程已经完成");
});
function syncWizardHeader(position) {
ui.wizardTitle.text = `步骤 ${position + 1} / ${wizardTitles.length}:${wizardTitles[position]}`;
if (position === 0) {
ui.wizardDesc.text = "先选择你准备调试的目标应用";
} else if (position === 1) {
ui.wizardDesc.text = "在这里打开需要的日志和自动动作";
} else {
ui.wizardDesc.text = "最后确认配置并完成";
}
ui.prevBtn.enabled = position > 0;
ui.nextBtn.text = position >= wizardTitles.length - 1 ? "完成" : "下一步";
}
这个例子值得学的点
viewpager不只是“左右滑动容器”,它很适合拿来做步骤页。setTitles([...])不一定非得搭配tabs用,很多时候只是为了让你自己维护一份清晰的步骤标题数组。page_selected很适合同步顶部标题、底部按钮状态、进度文字。
页面示例:实时搜索筛选列表页
这个例子适合做:
- 类 / 方法 / 字段筛选页
- 日志搜索页
- 模块搜索页
- 本地数据即时过滤页
这个例子的重点
sourceItems保存完整原始数据。filteredItems保存当前正在显示的数据。- 每次关键字变化时,重新计算
filteredItems,再整表刷新。
示例
const sourceItems = [
{ title: "findClass", type: "反射", desc: "查找类对象" },
{ title: "callMethod", type: "反射", desc: "调用实例方法" },
{ title: "http.get", type: "http", desc: "发起 GET 请求" },
{ title: "images.read", type: "images", desc: "读取图片文件" },
{ title: "events.onKeyDown", type: "events", desc: "监听按键事件" }
];
let filteredItems = sourceItems.slice();
ui.layout(
<vertical w="*" h="*" bg="#0b1220" padding="16">
<textinputlayout id="searchBox" hint="输入 API 名称或说明" boxBackgroundMode="outline">
<textinputedittext id="searchInput" textColor="#ffffff" hintColor="#64748b" />
</textinputlayout>
<horizontal marginTop="12" gravity="center_vertical">
<button id="searchBtn" text="搜索" w="0" layout_weight="1" bg="#2563eb" textColor="#ffffff" />
<button id="resetBtn" text="重置" w="0" layout_weight="1" marginLeft="10" bg="#334155" textColor="#ffffff" />
</horizontal>
<text id="searchCount" text="" textColor="#93c5fd" marginTop="12" />
<list id="resultList" h="0" layout_weight="1" marginTop="10">
<vertical padding="12">
<text text="{{item.title}}" textColor="#ffffff" textSize="16sp" />
<text text="{{item.type}}" textColor="#38bdf8" marginTop="4" />
<text text="{{item.desc}}" textColor="#94a3b8" marginTop="6" />
</vertical>
</list>
</vertical>
);
ui.resultList.setDataSource(filteredItems);
renderCount();
ui.searchBtn.click(applySearch);
ui.resetBtn.click(function () {
ui.searchInput.text = "";
filteredItems = sourceItems.slice();
ui.resultList.setDataSource(filteredItems);
ui.resultList.notifyDataSetChanged();
renderCount();
});
ui.resultList.on("item_click", function (item) {
dialogs.alert(item.title, `${item.type}\n${item.desc}`);
});
function applySearch() {
const keyword = ui.searchInput.text().trim().toLowerCase();
if (!keyword) {
filteredItems = sourceItems.slice();
} else {
filteredItems = sourceItems.filter(item => {
return (
item.title.toLowerCase().indexOf(keyword) >= 0 ||
item.type.toLowerCase().indexOf(keyword) >= 0 ||
item.desc.toLowerCase().indexOf(keyword) >= 0
);
});
}
ui.resultList.setDataSource(filteredItems);
ui.resultList.notifyDataSetChanged();
renderCount();
}
function renderCount() {
ui.searchCount.text = `当前显示 ${filteredItems.length} / ${sourceItems.length} 项`;
}
这个例子值得学的点
- 搜索页里最稳的思路通常不是“直接改原数组”,而是保留一份完整源数据,再计算一份显示数据。
- 过滤结果整体变化很大时,用
notifyDataSetChanged()比一个个局部通知更直接。 - 列表项点击后弹说明,这种写法很适合 API 浏览页、结果浏览页。
页面示例:任务状态切换与排序页
这个例子适合做:
- 任务优先级调整
- 队列重排
- 置顶常用项
- 简单版任务面板
这个页面要学的不是布局,而是更新思路
- 改标题或状态,用
notifyItemChanged(...) - 挪位置,用
notifyItemMoved(...) - 大批量整体重排,再考虑
notifyDataSetChanged()
示例
const jobs = [
{ title: "启动宿主", level: "高", state: "待开始" },
{ title: "注入脚本", level: "中", state: "待开始" },
{ title: "抓取日志", level: "低", state: "待开始" }
];
let selectedIndex = 0;
ui.layout(
<vertical w="*" h="*" bg="#111827">
<list id="jobList" h="0" layout_weight="1">
<horizontal padding="14" gravity="center_vertical">
<vertical layout_weight="1" w="0">
<text text="{{item.title}}" textColor="#ffffff" textSize="16sp" />
<text text="{{item.level}} / {{item.state}}" textColor="#94a3b8" marginTop="4" />
</vertical>
</horizontal>
</list>
<vertical padding="16">
<text id="jobInfo" text="" textColor="#93c5fd" />
<horizontal marginTop="12">
<button id="moveTopBtn" text="置顶" w="0" layout_weight="1" bg="#2563eb" textColor="#ffffff" />
<button id="toggleStateBtn" text="切状态" w="0" layout_weight="1" marginLeft="10" bg="#0f766e" textColor="#ffffff" />
</horizontal>
</vertical>
</vertical>
);
ui.jobList.setDataSource(jobs);
renderJobInfo();
ui.jobList.on("item_click", function (item, position) {
selectedIndex = position;
renderJobInfo();
});
ui.moveTopBtn.click(function () {
if (selectedIndex <= 0 || selectedIndex >= jobs.length) {
return;
}
const picked = jobs.splice(selectedIndex, 1)[0];
jobs.unshift(picked);
ui.jobList.adapter.notifyItemMoved(selectedIndex, 0);
selectedIndex = 0;
renderJobInfo();
});
ui.toggleStateBtn.click(function () {
const item = jobs[selectedIndex];
if (!item) {
return;
}
item.state = item.state === "待开始" ? "已完成" : "待开始";
ui.jobList.adapter.notifyItemChanged(selectedIndex);
renderJobInfo();
});
function renderJobInfo() {
const current = jobs[selectedIndex];
if (!current) {
ui.jobInfo.text = "当前没有选中任务";
return;
}
ui.jobInfo.text = `当前选中:${current.title},优先级:${current.level},状态:${current.state}`;
}
这个例子值得学的点
selectedIndex这种写法很常见,适合你在“列表 + 操作按钮”布局里记录当前焦点项。notifyItemMoved(...)的前提是你真的把数组顺序改了,不是只喊一声通知。- 这类页面一旦写通,后面做“置顶常用脚本”“移动模块顺序”“切换启用状态”都会很顺手。
