课程 1: JSON 解析

结束 Android 开发(入门)课程 的第二部分《多屏幕应用》后,来到第三部分《访问网络》,这部分课程要完成一个名为 Quake Report 的应用,这个应用从网络获取数据,列出全球范围内最近发生的地震信息,包括时间、地点、震级。

Quake Report App 分三节课完成,每个课程的进度分配如下:

  1. JSON Parsing
    从 USGS API 请求数据,了解返回的数据结构,并提取数据。
  2. HTTP Networking
    将数据输入 App,涉及 Android 权限、后台线程等内容。
  3. Threads & Parallelism
    了解 HTTP 请求的端对端路径,实时更新数据,并显示出来。

这是第三部分《访问网络》的第一节课,导师是 Chris Lei 和 Joe Lewis。这节课的重点是解析 API 返回的 HTTP 响应中包含的 JSON。因为此前课程都没有涉及网络访问的内容,所以这节课会循序渐进地介绍相关知识。首先了解 USGS API,再在导入已有代码后通过 Java 提取和格式化元数据,然后暂时通过 JSON 响应示例作为占位符数据,最后优化布局。

关键词:API、JSON、Key/Value Pair、Traversal Path、JSONObject、Utility class、SimpleDateFormat、String.split、DecimalFormat、drawable.shape、GradientDrawable、ContextCompat.getColor、switch Statement、Math.floor

USGS API

Quake Report App 要从网络获取地震数据,那么就要用到 API。API(应用程序编程接口,Application Programming Interface)是一个软件产品或服务将其一部分功能或数据提供给其它软件使用的一种方法,API 的提供者和使用者互相形成一种编程合作关系 (Programming Partnership),相互创造出更大的价值。针对地震数据的 API,Google 搜索 "earthquake api" 可以找到 USGS(美国地质勘探局,U. S. Geological Survey)网站提供相应的技术支持。点击 "For Developers" 目录下的 "API Documentation" 可以看到,USGS API 支持通过 URL 请求数据,格式如下:

https://earthquake.usgs.gov/fdsnws/event/1/[METHOD][?[PARAMETERS]]

在这里 URL 可分为三部分:

  1. 头部 https://earthquake.usgs.gov/fdsnws/event/1/,固定不变。
  2. 按照不同数据需求接上 METHOD,支持 catalogs、count、query 等。Quake Report App 要获取地震信息,所以这里用到 query
  3. 最后添加参数 ?[PARAMETERS],支持数据格式、时间、震级等。参数无需用 [] 包括,第一个参数前用 ? 连接,参数之间用 & 连接。

例如要获取 2014 年 1 月 1 日至 2014 年 1 月 2日之间震级大于五级的地震数据,并以 GeoJSON 格式返回数据,那么向 USGS API 请求数据的 URL 如下:

https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&starttime=2014-01-01&endtime=2014-01-02&minmagnitude=5
  1. methodquery,后面用 ? 连接参数。
  2. parameters 有四个,相互之间用 & 连接,参数分别为
    (1)format=geojson 指定数据以 GeoJSON 格式返回;
    (2)starttime=2014-01-01 指定数据开始时间为 2014 年 1 月 1 日;
    (3)endtime=2014-01-02 指定数据截止时间为 2014 年 1 月 2日;
    (4)minmagnitude=5 指定数据的震级为五级以上。

API 返回的 GeoJSON 数据 没有可读性,可以复制到 JSON Pretty Print 网站 格式化后查看。例如 "time" 是用 Unix 时间戳(从 1970 年 1 月 1 日零时开始所经过的毫秒数,整数,便于时间计算,详细信息可以观看这个 YouTube 视频)的形式记录,表示地震发生的时间;"felt" 表示 USGS 网站用户反馈的震感;"tsunami" 是一个布尔类型数据,表示地震是否触发海啸预警;"title" 是包含震级和震源地的英文字符;"coordinates" 是一个三维数据,包含震源的经度、纬度、深度。

JSON

