以编程方式将第三方的代码集成到您的 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 中添加更多样式,然后实现一个灵活的测验特性。