# 插件视图(PluginView)
PluginView 是插件 UI 系统的基础,所有 UI 组件都实现了 PluginView 接口。本文档将介绍如何使用 Builder 模式构建插件视图,以及如何通过组合布局来创建复杂的用户界面。
# 快速开始
// 创建一个简单的垂直布局
PluginView view = pluginUI.buildVerticalLayout()
.addTextView("title").text("欢迎使用")
.addTextView("desc").text("这是一个简单的示例")
.addButton("btn").text("点击我").onClick(v -> {
pluginUI.showToast("按钮被点击了");
})
.build();
// 在对话框中显示
pluginUI.buildDialog().setView(view).show();
# 支持的视图类型
插件 UI 系统提供了常见的组件,通过 Builder 的 addXxx() 方法添加。
# 基础组件
| 组件 | 说明 | Builder 方法 |
|---|---|---|
PluginView | 普通视图(可用作分割线等) | addView() |
PluginTextView | 文本视图,用于显示文本内容 | addTextView() |
PluginImageView | 图片视图,用于显示图片和图标 | addImageView() |
PluginButton | 按钮,支持多种风格 | addButton() |
PluginEditText | 文本编辑框,支持语法高亮 | addEditText() / addEditBox() |
PluginSpinner | 下拉选择框 | addSpinner() |
PluginProgressBar | 进度条(水平/圆形) | addProgressBar() |
# 复合按钮
| 组件 | 说明 | Builder 方法 |
|---|---|---|
PluginCheckBox | 复选框,用于多选场景 | addCheckBox() |
PluginSwitchButton | 开关按钮,用于开/关设置 | addSwitchButton() |
PluginRadioButton | 单选按钮,配合单选组使用 | addRadioButton() |
# 布局容器
| 组件 | 说明 | Builder 方法 |
|---|---|---|
PluginLinearLayout | 线性布局(水平/垂直) | addHorizontalLayout() / addVerticalLayout() |
PluginFrameLayout | 帧布局,子视图叠加显示 | addFrameLayout() |
PluginRadioGroup | 单选按钮组,管理单选逻辑 | addRadioGroup() |
提示:每个组件都有对应的 Builder 类(如
PluginTextViewBuilder),通过 Builder 可以链式设置组件的各种属性。详细的组件属性和用法请查看:
- Demo 演示:mt-plugin-v3-demo (opens new window) 中的
ExampleUI.java- API 源码注释:在 Android Studio 中通过查看依赖库源码的方式查看各接口的详细注释
# 布局思路
构建复杂视图的核心思路是:将界面拆分为多个水平布局和垂直布局的组合。
# 线性布局
插件 UI 提供两种线性布局:
// 垂直布局:子视图从上往下排列
pluginUI.buildVerticalLayout()
.addTextView().text("第一行")
.addTextView().text("第二行")
.addTextView().text("第三行")
.build();
// 水平布局:子视图从左往右排列
pluginUI.buildHorizontalLayout()
.addTextView().text("第一列")
.addTextView().text("第二列")
.addTextView().text("第三列")
.build();
# 嵌套布局
使用 children() 方法可以在布局中嵌套子布局,从而构建复杂界面:
// 垂直布局中嵌套水平布局
pluginUI.buildVerticalLayout()
.addHorizontalLayout().children(row -> row
.addTextView().text("姓名")
.addEditText().hint("请输入姓名")
)
.addHorizontalLayout().children(row -> row
.addTextView().text("邮箱")
.addEditText().hint("请输入邮箱")
)
.addButton().text("提交")
.build();
# 子视图样式
使用 childrenStyle() 方法可以为布局容器内的所有子视图设置统一样式。样式会向下继承——如果没有设置自定义样式,子视图会继承上一级的样式,最顶级的样式来自 pluginUI.defaultStyle():
pluginUI.buildVerticalLayout()
// 为这个水平布局的所有子视图设置统一样式
.addHorizontalLayout().childrenStyle(new PluginUI.StyleWrapper() {
@Override
protected void handleTextView(PluginUI pluginUI, PluginTextViewBuilder builder) {
super.handleTextView(pluginUI, builder);
builder.width(0).layoutWeight(1) // 均匀分配宽度
.textGravity(Gravity.CENTER)
.paddingDp(16)
.textColor(0xFF000000);
}
}).children(row -> row
.addTextView().text("1-1").backgroundColor(0xFFFF5555)
.addTextView().text("1-2").backgroundColor(0xFF55FF55)
.addTextView().text("1-3").backgroundColor(0xFF5555FF)
)
.build();
提示:
childrenStyle()与pluginUI.defaultStyle()的区别:
defaultStyle()设置整个 PluginUI 的默认样式,是最顶级的样式来源childrenStyle()只影响当前布局容器的所有子视图(包括嵌套的子视图)
# 布局示例
# 表单布局
表单是最常见的布局场景,通常由多行「标签 + 输入框」组成:
PluginView form = pluginUI.buildVerticalLayout()
// 第一行:用户名
.addHorizontalLayout().children(row -> row
.addTextView("label1").text("用户名")
.addEditText("input1").hint("请输入用户名")
)
// 第二行:密码
.addHorizontalLayout().children(row -> row
.addTextView("label2").text("密码")
.addEditText("input2").hint("请输入密码")
)
// 第三行:确认密码
.addHorizontalLayout().children(row -> row
.addTextView("label3").text("确认密码")
.addEditText("input3").hint("请再次输入密码")
)
.addButton().text("注册").widthMatchParent()
.build();
// 统一标签宽度,让输入框左对齐
form.unifyWidth("label1", "label2", "label3");
# 网格布局
通过嵌套水平布局,可以实现网格效果:
pluginUI.buildVerticalLayout()
.addHorizontalLayout().children(row -> row
.addTextView().text("1-1").width(0).layoutWeight(1).backgroundColor(0xFFFF5555)
.addTextView().text("1-2").width(0).layoutWeight(1).backgroundColor(0xFF55FF55)
.addTextView().text("1-3").width(0).layoutWeight(1).backgroundColor(0xFF5555FF)
)
.addHorizontalLayout().children(row -> row
.addTextView().text("2-1").width(0).layoutWeight(1).backgroundColor(0xFF55FF55)
.addTextView().text("2-2").width(0).layoutWeight(1).backgroundColor(0xFF5555FF)
.addTextView().text("2-3").width(0).layoutWeight(1).backgroundColor(0xFFFF5555)
)
.build();
提示:使用
width(0).layoutWeight(1)可以让多个视图均匀分配宽度。权重值越大,分配的空间越多。
# 分割线
使用 addView() 可以添加分割线:
pluginUI.buildVerticalLayout()
.addTextView().text("上方内容")
// 分割线:高度1px,宽度撑满,使用分割线颜色
.addView().height(1).widthMatchParent().backgroundColor(pluginUI.colorDivider())
.addTextView().text("下方内容")
.build();
# 视图 ID
视图 ID 用于在构建完成后查找和操作特定的视图。
# 设置 ID
在 Builder 中通过 id() 方法或直接在 addXxx() 中传入 ID:
// 方式一:在 addXxx() 中传入 ID
pluginUI.buildVerticalLayout()
.addTextView("myText").text("Hello")
.addButton("myButton").text("Click")
.build();
// 方式二:使用 id() 方法
pluginUI.buildVerticalLayout()
.addTextView().id("myText").text("Hello")
.addButton().id("myButton").text("Click")
.build();
# 查找视图
使用 findViewById() 或 requireViewById() 查找视图:
PluginView view = pluginUI.buildVerticalLayout()
.addTextView("text").text("Hello")
.addButton("button").text("修改文本")
.build();
// findViewById:找不到返回 null
PluginTextView textView = view.findViewById("text");
if (textView != null) {
textView.setText("World");
}
// requireViewById:找不到抛出异常(确定存在时使用)
PluginButton button = view.requireViewById("button");
button.setOnClickListener(v -> {
// ...
});
# ID 严格模式
默认情况下,ID 严格模式是开启的。在同一个根布局中,不允许出现重复的 ID:
// 默认开启严格模式,下面的代码会抛出异常
pluginUI.buildVerticalLayout()
.addTextView("text").text("Text 1")
.addTextView("text").text("Text 2") // 抛出异常:ID 重复
.build();
// 禁用严格模式后允许重复 ID
pluginUI.disableStrictIdMode()
.buildVerticalLayout()
.addTextView("text").text("Text 1")
.addTextView("text").text("Text 2") // 不会抛出异常
.build();
# getRootView 缓存优化
getRootView() 返回视图树的根容器。根视图会缓存 findViewById() 和 requireViewById() 的查找结果,因此在回调方法中通过根视图获取其他视图不会有性能问题。
这个特性使得全链式调用成为可能——在 Builder 的回调中直接通过 getRootView() 获取其他视图:
// 全链式调用:在 onClick 回调中通过 getRootView() 获取其他视图
pluginUI.buildVerticalLayout()
.addEditText("input").hint("请输入内容")
.addTextView("error").text("输入不能为空").textColor(Color.RED).gone()
.addButton("submit").text("提交").onClick(v -> {
// 通过根视图获取其他视图(结果会被缓存,性能无忧)
PluginViewGroup root = v.getRootView();
PluginEditText input = root.requireViewById("input");
PluginTextView error = root.requireViewById("error");
if (input.getText().toString().isEmpty()) {
error.setVisible();
} else {
error.setGone();
pluginUI.showToast("提交成功");
}
})
.showDialog();
# 宽度统一
unifyWidth() 方法用于统一多个视图的宽度,以最宽的视图为准。
# 使用场景
在表单布局中,不同标签的文本长度不同,会导致后面的输入框无法对齐:
未使用 unifyWidth() 时:
[用户名] [___________________]
[密码] [___________________]
[电子邮箱] [___________________]
↑ 标签宽度不一致,输入框起始位置不对齐
使用 unifyWidth() 后,所有标签会使用相同的宽度(以最宽的「电子邮箱」为准),输入框就能对齐了:
使用 unifyWidth() 后:
[用户名 ] [___________________]
[密码 ] [___________________]
[电子邮箱] [___________________]
↑ 标签宽度统一,输入框起始位置对齐
# unifyWidth 方法
有两种调用方式:
// 方式一:传入 ID 数组
view.unifyWidth("label1", "label2", "label3");
// 方式二:传入 View 数组
PluginTextView label1 = view.requireViewById("label1");
PluginTextView label2 = view.requireViewById("label2");
PluginTextView label3 = view.requireViewById("label3");
view.unifyWidth(label1, label2, label3);
# 完整示例
PluginView view = pluginUI.buildVerticalLayout()
.addHorizontalLayout().children(row -> row
.addTextView("label1").text("用户名")
.addEditText().hint("请输入用户名")
)
.addHorizontalLayout().children(row -> row
.addTextView("label2").text("密码")
.addEditText().hint("请输入密码")
)
.addHorizontalLayout().children(row -> row
.addTextView("label3").text("电子邮箱")
.addEditText().hint("请输入邮箱")
)
.build();
// 统一标签宽度
view.unifyWidth("label1", "label2", "label3");
# 注意事项
注意:
- 参与对齐的视图建议使用
WRAP_CONTENT作为宽度参数,以获得最佳效果- 如果视图使用
MATCH_PARENT或固定宽度,可能无法达到预期的对齐效果- 至少需要传入 2 个 ID 或视图才有意义
# 本地化文本支持
UI 组件直接支持 {key} 格式的本地化文本,无需手动调用 getString()。
# 支持的方法
所有接受文本参数的方法都支持本地化文本,包括但不限于:
text()- 设置文本内容hint()- 设置提示文本- 其他接受
CharSequence参数的方法
# 使用示例
// 使用本地化文本
pluginUI.buildVerticalLayout()
.addTextView().text("{plugin_name}") // 从 strings 语言包获取
.addButton().text("{ok}") // 使用 MT 内置词条
.addEditText().hint("{input_hint}") // 编辑框提示
.build();
// 等价于手动调用 getString()
pluginUI.buildVerticalLayout()
.addTextView().text(context.getString("plugin_name"))
.addButton().text(context.getString("ok"))
.addEditText().hint(context.getString("input_hint"))
.build();
提示:关于本地化文本的详细说明,包括语言包配置、MT 内置词条等,请查看 本地化文本。
# 常用属性设置
以下是 PluginView 接口提供的常用属性设置方法。
# 尺寸设置
// 尺寸常量
int MATCH_PARENT = -1; // 匹配父容器尺寸
int WRAP_CONTENT = -2; // 根据内容自适应尺寸
// 设置宽高(px)
view.setWidth(100);
view.setHeight(50);
view.setSize(100, 50);
// 设置宽高(dp)
view.setWidthDp(100);
view.setHeightDp(50);
view.setSizeDp(100, 50);
// Builder 链式调用
.addTextView().width(0).layoutWeight(1) // 使用权重分配宽度
.addButton().widthMatchParent() // 宽度撑满
.addView().height(1) // 分割线高度 1px
# 内外边距
// 内边距(px)
view.setPadding(16); // 四侧相同
view.setPadding(16, 8, 16, 8); // 左、上、右、下
view.setPaddingHorizontal(16); // 水平方向
view.setPaddingVertical(8); // 垂直方向
// 内边距(dp)
view.setPaddingDp(16);
view.setPaddingHorizontalDp(16);
// 外边距(px / dp)
view.setMargin(16);
view.setMarginDp(16);
view.setMarginTopDp(8);
// Builder 链式调用
.addTextView().paddingDp(16).marginTopDp(8)
# 可见性
// 可见性常量
int VISIBLE = 0; // 可见
int INVISIBLE = 4; // 不可见但占用布局空间
int GONE = 8; // 不可见且不占用布局空间
// 设置可见性
view.setVisibility(PluginView.VISIBLE);
view.setVisible(); // 等价于 setVisibility(VISIBLE)
view.setInvisible(); // 等价于 setVisibility(INVISIBLE)
view.setGone(); // 等价于 setVisibility(GONE)
// Builder 链式调用
.addTextView("error").text("错误提示").gone() // 初始隐藏
# 事件监听
// 点击事件
view.setOnClickListener(v -> {
pluginUI.showToast("被点击了");
});
// 长按事件
view.setOnLongClickListener(v -> {
pluginUI.showToast("被长按了");
return true; // 返回 true 表示消费事件(会有震动效果)
});
// Builder 链式调用
.addButton().text("点击").onClick(v -> { ... })
.addTextView().text("长按试试").onLongClick(v -> { return true; })
# 相关接口
- PluginUI - 插件UI核心
- PluginDialog - 插件对话框
← API - 插件UI API - 对话框 →