結束 Android 開發(fā)(入門)課程 的第二部分《多屏幕應用》后,來到第三部分《訪問網絡》,這部分課程要完成一個名為 Quake Report 的應用,這個應用從網絡獲取數(shù)據,列出全球范圍內最近發(fā)生的地震信息,包括時間、地點、震級。
Quake Report App 分三節(jié)課完成,每個課程的進度分配如下:
- JSON Parsing
從 USGS API 請求數(shù)據,了解返回的數(shù)據結構,并提取數(shù)據。 - HTTP Networking
將數(shù)據輸入 App,涉及 Android 權限、后臺線程等內容。 - 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 可分為三部分:
- 頭部
https://earthquake.usgs.gov/fdsnws/event/1/,固定不變。 - 按照不同數(shù)據需求接上
METHOD,支持 catalogs、count、query 等。Quake Report App 要獲取地震信息,所以這里用到query。 - 最后添加參數(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
-
method為query,后面用?連接參數(shù)。 -
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ù)組等。
- 第 2、3、4、7 行的格式相同,稱為鍵/值對 (Key/Value Pair)。
(1)冒號:左側的是鍵 (Key),由""包括,表示一類數(shù)據的名稱。
(2)冒號:右側的是值 (Value),表示一類數(shù)據的值,可以是數(shù)值、布爾類型、字符串、數(shù)組、對象等。其中字符串由""包括,使用\轉義。
(3)鍵/值對之間用,分隔。 - 第 6 行的鍵/值對,鍵是
categories,值是一個數(shù)組,由[]包括,元素之間用,分隔。 - 第 5 行的鍵/值對,鍵是
style,值是一個對象,由{}包括。對象是鍵/值對的集合,這里是categories和color兩個鍵/值對的集合,這就形成了嵌套結構。 - 整個 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。
- 解析 JSON 的代碼放在一個 Utility class 內,該類的構造函數(shù)為 private,表示不應該創(chuàng)建 Utility 對象,因為 Utility class 只用于存放靜態(tài)變量 (static variable) 和 static method,它們可以直接用類名訪問,無需實例化。
- 這節(jié)課先驗證 JSON 解析,利用
JSONObject(String json)構造函數(shù)傳入一個占位符數(shù)據,新建 JSONObject 對象,名為 baseJsonResponse,對應的 JSON 對象樹節(jié)點為 Root。 - 針對 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對象內的元素magdouble 數(shù)值;
(6)通過getString獲取properties對象內的元素place字符串;
(7)通過getLong獲取properties對象內的元素timelong 數(shù)值;
(8)通過getString獲取properties對象內的元素url字符串; - 上述 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)秀的應用。
- 顯示可讀的時間和日期
由于 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)所有未在特殊字符表中列出的字符都將在輸出字符串中直接顯示。例如,如果時間格式字符串包含 : 或 - 或 ,,則輸出字符串也將在相應位置直接包含相同的標點符號。
- 操控字符串
在 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ù)指定的起止位置對字符串進行裁剪,包括開始索引但不包括結束索引。
- 數(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 網站查看。
- 圓形背景
為 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ù)部分抹平。
- 布局優(yōu)化
如果要隱藏 ListView 項目間的分隔線,可以在 XML 中設置以下兩個屬性:
android:divider="@null"
android:dividerHeight="0dp"
設置 TextView 的 ellipsize 和 maxLines 兩個屬性表示:如果 TextView 中的文本長度超過兩行,就可以在文本結尾處中添加省略號 ("..."),而不是隨內容增加行數(shù)。
android:ellipsize="end"
android:maxLines="2"