事实上,USGS API 支持多种格式的响应数据,包括 CSV、XML、GeoJSON 等,这里选择 GeoJSON 并不意味着它是最好的,而是因为 JSON 是现今许多签名 Web 服务中最常用的响应格式,GeoJSON 则是 JSON 的一种特殊风格,定制用于表示地理信息。对于开发者而言,拥有使用 JSON 的经验后,其它格式也能快速上手。

JSON 的全称是 JavaScript Object Notation,但其实它与 Javascript 语言并不相关,名称用 JS 开头是因为最初设计时为了促进 Web 的有效沟通。实际上 JSON 是组织数据的一种策略型规则,它是独立的数据交换格式,可以使用任何语言解析,例如 Android 用到的 Java 语言。

JSON 结构

1   {
2       "size" : 9.5,
3       "wide" : true,
4       "country-of-origin" : "usa",
5       "style" : {
6           "categories" : [ "boot", "winklepicker" ],
7           "color" : "black"
8       }
9   }

上面是一段简单的 JSON 示例,虽然 JSON 采用完全独立于语言的文本格式,但是也使用了类似于 C 语言家族 (C++, Java, Python) 的习惯,包括字符串、对象、数组等。

  1. 第 2、3、4、7 行的格式相同,称为键/值对 (Key/Value Pair)。
    (1)冒号 : 左侧的是键 (Key),由 "" 包括,表示一类数据的名称。
    (2)冒号 : 右侧的是值 (Value),表示一类数据的值,可以是数值、布尔类型、字符串、数组、对象等。其中字符串由 "" 包括,使用 \ 转义。
    (3)键/值对之间用 , 分隔。
  2. 第 6 行的键/值对,键是 categories,值是一个数组,由 [] 包括,元素之间用 , 分隔。
  3. 第 5 行的键/值对,键是 style,值是一个对象,由 {} 包括。对象是键/值对的集合,这里是 categoriescolor 两个键/值对的集合,这就形成了嵌套结构。
  4. 整个 JSON 文件由 {} 包括,所以一个 JSON 就是一个对象,名称常用 root 表示。

详细的 JSON 结构介绍可以到官网查看。

JSON 对象树

JSON 存在嵌套结构,也就产生了 JSON 对象树,要访问其中的节点,就有了遍历路径 (Traversal Path) 的概念。例如要访问上面的 JSON 示例中的第一个 categories 元素,遍历路径如下:

Root > JSONObject with key "style" > JSONArray with key "categories" >  Look for the first element in the array

JSON 对象树节点的遍历路径对解析 JSON 至关重要,它的作用与之前提到的伪代码 (Pseudo code) 的作用类似,帮助开发者理清编程思路。复杂的 JSON 文件可以复制到 JSON Formatter 网站 格式化后,选择折叠或展开节点查看。

在 Android 中解析 JSON

// Create a JSONObject from the SAMPLE_JSON_RESPONSE string
JSONObject baseJsonResponse = new JSONObject(SAMPLE_JSON_RESPONSE);

// Extract the JSONArray associated with the key called "features",
// which represents a list of features (or earthquakes).
JSONArray earthquakeArray = baseJsonResponse.getJSONArray("features");

// For each earthquake in the earthquakeArray, create an {@link Earthquake} object
for (int i = 0; i < earthquakeArray.length(); i++) {
    // Get a single earthquake at position i within the list of earthquakes
    JSONObject currentEarthquake = earthquakeArray.getJSONObject(i);
    // For a given earthquake, extract the JSONObject associated with the
    // key called "properties", which represents a list of all properties
    // for that earthquake.
    JSONObject properties = currentEarthquake.getJSONObject("properties");
    // Extract the value for the key called "mag"
    double magnitude = properties.getDouble("mag");
    // Extract the value for the key called "place"
    String location = properties.getString("place");
    // Extract the value for the key called "time"
    long time = properties.getLong("time");
    // Extract the value for the key called "url"
    String url = properties.getString("url");

    // Add the new {@link Earthquake} to the list of earthquakes.
    earthquakes.add(new Earthquake(magnitude, location, time, url));
}

