索引
ECMAScript 6
之前的语法结构没有提供定义类的方式,为了实现面向对象编程,HT
核心包提供 def
函数用于定义类,
类基础这个文档里介绍了如何定义简单类、定义包,我们就不再赘述,本文着重介绍如何定义 UI
组件和容器
HT
提供了很多 Mixin
简化类定义,常用的 Mixin
列举如下:
ms_fire
:值为 Boolean
类型,为类增加属性变化事件支持,在类上增加 addPropertyChangeListener(缩写:mp)
、removePropertyChangeListener(缩写:ump)
、firePropertyChange(缩写:fp)
函数ms_ac
:值为数组格式,如 ms_ac: ['name', 'age']
,为类自动生成 getName/setName
,getAge/setAge
函数;这个 Mixin
依赖 ms_fire
;如果需要为组件定义样式属性,请使用 ui_ac
ui_ac
:值为数组格式,如 ui_ac: ['color']
,为类自动生成 getColor/setColor
函数;和 ms_ac
不同的是,这个 Mixin
一般只用于定义组件样式属性;此 Mixin
依赖 ms_fire
上面提到 ui_ac
和 ms_ac
都依赖 ms_fire
,从 ht.ui.border.Border
或 ht.ui.drawable.Drawable
或 ht.ui.View
继承过程中,因为父类已经添加了 ms_fire
这个 Mixin
,所以我们的子类直接使用 ms_ac
或 ui_ac
即可
下面我们使用 Mixin
简化类定义:
// 定义包
com = {};
com.hightopo = {};
// define Person class constructor
com.hightopo.Person = function(firstName, lastName) {
// 调用父类构造函数
com.hightopo.Person.superClass.constructor.call(this);
this.setFirstName(firstName);
this.setLastName(lastName);
};
ht.Default.def('com.hightopo.Person', Object, {
ms_fire: true,
ms_ac: ['firstName', 'lastName']
});
可以看到,使用 ms_fire
和 ms_ac
以后整个类的代码简化了很多;如果希望自己实现 getter/setter
函数而不是使用 ms_ac
自动生成,可以用下面的方式:
getFirstName: function() {
return this._firstName;
},
setFirstName: function(firstName) {
var oldValue = this._oldValue;
this._firstName = firstName;
// 派发属性变化事件
this.firePropertyChange('firstName', oldValue, firstName);
}
如果希望用自己的 getter/setter
代替 ui_ac
,请使用下面的方式:
getBackground: function() {
return this.getPropertyValue('background');
},
setBackground: function(background) {
this.setPropertyValue('background', background);
// 其它逻辑代码
}
注意代码中我们调用父类构造的方式:
// 调用父类构造函数
com.hightopo.Person.superClass.constructor.call(this);
如果我们的子类重写了父类函数,也要通过这种方式调用父类函数:
com.hightopo.Student = function(firstName, lastName) {
// 调用父类构造函数并传递参数
com.hightopo.Student.superClass.constructor.call(this, firstName, lastName);
};
ht.Default.def('com.hightopo.Student', com.hightopo.Person, {
/**
* 重写父类函数,先通过调用父类函数获取 firstName,再拼接字符串返回
**/
getFirstName: function() {
var firstName = com.hightopo.Student.superClass.getFirstName.call(this);
return 'student:' + firstName;
}
});
使用 ms_ac
和 ui_ac
定义类属性以后,经常需要设置属性默认值,对于 ms_ac
声明的变量,使用 _
加变量名就可以设置默认值,例如:
ht.Default.def('com.hightopo.Person', Object, {
ms_fire: true,
ms_ac: ['firstName', 'lastName'],
_firstName: 'Li' // 此类所有的对象的 firstName 属性默认值为 `Li`
});
ui_ac
用于生成样式属性,样式属性有多种设置方式,下面是不同设置方式的优先级:
button.setTextColor('red')
__
(双下划线)加变量名为 ui_ac
生成的样式属性指定默认值我们已经知道了如何定义类,接下来学习如何自定义组件;下面的例子中,我们自定义一个组件,并绘制图形:
// 构造函数
ht.ui.CustomView = function () {
ht.ui.CustomView.superClass.constructor.call(this);
};
ht.Default.def(ht.ui.CustomView, ht.ui.View, {
// 自动生成 getShape 和 setShape
ui_ac: ['shape', 'shapeWidth', 'shapeHeight'],
// shape 属性默认值,注意样式属性的默认值使用双下划线开头
__shape: 'circle',
__shapeWidth: 100,
__shapeHeight: 100,
/**
* 在入门手册中我们介绍过,组件都有首选尺寸,一般与内容有关
* 这里重写父类函数,根据 shapeWidth、shapeHeight 再加上 padding 和 border 作为组件的首选大小
* */
figurePreferredSize: function () {
var self = this,
shapeWidth = self.getShapeWidth(),
shapeHeight = self.getShapeHeight(),
size = {
width: self.getPaddingLeft() + self.getPaddingRight() +
self.getBorderLeft() + self.getBorderRight(),
height: self.getPaddingTop() + self.getPaddingBottom() +
self.getBorderTop() + self.getBorderBottom()
};
size.width += shapeWidth;
size.height += shapeHeight;
return size;
},
/**
* 此组件的首选大小与 shapeWidth、shapeHeight 有关,如果这两个属性如果发生变化,就需要重新计算首选大小
* 所以需要重写父类 getPreferredSizeProperties 函数,将 shapeWidth 和 shapeHeight 放到影响首选大小的属性 map 中并返回
* */
getPreferredSizeProperties: function() {
// 获取父类中会影响 preferredSize 的属性 map
var preferredSizeProperties = ht.ui.CustomView.superClass.getPreferredSizeProperties.call(this);
// 因为 map 是引用类型,为了避免影响原始 map,我们需要将其克隆
preferredSizeProperties = ht.Default.clone(preferredSizeProperties);
// 将 shapeWidth 和 shapeHeight 放到 map 中
preferredSizeProperties.shapeWidth = true;
preferredSizeProperties.shapeHeight = true;
// 返回 map
return preferredSizeProperties;
},
// 重写父类 validateImpl 函数,绘制形状
validateImpl: function (x, y, width, height) {
var self = this;
ht.ui.CustomView.superClass.validateImpl.call(self, x, y, width, height);
var g = self.getRootContext(),
shape = self.getShape();
// 正如入门手册中介绍的,所有绘制的开头都要加上 g.beginPath(),防止内部路径被重新绘制
g.beginPath();
g.fillStyle = 'yellow';
if (shape === 'rect') {
g.rect(x, y, width, height);
}
else {
g.arc(x + width / 2, y + height / 2, Math.min(width, height) / 2, 0, Math.PI * 2);
}
g.fill();
}
});
在这里例子里,我们重写了 figurePreferredSize
来计算首选大小,创建这个组件的时候,就可以根据它的首选大小放到页面中:
customView = new ht.ui.CustomView();
customView.setBorder(new ht.ui.border.LineBorder(1, '#0092d4'));
customView.setPadding(20);
customView.setShape('rect');
// 获取首选大小
var preferredSize = customView.getPreferredSize();
// 将首选大小中的宽度和高度作为组件最终显示的宽度和高度
customView.addToDOM(window, { x: 10, y: 10, width: preferredSize.width, height: preferredSize.height });
绝大多数组件需要与用户做互动,比如按钮组件,鼠标滑过时改变背景色,我们继续改造上面的例子,增加交互器:
// 定义交互器
ht.ui.CustomViewInteractor = function (comp) {
ht.ui.CustomViewInteractor.superClass.constructor.call(this, comp);
}
ht.Default.def(ht.ui.CustomViewInteractor, ht.ui.Interactor, {
// 处理鼠标按下事件
handle_mousedown: function (e) {
this.handle_touchstart(e);
},
// 处理 touch 事件
handle_touchstart: function (e) {
var self = this;
if (ht.Default.isLeftButton(e)) {
// 获取组件对象
var customView = self.getComponent();
// 改组件的 shape 属性
if (customView.getShape() === 'circle') {
customView.setShape('rect');
}
else {
customView.setShape('circle');
}
}
}
});
我们的交互器从 ht.ui.Interactor
继承,并且实现了 handle_mousedown
和 handle_touchstart
两个事件回调函数处理鼠标和 touch
事件,
在事件处理中更改组件的 shape
属性。
重写
setUpInteractors
的方式仍然可用,但是我们更推荐下面的getInteractorClasses
为了将交互器关联到组件上,需要重写组件的 getInteractorClasses
函数:
getInteractorClasses: function () {
return [ht.ui.CustomViewInteractor];
}
UI
库会遍历这个函数返回的数组,依次创建相应的交互器实例,交互器会遍历自身的 handle_xxx
函数,如果发现 xxx
匹配某种事件类型,就会在组件的 InteractionDiv
上增加相应的事件监听;交互器支持的事件回调函数如下:
handle_keydown
keydown
事件处理函数handle_keyup
keyup
事件处理函数handle_keypress
keypress
事件处理函数handle_input
input
事件处理函数handle_mousedown
mousedown
事件处理函数handle_mousemove
mousemove
事件处理函数handle_mouseup
mouseup
事件处理函数handle_mouseover
mouseover
事件处理函数 handle_mouseout
mouseout
事件处理函数 handle_mousescroll
mousescroll
事件处理函数 handle_contextmenu
contextmenu
事件处理函数 handle_mouseenter
mouseenter
事件处理函数 handle_mouseleave
mouseleave
事件处理函数 handle_touchstart
touchstart
事件处理函数 handle_touchmove
touchmove
事件处理函数 handle_touchend
touchend
事件处理函数 最后运行效果如下(点击切换绘制的图形):
有些情况下,比如拖拽,即使鼠标移出了组件范围,我们还是希望能够继续处理事件,所以交互器提供了下面的回调函数:
handleWindowMouseMove
window
上的 mousemove
事件处理函数handleWindowTouchMove
window
上的 touchmove
事件处理函数handleWindowMouseUp
window
上的 mouseup
事件处理函数handleWindowTouchEnd
window
上的 touchend
事件处理函数这四种事件回调函数与上面提到的 handle_xxx
事件处理函数不同,它们并不会一开始就在 window
上加监听,而是需要开发者手动加监听,参考下面的例子(拖拽移动矩形):
ht.ui.CustomViewInteractor = function (comp) {
ht.ui.CustomViewInteractor.superClass.constructor.call(this, comp);
}
ht.Default.def(ht.ui.CustomViewInteractor, ht.ui.Interactor, {
handle_mousedown: function (e) {
this.handle_touchstart(e);
},
handle_touchstart: function (e) {
var self = this;
if (ht.Default.isLeftButton(e)) {
ht.Default.preventDefault(e);
var customView = self.getComponent(),
point = customView.getViewPoint(e);
customView.setCenter(point);
// 在 window 上注册监听函数
ht.Default.startDragging(self);
}
},
handleWindowMouseMove: function (e) {
this.handleWindowTouchMove(e);
},
handleWindowTouchMove: function (e) {
ht.Default.preventDefault(e);
var self = this,
customView = self.getComponent(),
point = customView.getViewPoint(e);
customView.setCenter(point);
},
handleWindowMouseUp: function (e) {
},
handleWindowTouchEnd: function (e) {
}
});
在这个交互器的 handle_touchstart
中调用了 ht.Default.startDragging(self)
,这个函数内部会遍历交互器的 handleWindowXXX
函数,如果发现 XXX
匹配某种事件类型,就会在 window
(浏览器窗口)上增加相应的事件监听;鼠标松开后 window
上的监听器会自动被移除
另外一个细节是 point = customView.getViewPoint(e)
;point
格式为 { x: x, y: y }
,表示鼠标相对组件左上角的坐标;我们把这个坐标设置到组件的 center
属性上,让组件内的矩形以此为中心绘制
浏览器的拖拽操作会有一些默认行为,比如选中文本,改变鼠标样式等给用户造成困惑,所以我们调用了 ht.Default.preventDefault(e)
阻止这些浏览器默认行为
ht.ui.ViewGroup
是 UI
库中所有布局器的基类,开发者要自定义布局器,也需要从此类继承;下面的例子中,我们尝试定义一个简化版的 HBoxLayout
(将子组件从左侧往右排列):
// 构造函数
var SimpleHBoxLayout = function () {
SimpleHBoxLayout.superClass.constructor.call(this);
}
ht.Default.def(SimpleHBoxLayout, ht.ui.ViewGroup, {
// 计算布局器的首选大小,遍历所有的子组件并取宽度之和和高度的最大值作为首选尺寸
figurePreferredSize: function () {
var self = this,
children = self.getVisibleChildren(),
size = {
width: self.getPaddingLeft() + self.getPaddingRight() +
self.getBorderLeft() + self.getBorderRight(),
height: self.getPaddingTop() + self.getPaddingBottom() +
self.getBorderTop() + self.getBorderBottom()
};
for (var i = 0, size = children.size(); i < size; i++) {
var child = children.get(i),
childPreferredSize = child.getPreferredSize(),
layoutParams = self.getChildLayoutParams(child) || {},
marginLeft = layoutParams.marginLeft || 0,
marginRight = layoutParams.marginRight || 0,
marginTop = layoutParams.marginTop || 0,
marginBottom = layoutParams.marginBottom || 0;
size.width += childPreferredSize.width + marginLeft + marginRight;
size.height = Math.max(size.height, marginTop + marginBottom + childPreferredSize.height);
}
return size;
},
// 对子组件进行布局
validateImpl: function (x, y, width, height) {
var self = this;
SimpleHBoxLayout.superClass.validateImpl.call(self, x, y, width, height);
var children = self.getVisibleChildren(),
size = children.size(),
layoutX = 0,
scrollWidth = 0,
scrollHeight = 0;
for (var i = 0; i < size; i++) {
var child = children.get(i),
preferredSize = child.getPreferredSize(),
layoutParams = self.getChildLayoutParams(child) || {},
marginLeft = layoutParams.marginLeft || 0,
marginRight = layoutParams.marginRight || 0,
marginTop = layoutParams.marginTop || 0,
marginBottom = layoutParams.marginBottom || 0;
// 布局子组件:设置子组件的位置、尺寸并刷新子组件
self.layoutChild(child, layoutX + marginLeft, marginTop, preferredSize.width, preferredSize.height);
layoutX += marginLeft + preferredSize.width + marginRight;
}
// 调用 updateScrollBar 更新滚动条(layoutX 为滚动宽度)
self.updateScrollBar(width, height, layoutX, scrollHeight);
}
});
ht.ui.UIJSONSerializer
类用于对组件进行序列化和反序列化,如果是自定义的组件,需要实现序列化接口;上面自定义的 ht.ui.CustomView
组件增加 getSerializableProperties
函数即可支持序列化:
getSerializableProperties: function() {
// 先获取父类可序列化属性
var parentProperties = ht.ui.CustomView.superClass.getSerializableProperties.call(this);
// 在父类可序列化属性的基础上,新增 shape、shapeWidth、shapeHeight 三个属性
return ht.Default.addMethod(parentProperties, {
shape: true,
shapeWidth: true,
shapeHeight: true
});
}
对于某些复杂类型(比如日期对象)不能直接序列化和反序列化,需要重写 serializeProperty
和 deserializeProperty
:
serializeProperty: function (value, name, serializer) {// 参数:属性值、属性名、当前执行序列化的 JSONUISerializer 对象
var self = this;
if (value instanceof Date) {
return value.getTime();
}
else {
// 其它属性继续由序列化器处理
return serializer.serializeProperty(value, name, self);
}
},
deserializeProperty: function (value, name, serializer) {
var self = this;
if (name === 'date') { // 日期属性,根据业务确定
return new Date(value);
}
else {
// 其它属性继续由序列化器处理
return serializer.deserializeProperty(value, name, self);
}
}
日期对象不能直接使用 JSON.stringify
和 JSON.parse
处理,所以我们重写了上面两个函数,转换成数字处理