移動信息設備描述(Mobile Information Device PRofile,MIDP) 1.0 版本提供了一套基本組件,用于支持應用程序需要的大多數用戶界面(UI)。但是,如果您的需求比較復雜,那么一般必須要從 Canvas 派生子類,并重新設計。
MIDP 2.0 改變了所有這些?,F在您可以建立自定義組件,這樣您就可以對用戶交互進行細粒度的控制,而且可以適應現有的窗體框架,符合設備本身的觀感。
在這篇文章里,我們通過建立一個簡單的 Outliner MIDlet,來研究這些新的定制功能。大綱是用來組織想法、保持列表,甚至進行項目計劃的工具是一個在移動設備上非常有用的應用程序。
Outliner MIDlet讓用戶可以構建層次結構良好的窗體項目大綱。它們可以加入或刪除,縮進或凸出,還可以用一種在 MIDP 2.0 出現之前不可能的方法折疊和展開項目。
如果您對于使用 MIDP 建立用戶界面不熟,請讓我們回顧一下基礎知識。MIDP 1.0 提供了一些骨干 UI 組件,包括選項組(ChoiceGroup),日期字段(DateField), Gauge, 圖像項目(ImageItem), 字符串項目(StringItem), 以及文本字段(TextField)。
這些類全部擴展自公共基類 Item。和它們的 AWT 等價物非常相似,項目是我們用來控制底層本機 UI 小部件的抽象。因為本機實現在不同設備之間,可能有很大的差異,而事實上也是這樣,所以 Item 公共接口對于底層小部件的外觀和行為只提供了非常少的控制。
窗體的存在,是為了按行排列項目,使其最好地適合屏幕尺寸、適應項目運行所在設備的能力。至少從理論上講,MIDP 實現可以方便、無縫地使您的應用程序適應設備硬件;副作用是您對用戶界面觀感的影響受到限制。
有什么新東西? MIDP 2.0 改善了窗體,為項目布局提供了更好的控制,還提供了一個新類CustomItem,這個類讓您可以建立自己的窗體項目。Outliner 利用全部這些能力,為用戶提供以下特性:
1、應用程序顯示多行文本,用不同的數量縮進,形成一個可視的層次結構。窗體增強的布局能力使這種表示成為可能。
2、用戶可以折疊大綱的任何一行,把層次結構中該行之下的行隱藏起來。會有一個可視指示器,表示指定行是展開的還是折疊的。您可以覆蓋 CustomItem 的 paint() 方法,按照自己喜歡的方式畫出這樣的指示器。
3、用戶還可以按照任意順序重新排列行。移動一個行,也會同時移動它所有的下級行。現在,這個命令可以專門用于一個項目,這樣菜單就能夠做到上下文敏感。向上移動、向下移動、展開、以及折疊命令只有在適合項目當前狀態的時候才會出現。這些特性在 MIDP 1.0 里都是不可能的。下面讓我們看看這種魔術是如何做到的。
建立 Outline 項目類 Outliner 類自己就是一個普通的 MIDlet。功能的核心是 CustomItem 的子類,叫做 OutlineItem。你要實現自己的 CustomItem 類時需要做的事,在這個類里都做了,所以您應當好好在源代碼里看看它。構造函數是一個開始的好地方:
/**
* 用指定的初始縮進和文本建立
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 );
}
調用構造函數,把要顯示的文本、項目應當縮進的次數傳遞給構造函數,就可以建立 OutlineItem。
在構造函數里,第一項任務是調用超類的構造函數。
MIDP 項目不僅僅代表 UI 小部件本身,還有一個標簽向用戶標識部件。例如,一個文本字段是一個包含文本的框;它的標簽通常出現在它的左邊,是描述文本框中內容的單詞,比如 Name 或 PassWord。我們的構造函數把 null 作為必要參數傳給超類的構造函數。
在使用傳遞給構造函數的參數初始化對象的狀態之后,下一步就是配置項目的布局指令,把一些命令直接加給項目。稍后我會解釋布局指令和專門用于項目的命令。
CustomItem 有5個我們必須實現的抽象方法。在這些方法里,paint() 方法可以讓您控制項目的外觀。paint()方法的參數包含項目的寬度和高度,它們由窗體的布局邏輯、圖形對象、轉換方式決定,所以它的原點項目的左上角。下面是 OutlineItem 中 paint() 的實現:
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,所以這個布局指令組合會把每個項目放在自己的行里,每行的寬度與窗體寬度一樣,高度為項目的期望高度。
游歷窗體
迄今為止,我們一直側重的是定制項目的外觀?,F在,我們要考慮一下它的行為,它對用戶輸入響應的感知。
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
(出處:http://www.49028c.com)
新聞熱點
疑難解答