下載源代碼: Outliner.Java
移動信息設備描寫(Mobile Information Device Profile,MIDP) 1.0 版本供給了一套基礎組件,用於支撐利用程序需要的大多數用戶界面(UI)。但是,假如您的需求比擬復雜,那麼一般必需要從 Canvas
派生子類,並重新設計。
MIDP 2.0 轉變了所有這些。現在您可以建立自定義組件,這樣您就可以對用戶交互進行細粒度的把持,而且可以適應現有的窗體框架,符合設備本身的觀感。
在這篇文章裡,我們通過建立一個簡略的 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
利用全部這些才能,為用戶供給以下特征:
利用程序顯示多行文本,用不同的數目縮進,形成一個可視的層次結構。窗體
加強的布局才能使這種表現成為可能。 用戶可以折疊大綱的任何一行,把層次結構中該行之下的行隱躲起來。會有一個可視唆使器,表現指定行是展開的還是折疊的。您可以籠罩 CustomItem 的
paint()
方法,按照自己愛好的方法畫出這樣的唆使器。 用戶還可以按照任意次序重新排列行。移動一個行,也會同時移動它所有的下級行。現在,這個命令可以專門用於一個項目,這樣菜單就能夠做到高低文敏感。向上移動、向下移動、展開、以及折疊命令只有在合適項目當前狀態的時候才會呈現。
這些特征在 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
的窗體只包含OutlineItem
s,所以這個布局指令組合會把每個項目放在自己的行裡,每行的寬度與窗體寬度一樣,高度為項目標期看高度。
游歷窗體迄今為止,我們一直著重的是定制項目標外觀。現在,我們要考慮一下它的行動——它對用戶輸進響應的感知。
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,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 讓這一目標變得更輕易,所以請充分利用這個機會。
結束語現在您可以用各種以前在 MIDP 1.0 中沒有的可視後果和驗證來編寫定制組件了。
窗體
加強的布局才能,為您供給了對表現更多的把持,CustomItem
讓您不必編寫和保護設備專用的代碼,就可以調劑行動符合設備自身的觀感。MIDP 2.0 為基於窗體的移動利用程序中的用戶界面的定制,開啟了近乎沒有限制的大門。
致謝我要感謝 Roger Riggs 對代碼改良的建議,還要感謝 Brian Christeson 對文字所做的無數改良。最後,我還要感謝我的家庭,使我能夠抽身出來寫作。
關於作者Michael Powers 是 mpowersLLC 的負責人,是桌面及無線平台軟件參謀,從 Java 問世起,他就一直在應用各種不同情勢的 Java 技巧。他的獲獎作品 Piranha Pricecheck MIDlet 正在風靡,可以從 mpowers.Net 免費下載該作品。