Android 提供了强大的 JSONObject class 用于解析 JSON,在了解遍历路径后通过丰富的 getter method 即可灵活处理 JSON。

  1. 解析 JSON 的代码放在一个 Utility class 内,该类的构造函数为 private,表示不应该创建 Utility 对象,因为 Utility class 只用于存放静态变量 (static variable) 和 static method,它们可以直接用类名访问,无需实例化。
  2. 这节课先验证 JSON 解析,利用 JSONObject(String json) 构造函数传入一个占位符数据,新建 JSONObject 对象,名为 baseJsonResponse,对应的 JSON 对象树节点为 Root。
  3. 针对 Quake Report App 需要的震级、地点、时间、URL 信息,按照遍历路径通过相应的 getter method 获取数据。
    (1)通过 getJSONArray 获取 Root 内键为 features 的数组;
    (2)通过 length() 获取 features 数组的长度;
    (3)通过 getJSONObject 获取 features 数组的元素 currentEarthquake 对象;
    (4)通过 getJSONObject 获取 currentEarthquake 对象内的元素 properties 对象;
    (5)通过 getDouble 获取 properties 对象内的元素 mag double 数值;
    (6)通过 getString 获取 properties 对象内的元素 place 字符串;
    (7)通过 getLong 获取 properties 对象内的元素 time long 数值;
    (8)通过 getString 获取 properties 对象内的元素 url 字符串;
  4. 上述 getter method 在传入不存在的键时会产生 JSONException 错误,这里可以使用对应的 opt method 代替,例如 optString(String name) 在传入不存在的字符串时会返回一个空的字符串,optInt(String name) 在传入无法转换为 int 的数据时会返回 0。

更多 JSONObject 的信息可以参考这个入门教程

功能实现和布局优化

设计师提供的应用 UI 原型,开发者要编程实现,双方合作设计出杀手级的用户体验 (Designing a killer user experience)。如果没有设计师也没有关系,多花点时间按照 Material Design 设计也可以写出优秀的应用。

  1. 显示可读的时间和日期

由于 USGS API 返回的地震发生时间数据是以 Unix 时间的形式记录的,在应用中要显示成可读的时间和日期。Android 提供了神奇的 SimpleDateFormat class 来处理这个问题:将 Unix 时间传入 Date 对象,新建 SimpleDateFormat 对象并指定所需的时间格式,最后调用 SimpleDateFormat.format method 就实现了时间的格式化。

// The time in milliseconds of the earthquake.
long timeInMilliseconds = 1454124312220L;
// Create a new Date object from the time in milliseconds of the earthquake.
Date dateObject = new Date(timeInMilliseconds);
// Create a new SimpleDateFormat object by assigning the format of the date.
SimpleDateFormat dateFormatter = new SimpleDateFormat("MMM DD, yyyy");
// Get the formatted date string (i.e. "Mar 3, 1984") from a Date object.
String dateToDisplay = dateFormatter.format(dateObject);

在 SimpleDateFormat 中,时间格式通过字符表示:

Letter Date or Time Component Example
y Year 1996; 96
M Month in year (context sensitive) July; Jul; 07
D Day in year 189
d Day in month 7
H Hour in day (0-23) 0
m Minute in hour 30
s Second in minute 55
S Millisecond 978

完整表格可以到 Android Developers 网站查看。

(1)区分大小写,例如 D 表示一年中的天数,d 表示一月中的天数。
(2)一个字符仅表示一位数字,例如 1996 年在 yyyy 时显示 1996,在 yy 时显示 96。
(2)所有未在特殊字符表中列出的字符都将在输出字符串中直接显示。例如,如果时间格式字符串包含 :-,,则输出字符串也将在相应位置直接包含相同的标点符号。

  1. 操控字符串

在 Quake Report App 中,需要将 USGS API 返回的地点数据分成两部分显示,第一行显示地震与城市之间的距离,第二行指定具体的城市。

