索引
本文档是 UI
库的入门介绍手册,希望开发者通过阅读此文档能了解 UI
库的特性并且掌握组件的基础用法,如设置边框、背景、增加事件监听和使用布局器对组件进行布局
UI
库是一套功能强大的界面组件库,基于 HT
核心包的优秀架构和 HTML5
先进的 Canvas
机制,具有易上手、高性能、易扩展、组件丰富、跨平台等特点。
主要特性:
Border
接口支持定义任意类型的组件边框Drawable
接口支持定义任意类型的组件背景和图标JSON
字符串的方式存储CSS
的方式外部配置风格,可以方便的定义多套界面风格UI
框架集成下面列出了 UI
库中包含的组件和接口:
使用 UI
组件需要引入类库:
<script sr='ht.js'></script>
<script sr='ht-ui.js'></script>
接下来我们看一个最简单的例子,创建一个按钮,显示在页面中:
// 创建按钮
var button = new ht.ui.Button();
button.setText('Hello World');
// 增加 click 事件监听
button.on('click', function(e) {
alert('Hello World');
});
// 将按钮加到页面中
button.addToDOM(window, {x: 10, y: 10, width: 100, height: 26});
UI
库中每个组件都拥有一个 DOM
树结构,包含交互 Div
、绘制 Canvas
等。典型组件的结构如下:
View(Div)
: 组件最根层的 DOM
节点,可通过 getView()
获取;这个 Div
上的 htComp
属性指向组件引用,如 button.getView().htComp
可以获取到按钮实例RootCanvas(Canvas)
: 组件所有的内容都是绘制到这个 Canvas
中,如边框、背景、文字、图标等ContentDiv(Div)
: 内容 Div
;如果组件是布局器,所有的子组件会添加到这个 Div
中ScrollBar(Div)
: 滚动条 Div
,包含横竖两个滚动条 Div
DisabledDiv(Div)
: 组件处于 disabled
的状态时的遮罩 Div
组件的每个 DOM
结构都有 ht
属性说明用途,在浏览器的 DOM
树面板(如 Chrome
浏览器的 Elements
面板)中可以清晰的看到组件的 DOM
结构,
如 button.getView()
的 ht
属性值为 ht-view ht.ui.Button
界面布局出现问题时,比如一个按钮不显示,我们需要确定按钮的 DOM
布局出现了问题还是绘制逻辑有问题,通过在浏览器查看 DOM
结构就可以确认是否是 DOM
布局问题
如果要通过 API
获取 ht
属性值,需要使用:button.getView().getAttribute('ht')
UI
库中 ht.ui.border
包下包含了边框接口 ht.ui.border.Border
和一系列的实现类;所有的组件都可以通过 setBorder
设置这些类型的边框;下面的例子我们通过文本框的例子查看各种边框的效果:
本质上,边框分为两类:Canvas
绘制的边框和 CSS
边框;接下来我们尝试自定义这两种类型的边框:
上面 demo
中前两个示例我们自定义了 Canvas
绘制的边框,思路是自定义类从 ht.ui.border.Border
继承(类继承请参考类继承请参考定义类文档),
重写 getLeft
、getRight
、getTop
、getBottom
四个函数指定边框宽度;重写 drawBorder
函数绘制边框;如果需要序列化,还需要重写 getSerializableProperties
函数;需要注意,绘制任意内容(包括自定义组件时的绘制)之前,一定要调用 g.beginPath()
开始一个新路径,否则会导致 HT
内部的路径被重新绘制
最后一个例子我们自定义了一个 CSS
类型的边框,与前两个例子不同的是,需要重写 isCSSBorder
函数返回 true
以通知组件边框类型;
并且重写 tearDownBorder
函数清空 CSS
样式
UI
库的 ht.ui.drawable
包中包含了可绘制接口 ht.ui.drawable.Drawable
和一系列的实现类;
一般来说,组件的背景、图标都支持设置 Drawable
对象(并且会有相应的简化 API
,如 Button
组件的 setIconDrawable
函数有对应的简化 API
: setIcon
),具体哪些属性支持 Drawable
可参考相关组件的 API
文档;
下面的例子我们通过组件背景查看通用 Drawable
的效果:
所有组件都有 background
属性用于控制显示的背景,并且这是个可绘制类型,意味着组件的背景可以是颜色、图片、九宫格,甚至是自己绘制的内容
上面 demo
中第一个例子演示了 ht.ui.drawable.ColorDrawable
的用法,通过参数我们可以实现圆角颜色背景
第二个例子演示了 ht.ui.drawable.ImageDrawable
的用法,这个绘制类型提供 fill
、uniform
、centerUniform
、center
四种 stretch
方式,例子中演示了四种 stretch
方式的绘制区别;stretch
默认为 centerUniform
第三个例子演示了 ht.ui.drawable.NinePatchImageDrawable
的用法,这个绘制类型可实现九宫格绘制;
这里的九宫格图片格式与 Android
中九宫格图片格式基本保持一致:上方像素点和左侧像素点相交区域作为拉伸区域;
与 Android
九宫格格式的区别是:HT
的九宫格不支持右侧和下侧的边距设置,作为代替,右上角的像素点数量表示屏幕像素精度
(非必需);在例子中,我们在右上角填充了两个像素点,表示两倍像素精度,绘制的时候,上部区域 44px
的高度被绘制在 22px
的区域内
为了方便设计九宫格图片,我们将 Android Studio
中设计工具提取出来,需要的开发者可以在我们官网直接下载:
draw9patch.jar;注意:这个工具依赖 Java
运行时环境
工具的用法网络上有很多资源,我们这里不再赘述,下面是使用 draw9path
工具编辑例子中九宫格的截图:
上面例子代码中我们设置 Drawable
是通过对象的方式(test.9.
是在 res.js
中注册的图片):
view2.setBackgroundDrawable(new ht.ui.drawable.NinePatchImageDrawable('test.9.'));
我们也可以使用简化 API
:(末尾去掉相应的 Drawable
):
view2.setBackground('test.9.');
test.9.
会被自动转换成 ht.ui.drawable.NinePatchImageDrawable
对象,创建过程中 test.9.
作为构造函数参数传入
也就是说,如果我们使用 Drawable
的简化 API
,会有转换规则将参数转换为相应的 Drawable
对象,自动转换规则如下:
#
或 rgb
开头的字符串自动转换成 ht.ui.drawable.ColorDrawable
.9.
的字符串自动转换成 ht.ui.drawable.NinePatchImageDrawable
['ht.ui.drawable.ImageDrawable', 'node_image', 'fill']
,按照数组第一个元素创建相应的 Drawable
对象,剩下的元素作为构造函数参数传入ht.ui.drawable.ImageDrawable
和边框接口类似,我们也可以实现 Drawable
接口自己绘制内容;下面的例子我们实现了渐变类型的绘制类型:
我们的思路是自定义类从 ht.ui.drawable.Drawable
继承(类继承请参考定义类文档),
重写 draw
函数绘制背景内容;如果需要序列化,还需要重写 getSerializableProperties
函数;需要注意,绘制任意内容(包括自定义组件时的绘制)之前,一定要调用 g.beginPath()
开始一个新路径,否则会导致 HT
内部的路径被重新绘制
目前产品中已经提供 ht.ui.drawable.GradientColorDrawable,因此此案例仅供学习使用
UI
库提供了一系列布局器(参考本文档第一个例子)用于对其它组件进行布局;HBoxLayout
、BorderLayout
、RelativeLayout
等通用布局器统一使用 addView(view, layoutParams, index)
添加子组件;Dialog
、TablePane
等专用容器则使用自己的特殊 API
添加组件,如 Dialog
使用 setContent(view)
设置内容区域的子组件
一个组件显示的最终宽度和高度由 width
和 height
属性控制,如果组件是通过 addToDOM
直接加到页面中的,那么我们可以手动调用 setWidth
和 setHeight
设置组件的尺寸;但是如果组件是加到布局器里的,那么布局器在布局时会调用子组件的 setWidth
和 setHeight
,我们手动设置的值就无效了,会被布局器设置的值覆盖掉;
这并不意味着我们不能控制布局器中的子组件的尺寸了,布局器增加子组件时通过第二个参数指定布局参数,允许我们设置子组件的布局宽高甚至对齐方式等:
borderLayout.addView(button, {
width: 100, // 这里的宽度是布局宽度,不同于按钮的 `width` 属性,最终存储在 button.getLayoutParams()
height: 'match_parent', // 表明高度要填满父容器
align: 'left' // 水平方向左对齐
});
所有组件的布局参数中的 width
和 height
基本都支持以下三种属性值:
'match_parent'
:表示最终显示的宽度或高度要填满父容器'wrap_content'
: 表示最终显示的宽度或高度使用组件自身的首选尺寸,这种模式下,调用 view.setPreferredSize(width, height)
可改变组件最终显示尺寸(如果不调用则自动计算 preferredSize
)每个组件,包括布局器(除了右键菜单)都有 preferredSize
,minSize
、maxSize
三个尺寸相关属性;preferredSize
是指组件的首选尺寸
,这个属性包含 width
和 height
两个子属性;组件的首选尺寸与是否显示、显示在哪里无关:比如按钮的首选尺寸是通过文字的尺寸加上图标的尺寸再加上边框宽度和内边距计算出来
minSize
是指组件的最小尺寸
maxSize
是指组件的最大尺寸
每个组件都有相应的计算函数用于计算这三个尺寸,如果开发者觉得组件自身的计算不精确或者某些情况下需要强制指定大小,
可以通过 setPreferredSize
、setMaxSize
、setMinSize
来进行强制指定以替换掉自动计算的结果;
当然,如果调用强制指定函数时只传入一个 undefined
参数,那么会重新启用自动计算,
如调用 setPreferredSize(undefined)
后,组件会重新自动计算 preferredSize
布局器在布局组件时需要考虑子组件的这三个属性,以 HBoxLayout
为例:
// addView 时指定 layoutParams 的 width 和 height 为 wrap_content,
// 布局器布局 button 时,使用 button 的 preferredSize
hBox.addView(button, {
width: 'wrap_content',
height: 'wrap_content'
});
// addView 时指定 layoutParams 的 width 和 height 为 match_parent,
// 布局器布局 button1 时,将按钮拉伸直至填满容器的剩余空间
// 设置最大尺寸和最小尺寸
button1.setMaxSize(500, 20);
button1.setMinSize(300, 20);
hBox.addView(button1, {
width: 'match_parent',
height: 'match_parent'
});
// addView 时指定 layoutParams 的 width 和 height 指定为固定像素,
// 布局器布局 button2 时,button2 尺寸固定为 100 * 20
hBox.addView(button2, {
width: 100,
height: 20
});
上面例子中,我们创建了一个 HBoxLayout
放到页面中,因为 addToDOM
没有任何参数,所以布局器会填满整个页面,并且浏览器窗口大小如果发生变化,这个布局器也会自动刷新
例子中的第一个按钮的 layoutParams
中的 width
和 height
都是 wrap_content
(可以不一致,比如 height
改为 match_parent
也是可以的),那么 button
最终的尺寸就是它的 preferredSize
第二个按钮的 layoutParams
中的 width
和 height
都是 match_parent
,意味着组件的最终尺寸会尽量填满布局器的剩余空间;如果布局器的尺寸发生变化,button1
的尺寸也会动态调整以尽量填满布局器剩余空间
但是需要注意,我们同时给组件设置了 minSize
和 maxSize
,button1
的尺寸虽然会随着布局器尺寸动态调整,但是会始终被限制这两个尺寸之间,而不会超过它们
最后一个按钮就比较简单了,我们在 layoutParams
参数中明确地指定了宽度值和高度值,因此 100 * 20
就是 button2
的最终尺寸
布局器其它常用 API
:
addView(child, layoutParams, index)
增加子组件removeView(child)
删除子组件removeViewAt(index)
根据下标删除子组件clear()
删除所有的子组件getChildren()
获取所有的子组件findViewById(id, recursive)
根据 id
查找子组件在开发完整系统时,经常需要将其它框架的内容嵌入到 UI
库的布局中,比如一段 HTML
内容,或者 HT
核心包中的 GraphView
组件;UI
库提供了下面两种包装类:
ht.ui.HtmlView
包装任意 HTML
内容,如 HTML
文本、DOM
对象ht.ui.HTView
包装 HT
核心包中的组件下面的例子我们使用 HtmlView
显示 iframe
和使用 HTView
显示核心包里的 3D
拓扑组件
接下来我们看一个 ECharts
的例子(需要访问网络请求 ECharts
类库):
所有组件都提供了两种事件监听接口:
addViewListener(listener, scope, ahead)
用于监听组件生命周期和交互事件等,如按钮的 click
事件;每个组件支持的事件类型不尽相同,请参考相应组件的 API
文档开头addPropertyChangeListener(listener, scope, ahead)
监听组件属性变化事件,如文本框的值变化下面的例子中,我们监听了文本框的属性变化事件,将文本框的值显示在 Label
中
// 用户输入过程中立即派发事件
textField.setInstant(true);
// 监听属性变化事件,更新 Label
textField.addPropertyChangeListener(function(e) {
if (e.property === 'value') {
label.setText('Value: ' + e.newValue);
}
});
第三种常用事件是 HT
没有直接封装的 HTML
的原生事件类型,如 mousedown
、mouseup
、dblclick
等,如果需要在组件上监听这些事件,可以使用下面的方式:
// 获取到组件的根层 Div,增加原生事件监听
button.getView().addEventListener('mousedown', function(e) {
});
自 3.0.6
开始,我们提供了更简单的事件监听接口:on
、onOnce
、off
,和旧的事件监听对比如下:
属性变化事件处理:
// 旧的监听接口
textField.addPropertyChangeListener(function(e) {
if (e.property === 'value') {
console.log(e);
}
});
// 新的监听接口,p 为 property 的缩写
textField.on('p:value', function(e) {
console.log(e);
});
// 旧的监听删除
textField.removePropertyChangeListener(handler);
// 新的监听删除
textField.off('p:value', handler);
// 对于自定义的 Attr 属性,可以用下面的方式进行监听(textField.setAttr('customProperty', 'value') 会触发 a: 事件):
textField.on('a:customProperty', function(e) {
console.log(e);
});
view
事件处理:
// 旧的监听接口
button.addViewListener(function(e) {
if (e.kind === 'click') {
console.log(e);
}
});
// 新的监听接口
button.on('click', function(e) {
console.log(e);
});
// 旧的监听删除
button.removeViewListener(handler);
// 新的监听删除
button.off('click', handler);
原生 DOM
事件处理:
// 旧的监听接口
button.getView().addEventListener('mousedown', function(e) {
console.log(e);
});
// 新的监听接口,d 为 DOM 的缩写
button.on('d:mousedown', function(e) {
console.log(e);
});
// 旧的监听删除
button.getView().removeEventListener('mousedown', handler);
// 新的监听删除
button.off('d:mousedown', handler);
一次性事件处理:
// 旧的监听接口
var handler = function(e) {
if (e.kind === 'click') {
console.log(e);
button.removeViewListener(handler);
}
};
button.addViewListener(handler);
// 新的监听接口
button.onOnce('click', function(e) {
console.log(e);
});
新的监听接口本质上是为原来三种事件监听接口提供了快捷方式,因此旧的事件监听接口仍然可用;但是需要注意,同一个事件监听器的注册和删除应该使用同一种方式,比如下面的例子:
var handler = function(e) {
if (e.kind === 'click') {
}
}
// 旧接口注册监听器
button.addViewListener(handler);
// 新接口删除监听器
button.off('click', handler);
上面这种方式是错误的,同一个事件监听器不能混用新旧接口,正确方式如下:
都使用旧接口
var handler = function(e) {
if (e.kind === 'click') {
}
}
// 旧接口注册监听器
button.addViewListener(handler);
// 旧接口删除监听器
button.removeViewListener(handler);
或者都使用新接口
var handler = function(e) {
console.log(e);
}
// 旧接口注册监听器
button.on('click', handler);
// 旧接口删除监听器
button.off('click', handler);
有的开发者比较喜欢使用缩写的组件名,如:
// btn 前缀即为 button 的缩写
var btnSubmit = new ht.ui.Button();
如果您习惯这种方式,可以按照下面的规范使用缩写:
btn Button
mnb MenuButton
tgl ToggleButton
chk CheckBox
rad RadioButton
rds Radios
lbl Label
cmb ComboBox
sld Slider
prg ProgressBar
msg Message
txt TextField
num NumberInput
txa TextArea
dtp DateTimePicker
clp ColorPicker
lsv ListView
trv TreeView
tbv TableView
ttv TreeTableView
ttp TreeTablePane
tbp TablePane
prv PropertyView
prp PropertyPane
htv HTView
hmv HtmlView
grd Grid
mnu Menu
ctm ContextMenu
vg ViewGroup
hbx HBoxLayout
vbx VBoxLayout
tab TabLayout
brd BorderLayout
spl splitlayout
tbl TableLayout
tbr TableRow
flw FlowLayout
pop Popover
rlt RelativeLayout
pnl Panel
dlg Dialog
alr Alert
如果碰到没有列出的组件,您应该使用团队内部规范