面向大众的移动技术: Overheard Word 的单词和手势

以编程方式将第三方的代码集成到您的 Android UI

从 GitHub 或另一个存储库采集第三方代码,可能让您觉得自己像在糖果店里的小孩一样,但还是需要一些技巧来将这些代码与您的 Android UI 相集成。这个月,Andrew Glover 将向您展示如何利用基于 JSON 的单词引擎和一些预焙制的滑动手势功能,将 Overheard Word 演示应用程序提升一个层次。事实证明,Android 可以轻松容纳第三方代码,但如果希望应用程序的 UI 可以顺利地运行它,您仍然必须执行一些缜密的逻辑。

Andrew Glover, CTO, App47

Andrew GloverAndrew Glover 是开发人员、作家、演讲家和企业家,酷爱行为驱动开发、持续集成和敏捷软件开发。他是 easyb 行为驱动开发 (BDD) 框架的创始人,还是三部图书的合著者:Continuous IntegrationGroovy in ActionJava Testing Patterns。您可以通过他的 博客 并在 Twitter 上关注他来掌握他的最新动态。



2013 年 8 月 29 日

如果您到目前为止已经阅读并遵循 本系列 中的演示,那么就已经建立了一些基本的 Android 开发技能。除了搭建一个 Android 开发环境并编写您的第一个 Hello World 应用程序,您已经学会了如何在一个自定义的移动应用程序中用滑动手势代替按钮轻触,并实施菜单(或工具栏)和图标。在本文中,您继续沿着这条轨迹,学习如何使用第三方库来增加或增强应用程序的功能。首先,我们将安装一些开源库并读取文件,然后将以编程方式集成新的功能与一个演示应用程序的 UI。

正如我在以前的文章中所做的,我会用我的 Overheard Word 应用程序进行演示。如果您还没有克隆 Overheard Word 的 GitHub 库,您应该先这成这一步,以便可以执行后面的步骤。

关于本系列

移动应用程序发布正呈现爆炸式增长,移动开发技术正当其时。本系列文章将向那些有编程经验但又刚刚接触移动领域的开发人员介绍这个领域的发展情况。本系列首先使用 Java 代码编写本机应用程序,然后扩展您的工具箱,包括 JVM 语言、脚本框架、HTML5/CSS/JavaScript、第三方工具等。您将逐步掌握所需的技能,以便满足几乎所有移动开发场景的需求。

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)。

环境约束

虽然您可以想像得到如何将喜欢的任何第三方库添加到应用程序中,但永远不要忘记移动环境的局限性。运行您的应用程序的设备毕竟不是什么可靠的 Web 服务器!用户会感谢您提高数据处理的能力,并尽可能减少第三方附件的下载大小。

除了将单词引擎库插入到我的应用程序中,我还准备添加另一个 名称为 Gesticulate 的第三方应用程序。就像使用单词引擎库一样,您可以通过从 GitHub 上 克隆或下载 它的源代码来获得 Gesticulate。然后运行 ant,您会得到一个 JAR 文件,您可以将这个文件放进应用程序的 libs 目录。


更新 UI

现在,我们进入本文的核心,即更新 UI,以集成您刚刚免费抢购到的所有第三方代码。幸运的是,我利用我的 Overheard Word 应用程序提前为这一刻做好了计划。回到我编写该应用程序的 第一次迭代 时,我定义了一个简单的布局,其中包括一个单词、其词性,以及一个定义,如 图 1 所示:

图 1. Overheard Word 的默认视图
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 目录
显示一些单词的 raw 目录视图

接下来,Eclipse 重新构建项目,而我的 R 文件可以方便地引用新文件,如 图 3 所示:

图 3. 更新后的 R 文件
更新后的 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 实例列表产生随机单词和函数。所以,我的下一步是利用新建的 WordList 初始化引擎,如 清单 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 有单词了!
显示单词 ‘sagacious’ 的 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 一直运行得很好,现在,我们已经开始在基本的 Andr​​oid 外壳之上添加一些更有趣的特性。当然,还有更多的事情要做,但我们现在拥有一个正常运作的应用程序,它可以处理滑动,有图标和菜单,甚至可以从一个自定义的第三方单词文件中读取。下个月,我会告诉您如何在 Overheard Word 中添加更多样式,然后实现一个灵活的测验特性。

参考资料

学习

获得产品和技术

  • Overheard Word:一个正在开发中的 Android 演示应用程序,托管在 GitHub 上。
  • Thingamajig:一个简单的单词定义引擎,用于 Overheard Word。
  • Gesticulate:为您执行 Android 滑动检测的计算。
  • 下载 Android:Android SDK 为您提供了 API 库和开发人员工具,可使用这些资源为 Android 构建、测试和调试应用程序。

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=942864
ArticleTitle=面向大众的移动技术: Overheard Word 的单词和手势
publish-date=08292013