// Get the original location string from the Earthquake object,
// which can be in the format of "5km N of Cairo, Egypt" or "Pacific-Antarctic Ridge".
String originalLocation = currentEarthquake.getLocation();

// If the original location string (i.e. "5km N of Cairo, Egypt") contains
// a primary location (Cairo, Egypt) and a location offset (5km N of that city)
// then store the primary location separately from the location offset in 2 Strings,
// so they can be displayed in 2 TextViews.
String primaryLocation;
String locationOffset;

// Check whether the originalLocation string contains the " of " text
if (originalLocation.contains(LOCATION_SEPARATOR)) {
    // Split the string into different parts (as an array of Strings)
    // based on the " of " text. We expect an array of 2 Strings, where
    // the first String will be "5km N" and the second String will be "Cairo, Egypt".
    String[] parts = originalLocation.split(LOCATION_SEPARATOR);
    // Location offset should be "5km N " + " of " --> "5km N of"
    locationOffset = parts[0] + LOCATION_SEPARATOR;
    // Primary location should be "Cairo, Egypt"
    primaryLocation = parts[1];
} else {
    // Otherwise, there is no " of " text in the originalLocation string.
    // Hence, set the default location offset to say "Near the".
    locationOffset = getContext().getString(R.string.near_the);
    // The primary location will be the full location string "Pacific-Antarctic Ridge".
    primaryLocation = originalLocation;
}

(1)通过 contains(CharSequence cs) 判断字符串是否包含指定的字符,其中由于 String 是 CharSequence 的扩展类,所以这里 CharSequence 作为输入参数时,可以传入 String。
(2)通过 split(String string) 根据输入参数指定的位置对字符串进行拆分,返回值为拆分后的字符串数组。拆分后的字符串与输入参数的匹配次数和位置有关,不包含输入参数字符,详细信息可以到 Android Developers 网站查看。
(3)除了上述操纵字符串的 method,另外几个常用的有 length() 获取字符串的字符数量,indexOf(String string) 返回输入参数首次在字符串匹配的位置索引,substring(int start, int end) 根据输入参数指定的起止位置对字符串进行裁剪,包括开始索引但不包括结束索引。

  1. 数字对齐

在 Quake Report App 中,需要将震级数字保留一位小数显示,所以要格式化数字。与格式化时间类似,Android 提供了 DecimalFormat class 来处理这个问题。NumberFormat class 也可用于处理所有类型数字的格式,但它是一个抽象类, 而 DecimalFormat 是一个具象类,因此 DecimalFormat 相对而言比较简单,特别是对于这种简单的数字格式化需求。

// Get the magnitude from Earthquake object.
double magnitude = currentEarthquake.getMagnitude();
// Create a new DecimalFormat object by assigning the format of the digit.
DecimalFormat formatter = new DecimalFormat("0.0");
// Get the formatted magnitude digit.
String formattedMagnitude = formatter.format(magnitude);

与 SimpleDateFormat 类似,在 DecimalFormat 中,数字格式通过字符表示:

Symbol Location Meaning
0 Number Digit(数字的占位符)
# Number Digit, zero shows as absent(数字,但不显示前导零)
. Number Decimal separator or monetary decimal separator
% Prefix or suffix Multiply by 100 and show as percentage

完整表格可以到 Android Developers 网站查看。

  1. 圆形背景

为 Quake Report App 的 magnitude TextView 添加圆形背景,由于背景颜色需要根据震级大小变化,所以在这里没有添加多个不同颜色的图像资源,而是通过在 XML 中定义圆圈形状,然后在 Java 中对颜色进行操作的方法实现,减少所需的资源数量。

(1)在 res/drawable 目录下添加 New > Drawable resource file

In magnitude_circle.xml

<!-- Background circle for the magnitude value -->
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="@color/magnitude1" />
    <size
        android:width="36dp"
        android:height="36dp" />
    <corners android:radius="18dp" />
</shape>

