課程 1: JSON 解析

結束 Android 開發(fā)(入門)課程 的第二部分《多屏幕應用》后,來到第三部分《訪問網絡》,這部分課程要完成一個名為 Quake Report 的應用,這個應用從網絡獲取數(shù)據,列出全球范圍內最近發(fā)生的地震信息,包括時間、地點、震級。

Quake Report App 分三節(jié)課完成,每個課程的進度分配如下:

  1. JSON Parsing
    從 USGS API 請求數(shù)據,了解返回的數(shù)據結構,并提取數(shù)據。
  2. HTTP Networking
    將數(shù)據輸入 App,涉及 Android 權限、后臺線程等內容。
  3. Threads & Parallelism
    了解 HTTP 請求的端對端路徑,實時更新數(shù)據,并顯示出來。

這是第三部分《訪問網絡》的第一節(jié)課,導師是 Chris Lei 和 Joe Lewis。這節(jié)課的重點是解析 API 返回的 HTTP 響應中包含的 JSON。因為此前課程都沒有涉及網絡訪問的內容,所以這節(jié)課會循序漸進地介紹相關知識。首先了解 USGS API,再在導入已有代碼后通過 Java 提取和格式化元數(shù)據,然后暫時通過 JSON 響應示例作為占位符數(shù)據,最后優(yōu)化布局。

關鍵詞: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 要從網絡獲取地震數(shù)據,那么就要用到 API。API(應用程序編程接口,Application Programming Interface)是一個軟件產品或服務將其一部分功能或數(shù)據提供給其它軟件使用的一種方法,API 的提供者和使用者互相形成一種編程合作關系 (Programming Partnership),相互創(chuàng)造出更大的價值。針對地震數(shù)據的 API,Google 搜索 "earthquake api" 可以找到 USGS(美國地質勘探局,U. S. Geological Survey)網站提供相應的技術支持。點擊 "For Developers" 目錄下的 "API Documentation" 可以看到,USGS API 支持通過 URL 請求數(shù)據,格式如下:

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

在這里 URL 可分為三部分:

  1. 頭部 https://earthquake.usgs.gov/fdsnws/event/1/,固定不變。
  2. 按照不同數(shù)據需求接上 METHOD,支持 catalogs、count、query 等。Quake Report App 要獲取地震信息,所以這里用到 query。
  3. 最后添加參數(shù) ?[PARAMETERS],支持數(shù)據格式、時間、震級等。參數(shù)無需用 [] 包括,第一個參數(shù)前用 ? 連接,參數(shù)之間用 & 連接。

例如要獲取 2014 年 1 月 1 日至 2014 年 1 月 2日之間震級大于五級的地震數(shù)據,并以 GeoJSON 格式返回數(shù)據,那么向 USGS API 請求數(shù)據的 URL 如下:

https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&starttime=2014-01-01&endtime=2014-01-02&minmagnitude=5
  1. methodquery,后面用 ? 連接參數(shù)。
  2. parameters 有四個,相互之間用 & 連接,參數(shù)分別為
    (1)format=geojson 指定數(shù)據以 GeoJSON 格式返回;
    (2)starttime=2014-01-01 指定數(shù)據開始時間為 2014 年 1 月 1 日;
    (3)endtime=2014-01-02 指定數(shù)據截止時間為 2014 年 1 月 2日;
    (4)minmagnitude=5 指定數(shù)據的震級為五級以上。

API 返回的 GeoJSON 數(shù)據 沒有可讀性,可以復制到 JSON Pretty Print 網站 格式化后查看。例如 "time" 是用 Unix 時間戳(從 1970 年 1 月 1 日零時開始所經過的毫秒數(shù),整數(shù),便于時間計算,詳細信息可以觀看這個 YouTube 視頻)的形式記錄,表示地震發(fā)生的時間;"felt" 表示 USGS 網站用戶反饋的震感;"tsunami" 是一個布爾類型數(shù)據,表示地震是否觸發(fā)海嘯預警;"title" 是包含震級和震源地的英文字符;"coordinates" 是一個三維數(shù)據,包含震源的經度、緯度、深度。

JSON

事實上,USGS API 支持多種格式的響應數(shù)據,包括 CSV、XML、GeoJSON 等,這里選擇 GeoJSON 并不意味著它是最好的,而是因為 JSON 是現(xiàn)今許多簽名 Web 服務中最常用的響應格式,GeoJSON 則是 JSON 的一種特殊風格,定制用于表示地理信息。對于開發(fā)者而言,擁有使用 JSON 的經驗后,其它格式也能快速上手。

