在本文中,我們關心的是BlueBill Mobile類,尤其是管理所有Search Species屏幕之後邏輯的控制器;因此本文有助於你了解JavaFX的語言性能。而且我們會舉出一些實例來闡述要介紹的技巧和典型JavaFX結構的陷阱。
筆者想應用程序中嵌入了更新的屏播。視頻播放要求使用QucikTime。
這裡的概念是在搜索框中鍵入查詢時,英文函數或科學名稱函數會對清單過濾。此外,當這些生效的時候,BlueBill Mobile還可以執行自動完成輸入。例如,如果在鍵入查詢的時候你仔細查看視頻會發現只輸入了"a-r-d-a-c"來選擇"Ardea Cinerea";或用於"Pied Avocet"的"p-i-e-< space>-a"。BlueBill Mobile 會自動會剩余部分進行補充因為在某些情況下,不存在其他選擇。這是用來改善移動設備性能的重要功能:你可以以較少的輸入達到相同目的。
按照MVC模式,就非常有必要在單獨的控制器中概括這種模式;此外,也很容易對這種模式進行單元測試。
首先,讓我們看一下代表了分類群的模式類:
package it.tidalwave.bluebillmfx.taxon.model; import java.lang.Comparable; public class Taxon extends Comparable { public-read protected var displayName : String; public-read protected var scientificName : String; public-read protected var id : String; override function compareTo (other : Object) { return displayName.compareTo((other as Taxon).displayName); } override function toString() { return "{displayName} ({scientificName}) ({id})" } } public function displayNameGetter (taxon : Taxon): String { return taxon.displayName; } public function scientificNameGetter (taxon : Taxon): String { return taxon.scientificName; } public def namePropertyGetters = [displayNameGetter, scientificNameGetter];
類托架外面定義的函數和變量相當於Java靜態分析。
這裡我們省略了一些不相關的實際項目。基本上,該模式暴露了三個屬性,其中有意思的兩個分別是displayName和scientificName。我們也可以定義兩個函數來處理這兩個問題,我們會把這些函數放在namePropertyGetters序列中。
package it.tidalwave.bluebillmfx.taxon.controller; import it.tidalwave.bluebillmfx.taxon.model.Taxon; public class TaxonSearchController { public var selectedTaxon = bind if (selectedTaxonIndex < 0) then null else filteredTaxons[selectedTaxonIndex]; public var selectedTaxonIndex : Integer = -1; public var taxons: Taxon[]; public var filter = "" on replace { filteredTaxons = taxons[taxon | matches(taxon, filter)]; update(); } public-read var autoCompleted = ""; public var filteredTaxons: Taxon[]; protected function matches (taxon : Taxon, string: String) : Boolean { if (string == "") { return true; } for (propertyGetter in Taxon.namePropertyGetters) { if (propertyGetter(taxon).toLowerCase().startsWith(filter.toLowerCase())) { return true; } } return false; } protected function update(): Void { def autoCompletedTry = commonLeadingSubstring(filteredTaxons, findMatchingPropertyGetter()); // // Sometimes it can't find a better auto-completion than the current filter, since it searches the displayName // and the scientificName at the same time. In this case, we just ignore the new value. // if (autoCompletedTry.length() > filter.length()) { autoCompleted = autoCompletedTry; } selectedTaxonIndex = if (sizeof filteredTaxons == 1) then 0 else -1; println("selectedTaxonIndex: {selectedTaxonIndex}") } protected function findMatchingPropertyGetter(): function (:Taxon): String { for (taxon in filteredTaxons) { for (propertyGetter in Taxon.namePropertyGetters) { if (propertyGetter(taxon).toLowerCase().startsWith(filter.toLowerCase())) { return propertyGetter; } } } return null; } // some stuff later }
這個類揭示了以下的屬性:
·taxons:你需要用完整的鳥類列表來填充
·filter: 字符串包括需要輸入到搜索欄中的文本
·filteredTaxons: 種類由filter字符串過濾
·autoCompleted: 控制器猜測的自動完成輸入字符串
·selectedTaxon: 如果filter向下細分種類,它就會分配到這個變量
·selectedTaxonIndex: -1如果無法獲取時,selectedTaxon的索引。
最新的四種屬性由客戶代碼來綁定,這樣做可以獲取更改提示。
Filter獲取了一個觸發事件,也就是變量值更改時所執行的代碼。觸發器用JavaFX運算符 ︳執行了過濾操作:我們可以將觸發事件的第一行當作分配到taxons序列中的filteredTaxons來讀取,在這一序列中,matches()函數返回值為true。第二行的代碼調用了接下來要介紹的update()函數。
出於某些原因,這種方法並不一定奏效,因為filteredTaxons通常會被整體掃描。有多種方法可用來加速選擇過程,但是本文不會在這一方法真正應用到手機前前作出過早的優化。在筆記本上,它可以加快1000個項目的速度。
Matches()函數在所有屬性上執行了一次迭代以獲取函數並檢查看相關屬性是否以過濾值啟動。
創建獲得屬性值函數的序列的一大好處是我們可以通過定義新的函數輕松添加新的匹配標准:例如,其他語言中的本地化名稱。控制器可能會使用在搜索過程中使用這些名稱,而我們則不需要再做多余修改。
Update()函數運算出了自動完成輸入提示。它會提取filteredTaxons序列以及用於當前選擇的獲取屬性函數,還會調用剛剛在字符串屬性的序列中找到了通用子字符串的commonLeadingSubstring()。它不是每次都會作出很好的自動完成輸入猜想,因此有時建議甚至比當前過濾器還短,而這種情況我們大可忽略不計。請不要忽視指定臨時變量的重要性:由於自動完成輸入可能被綁定,因此我們不想為其指定一個會迅速失效的值。
要明白這一點的重要性,這不僅僅是避免無用更新,還能避免程序被破壞。在實際程序中,自動完成輸入更改時,TextBox會更新,因此過濾器也會隨之更新:已經輸入了"cal"後,再輸入一個"i",那麼TextBox暫時會顯示"cali",然後自動輸入完成的猜測失敗,它會返回一個"cal",TextBox中的字符串會變為"cal":這時候你要堅持自己的想法!綁定確實很強大,但是它也同時具有負面效應。
最後一步操作中,代碼會檢查看我們是否獲取單獨的已選定鳥類。
或許,你對於自動完成輸入失敗的原因仍然感到很困惑。畢竟,我們正在逐步縮小項目列表。因此,如果你已經輸入了"cali",那麼所有經過過濾的種類會以"cali"開頭,對嗎?如果你過濾的是一套單一名稱,情況就應該是這樣;但是我們是同時對兩套名稱執行搜索,那麼就會產生矛盾。看看下例由"cali"過濾器選取的名稱組(英語,科學的):("Calandra Lark", "Melanocorypha calandra"), ("Dunlin", "Calidris alpina"), ("California Quail", "Callipepla californica")
另一個有意思的地方是findMatchPropertyGetter()。它必須猜測當前過濾器是否是以"英語"或"科學"名稱運行,而且它還會返回相關的屬性獲取函數。基本上,控制器已經獲取了matches()函數中的這一信息,但是我們會將其移走。可能會有人思考讓matches()函數返回一個以上的布林值,但是這是不可能的,因為它是由運算符 ︳過濾序列的時候使用的:該運算符需要一個布林值。或許我們可以為稍後調用信息的操作指定一個成員變量,不過此時的代碼應該會更具可讀性。
為了對文章進一步作補充說明,這裡給大家列出了最後兩個忽略的函數:
protected function commonLeadingSubstring (taxons: Taxon[], propertyGetter: function (:Taxon): String): String { if (sizeof taxons == 0) { return ""; } if (sizeof taxons == 1) { return propertyGetter(taxons[0]); } var common = propertyGetter(taxons[0]); for (other in taxons[1..]) { common = commonLeadingSubstring(common, propertyGetter(other)); if (common == "") { break; // don't waste time in further iterations, "" it's for sure the final result } } return root; } function commonLeadingSubstring (string1 : String, string2 : String): String { return if (string1.length() > string2.length()) { commonLeadingSubstring(string2, string1); } else if (string1 == "") { ""; } else if (string2.startsWith(string1)) { string1; } else { commonLeadingSubstring(string1.substring(0, string1.length() - 1), string2); } }
這裡的邏輯很簡單。通常主要的字符串搜索被分解成了臨近字符串對;而對於單一對的搜索則有遞歸執行。
這裡顯示了視圖類是如何綁定到控制器的:
package it.tidalwave.bluebillmfx.taxon.view; public class TaxonSearchScreen { public var taxons : Taxon[]; var filter = ""; public-read def controller = TaxonSearchController { taxons: bind taxons filter: bind filter } def autoCompleted = bind controller.autoCompleted on replace { if (autoCompleted != "") { filter = autoCompleted; } } def list = ListBox { items: bind controller.filteredTaxons }; def searchBox = TextBox { text: bind filter with inverse }; }
你必須用所有可得的種類加載taxon;ListBox會隨著過濾的種類自動更新,TextBox與過濾器是雙重指令型綁定。之所以需要雙重綁定是因為向搜索欄中輸入時,一個指令用於給控制器發出新的選擇命令,另一個則是自動完成輸入時的更新。