Java界面设计应该是一项充满创造性、富有乐趣的工作,但是却往往被认为非常的枯燥和繁琐。究其原因,是因为界面布局领域所采用的描述概念和具体的实现语言之间存在很大的语义隔阂。而一般的界面开发工具提供的所见即所得以及界面布局管理器等方案也无法很好地解决这个问题。
在本文中,我们会给出一种更好的解决方案,我们不是去试图把界面设计者头脑中的设计概念和样式逐步降级、分解成所使用的实现语言能够理解的低层概念,也不是提供一些已经完成的、确定的但难以扩充和更改的布局样式库供界面设计者使用。我们所提供的是一种专门用于描述高层界面设计样式的语言。通过这种语言,界面设计者可以直接、明确地描述出他们头脑中的布局设计样式;通过这种语言,界面设计者可以自己方便地、灵活地制定自己需要的布局样式。此外,本文中给出的设计思想对于其他领域的设计也有很好的借鉴作用。
创造性,还是乏味?
界面设计是一项非常有创造性,甚至富有艺术性的工作,一个简洁、易用、漂亮的界面在带给使用者方便的同时,也会给界面设计者带来极大的成就感。但是,在现实中,情况似乎并非如此,很多人都认为做界面是一项非常繁琐、机械、乏味的工作,并千方百计地去逃避界面相关的工作。这是为什么呢?
原因很简单,因为做界面其实涉及两项工作,一项是界面的一些设计创意,包括界面的布局样式以及和使用者的交互方式,这项工作充满挑战和乐趣。但是,这些设计创意最终是要落实到实现上的,这就是第二项工作。此时,你头脑中那些清晰、完整的设计概念开始变得琐碎,你不得不和那些低层次的坐标位置打交道。更糟糕的是,当你好不容易做好了一个界面,但是发现其中某些元素的布局需要一些调整时,这个你本应认为是一个很简单的改变却造成大量重复的低层次坐标位置更改时,你肯定会认为做界面是多么的机械和乏味呀!
其实,造成这种认识的根源在于界面设计创意和实现这些创意概念的语言之间存在很大的断层。这样,在具体实现时,你就必须得把这些清晰、完整的布局样式降级成一些琐碎、没有什么意义的低层次的坐标值,使得实现语言能够理解。这项工作不仅乏味,而且最终的实现也非常的脆弱 —— 一个在布局样式层面非常简单的更改,就会造成实现层面的巨大变动。比如:我们可以说把一组元素同时按比例缩小 10%,做过界面的朋友肯定知道这个更改意味着什么。
为了应对这个断层的问题,目前几乎所有的涉及界面制作的开发工具都提供了相同的解决方法:可视化的界面设计工具以及布局管理器。但是这两种方法都没有从根本上解决这个问题。
可视化界面设计工具确实避免了不少繁琐的界面元素摆放工作,但是对于专业的界面设计来说,通过拖放设计出来的界面在准确度和规范性上都有待提高,此外还有更为重要的一点,那就是存在于设计者头脑中的布局样式仍然没有被明确地描述出来,而是被降级成一个个摆放在一起的零散的组件,虽然这些组件本身是可视的。这个语义断层的存在同样会使得通过可视化界面设计工具设计出来的界面非常脆弱。
布局管理器试图通过提供一些常用的布局样式来解决这个问题。但是,这种做法非常僵化,也就是说你只能使用现有的布局管理器,如果它们无法满足你的要求,你也无法自己定制。此外,这些布局管理器仅仅适合于一些简单的情况。对于一些复杂的布局样式来说,它们的描述能力就显得非常的不足。那些曾经和 GridBagLayOut 斗争过的朋友对此肯定深有体会。
在本文中,我们会给出一种更好的解决方案,我们不是去试图把界面设计者头脑中的设计概念和样式逐步降级、分解成所使用的实现语言能够理解的低层概念,也不是提供一些已经完成的、确定的但难以扩充和更改的布局样式库供界面设计者使用。我们所提供的是一种专门用于描述高层界面设计样式的语言。通过这种语言,界面设计者可以直接、明确地描述出他们头脑中的布局设计样式,通过这种语言,界面设计者可以自己方便地、灵活地制定自己需要的布局样式。也就是说,本来仅存在于界面设计者头脑中的抽象布局样式,现在也变得可描述,可编程了。
界面布局语言介绍
在学习界面布局语言的设计之前,先来了解一下该语言的使用是非常有帮助的。我们的界面布局语言非常简单,简单到只有一种原子:Component。Component 是一种基本的布局元素,可以对 Component 进行平移和伸缩,使其和给定的一个布局空间 Rectangle 匹配。比如对于 Button 这个 Component 来讲,它具有传统按钮的外观,但是它在布局上所占的实际空间则是由为其指定的 Rectangle 决定的。此外,Component 要最终在界面上显示出来,就必须有一个物理上的 Container。也就是说,界面设计应该是一项充满创造性、富有乐趣的工作,但是却往往被认为非常的枯燥和繁琐。究其原因,是因为界面布局领域所采用的描述概念和具体的实现语言之间存在很大的语义隔阂。而一般的界面开发工具提供的所见即所得以及界面布局管理器等方案也无法很好地解决这个问题。
在本文中,我们会给出一种更好的解决方案,我们不是去试图把界面设计者头脑中的设计概念和样式逐步降级、分解成所使用的实现语言能够理解的低层概念,也不是提供一些已经完成的、确定的但难以扩充和更改的布局样式库供界面设计者使用。我们所提供的是一种专门用于描述高层界面设计样式的语言。通过这种语言,界面设计者可以直接、明确地描述出他们头脑中的布局设计样式;通过这种语言,界面设计者可以自己方便地、灵活地制定自己需要的布局样式。此外,本文中给出的设计思想对于其他领域的设计也有很好的借鉴作用。
创造性,还是乏味?
界面设计是一项非常有创造性,甚至富有艺术性的工作,一个简洁、易用、漂亮的界面在带给使用者方便的同时,也会给界面设计者带来极大的成就感。但是,在现实中,情况似乎并非如此,很多人都认为做界面是一项非常繁琐、机械、乏味的工作,并千方百计地去逃避界面相关的工作。这是为什么呢?
原因很简单,因为做界面其实涉及两项工作,一项是界面的一些设计创意,包括界面的布局样式以及和使用者的交互方式,这项工作充满挑战和乐趣。但是,这些设计创意最终是要落实到实现上的,这就是第二项工作。此时,你头脑中那些清晰、完整的设计概念开始变得琐碎,你不得不和那些低层次的坐标位置打交道。更糟糕的是,当你好不容易做好了一个界面,但是发现其中某些元素的布局需要一些调整时,这个你本应认为是一个很简单的改变却造成大量重复的低层次坐标位置更改时,你肯定会认为做界面是多么的机械和乏味呀!
其实,造成这种认识的根源在于界面设计创意和实现这些创意概念的语言之间存在很大的断层。这样,在具体实现时,你就必须得把这些清晰、完整的布局样式降级成一些琐碎、没有什么意义的低层次的坐标值,使得实现语言能够理解。这项工作不仅乏味,而且最终的实现也非常的脆弱 —— 一个在布局样式层面非常简单的更改,就会造成实现层面的巨大变动。比如:我们可以说把一组元素同时按比例缩小 10%,做过界面的朋友肯定知道这个更改意味着什么。
为了应对这个断层的问题,目前几乎所有的涉及界面制作的开发工具都提供了相同的解决方法:可视化的界面设计工具以及布局管理器。但是这两种方法都没有从根本上解决这个问题。
可视化界面设计工具确实避免了不少繁琐的界面元素摆放工作,但是对于专业的界面设计来说,通过拖放设计出来的界面在准确度和规范性上都有待提高,此外还有更为重要的一点,那就是存在于设计者头脑中的布局样式仍然没有被明确地描述出来,而是被降级成一个个摆放在一起的零散的组件,虽然这些组件本身是可视的。这个语义断层的存在同样会使得通过可视化界面设计工具设计出来的界面非常脆弱。
布局管理器试图通过提供一些常用的布局样式来解决这个问题。但是,这种做法非常僵化,也就是说你只能使用现有的布局管理器,如果它们无法满足你的要求,你也无法自己定制。此外,这些布局管理器仅仅适合于一些简单的情况。对于一些复杂的布局只要给定了一个 Rectangle 和一个 Container,一个 Component 就可以在界面上指定的布局位置呈现出来。
例如,当我们使用布局语言在一个 JFrame 上坐标位置为 (0,0) 展示一个 width 为 200,height 为 60 的按钮时,我们可以这样来描述(为了简洁起见,后面的代码实例中均略去 Layout 名字空间前缀):
Button().title(“button1”).at(0,0,200,60).in(this.getContentPane()); |
仅仅提供这样一种原子元素的语言显然无法满足我们前面提到的目标。在我们的界面布局语言中,还提供了两种在布局中非常常用的两种从已有组件构造新组件的组合手段:above 和 beside。其中 above 组合子接收 3 个参数:两个现有 Component 以及一个比例,它会产生出一个新的复合 Component,其中按照给定的比例把第一个 Component 摆放在第二个 Component 之上。Beside 组合子接收同样的 3 个参数,并且也产生出一个新的复合 Component,其中按照给定的比例把第一个 Component 摆放在第二个Component左边。
例如,如果我们希望在一个给定的 Container C 上的 Rectangle(0,0,300,40) 中,平行摆放一个 TextField 和一个 Button,且希望 TextField 占据 80% 的比例时,可以这样来描述:
beside(TextField(), Button().title(“ok”), 0.8).at(0,0,300,40).in(C) |
同样,我们可以使用 above 来进行如下描述:
above(TextField(), Button().title(“ok”), 0.5).at(0,0,300,60).in(C) |
值得注意的是,在我们的界面布局语言中,Component 在 beside 和 above 操作下是封闭的,也就是说 beside 和 above 操作的结果同样也是 Component,并完全可以作为基本的 Component 来再次进行 beside 和 above 组合。这样我们就可以使用这两个简单的操作生成更加复杂的 Component 来,从而完成复杂的界面布局。比如,我们可以这样来进行描述:
Component L = beside(TextField (), Button().title(“…”), 0.8); above(L, Button().title(“ok”), 0.5). at(0,0,300,60).in(C) |
为了保证界面布局语言的完备性,我们增加了一种特殊的原子元素:Empty。它的作用只是占据一定的布局空间。比如,如果我们希望在一个布局空间中右半边放置一个 Button,左半边空置,就可以这些描述:
beside(Empty(), Button(), 0.5).at(0,0,200,40).in(C) |
读者在后面可以看到,正是这个 Empty 以及 beside 和 above 操作的闭包性质为我们描述任意复杂的布局样式提供了可能。
在有了这些基础的布局元素和组合手段后,我们就可以通过组合手段来把一些典型的布局样式抽象出来。在下一小节中读者将会看到,布局语言中的 beside 和 above 组合操作其实就是 Java 中的普通方法,因此我们的布局语言中不需要什么特别的抽象手段。也就是说,我们可以直接使用 Java 中已有的抽象手段。
例如,如果我们希望抽象出这样一种布局样式,其中给定一个布局空间和一个布局组件,我们期望该组件能够按照指定的纵、横留白比例位于该布局空间的中心地带。我们可以把该布局样式抽象出来,并命名它为 center。并可以在更复杂的布局样式中把 center 当作一个基本语素使用。center 的实现如下:
public Component center(Component cp, float hRatio, float vRatio) { float s1 = (1-2.0* hRatio)/ (1.0 - hRatio); float s2 = (1-2.0*vRatio)/ (1.0-vRatio); Component u = above(Empty(), above(cp, Empty(), s2), vRatio); return beside(Empty(), beside(u,Empty(), s1), hRatio); } |
当我们想把一个按钮放置按照在横向 0.2,纵向 0.1 的留白比例放在布局空间 (0,0,100,30) 中时,我们可以简单的进行如下描述:
center(Button().title(“I am at center.”), 0.1,0.1).at(0,0,300,60).in(C) |
我们还可以构建出 h_seq 和 v_seq 这样的布局样式,它们分别为把一组给定的布局元素横向顺序排列和纵向顺序排列,其实现如下:
public Component h_seq(Component[] cps) { int len = cps.length; if(len == 1) return cps[0]; return beside(cps[0], h_seq(slice(cps, 1, len)), 1.0/len); } public Component v_seq(Component[] cps) { int len = cps.length; if(len == 1) return cps[0]; return above(cps[0], v_seq(slice(cps, 1, len)), 1.0/len); } |
其中 slice 方法有 3 个参数,一个为布局元素数组,另外两个为区间的起止位置,该方法把给定布局元素数组中指定起止位置的区间部分作为一个新的布局元素数组返回。这两个方法的实现都比较简单直接。下面是两个应用例子:
Component[] cps = new Component[] { Button().title(“1”), Button().title(“2”), Button().title(“3”) }; h_seq(cps).at(0,0,300,60).in(C) v_seq(cps).at(0,0,150,200).in(C) |
在 center、h_seq、v_seq 这些布局样式的基础上,我们可以定义出更加高阶的样式来,比如,给定一布局元素序列,我们希望它们在给定的布局空间中按照 N 行、M 列排列。我们称之为 block,其实现如下:
public Component block (Component[] cps, int N, int M) { Component[][] fcps = formalize(cps, N, M); Component[] rows = new Component[fcps.length]; for(int i = 0; i < fcps.length; i++) { rows[i] = h_seq(fcps[i]); } return v_seq(rows); } |
其中 formalize 是一个工具方法,它把一个给定的布局元素数组规范化为 N 行 M 列的形式,如果不足则用 Empty 组件补齐。
如果希望在 block 中,每个元素都可以指定一些横向和纵向的留白,则可以定义一个 block_with_margin 布局样式,其实现如下:
public Component block_with_margin(Component[] cps, int N, int M, float hRatio, float vRatio) { Component[] ncps = new Component[cps.length]; for(int i=0; i<cps.length; i++) { ncps[i] = center(cps[i], hRatio, vRatio); } return block(ncps, N, M); } |
好了,现在我们来看一个稍微复杂一些的例子,我们将使用前面制作的一些布局样式构建一个迷你计算器的外观,如下图所示:
对应的描述代码如下:
Component[] cs = new Component[]{ Button().title("0"), Button().title("1"), Button().title("2"), Button().title("+"), Button().title("3"), Button().title("4"), Button().title("5"), Button().title("-"), Button().title("6"), Button().title("7"), Button().title("8"), Button().title("*"), Button().title("9"), Button().title("="), Button().title("%"), Button().title("/") }; Component opLayout = block(cs,4,4); above( above( TextField(), beside( Button().title("Backspace"), Button().title("C"),0.5), 0.5), block(cs,4,4), 0.3).at(0,0,300,200).in(C); |
如果我们现在希望将所有数字以及操作按钮按照横向和纵向各 2% 进行留白,我们所要做的仅仅是一行的改动,就是把:
Component opLayout = block(cs,4,4); |
更改为:
Component opLayout = block_with_margin(cs, 4, 4, 0.02, 0.02); |
这意味着什么呢?这意味着我们可以直接使用布局语言进行界面制作,我们可以直接针对布局进行编程,我们所写出来的界面代码就是我们的布局规格说明。
从上面的介绍中,读者可以看出,我们的界面布局语言可以非常方便地定义出一些常见的布局样式,还可以把这些样式组合成更为复杂的一些高阶布局样式,并且这种组合是没有任何限制的。此外,这些布局样式的定义描述方式是和界面设计者头脑中所使用的一些布局词汇和规则贴近的。通过使用界面布局语言,界面设计者完全可以摆脱那些呆板、机械又难以定制和扩展的布局管理器,可以轻松地把头脑中的布局创意直接描述出来,逐步形成自己的布局样式库,充分享受这种创造性的工作所带来的乐趣。