android:shape 属性设置为 oval(椭圆形),宽度、高度、转角半径三者配合好,画出一个半径为 18dp 的圆形。

(2)在 magnitude TextView 中应用 magnitude_circle.xml

android:background="@drawable/magnitude_circle"

(3)在 Java 中操作背景颜色

// Fetch the background from the TextView, which is a GradientDrawable.
GradientDrawable magnitudeCircle = (GradientDrawable) magnitudeView.getBackground();
// Get the appropriate background color based on the current earthquake magnitude
int magnitudeColor = getMagnitudeColor(currentEarthquake.getMagnitude());
//  Set the color on the magnitude circle
magnitudeCircle.setColor(magnitudeColor);

这里新建了一个 GradientDrawable 对象,指向 magnitude TextView 的背景,最终通过 setColor method 来改变背景颜色。中间是一个辅助 method,根据当前的地震震级返回正确的颜色值,代码如下:

/**
 * Return the color for the magnitude circle based on the intensity of the earthquake.
 *
 * @param magnitude of the earthquake
 */
private int getMagnitudeColor(double magnitude) {
    int magnitudeColorResourceId;
    int magnitudeFloor = (int) Math.floor(magnitude);
    switch (magnitudeFloor) {
        case 0:
        case 1:
            magnitudeColorResourceId = R.color.magnitude1;
            break;
        case 2:
            magnitudeColorResourceId = R.color.magnitude2;
            break;
        case 3:
            magnitudeColorResourceId = R.color.magnitude3;
            break;
        case 4:
            magnitudeColorResourceId = R.color.magnitude4;
            break;
        case 5:
            magnitudeColorResourceId = R.color.magnitude5;
            break;
        case 6:
            magnitudeColorResourceId = R.color.magnitude6;
            break;
        case 7:
            magnitudeColorResourceId = R.color.magnitude7;
            break;
        case 8:
            magnitudeColorResourceId = R.color.magnitude8;
            break;
        case 9:
            magnitudeColorResourceId = R.color.magnitude9;
            break;
        default:
            magnitudeColorResourceId = R.color.magnitude10plus;
            break;
    }

    return ContextCompat.getColor(getContext(), magnitudeColorResourceId);
}

(1)由于 GradientDrawable 的 setColor method 需要传入 int argb,而不是颜色资源的 ID,所以这里需要转换一下,用到 ContextCompat.getColor method。

(2)由于震级数值是非布尔类型的离散值,所以这里引入一种新的 switch 流控语句,它可以替代 if-else 的多级嵌套,免除每一层都需要判断变量值的重复工作。

  • switch 语句涉及了许多 Java 关键字,如 switchcasebreakdefault
  • switch 后的 () 内传入需要执行的参数,随后在 {} 内从上至下寻找 case 后匹配的数据,若输入参数匹配其中一个 case 后的数据,则运行 : 下的代码,直到运行至 break;
  • 如果 switch 的输入参数没有匹配任何 case 后的数据,那么代码会运行 default: 下的代码。虽然 default 代码不是强制的,但是为了增加代码的鲁棒性,通常都会写在 switch 语句的最后。
  • 如果 case 下的代码没有 break;,那么代码会运行到下一个 case,直到运行至 break;。因此,这种形式的代码实际上形成了一种或逻辑,例如上述一段代码的逻辑是,如果 magnitudeFloor 为 0 或 1,那么 magnitudeColorResourceId 赋为 R.color.magnitude1,然后跳出 switch 语句。

(3)由于 switch 语句无法输入 double 数值,所以这里需要震级值转换为 int,用到 Math.floor 将震级值的小数部分抹平。

  1. 布局优化

如果要隐藏 ListView 项目间的分隔线,可以在 XML 中设置以下两个属性:

android:divider="@null"
android:dividerHeight="0dp"

设置 TextView 的 ellipsizemaxLines 两个属性表示:如果 TextView 中的文本长度超过两行,就可以在文本结尾处中添加省略号 ("..."),而不是随内容增加行数。

android:ellipsize="end"
android:maxLines="2"
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270