JSON 的全稱是 JavaScript Object Notation,但其實它與 Javascript 語言并不相關,名稱用 JS 開頭是因為最初設計時為了促進 Web 的有效溝通。實際上 JSON 是組織數(shù)據的一種策略型規(guī)則,它是獨立的數(shù)據交換格式,可以使用任何語言解析,例如 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) 的習慣,包括字符串、對象、數(shù)組等。

  1. 第 2、3、4、7 行的格式相同,稱為鍵/值對 (Key/Value Pair)。
    (1)冒號 : 左側的是鍵 (Key),由 "" 包括,表示一類數(shù)據的名稱。
    (2)冒號 : 右側的是值 (Value),表示一類數(shù)據的值,可以是數(shù)值、布爾類型、字符串、數(shù)組、對象等。其中字符串由 "" 包括,使用 \ 轉義。
    (3)鍵/值對之間用 , 分隔。
  2. 第 6 行的鍵/值對,鍵是 categories,值是一個數(shù)組,由 [] 包括,元素之間用 , 分隔。
  3. 第 5 行的鍵/值對,鍵是 style,值是一個對象,由 {} 包括。對象是鍵/值對的集合,這里是 categoriescolor 兩個鍵/值對的集合,這就形成了嵌套結構。
  4. 整個 JSON 文件由 {} 包括,所以一個 JSON 就是一個對象,名稱常用 root 表示。

詳細的 JSON 結構介紹可以到官網查看。

JSON 對象樹

JSON 存在嵌套結構,也就產生了 JSON 對象樹,要訪問其中的節(jié)點,就有了遍歷路徑 (Traversal Path) 的概念。例如要訪問上面的 JSON 示例中的第一個 categories 元素,遍歷路徑如下:

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

JSON 對象樹節(jié)點的遍歷路徑對解析 JSON 至關重要,它的作用與之前提到的偽代碼 (Pseudo code) 的作用類似,幫助開發(fā)者理清編程思路。復雜的 JSON 文件可以復制到 JSON Formatter 網站 格式化后,選擇折疊或展開節(jié)點查看。

在 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 內,該類的構造函數(shù)為 private,表示不應該創(chuàng)建 Utility 對象,因為 Utility class 只用于存放靜態(tài)變量 (static variable) 和 static method,它們可以直接用類名訪問,無需實例化。
  2. 這節(jié)課先驗證 JSON 解析,利用 JSONObject(String json) 構造函數(shù)傳入一個占位符數(shù)據,新建 JSONObject 對象,名為 baseJsonResponse,對應的 JSON 對象樹節(jié)點為 Root。
  3. 針對 Quake Report App 需要的震級、地點、時間、URL 信息,按照遍歷路徑通過相應的 getter method 獲取數(shù)據。
    (1)通過 getJSONArray 獲取 Root 內鍵為 features 的數(shù)組;
    (2)通過 length() 獲取 features 數(shù)組的長度;
    (3)通過 getJSONObject 獲取 features 數(shù)組的元素 currentEarthquake 對象;
    (4)通過 getJSONObject 獲取 currentEarthquake 對象內的元素 properties 對象;
    (5)通過 getDouble 獲取 properties 對象內的元素 mag double 數(shù)值;
    (6)通過 getString 獲取 properties 對象內的元素 place 字符串;
    (7)通過 getLong 獲取 properties 對象內的元素 time long 數(shù)值;
    (8)通過 getString 獲取 properties 對象內的元素 url 字符串;
  4. 上述 getter method 在傳入不存在的鍵時會產生 JSONException 錯誤,這里可以使用對應的 opt method 代替,例如 optString(String name) 在傳入不存在的字符串時會返回一個空的字符串,optInt(String name) 在傳入無法轉換為 int 的數(shù)據時會返回 0。

更多 JSONObject 的信息可以參考這個入門教程

功能實現(xiàn)和布局優(yōu)化

設計師提供的應用 UI 原型,開發(fā)者要編程實現(xiàn),雙方合作設計出殺手級的用戶體驗 (Designing a killer user experience)。如果沒有設計師也沒有關系,多花點時間按照 Material Design 設計也可以寫出優(yōu)秀的應用。

  1. 顯示可讀的時間和日期

