课程 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 中,时间格式通过字符表示:

LetterDate or Time ComponentExample
yYear1996; 96
MMonth in year (context sensitive)July; Jul; 07
DDay in year189
dDay in month7
HHour in day (0-23)0
mMinute in hour30
sSecond in minute55
SMillisecond978

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

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

  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 中,数字格式通过字符表示:

SymbolLocationMeaning
0NumberDigit(数字的占位符)
#NumberDigit, zero shows as absent(数字,但不显示前导零)
.NumberDecimal separator or monetary decimal separator
%Prefix or suffixMultiply 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"
复制代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值