自定义组件 / 自定义控件
2022/10/22大约 5 分钟
提示
Auto.js Pro 软件内也带有自定义控件示例,参见 示例 → 界面控件。
本页介绍如何在 Auto.js Pro v8(Rhino UI)里创建 可复用的自定义组件/控件。核心入口是:
$ui.registerWidget(name, widget)
你可以把一段布局 + 交互封装成一个新标签(例如 <counter/>、<userCard/>),在多个页面中复用,并支持:
- 自定义属性(在 XML 上写
title="..."、value="...") - 自定义事件(在组件内部触发回调或向外抛事件)
- 实例方法(例如
setValue()、reset()) - 组合子布局(组件内部再
inflate其它 XML)
术语说明:在本文中,“组件/控件/Widget”都指 UI 里的可复用封装。它既可以是 Android 原生 View,也可以是由多个 View 组合出来的“复合控件”。
1. 最小示例:注册并使用一个自定义组件
目标:创建一个 <hello/> 组件,显示一行文字。
"ui";
// 1) 注册组件:name 对应 XML 标签名
$ui.registerWidget("hello", function (ctx, attrs) {
// ctx: Android Context
// attrs: XML 上写的属性集合(不同版本实现可能略有差异)
// 最简单:返回一个已经 inflate 好的 View
return $ui.inflate(
<frame padding="12dp">
<text id="t" text="Hello" textSize="16sp" />
</frame>
);
});
// 2) 使用组件
$ui.layout(
<vertical padding="16dp">
<hello />
</vertical>
);2. 读取 XML 属性(props)
通常你希望在 XML 里给组件传参,例如:
<hello text="你好" />在组件里读取属性的思路是:从 attrs 里取出对应键值,然后设置到内部 View 上。不同 Auto.js Pro 版本的 attrs 表现可能不同(有的更像对象,有的更像可迭代集合)。为了写出“更稳”的代码,建议做兼容读取。
下面给一个 通用兜底写法(拿不到就用默认值):
function getAttr(attrs, name, defaultValue) {
try {
// 常见情况:attrs[name]
if (attrs && typeof attrs === "object" && name in attrs) return attrs[name];
// 有些实现提供 get(name)
if (attrs && typeof attrs.get === "function") {
const v = attrs.get(name);
return v == null ? defaultValue : v;
}
} catch (e) {}
return defaultValue;
}然后在组件里:
$ui.registerWidget("hello", function (ctx, attrs) {
const text = getAttr(attrs, "text", "Hello");
const v = $ui.inflate(
<frame padding="12dp">
<text id="t" textSize="16sp" />
</frame>
);
v.t.setText(String(text));
return v;
});3. 组件对外暴露方法(实例方法)
如果你想让外部脚本这样调用:
ui.myHello.setText("Hi");你需要在返回的 View 上挂载方法。因为 Auto.js 的 UI 绑定允许通过 id 取到 View 对象(例如 ui.myHello),所以把方法挂到 View 上是最直接的方式。
$ui.registerWidget("hello", function (ctx, attrs) {
const v = $ui.inflate(
<frame padding="12dp">
<text id="t" textSize="16sp" />
</frame>
);
v.setText = function (s) {
// 注意:UI 操作要在 UI 线程
ui.post(() => v.t.setText(String(s)));
};
return v;
});4. 自定义事件:从组件内部通知外部
常见需求:组件内部按钮被点击时,把事件“抛出”给外部。
4.1 方式 A:传入回调(最简单)
在 XML 里传 onChange 之类的“回调名”通常不太方便(XML 表达式能力因环境而异)。更稳的方式是:外部拿到组件实例后,给它设置回调。
// 外部
ui.counter.onChange = function (value) {
toastLog("value=" + value);
};组件内部:
if (typeof v.onChange === "function") v.onChange(nextValue);4.2 方式 B:借用 View 的事件监听(推荐用于点击类)
如果组件本身就是可点击的容器,可以让外部直接写:
ui.counter.on("click", () => {});组件内部只需要保证最外层返回的是一个正常 View(并且 clickable="true" 或可绑定点击事件)。
5. 完整示例:计数器组件(属性 + 方法 + 事件)
该组件提供:
- 属性:
title、value - 方法:
getValue()、setValue(v)、reset() - 事件:
onChange(value)
"ui";
function getAttr(attrs, name, defaultValue) {
try {
if (attrs && typeof attrs === "object" && name in attrs) return attrs[name];
if (attrs && typeof attrs.get === "function") {
const v = attrs.get(name);
return v == null ? defaultValue : v;
}
} catch (e) {}
return defaultValue;
}
$ui.registerWidget("counter", function (ctx, attrs) {
const title = String(getAttr(attrs, "title", "Counter"));
const initialValue = Number(getAttr(attrs, "value", 0)) || 0;
const v = $ui.inflate(
<vertical padding="12dp" bg="#ffffff">
<text id="title" textSize="16sp" textColor="#222222" />
<horizontal marginTop="8dp" gravity="center_vertical">
<button id="minus" text="-" w="48dp" h="40dp" />
<text id="value" textSize="18sp" marginLeft="12dp" marginRight="12dp" />
<button id="plus" text="+" w="48dp" h="40dp" />
<button id="resetBtn" text="Reset" marginLeft="12dp" h="40dp" />
</horizontal>
</vertical>
);
let value = initialValue;
v.title.setText(title);
v.value.setText(String(value));
function emitChange() {
if (typeof v.onChange === "function") {
try {
v.onChange(value);
} catch (e) {
console.error(e);
}
}
}
function setValue(next) {
value = Number(next) || 0;
v.value.setText(String(value));
emitChange();
}
// 对外方法
v.getValue = () => value;
v.setValue = (next) => ui.post(() => setValue(next));
v.reset = () => ui.post(() => setValue(initialValue));
// 内部交互
v.minus.on("click", () => setValue(value - 1));
v.plus.on("click", () => setValue(value + 1));
v.resetBtn.on("click", () => setValue(initialValue));
return v;
});
$ui.layout(
<vertical padding="16dp">
<counter id="counter" title="My counter" value="3" />
<text id="log" marginTop="12dp" text="Ready" />
<horizontal marginTop="12dp">
<button id="btnSet10" text="Set to 10" />
<button id="btnRead" text="Read value" marginLeft="12dp" />
</horizontal>
</vertical>
);
ui.counter.onChange = (v) => {
ui.log.setText("value = " + v);
};
ui.btnSet10.on("click", () => ui.counter.setValue(10));
ui.btnRead.on("click", () => toastLog("current = " + ui.counter.getValue()));6. 常见踩坑与建议
6.1 UI 线程
- 所有 View 的更新必须在 UI 线程进行。
- 若你在子线程里想更新组件,请用
$ui.post(...)/$ui.run(...)。
6.2 不要在组件里做耗时任务
- 网络请求、文件 IO、大量计算请放到子线程。
- 子线程结束后,再
ui.post回来更新 UI。
6.3 组件命名与冲突
id与ui现有属性可能冲突(例如layout/post/run),冲突时用$ui.findView(id)获取。- 自定义组件标签名建议用有意义的英文小写(例如
counter、userCard)。
6.4 组件复用与状态
- 尽量把“状态”封装在组件内部(如示例
value),对外只暴露必要方法与事件。 - 如果要做“受控组件”(外部传入 value 并完全由外部驱动),建议提供
setValue()并在外部统一管理状态。
7. 相关链接
$ui.registerWidgetAPI:见src/en/v8/ui/api.md中的$ui.registerWidget(...)章节(英文)- UI 规范:
src/v8/ui/Guidelines.md/src/en/v8/ui/guidelines.md - 使用 WebView 写 UI:
src/v8/ui/webview.md