由于 USGS API 返回的地震發(fā)生時間數(shù)據是以 Unix 時間的形式記錄的,在應用中要顯示成可讀的時間和日期。Android 提供了神奇的 SimpleDateFormat class 來處理這個問題:將 Unix 時間傳入 Date 對象,新建 SimpleDateFormat 對象并指定所需的時間格式,最后調用 SimpleDateFormat.format method 就實現(xiàn)了時間的格式化。

// 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)區(qū)分大小寫,例如 D 表示一年中的天數(shù),d 表示一月中的天數(shù)。
(2)一個字符僅表示一位數(shù)字,例如 1996 年在 yyyy 時顯示 1996,在 yy 時顯示 96。
(2)所有未在特殊字符表中列出的字符都將在輸出字符串中直接顯示。例如,如果時間格式字符串包含 :-,,則輸出字符串也將在相應位置直接包含相同的標點符號。

  1. 操控字符串

在 Quake Report App 中,需要將 USGS API 返回的地點數(shù)據分成兩部分顯示,第一行顯示地震與城市之間的距離,第二行指定具體的城市。

// 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 作為輸入參數(shù)時,可以傳入 String。
(2)通過 split(String string) 根據輸入參數(shù)指定的位置對字符串進行拆分,返回值為拆分后的字符串數(shù)組。拆分后的字符串與輸入參數(shù)的匹配次數(shù)和位置有關,不包含輸入參數(shù)字符,詳細信息可以到 Android Developers 網站查看。
(3)除了上述操縱字符串的 method,另外幾個常用的有 length() 獲取字符串的字符數(shù)量,indexOf(String string) 返回輸入參數(shù)首次在字符串匹配的位置索引,substring(int start, int end) 根據輸入參數(shù)指定的起止位置對字符串進行裁剪,包括開始索引但不包括結束索引。

  1. 數(shù)字對齊

在 Quake Report App 中,需要將震級數(shù)字保留一位小數(shù)顯示,所以要格式化數(shù)字。與格式化時間類似,Android 提供了 DecimalFormat class 來處理這個問題。NumberFormat class 也可用于處理所有類型數(shù)字的格式,但它是一個抽象類, 而 DecimalFormat 是一個具象類,因此 DecimalFormat 相對而言比較簡單,特別是對于這種簡單的數(shù)字格式化需求。

// 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 中,數(shù)字格式通過字符表示:

Symbol Location Meaning
0 Number Digit(數(shù)字的占位符)
# Number Digit, zero shows as absent(數(shù)字,但不顯示前導零)
. 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 中對顏色進行操作的方法實現(xiàn),減少所需的資源數(shù)量。

(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)由于震級數(shù)值是非布爾類型的離散值,所以這里引入一種新的 switch 流控語句,它可以替代 if-else 的多級嵌套,免除每一層都需要判斷變量值的重復工作。

  • switch 語句涉及了許多 Java 關鍵字,如 switch、case、break、default。
  • switch 后的 () 內傳入需要執(zhí)行的參數(shù),隨后在 {} 內從上至下尋找 case 后匹配的數(shù)據,若輸入參數(shù)匹配其中一個 case 后的數(shù)據,則運行 : 下的代碼,直到運行至 break;
  • 如果 switch 的輸入參數(shù)沒有匹配任何 case 后的數(shù)據,那么代碼會運行 default: 下的代碼。雖然 default 代碼不是強制的,但是為了增加代碼的魯棒性,通常都會寫在 switch 語句的最后。
  • 如果 case 下的代碼沒有 break;,那么代碼會運行到下一個 case,直到運行至 break;。因此,這種形式的代碼實際上形成了一種或邏輯,例如上述一段代碼的邏輯是,如果 magnitudeFloor 為 0 或 1,那么 magnitudeColorResourceId 賦為 R.color.magnitude1,然后跳出 switch 語句。

(3)由于 switch 語句無法輸入 double 數(shù)值,所以這里需要震級值轉換為 int,用到 Math.floor 將震級值的小數(shù)部分抹平。

  1. 布局優(yōu)化

如果要隱藏 ListView 項目間的分隔線,可以在 XML 中設置以下兩個屬性:

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

設置 TextView 的 ellipsizemaxLines 兩個屬性表示:如果 TextView 中的文本長度超過兩行,就可以在文本結尾處中添加省略號 ("..."),而不是隨內容增加行數(shù)。

android:ellipsize="end"
android:maxLines="2"
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容