/** * 用指定的初始缩进和文本建立 OutlineItem */ public OutlineItem ( int inIndent, String inText ) { // 我们不想要系统提供的标签 super( null ); indent = inIndent; text = inText; hiddenChildren = null; // 定义布局 setLayout( LAYOUT_EXPAND | LAYOUT_TOP | LAYOUT_NEWLINE_AFTER ); // 加入一直适用的命令 addCommand( editCommand ); addCommand( insertCommand ); } |
public void paint ( Graphics g, int w, int h ) { // 用背景色全部清除 g.setColor( DISPLAY.getColor ( DISPLAY.COLOR_BACKGROUND ) ); g.fillRect( 0, 0, w, h ); // 现在用前景色来画图 g.setColor( DISPLAY.getColor ( DISPLAY.COLOR_FOREGROUND ) ); if ( isCollapsed() ) { // 画一个代表隐藏项目的填充的圆 g.fillArc( indent * INDENT_MARGIN + 2, 2, FONT_HEIGHT-7, FONT_HEIGHT-7, 0, 360 ); } else { // 没有隐藏项目,所以画一个空心圆 g.drawArc( indent * INDENT_MARGIN + 2, 2, FONT_HEIGHT-7, FONT_HEIGHT-7, 0, 360 ); } // 画出文本 g.drawString( text, indent * INDENT_MARGIN + FONT_HEIGHT, 0, g.TOP | g.LEFT ); } |
通过二次调用 Display 的 getColor() 方法,我们找到设备的默认背景色和前景色,每次把适当的常数传递给这个方法,先用 COLOR_BACKGROUND(背景色),然后用 COLOR_FOREGROUND(前景色)。
不管我们选择使用默认颜色还是指定自己的颜色,OutlineItem.paint()都会用背景色填充一个矩形,然后切换成前景色画剩下的内容。
请注意:MIDP 规范要求,在画图的时候,必须覆盖项目显示区域的每一个象素。有些实现可能会在调用 paint()之前清除项目覆盖的区域,但是其它一些实现可能不会。如果您没有先用背景色填充矩形,那么您就会冒着失去可移植性的风险。
然后 OutlineItem 以每一缩进级别 8 个象素,从左向右画圆,如果项目处在折叠状态,就填充圆。圆的宽度和高度由字体的高度决定,所以圆的大小会根据不同的字体和尺寸在设备上恰当地缩放。因为圆的尺寸永远不会超过字体高度,所以文本挨着缩进边际加上字体宽度之后向右偏移。
请注意:长的字符串可能会弄乱屏幕,因为 OutlineItem 没有把行的长度考虑在内。我很乐意把文本环绕的实现作为一个练习留给您。
剩下的抽象方法让 CustomItem 基类请求子类计算项目的合适的最小尺寸和期望尺寸。OutlineItem 的实现很简单:
public int getMinContentHeight() { return FONT_HEIGHT; } public int getMinContentWidth() { return indent * INDENT_MARGIN + FONT_HEIGHT; } public int getPrefContentWidth ( int height ) { return indent * INDENT_MARGIN + FONT.stringWidth ( text ) + FONT_HEIGHT; } public int getPrefContentHeight ( int width ) { return FONT_HEIGHT; } |
您可以把这些方法当作 CustomItem 所实现的最小尺寸和期望尺寸访问器的回调。除非您覆盖它们,否则您的项目的 getMinimumWidth() 方法将返回 getMinContentWidth() 方法的结果,而项目的 getMinimumHeight() 方法将返回 getMinContentHeight()方法的结果。
项目的期望宽度和高度几乎是用同样的方式决定,区别在于应用程序可以修改期望尺寸。一旦调用项目的 setPreferredWidth()方法或 setPreferredHeight()方法,那么对应的尺寸就相当于被锁定了。
获取期望尺寸的调用将总是返回锁定的值。在建立项目时,两个维度都被解锁,您可以通过把某一维的尺寸设置为 -1 来解除它的锁定。
只有当 getPrefContentWidth()方法和 getPrefContentHeight()方法各自的维度解除锁定的时候,才调用这二个方法。它们应当返回最佳尺寸,让项目内容最佳显示,行环绕最小,没有剪辑。
OutlineItem 没有环绕,所以最小高度和期望高度都等于当前字体的高度。期望宽度就是当前文本的宽度,等于文本当前字体所占空间加上扩展指示器的空间加上当前缩进的空间。最小宽度就是指示器的空间加上缩进的空间。
窗体布局
要建立布局,窗体不仅需要每个项目的最小尺寸和期望尺寸,还需要每个项目的布局指令:一位的标志,用于指定对齐和断行。项目的布局指令组合成一个整数。
如果您没有指定任何布局指定,那么会得到默认的兼容 MIDP 1.0的布局,在这种布局里,项目按行摆放,一个接一个。要指定不同的布局,可以使用位运算符 OR ,把各个预定义的的布局指令组合成一个整数,把它传递给 setLayout()方法。
Item类定义了布局指令常量:
LAYOUT_LEFT LAYOUT_RIGHT LAYOUT_CENTER LAYOUT_SHRINK LAYOUT_EXPAND LAYOUT_TOP LAYOUT_BOTTOM LAYOUT_VCENTER LAYOUT_VSHRINK LAYOUT_VEXPAND LAYOUT_NEWLINE_BEFORE LAYOUT_NEWLINE_AFTER |
窗体的布局算法有点复杂。在类的文档里有非常详细的解释,我只是归纳一句:算法符合“springs-and-struts”模式,工作起来有点象是 AWT 的 SpringLayout 和 GridBagLayout 布局管理器之间的交叉。
对于水平和垂直维度来说,每个项目都有对齐方式以及缩小或放大到适合指定行空间的设置。通过查询换行是在项目之前还是在项目之后,还可以指定项目是在行首还是在行尾出现。
为了保证移植性,您指定的布局指令不应该比实际需要多。默认的对齐选项,在不同的实现和不同的语言之间,会有差异,一般在那些不是从左到右阅读的语言上会出现。您不必指定布局,但是如果指定布局,会给项目一个默认布局,可能会帮助项目在外观或行为上实现预期的一致性。OutlineItem 在构造函数里用下面这个调用设置自己的布局指令:
// 定义布局 setLayout( LAYOUT_EXPAND | LAYOUT_TOP | LAYOUT_NEWLINE_AFTER ); |
布局指令包括 LAYOUT_EXPAND, LAYOUT_TOP,以及LAYOUT_NEWLINE_AFTER。不需要水平对齐选项,因为项目会充满所有可用水平空间。因为没有指定 LAYOUT_VSHRINK 和 LAYOUT_VEXPAND,窗体会用自己的期望高度设置项目的高度,并用顶端对齐方式在行的垂直空间里对齐项目。
在窗体中的项目,会在它的位置后面得到一个断行,所以每个项目都出现在自己的行里。因为 Outliner 的窗体只包含 OutlineItems,所以这个布局指令组合会把每个项目放在自己的行里,每行的宽度与窗体宽度一样,高度为项目的期望高度。
游历窗体
迄今为止,我们一直侧重的是定制项目的外观。现在,我们要考虑一下它的行为,它对用户输入响应的感知。
MIDP窗体有自己的内置术语,叫做游历(traversal)。这与桌面应用程序中切换输入焦点的概念类似。不管是在桌面还是在移动环境里,在任何给定时刻,只有一个UI组件拥有焦点,这意味着所有的用户输入动作都被导向这个组件。
例如,如果文本字段拥有焦点,那么按下键盘就会造成在文本字段的插入点之后出现字符。在典型的桌面应用程序里,箭头键在文本字段内移动插入点,制表键则把焦点转移到下一个组件。
移动设备可能没有完整键盘。实际上,它甚至可能没有四个方向箭头。如果移动设备有方向键,那么左、右键可能负责移动插入点,上、下键可能负责转移焦点为。
如果只有二个方向键,那么上、下键可能承担双重责任:移动插入点,在插入点到达字段的开始或结束位置的时候,转移焦点。
因为设备的差异很大,所以MIDP为定制项目提供了一种机制,支持用一致的、可移植的方式进行游历。在 CustomItem 的一个方法里包含了这个机制:
protected boolean traverse( int dir, int viewportWidth, int viewportHeight, int[] visRect_inout ) |
当用户按下能够引起我们的项目接收焦点的导航键时(通常是箭头键),就调用定制项目的 traverse() 方法。如果方法返回 true,那么用户下次按下导航键时,还会调用这个方法,循环往复,直到方法返回 false 为止。
传递给 traverse()方法的第一个参数,是造成焦点转移到我们项目的按键。参数值是 Canvas 类中定义的方向性游戏动作(game actions)中的一个:Canvas.UP, Canvas.DOWN, Canvas.LEFT,和 Canvas.RIGHT,或者为空值 CustomItem.NONE。如果值为NONE,那么一些与平台相关的事件,例如改变窗体大小,会使项目获得焦点。
剩下的参数负责描述屏幕的尺寸和项目在屏幕上的可见区域。某些项目,特别是是那些显示大量文本的项目,比屏幕的尺寸大,它们必须能够响应游历事件,滚动它们的可视内容。traverse()方法的文档详细解释了这些参数,但是您现在没有必要考虑它们。
如果您想让项目对用户的按键响应仍然保留焦点,那么您的实现就应当返回 true。在 CustomItem 中的实现总返回false, 这样形成了与 StringItem 的行为类似的行为:按下任何导航键,都会把焦点转移到不同的项目。这个行为对于 CustomItem 的大多数简单子类都合适。
更具交互性的项目可能需要覆盖traverse()方法来定制导航键的行为。一个比较好的例子是Gauge项目,在某些实现里,按下右键和左键可以增减组件里的值。当值达到最大或最小值时,traverse()方法返回false,允许按键把焦点移动到与按键方向对应的下一个组件上。文本字段的某些实现工作也来也类似,把插入点向左或向右移动,只在插入点到达字段的开始或结束时才转移焦点。
对于OutlineItem,上、下方向键按照常规把焦点转移到另一个组件。因为没有插入点需要考虑,所有的编辑都在另外一个屏幕处理,OutlineItem 把右键和左键解释为缩进或凸出文本。在没有水平方向键的设备上,outliner MIDlet 会显示额外的菜单项,表示缩进文本或凸出文本,就象我稍后说明的那样。下面是 traverse 的实现:
/** * 用来在可能的时候缩进项目或凸出项目。 */ protected boolean traverse( int dir, int viewportWidth, int viewportHeight, int[] visRect_inout ) { // 用这个标记来区分焦点是 // 游历进本项目,还是 // 在本项目内游历: if ( traversingItem != this ) { // 游历进:标记自己,返回 true traversingItem = this; return true; } // 处理在本项目内的游历 switch ( dir ) { case Canvas.RIGHT: if ( isIndentable() ) { indent(); } repaint(); return true; case Canvas.LEFT: if ( isOutdentable() ) { outdent(); } repaint(); return true; case NONE: // 什么都不做:只是重绘窗体布局 return true; default: // 退出 } return false; } |
请注意:当焦点游历进您的项目时,就会调用traverse()方法,只要您的实现返回true,那么每次焦点都是在您的项目内游历。您应当在代码中区分这些情况。OutlineItem 保持了一个静态引用,用它来判断项目是否由于这次调用 traverse() 而获得焦点。
如果是这样,它就返回 true ,这样它就可以接收下一个方向键。对于接下来针对 traverse()的调用,OutlineItem查看按下了哪个键。
如果按下的是右键或左键,它就修改缩进级别。在 NONE 的情况下,OutlineItem 什么也不做,但是返回 true,以便重新获得焦点。对于剩下的二种情况,上和下,则返回 false ,允许焦点游历到下一个或上一个项目。
因为改变缩进级别也会改变项目的可视外观,所以traverse()方法调用repaint()方法,告诉窗体重绘它自己。因为OutlineItem没有做文字环绕,所以项目的期望尺寸不会变化。如果尺寸发生了变化,traverse() 方法应用调用 invalidate() ,而不是调用 repaint(),好让窗体重新安排它的布局。
调整用户交互
对于新增的灵活性,MIDP 2.0 让您可以把命令和窗体上的单独项目关联。当项目拥有焦点时,项目的命令就会和窗体的命令组合在一起,形成上下文敏感的菜单。响应项目菜单的命令,不需要付出比响应窗体的更多的努力;它不过是另外一个接口。
Outliner 实现了 ItemCommandListener 接口,把自己加为窗体中每个项目的监听器。向项目加入命令或删除命令的方式,按照您期望的方式进行:只需调用 addCommand() 方法和 removeCommand() 方法。
因为每个 OutlineItem 都会跟踪自己的状态,它是展开的还是的,它是缩进的、凸出的,是上移还是下移,每个定制项目都管理着自己的适用命令列表。每当一个OutlineItem的状态变化时,项目都会调用自己的 updateCommands() 命令。
private void updateCommands() { if ( !hasPointerPress() ) { removeCommand( expandCommand ); removeCommand( collapseCommand ); // 进入展开或折叠的命令 if ( isCollapsed() ) addCommand( expandCommand ); else addCommand( collapseCommand ); } if ( !hasHorizontalTraversal() ) { removeCommand( indentCommand ); removeCommand( outdentCommand ); // 进入缩进、凸出的命令 if ( isIndentable() ) addCommand( indentCommand ); if ( isOutdentable() ) addCommand( outdentCommand ); } removeCommand( upCommand ); removeCommand( downCommand ); if ( canMoveUp() ) addCommand( upCommand ); if ( canMoveDown() ) addCommand( downCommand ); removeCommand( deleteCommand ); if ( getIndex() > 0 ) { // 如果不为根 addCommand( deleteCommand ); } } |
因为有这么多的命令,所以把项目目前状态和位置下不使用的命令隐藏起来,感觉会好些。为了保持逻辑简单,我们从项目中删除了所有命令,然后再有选择地重新插入命令。需要了解的是,删除项目不需要的命令不会有损害,而且如果您把同一命令加入了二次,也不会得到重复。
另外一个好消息是:象我们一样地经常重建命令列表,对应用程序的响应性没有什么可以感觉到的冲击。我们回忆一下,OutlineItem用水平导航键来改变缩进级别。在没有水平导航键的设备上,缩进和凸出的命令是必需的,但是在有水平导航键的设备上会形成冗余。幸运的是,有一种方法可以让这些命令只在需要的时候才可见。
CustomItem有一个getInteractionModes()方法,我们可以调用它来确定设备提供了哪些UI能力。这个方法返回一个位掩码,您可以用它来测试CustomItem定义的接口能力常数:
KEY_PRESS, KEY_RELEASE, KEY_REPEAT, POINTER_PRESS, POINTER_DRAG, POINTER_RELASE, TRAVERSE_HORIZONTAL, 和TRAVERSE_VERTICAL。 updateCommands()方法调用 hasHorizontalTraversal()方法和 hasPointerPress()方法, 来利用这个功能: private boolean hasPointerPress() { return ( getInteractionModes() & POINTER_PRESS ) != 0; } private boolean hasHorizontalTraversal() { return ( getInteractionModes() & TRAVERSE_HORIZONTAL ) != 0; } |
如果设备没有水平导航键,或者设备支持尖笔或者鼠标,那么OutlineItem就隐藏缩进和凸出命令。就像Canvas所做的那样,CustomItem用固定的设备支持一些用于响应轻击的方法。OutlineItem重写了pointerPressed(),这样,如果用户在展开指示器上轻击,就可以切换展开状态。
protected void pointerPressed ( int x, int y ) { // 如果在小部件区域内 if ( x < FONT_HEIGHT ) { if ( isCollapsed() ) expand(); else collapse(); } } |
传递给这个方法的坐标相对于项目显示区域的左上角,所以简单地比较X坐标就会揭示指针按下的位置是不是靠近展开指示器。流行的新的 MIDP2.0 设备,比如 Sony Ericsson P900 和 Palm Tungsten 系列接受尖笔输入,所以在您的应用程序里实现对指针交互的支持,会是个好主意。
MIDP 初始的设计目标之一,就是让您能够编写在可以在具有不同能力、窗体因素的不同设备上运行的应用程序。MIDP 2.0 让这一目标变得更容易,所以请充分利用这个机会。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/374079/viewspace-131748/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/374079/viewspace-131748/