UI开发的新时代----认识迅雷界面引擎
第一部分:交互开发技术概述
软件产品的交互开发一直以来都不是一件令人愉悦的事情。首先,由于每个人编写的第一个图形应用程序就已经使用了一些交互开发技术,而且由于IDE工具的强大,容易总结出交互开发就是“拖拖控件,改改属性,写写响应”的经验,所以很容易被认为是没什么技术含量的工作。但实际上,这是一个特别不容易的工作:因为作为软件产品的脸面,上至公司老板,下至任意一个普通用户,大家都可以对你的工作成果拼头论足,并提出修改意见,而这些修改意见反应到产品的方案修改上,总是会让修改成本与项目的修改复杂度不呈线性关系。很多刚刚从事这一行的项目经理总是不能理解,为什么按一个方案修改交互,需要1天,而另一个看起来似乎差不多的方案,却要改上一个月?
从另一个角度也可以发现这里的技术门槛不易:其它领域的设计模式,经验总结文章已经汗牛充栋,各种各样的新思路和在这些思路上建立起来的各种开源库,每过几年基本就要洗上一轮,但在交互开发上,除了各个厂商提供的平台开发方法,几乎没有被公认和被广泛使用的界面开发库,更不要提相关方法论和模式的升华,总结与创新了。
但是,软件产品的交互体验,特别是互联网产品的交互体验,如今越来越多的受到人们的重视。从过去能实现功能完成交互,到后来提供一致的操作习惯,流畅的实现整个交互流程,如今还渴望交互体验能基于用户的使用心理设计,更加美观,并提高产品的整体品味。如今全球市值最高的Apple公司,正是靠着其ipod,iphone,ipad系列产品的优秀交互体验和近乎完美的工业设计,征服了广大用户。
迅雷作为中国非常流行的一款Windows平台下的客户端产品,对于改进产品的交互体验有着强烈的愿望。迅雷5.8完成了工具软件的关键功能提升,随后的版本进化,希望能进一步改进产品的交互体验: 更美观也更现代。迅雷5.9(迅雷6)是基于传统Windows界面开发技术改进交互体验的结果,这个结果虽然还不错,但带来了另一个问题:开发成本的提高。迅雷作为一款由多个部门合作开发的(包括合作开发交互)客户端产品,传统的开发技术不能很好的组合各个部门开发的模块,开发成本与稳定性都有问题。公司迫切的需要一个能解决这些问题的下一代界面引擎,并在多个方面使用各种方式开始了勇敢的尝试。
第二部分 过去我们如何开发按钮
现在我将通过一个经典的windows下”自画按钮”的工程实例,和大家一起观察UI开发的过程。”自画按钮”是一个非常常见的产品需求,相信不少读者都有过相似的经历(下列故事纯属虚构,如有雷同,真是巧合).
那年正是Windows XP最流行的时候,我们的产品也跟进时代的发展,将界面风格升级到了XP Style,同时我们的产品还兼容Windows2000,设计和产品都希望我们的软件在两个系统下都能有一致的外观表现。于是,我就需要开发一个能在两个平台下都长得一样的按钮。这个活并不困难,我们先看看需求:
看了需求之后,作为一个合格的Windows开发工程师,很快就完成了。实现代码大致如下:
CButton::OnPaint() {
DrawBkg(m_state,0,0,width,height);
DrawText(m_state,m_btnText,width/2-textWidth/2,height/2-textHeight/2);
if (isFocus)
{
DrawFocusRect(2,2,width-2,height-2);
}
}
CButton::OnLButtonDown(){
ChangeState(BTN_STATE_DOWN);
}
CButton::OnLButtonUp(){
ChangeState(BTN_STATE_NORMAL);
FireEventOnClick();
}
分析实现代码,大家可以看到基本思路是这样的
1. 确定按钮有几个状态,然后根据这些状态下按钮的外观,确定如何实现OnPaint函数。
2. 处理WM_LBUTTONDOWN,WM_MOUSEMOVE等输入消息,在这些消息里改按钮的状态
3. 在WM_LBUTTONUP,WM_KEYUP等消息里,Fire一个按钮自己定义的OnClick事件
很好,我们的软件用上了新按钮,问题解决了,产品经理和设计师都很满意。咱也结束了一天的工作,安心回家睡个好觉。
又过了几天,公司的产品总监在产品会上提到,我们要统一所有产品的交互逻辑,特别是某些按钮,根据现在的交互规范,应该加上醒目的图标。会后设计师打开PHOTOSHOP,很快就把按新规范调整的按钮发给了我。如下图
这次我没有立刻开始实现,因为公司的另一产品也用到了我实现的这个按钮,我要保证原有的按钮能继续正常工作,我打算和那边的研发商量一下怎么改。这里我有两个方案。
方案一:
1.定义 CIconButton,继承CButton
2.CIconButton添加方法SetIcon
3.在CIconButton::OnPaint里添加如下代码
OnPaint() {
DrawBkg(m_state);
DrawBkg(m_state);
DrawIcon(m_state);
DrawText(m_state);
DrawText(m_state);
}
4.使用新的CIconButton完成需求
方案二、
1.在CButton里添加两个扩展点回调
OnPaint() {
if(OnDrawBkgCallback())
if(OnDrawBkgCallback())
DrawBkg(m_state);
if(OnDrawItemCallback())
DrawText(m_state);
}
并提供设置回调的接口
SetOnDrawBkgCallback()
SetOnDrawItemCallback()
2.产品通过SetOnDrawItemCallBack完成需求
void OnBtnDrawItemCallback()
{
DrawIcon();
return true;
}
m_button.SetOnDrawItemCallback(OnBtnDrawItemCallback);
在方案的选择上,大家有了一些争议:方案一看起来比较直接,而且实现起来也比较快,最重要的是使用我的控件库的产品开发工程师特别希望这么改;而方案二实现需要更多的代码,并且产品开发工程师需要学习和编写更多的代码才能完成这个需求,不过这个方案的未来我更看好。最后我们选定了一个方案(在这里选哪一个都能完成需求),加班写好代码,提交测试,发布新的界面控件库,制作新的安装包,一阵忙碌,这个小小的需求升级总算是结束了。
又过了几天,公司大老板通过邮件告诉大家,公司产品的大客户比较喜欢Windows Rabbion风格。于是产品经理再次调整设计,要求一些按钮要改成如下样式:
很明显,这次图标跑到按钮文本上面去了,这就是最新的时代潮流。虽然很不情愿,但这不就是生活么?隔壁卡位的设计师,半个小时搞定了所有需要调整的地方的效果图,按时下班回家了,而我们工程师还必须加班! 这次的工作量就和上次修改的方案有关了:
如果我们上一步使用了方案一,这次我们日子没那么好过了。我们需要创建一个新的CIconButton2继承自CIconButton,再加一个新接口SetIcon2,可以在设置图标的同时调整图标在文本的什么位置(这次还耍了个小聪明,这个接口可以支持图标在文本的上下左右4个方位) 。
而使用方案二的优越性则在这里得到了体现:作为按钮的开发者,并不需要修改一行代码,你只要继续指导一下需要实现这个需求的产品开发工程,调整一下OnBtnDrawItemCallback的实现即可。
实际上,在迅雷,我们有过一次在方案一这条路上走到头的经历,迅雷的代码里有一个叫 CXLButton的怪物,提供了近200个接口,有快2万行代码。当编写这个类的哥们走人以后,接手这个Class的新人发现这个按钮的用途(在最后一个版本的需求里)只是用来实现一个图标在文字上面的工具栏按钮,”这垃圾代码是谁写的!”,他逢人就要这么嘲笑一下,然后花了一个下午的时间写了一个新类 CXLButton2,清晰明了的实现了这个需求。但我们都知道,这是另一个轮回的开始。而且,有资格使用方案一还意味着控件开发工程师愿意帮助产品开发工程师修改接口与实现,如果这两组工程师不在一个公司,更大的成本会花在人与人的沟通上甚至无法进行。
Microsoft提供的标准控件,通过类似方案2的方法提供可扩展性。方案二在过去,都被普遍认为是一个出色的解决可复用性问题的模式(我很少在招聘的时候能看到有人能在开发控件的时候通过这种方式提供可扩展性),但其缺点之一就是让控件的使用有点复杂,学习成本很高。比如我至今都没有通过MSDN 完全搞清楚过微软TreeView提供的这种扩展事件的细节,还好有一份泄漏的Windows源代码,让我对这些回调之间的关系能有清楚的认识。而且这个方案会让产品开发工程师学习很多高度依赖控件库内部实现的知识,如果产品更换的底层界面库,那么大量的这种经验就没有意义了。
而且,方案二真的能以不变应万变,支持所有的需求变化么?大家可以自行思考。
通过这个可扩展的按钮控件的开发实例,我们总结了三个交互开发的经验:
一、 交互是最易改变的需求
二、 开发一个可复用的交互控件的主要工作有
1)根据最初的需求决定方法,事件
2)根据最初的需求决定如何实现OnPaint,如何将原始的OS输入事件转成逻辑事件
3)在OnPaint里织入扩展点。
1)根据最初的需求决定方法,事件
2)根据最初的需求决定如何实现OnPaint,如何将原始的OS输入事件转成逻辑事件
3)在OnPaint里织入扩展点。
三、交互开发涉及的技术点很多,实现时注意细节,注规避常见问题,当绘制特别复杂的时候还要仔细优化性能。
第三部分 新的思路
从头观察上面的例子,我发现: 不管怎么修改交互需求,隔壁卡位的设计师总是很少加班,他们的工作量似乎能很好的和交互需求的修改量成正比,而且没有什么局限。相反,我们的工作总是存在一些天花板,一旦需求改变越过了开发时预设的一些前提条件,需求修改的响应时间会大幅增加。
不管是通过那种方法,有限的接口和有限的扩展点,理论上满足不了近乎无限的需求变化。而我们观察所谓的通过事件扩展控件的方案,其核心是希望OnPaint能够根据各种参数,调整工作流程。而OnPaint里实现的功能,说到底就是 “在指定位置使用适当的函数绘制文本或贴图”。要想让一段代码的流程具有很强的可变性,是非常困难的。从某种意义上说,OnPaint函数就是万恶之源。
明白了问题的根源,那么如何解决问题就有了方向:我们要在交互开发的过程中干掉OnPaint。实际上在这之前已经有技术实现了这一点:HTML本身并没有提供绘制函数,而实际上,前端交互开发是目前所有交互开发技术里流程分工合理,并且需求修改响应速度保持线形增长,容易学习,有完善工具链条的技术。我们可以在传统app开发里使用HTML技术么? 很多人回答YES,并做出了开创性的尝试。不过经过了一些思考之后我们并没有选择HTML(原因我们以后会在另外的文章里详细的介绍)。经过了一些借鉴和总结,我围绕这个问题提出了一个新的概念:可以通过定义原子UI对象(UIObject)之间的父子关系和位置关系组成对象树(UIObjTree)来描述界面,UIObject的类型是相当有限的。而按逻辑构建的UIObjTree的实际上组织了OnPaint里“按什么样的顺序在什么地方画什么”的问题。
这个概念有点抽象?还是你已经完全明白了?都没关系,还是刚刚例子里的按钮,按这种方法分析以后,我们会得到一个怎样的UIObjTree呢?
非常直观的对象树,只有两个UIObject。接下来的需求修改就变成了修改这个数据结构。而树结构的修改接口提供起来是非常简单缺完备的:添加节点,删除节点,查找节点,修改节点属性。我们看看在这个结构上如何响应上面的需求
你会发现抽象UI得到的UIOjbTree会与设计师提供的PSD图结构非常相似,这就非常符合真正的开发情况:开发得到的需求并不直接来自产品经理,而是来自设计师。如果技术能够将设计师的成果直接转化成App可以使用的数据结构,那么这样的技术无疑在实现能力和开发效率上,能与设计师相同。(这个理论迅雷通过迅雷7 ,XMP多个产品的大规模实践,已经得到了有效的证明)所以BOLT界面引擎的创新,根本上是提出了一种新的抽象交互的思路。
这里建议您可以考虑将一些常见的控件,或则您现在正在开发的软件的交互解构成UIObjTree,试一试吧。
围绕这个概念来构建整个界面引擎,我们还获得了一系列进一步的好处:
基于同样的模式分析交互,能输出近乎一致的结果。
可以将界面布局从逻辑代码中独立出来。
对界面的抽象是平台无关的。(虽然我们没有把跨平台作为BOLT界面引擎的关键目标)
让对象的属性在一段时间里按规律变化,就能实现动画。开发各种特效也有了一致的思路
基于对象树枝叶嫁接的复用和界面模块划分,易于理解。并提供了统一的可访问性 (所谓的可访问性,是指你可以在控件的开发者不提供实现的基础之上,只通过UIObjTree的树操作借口,就能访问并修改其表现)
交互开发技术的变迁
整理上面的思路,我们得到交互开发技术,或则就是界面库的分代标准:
第一代 :SDK开发,使用系统默认控件
第二代 :基于窗口子类化的自绘控件皮肤库。绘制通常基于GDI。控件类型和系统一致。
第三代 :提供一套完整的体系(窗口,绘制等),所有的控件都基于这个体系开发。本身提供很多功能更强的控件,并有统一的方法开发/使用新控件。这一代库主要是解决实现能力问题。由于Windows的系统限制,二代库有很多功能局限。(比如按钮发光这个需求需要按钮的实现代码可以绘制到按钮子窗口以外的)
第四代 :布局文件+脚本语言的开发模式,依靠”组合”抽象界面并围绕这个概念搭建 。不提供控件但提供易学易用的控件开发模式。不提供内置特效但能让使用者轻易开发自己的界面特效。
这个分代标准基本上是参考windows上的界面库发展而制定的,第二代和第三代库本身其实没有什么本质上的区别,主要是Windows由于兼容原因,提供的UI相关API和窗口混和器过于古典(子窗口与父窗口之间只有遮盖关系,没有办法混合,很多GDI函数都无视Alpha通道),使得开发一套完整的界面库反而还需要自己实现窗口管理和图形库这些有一定技术门槛的基础设施。新的OS本身提供的UI框架本身就解决了这些问题,所以这些新的OS上的库就直接是3代库了。
需要指出的是,有一些技术框架也提供基于组合的方法构建界面的方法.比如现在相当流行的iOS上的cocoa框架,很多时候可以只通过组合各种View的方法来创建新的控件。但这个框架我依旧认为他是三代库,因为框架还是依旧以绘制为核心构建,比如你可以组合多个不同属性的按钮构成一个新控件,但你无法获得按钮上文字的SubView,没有获得统一的可访问性。
第四部分 Bolt界面引擎概念介绍
关键概念
• 使用XML文件来定义UIObjTree,用Lua脚本来实现界面逻辑
• UIObject的类型包括ImageObject,TextObject,还有一些精心设计的原子类型 《引擎内置的元对象介绍》
• 围绕核心概念建立了一系列辅助设施
基本工作原理
BOLT界面引擎这里提供了两个重要的核心概念HostWnd ,Render。Render可以把一颗构建完成的UIObjTree渲染成一张位图。而HostWnd是界面引擎核心与操作系统之间的桥梁,能把这张位图通过系统提供的API画到屏幕上,并能转化操作系统的键盘/鼠标等事件成为引擎的定义得标准输入事件(Action)。
这张图里还提到了我们的UI资源管理模块和XML布局文件读取与管理模块。
Render的基本工作原理
DirtyRect(脏矩形)是驱动Render工作的核心。关于脏矩形,在很多2D游戏开发的文章里都有详细介绍。当一颗UIObjTree上的产生了脏矩形,其对应的Render在下次渲染时就会开始有动作,否则就什么也不干。Render会选取与脏矩形相交的所有UIObject,然后按这些UIObject的z-order排序(从小到大)排序 ,再按这个顺序依次调用这些UIObject的Draw方法。由于所有的UIObject都是由引擎实现的,所以这个Draw方法也是一个不可见的内部函数,Draw的实现会调用一些我们精心优化过的图形图像绘制函数,这一切就构成了Bolt界面引擎的高速渲染引擎。
注:虚线表示的矩形是脏矩形,那么Render只会渲染与这个区域相交的UIObject。
BOLT界面引擎里控件的概念
基于UIObject和UIObjTree,开发控件就是设计一个可复用的“对象树片断”,而使用控件就是由界面引擎完成这个对象树片断如何嫁接到对象树上。
这里通过一个简单的例子演示一下这个过程
这样的一个MessageBox。抽象成UIObjTree
我们可以定义Button的对象树片断是:
合并以后
而且很明显,引入控件的概念,能把一颗复杂的UIObjTree分解成多个片断交给团队开发,而最后合成的UIObjTree依旧保持了各个节点的可访问性。
第五部分 未来展望
Bolt界面引擎的核心概念,是完全创新与独立的,并不依赖任何操作系统。所以BOLT界面引擎的发展方向之一是把界面引擎移植到各种各样的平台,目前最成熟稳定的平台是Windows,有工业级产品的质量。我们在Andiord平台和MacOS平台都有初步移植的版本,但我们还需要花费很多精力在合适的机会完成让这些平台的界面引擎更完美
我们在移植到Andiord平台时发现,界面引擎要想在这些手持设备上流程运行,原有的基于CPU指令集优化的高速渲染器是不好的。一是CPU性能达不到,不够流畅,二是太耗电,这里迫切需要使用设备提供的硬件加速功能。得益于界面引擎不希望用户编写绘制代码的核心概念,我们只需要重新调整高速渲染器就可以实现硬件加速。目前我们正在尝试各种各样的支持硬件加速的框架方案:既能高效使用硬件的能力 ,又能兼顾CPU算法的灵活性。
我们目前的主要精力都放在引擎本身的完善上,但实际上,如同HTML与Dreamweaver一样,界面引擎在设计之初就可以让布局部分由使用专业工具的专业人员完成,而不是负责编写逻辑代码的工程师。我们对我们的布局XML格式的简单优雅和可扩展性都有充分的信心,我们衷心的希望各位同行在理解认可了我们的概念后支持我们的标准,这样大家就能开发各种辅助工具,互相通用,共同改进BOLT界面引擎的工具链支持。