面向大眾的移動技術: Overheard Word 的單詞和手勢
以編程方式將第三方的代碼集成到您的 Android UI
從 GitHub 或另一個存儲庫采集第三方代碼,可能讓您覺得自己像在糖果店里的小孩一樣,但還是需要一些技巧來將這些代碼與您的 Android UI 相集成。這個月,Andrew Glover 將向您展示如何利用基于 JSON 的單詞引擎和一些預焙制的滑動手勢功能,將 Overheard Word 演示應用程序提升一個層次。事實證明,Android 可以輕松容納第三方代碼,但如果希望應用程序的 UI 可以順利地運行它,您仍然必須執行一些縝密的邏輯。
如果您到目前為止已經閱讀并遵循本系列中的演示,那么就已經建立了一些基本的 Android 開發技能。除了搭建一個 Android 開發環境并編寫您的***個 Hello World 應用程序,您已經學會了如何在一個自定義的移動應用程序中用滑動手勢代替按鈕輕觸,并實施菜單(或工具欄)和圖標。在本文中,您繼續沿著這條軌跡,學習如 何使用第三方庫來增加或增強應用程序的功能。首先,我們將安裝一些開源庫并讀取文件,然后將以編程方式集成新的功能與一個演示應用程序的 UI。
正如我在以前的文章中所做的,我會用我的 Overheard Word 應用程序進行演示。如果您還沒有克隆 Overheard Word 的 GitHub 庫,您應該先這成這一步,以便可以執行后面的步驟。
Thingamajig:一個可插拔的單詞引擎
Overheard Word 是英語語言的應用程序,可以幫助用戶學習新單詞,并動態建立詞匯表。在以前的文章中,我們首先開發了一個基本的應用程序,然后增加了 滑動手勢 實現更方便的導航,并增加了 圖標 形成更漂亮的 UI。到目前為止,一切都很好,但這個應用程序不會走得很遠,因為它缺少某種成分:Overheard Word 需要一些單詞!
為了使 Overheard Word 成為真正的多詞 應用程序,我已經建立了一個小型單詞引擎 Thingamajig,它封裝單詞的概念及其相應的定義。Thingamajig 處理通過 JSON 文檔創建單詞及其定義,并且完全沒有依賴于 Android。像 Overheard Word 應用程序一樣,我的單詞引擎 托管在 GitHub 上。您可以克隆存儲庫,或下載源代碼,然后運行 ant
。
您***會獲得一個 8KB 的輕量級 JAR 文件,可以將該文件復制到您的 libs
目錄中。如果您的 Android 項目已正確設置,那么 IDE 會自動將 libs
目錄中的任何文件識別為一個依賴關系。
當時,我的單詞引擎中的代碼是通過 JSON 文檔實例進行初始化的。JSON 文檔可以是駐留在設備上的文件系統中的一個文件、對 HTTP 請求的響應,甚至是對數據庫查詢的響應 — 就目前來說,這并不重要。重要的是,您有一個實用的庫,讓您可以使用 Word
對象。每個 Word
對象包含一個 Definition
對象的集合,以及一個相應的詞性。雖然單詞及其定義之間的關系遠不是如此簡單,但它現在是可行的。之后,我們可以添加例句、同義詞和反義詞。
Android 中的第三方庫
將第三方庫集成到 Android 項目很容易;事實上,每一個 Android 啟動項目都包括一個特殊的目錄 libs
,您可以將第三方 JAR 放在該目錄中。在 Android 的構建過程中,普通的 JVM 文件和第三方 JAR 都被轉換為可以兼容 Dalvik(這是專用的 Android VM)。
除了將單詞引擎庫插入到我的應用程序中,我還準備添加另一個 名稱為 Gesticulate 的第三方應用程序。就像使用單詞引擎庫一樣,您可以通過從 GitHub 上 克隆或下載 它的源代碼來獲得 Gesticulate。然后運行 ant
,您會得到一個 JAR 文件,您可以將這個文件放進應用程序的 libs
目錄。
更新 UI
現在,我們進入本文的核心,即更新 UI,以集成您剛剛免費搶購到的所有第三方代碼。幸運的是,我利用我的 Overheard Word 應用程序提前為這一刻做好了計劃?;氐轿揖帉懺搼贸绦虻?***次迭代 時,我定義了一個簡單的布局,其中包括一個單詞、其詞性,以及一個定義,如 圖 1 所示:
圖 1. Overheard Word 的默認視圖
該布局使用占位符文本定義,我準備使用從我的可插拔單詞引擎獲取的實際單詞替換占位符文本。所以,我會初始化一系列單詞,抓取其中一個,并使用其值相應地更新 UI。
為了更新 UI,我必須能夠獲得每個視圖元素的一個句柄,并為這些元素提供值。例如,一個顯示的單詞(如 Pedestrian)被定義為一個 TextView
,其 ID 是 word_study_word
,如 清單 1 所示:
清單 1. 在一個布局中定義的 TextView
- <TextView
- android:id="@+id/word_study_word"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:layout_marginBottom="10dp"
- android:layout_marginTop="60dp"
- android:textColor="@color/black"
- android:textSize="30sp"
- android:text="Word"/>
如果您仔細看的話,就會發現我已經在 XML 中將文本設置為 “Word
”;但是,我能夠以編程方式,通過獲取 TextView
實例的引用,并調用 setText
來設置該值。
在可以進行下一步之前,我需要建立一個單詞列表。您或許還記得我的單詞引擎可以通過 JSON 文檔創建 Word
實例,因此,我需要做的只是設置我的 Android 應用程序,讓它包括并讀取這些自定義文件。
使用文件:res 和 raw 目錄
默認情況下,一個 Android 應用程序具有讀取設備的基礎文件系統的權限,但您只可以訪問應用程序本身下面的本地文件系統。因此,您可以在您的應用程序中包括文件,并相應地引用它們。(這意味著,您可以讀取和寫入相對于您的應用程序是本地 的文件;寫入到 SD 卡等在您的應用程序之外的文件系統則需要專門的權限。)因為我的單詞引擎可以采用一個 JSON 文檔來初始化單詞列表,所以就沒有什么可以阻止我包括一個應用程序會在運行時讀取的 JSON 文檔。
就像在我以前的文章中所演示的圖標資產一樣,您可以將文件存儲在 res
目錄中。如果我發現自己需要自定義文件,我喜歡將它們添加到被稱為 raw
的一個目錄中。我放在該目錄中的任何文件都可以通過生成的 R
文件來引用,我在 Overheard Word 中已經使用過幾次該文件?;旧?,Android 平臺利用來自 res
目錄的資產,并建立一個名稱為 R
的類,然后,該類為這些資產提供一個句柄。如果資產是一個文件,那么 R
文件將提供一個引用,以打開文件并獲取其內容。
我在 res
目錄結構中創建一個 raw
目錄,并將一個 JSON 文檔放在該目錄中,如 圖 2 所示:
圖 2. 包含新單詞的 raw 目錄
接下來,Eclipse 重新構建項目,而我的 R
文件可以方便地引用新文件,如 圖 3 所示:
圖 3. 更新后的 R 文件
當我有一個文件的句柄時,我就可以打開它,讀取它,并最終構建一個 JSON 文檔來作為生成一個單詞列表的基礎。
構建一個單詞列表
當應用程序啟動時,我就開始執行一系列步驟,加載原始 JSON 文檔,并構建一個單詞列表。我將創建一個名稱為 buildWordList
的方法來處理這些步驟,如 清單 2 所示:
清單 2. 在 Android 中讀取文件的內容
- private List<Word> buildWordList() {
- InputStream resource =
- getApplicationContext().getResources().openRawResource(R.raw.words);
- List<Word> words = new ArrayList<Word>();
- try {
- StringBuilder sb = new StringBuilder();
- BufferedReader br = new BufferedReader(new InputStreamReader(resource));
- String read = br.readLine();
- while (read != null) {
- sb.append(read);
- read = br.readLine();
- }
- JSONObject document = new JSONObject(sb.toString());
- JSONArray allWords = document.getJSONArray("words");
- for (int i = 0; i < allWords.length(); i++) {
- words.add(Word.manufacture(allWords.getJSONObject(i)));
- }
- } catch (Exception e) {
- Log.e(APP, "Exception in buildWordList:" + e.getLocalizedMessage());
- }
- return words;
- }
您應該注意到在 buildWordList
方法中執行的一些操作。首先,請注意如何使用 Android 平臺的調用創建 InputStream
,Android 平臺的調用最終引用在 raw
目錄中發現的 words.json
文件。我沒有使用 String
來代表路徑,這使得代碼可跨多種設備進行移植。接下來,我使用包含 在 Android 平臺中的一個簡單的 JSON 庫,將內容(通過 String
表示)轉換成一系列的 JSON 文檔。在 Word
上的靜態方法 manufacture
讀取一個 JSON 文檔,該文檔代表一個單詞。
單詞的 JSON 格式如 清單 3 所示:
清單 3. JSON 代表一個單詞
- {
- "spelling":"sagacious",
- "definitions":[
- {
- "part_of_speech":"adjective",
- "definition":"having or showing acute mental discernment
- and keen practical sense; shrewd"
- }
- ]
- }
我的 WordStudyEngine
類是 Thingamajig 的主要外觀。它從 Word
實例列表產生隨機單詞和函數。所以,我的下一步是利用新建的 Word
List
初始化引擎,如 清單 4 所示:
清單 4. 初始化 WordStudyEngine
- List<Word> words = buildWordList();
- WordStudyEngine engine = WordStudyEngine.getInstance(words);
當有一個已初始化的引擎實例,我就可以向其請求一個單詞(自動隨機排列),然后相應地更新 UI 的三個元素。例如,我可以更新定義的單詞 部分,如 清單 5 所示:
清單 5. 以編程方式更新 UI 元素
- Word aWord = engine.getWord();
- TextView wordView = (TextView) findViewById(R.id.word_study_word);
- wordView.setText(aWord.getSpelling());
清單 5 中的 findViewById
是一個 Android 平臺調用,它讀取一個整數 ID,您可以從您的應用程序的 R
類中獲得該 ID。您能夠以編程方式將文本設置為 TextView
。您也可以設置字體類型,字體顏色,或文字顯示的大小,類似于這樣:wordView.setTextColor(Color.RED)
。
在 清單 6 中,我基本上按照相同的流程來更新應用程序的 UI 的定義和詞性元素:
清單 6. 更多編程式更新
- Definition firstDef = aWord.getDefinitions().get(0);
- TextView wordPartOfSpeechView = (TextView) findViewById(R.id.word_study_part_of_speech);
- wordPartOfSpeechView.setText(firstDef.getPartOfSpeech());
- TextView defView = (TextView) findViewById(R.id.word_study_definition);
- defView.setText(formatDefinition(aWord));
請注意在 清單 5 和 清單 6 中,如何使用 R
文件按名稱引用布局元素。駐留在我的 Activity
類中的 formatDefinition
方法讀取一個定義字符串,并將其首字母變成大寫。該方法還格式化字符串,若句末沒有句號,它就會在句末使用一個句號。(請注意,Thingamajig 與格式化沒有任何關系 — 它只是一個單詞引擎!)
我已完成這些 UI 元素的更新,所以就可以啟動我的應用程序,并檢查結果。瞧!我現在要學習一個合法的單詞!
圖 4. Overheard Word 有單詞了!
添加手勢:將滑動連接到單詞
現在,我可以有效地顯示一個單詞,我希望讓用戶能夠快速滑動我的單詞引擎中的所有單詞。為了更簡單,我準備使用 Gesticulate,這是我自己的第三方庫,可以計算滑動速度和方向。我也把滑動邏輯放進一個名稱為 initializeGestures
的方法中。
初始化了滑動手勢后,***步是將顯示一個單詞的邏輯移動到一個新方法中,當有人滑動時,我可以調用該方法來顯示一個新單詞。更新后的 onCreate
方法(最初是在 Android 創建應用程序實例時調用它)如 清單 7 所示:
清單 7. 當初始化手勢時,onCreate 即可顯示一個單詞
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Log.d(APP, "onCreated Invoked");
- setContentView(R.layout.activity_overheard_word);
- initializeGestures();
- List<Word> words = buildWordList();
- if (engine == null) {
- engine = WordStudyEngine.getInstance(words);
- }
- Word firstWord = engine.getWord();
- displayWord(firstWord);
- }
請注意 engine
變量,我將它定義為 OverheardWord Activity
本身的一個 private static
成員變量。我將簡單地解釋為什么要這樣做。
接下來,我進入 initGestureDetector
方法,如果您按照所克隆的代碼,就會發現在 initializeGestures
方法中引用了它。如果你還記得,initGestureDetector
方法有一個邏輯,當用戶在設備屏幕上向上、向下、向左或向右滑動時,它就會執行一個操作。在 Overheard Word 中,當用戶從右到左滑動時(這是左滑),我想顯示一個新單詞。我首先刪除 Toast
消息,這是該代碼的占位符,將其替換為一個對 displayWord
的調用,如 清單 8 所示:
清單 8. 當向左滑動時,initGestureDetector 即可顯示一個單詞
- private GestureDetector initGestureDetector() {
- return new GestureDetector(new SimpleOnGestureListener() {
- public boolean onFling(MotionEvent e1, MotionEvent e2,
- float velocityX, float velocityY) {
- try {
- final SwipeDetector detector = new SwipeDetector(e1, e2, velocityX, velocityY);
- if (detector.isDownSwipe()) {
- return false;
- } else if (detector.isUpSwipe()) {
- return false;
- } else if (detector.isLeftSwipe()) {
- displayWord(engine.getWord());
- } else if (detector.isRightSwipe()) {
- Toast.makeText(getApplicationContext(),
- "Right Swipe", Toast.LENGTH_SHORT).show();
- }
- } catch (Exception e) {}
- return false;
- }
- });
- }
我的單詞引擎由 engine
變量表示,它需要在整個 Activity
中都可以被訪問。這就是為什么我將其定義為一個成員變量,以確保每次有左滑時都會顯示一個新單詞。
來回滑動
現在,當打開應用程序,并開始滑動,每當向左滑動時,我都會看到顯示一個新單詞和定義。但是,當向其他方向滑動時,我只會得到一條微小的消息,顯示我已經滑動了。如果我能夠通過向右滑動回到前一個單詞,豈不是更好嗎?
回退似乎不應該是困難的。也許我可以使用一個堆棧,只是彈出由前一次左滑放在那里的***元素?但是,當用戶在回退后再次左滑時,這種想法是站不住腳的。如果***的位置被彈出,則將顯示一個新 單詞,而不是用戶以前看到的單詞。仔細思考后,我傾向于嘗試一個鏈表的方法,當用戶來回滑動時,可以擺脫之前瀏覽的單詞。
我將首先創建所有已顯示單詞的一個 LinkedList
,如 清單 9 所示。然后,我會保持讓一個指針指向該列表中的元素的索引,我可以用它來檢索已經看到的單詞。當然,我會將這些定義為 static
成員變量。我也將我的指針初始化為 -1
,并且每當將一個新單詞添加到 LinkedList
實例時,我就將其遞增。就像任何語言中大部分類似于數組支持的集合一樣,LinkedList
是從 0 開始建立索引。
清單 9. 新的成員變量
- private static LinkedList<Word> wordsViewed;
- private static int viewPosition = -1;
在 清單 10 中,我將我的應用程序的 onCreate
中的 LinkedList
初始化:
清單 10. 初始化 LinkedList
- if (wordsViewed == null) {
- wordsViewed = new LinkedList<Word>();
- }
現在,當顯示***個單詞時,我需要將其添加到 LinkedList
實例(wordsViewed
)并遞增指針變量 viewPosition
,如 清單 11 所示:
清單 11. 不要忘記遞增視圖位置
- Word firstWord = engine.getWord();
- wordsViewed.add(firstWord);
- viewPosition++;
- displayWord(firstWord);
滑動的邏輯
現在,我進入邏輯 的主要部分,這需要一些思考。當用戶向左滑動時,我想顯示下一個單詞,當他或她向右滑動時,我要顯示前一個單詞。如果用戶再次向右滑動,那么應該顯示第二 個之前的單詞,以此類推。應該繼續該操作,直到用戶回到***個顯示的單詞。然后,如果用戶開始再次向左滑動,單詞列表應該以和之前相同的順序出現。***的 部分有點棘手,因為我的 WordStudyEngine
的默認實現是以隨機的 順序返回一個單詞。
在 清單 12 中,我覺得自己已經解決了想要執行的操作的邏輯。***個布爾值賦值被封裝在一個稱為 listSizeAndPositionEql
的方法中,它很簡單:wordsViewed.size() == (viewPosition + 1)
。
如果 listSizeAndPositionEql
被賦值為 true,那么應用程序通過 displayWord
方法顯示一個新單詞。然而,如果 listSizeAndPositionEql
是 false,那么用戶必須向后滑動;因此,使用 viewPosition
指針從列表中檢索相應的單詞。
清單 12. 用于來回滾動的 LinkedList
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) {
- try {
- final SwipeDetector detector = new SwipeDetector(e1, e2, velX, velY);
- if (detector.isDownSwipe()) {
- return false;
- } else if (detector.isUpSwipe()) {
- return false;
- } else if (detector.isLeftSwipe()) {
- if (listSizeAndPositionEql()) {
- viewPosition++;
- Word wrd = engine.getWord();
- wordsViewed.add(wrd);
- displayWord(wrd);
- } else if (wordsViewed.size() > (viewPosition + 1)) {
- if (viewPosition == -1) {
- viewPosition++;
- }
- displayWord(wordsViewed.get(++viewPosition));
- } else {
- return false;
- }
- } else if (detector.isRightSwipe()) {
- if (wordsViewed.size() > 0 && (listSizeAndPositionEql() || (viewPosition >= 0))) {
- displayWord(wordsViewed.get(--viewPosition));
- } else {
- return false;
- }
- }
- } catch (Exception e) {}
- return false;
- }
注意,如果 viewPosition
是 -1
,那么用戶已一直向后滑動到起點 — 在這種情況下,列表中的指針被遞增回到 0。這是因為基于 Java 的 LinkedList
實現中沒有 -1
元素。(在某些其他語言中,負位置值從列表的后部開始,所以 -1
是尾元素。)
一旦掌握了左滑,處理右滑邏輯就會相當容易。實際上,如果在 wordsViewed
實例中有一個單詞,那么您需要使用一個遞減的指針值來訪問先前查看過的單詞。
試試看:啟動一個 Overheard Word 實例,并來回滑動,以學習一些單詞。每次滑動時,UI 將被相應地更新。
結束語
Overheard Word 一直運行得很好,現在,我們已經開始在基本的 Android 外殼之上添加一些更有趣的特性。當然,還有更多的事情要做,但我們現在擁有一個正常運作的應用程序,它可以處理滑動,有圖標和菜單,甚至可以從一個自定義 的第三方單詞文件中讀取。下個月,我會告訴您如何在 Overheard Word 中添加更多樣式,然后實現一個靈活的測驗特性。