《第一行代碼:Android篇》學習筆記(十四)

来源:https://www.cnblogs.com/1693977889zz/archive/2022/05/12/16256373.html
-Advertisement-
Play Games

本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。 每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,非常 ...


本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,非常感激郭霖先生提供這麼好的書籍。

第14章 進入實戰——開發酷歐天氣

在本章將編寫一個功能較為完整的天氣預報程式,那麼第一步需要給這個軟體起名字,這裡就叫它酷歐天氣吧,英文名就叫作CoolWeather。確定了名字之後,下麵就可以開始動手了。

14.1 功能需求及技術可行性分析

在開始編碼之前,需要先對程式進行需求分析,想一想酷歐天氣中應該具備哪些功能。將這些功能全部整理出來,這裡我認為酷歐天氣中至少應該具備以下功能:

❑ 可以羅列出全國所有的省、市、縣;

❑ 可以查看全國任意城市的天氣信息;

❑ 可以自由地切換城市,去查看其他城市的天氣;

❑ 提供手動更新以及後臺自動更新天氣的功能。

雖然看上去只有4個主要的功能點,但如果想要全部實現這些功能卻需要用到UI、網路、數據存儲、服務等技術,因此還是非常考驗你的綜合應用能力的。

分析完了需求之後,接下來就要進行技術可行性分析了。

首先需要考慮的一個問題就是,我們如何才能得到全國省市縣的數據信息,以及如何才能獲取到每個城市的天氣信息。比較遺憾的是,現在網上免費的天氣預報介面已經越來越少,很多之前可以使用的介面都慢慢關閉掉了,包括本書第1版中使用的中國天氣網的介面。

因此,這次我也是特意用心去找了一些更加穩定的天氣預報服務,比如彩雲天氣以及和風天氣都非常不錯。這兩個天氣預報服務雖說都是收費的,但它們每天都提供了一定次數的免費天氣預報請求。其中彩雲天氣的數據更加實時和專業,可以將天氣預報精確到分鐘級,每天提供1000次免費請求;和風天氣的數據相對簡單一些,比較適合新手學習,每天提供3000次免費請求。

那麼簡單起見,這裡我們就使用和風天氣來作為天氣預報的數據來源,每天3000次的免費請求對於學習而言已經是相當充足了。

解決了天氣數據的問題,接下來還需要解決全國省市縣數據的問題。同樣,現在網上也沒有一個穩定的介面可以使用,那麼為了方便你的學習,我專門架設了一臺伺服器用於提供全國所有省市縣的數據信息,從而幫你把道路都鋪平了。那麼下麵我們來看一下這些介面的具體用法。比如要想羅列出中國所有的省份,只需訪問如下地址:

http://guolin.tech/api/china

伺服器會返回我們一段JSON格式的數據,其中包含了中國所有的省份名稱以及省份id,如下所示:

[{"id":1,"name":"北京"},{"id":2,"name":"上海"},{"id":3,"name":"天津"},
 {"id":4,"name":"重慶"},{"id":5,"name":"香港"},{"id":6,"name":"澳門"},
 {"id":7,"name":"臺灣"},{"id":8,"name":"黑龍江"},{"id":9,"name":"吉林"},{"id":10,"name":"遼寧"},{"id":11,"name":"內蒙古"},{"id":12,"name":"河北"},{"id":13,"name":"河南"},{"id":14,"name":"山西"},{"id":15,"name":"山東"},{"id":16,"name":"江蘇"},{"id":17,"name":"浙江"},{"id":18,"name":"福建"},{"id":19,"name":"江西"},{"id":20,"name":"安徽"},{"id":21,"name":"湖北"},{"id":22,"name":"湖南"},{"id":23,"name":"廣東"},{"id":24,"name":"廣西"},{"id":25,"name":"海南"},{"id":26,"name":"貴州"},{"id":27,"name":"雲南"},{"id":28,"name":"四川"},{"id":29,"name":"西藏"},{"id":30,"name":"陝西"},{"id":31,"name":"寧夏"},{"id":32,"name":"甘肅"},{"id":33,"name":"青海"},{"id":34,"name":"新疆"}]

可以看到,這是一個JSON數組,數組中的每一個元素都代表著一個省份。其中,北京的id是1,上海的id是2。那麼如何才能知道某個省內有哪些城市呢?其實也很簡單,比如江蘇的id是16,訪問如下地址即可:

http://guolin.tech/api/china/16

[{"id":113,"name":"南京"},{"id":114,"name":"無錫"},{"id":115,"name":"鎮江"},{"id":116,"name":"蘇州"},{"id":117,"name":"南通"},{"id":118,"name":"揚州"},{"id":119,"name":"鹽城"},{"id":120,"name":"徐州"},{"id":121,"name":"淮安"},{"id":122,"name":"連雲港"},{"id":123,"name":"常州"},{"id":124,"name":"泰州"},{"id":125,"name":"宿遷"}]

這樣我們就得到江蘇省內所有城市的信息了,可以看到,現在返回的數據格式和剛纔查看省份信息時返回的數據格式是一樣的。相信此時你已經可以舉一反三了,比如說蘇州的id是116,那麼想要知道蘇州市下又有哪些縣和區的時候,只需訪問如下地址:

http://guolin.tech/api/china/16/116

[{"id":937,"name":"蘇州","weather_id":"CN101190401"},{"id":938,"name":"常熟","weather_id":"CN101190402"},{"id":939,"name":"張家港","weather_id":"CN101190403"},{"id":940,"name":"昆山","weather_id":"CN101190404"},{"id":941,"name":"吳中","weather_id":"CN101190405"},{"id":942,"name":"吳江","weather_id":"CN101190407"},{"id":943,"name":"太倉","weather_id":"CN101190408"}]

通過這種方式,我們就能把全國所有的省、市、縣都羅列出來了。那麼解決了省市縣數據的獲取,我們又怎樣才能查看到具體的天氣信息呢?這就必須要用到每個地區對應的天氣id了。觀察上面返回的數據,你會發現每個縣或區都會有一個weather_id,拿著這個id再去訪問和風天氣的介面,就能夠獲取到該地區具體的天氣信息了。

下麵我們來看一下和風天氣的介面該如何使用。首先你需要註冊一個自己的賬號,註冊地址是:http://guolin.tech/api/weather/register

註冊好了之後使用這個賬號登錄,如圖:

image

用這個賬號登錄,就能看到自己的API Key,以及每天剩餘的訪問次數:

image

有了API Key,再配合剛纔的weather_id,我們就能獲取到任意城市的天氣信息了。比如說蘇州的weather_id是CN101190401,那麼訪問如下介面即可查看蘇州的天氣信息:

http://guolin.tech/api/weather?cityid=CN101190401&&key=bc0418b57b2d4918819d3974ac1285d9

其中,cityid部分填入的就是待查看城市的weather_id, key部分填入的就是我們申請到的API Key。這樣,伺服器就會把蘇州詳細的天氣信息以JSON格式返回給我們了。不過,由於返回的數據過於複雜,這裡我做了一下精簡處理,如下所示:

{
    "HeWeather":[
        {
            "status":ok,
            "basic":{},
            "aqi":{},
            "now":{},
            "suggestion":{},
            "daily_forecast":[]
        }
    ]
}

返回數據的格式大體上就是這個樣子了,其中status代表請求的狀態,ok表示成功。basic中會包含城市的一些基本信息,aqi中會包含當前空氣質量的情況,now中會包含當前的天氣信息,suggestion中會包含一些天氣相關的生活建議,daily_forecast中會包含未來幾天的天氣信息。

訪問http://guolin.tech/api/weather/doc這個網址可以查看更加詳細的文檔說明(該地址404了):

image

數據都能獲取到了之後,接下來就是JSON解析的工作了,確定了技術完全可行之後,接下來就可以開始編碼了。不過彆著急,我們準備讓酷歐天氣成為一個開源軟體,並使用GitHub來進行代碼托管,因此先讓我們進入到本書最後一次的Git時間。

14.2 Git時間——將代碼托管到GitHub上

GitHub是全球最大的代碼托管網站,主要是藉助Git來進行版本控制的。任何開源軟體都可以免費地將代碼提交到GitHub上,以零成本的代價進行代碼托管。GitHub的官網地址是https://github.com/。官網的首頁如圖:

image

首先你需要有一個GitHub賬號才能使用GitHub的代碼托管功能,點擊Signup for GitHub按鈕進行註冊,然後填入用戶名、郵箱和密碼。(已有賬號,下圖為書中圖)

image

擊Create an account按鈕來創建賬戶,接下來會讓你選擇個人計劃,收費計劃有創建私人版本庫的許可權,而我們的酷歐天氣是開源軟體,所以這裡選擇免費計劃就可以了,如圖:

image

接著點擊Continue按鈕會進入一個問卷調查界面,如圖:

image

如果你對這個有興趣就填寫一下,沒興趣的話直接點擊最下方的skip thisstep跳過就可以了。這樣我們就把賬號註冊好了,會自動跳轉到GitHub的個人主頁,如圖:

image

接下來就可以點擊Create a new repository按鈕來創建一個版本庫了。(由於我們是剛剛註冊的賬號,在創建版本庫之前還需要做一下郵箱驗證,驗證成功之後就能開始創建了。)

這裡將版本庫命名為coolweather,然後選擇添加一個Android項目類型的.gitignore文件,並使用Apache License 2.0來作為酷歐天氣的開源協議,如圖:

image

接著,點擊Create repository按鈕,coolweather這個版本庫就創建完成了,如圖。版本庫主頁地址是:https://github.com/guolindev/coolweather

image

可以看到,GitHub已經自動幫我們創建了.gitignore、LICENSE和README.md這3個文件,其中編輯README.md文件中的內容可以修改酷歐天氣版本庫主頁的描述。

創建好了版本庫之後,我們就需要創建酷歐天氣這個項目了。在AndroidStudio中新建一個Android項目,項目名叫作CoolWeather,包名叫作com.coolweather.android,如圖:

image

一直點擊Next就可以完成項目的創建,所有選項都使用預設的就好。

接下來的一步非常重要,我們需要將遠程版本庫克隆到本地。首先必須知道遠程版本庫的Git地址,點擊Clone or download按鈕就能夠看到了,如圖:

image

點擊右邊的複製按鈕,可以將版本庫的Git地址複製到剪貼板,酷歐天氣版本庫的Git地址是:https://github.com/guolindev/coolweather.git(寫自己創建的地址哦)

然後打開Git Bash並切換到CoolWeather的工程目錄下,接著輸入git clone https://github.com/guolindev/coolweather.git(寫自己創建的地址哦)來把遠程版本庫克隆到本地,如圖:

註意:【github】將預設分支由 main 修改為 master 博客地址:https://blog.csdn.net/m0_37697335/article/details/120633567

image

看到圖中所給的文字提示就表示克隆成功了,並且.gitignore、LICENSE和README.md這3個文件也已經被覆制到了本地,可以進入到coolweather目錄,並使用ls -al命令查看一下,如圖:

image

現在,我們需要將這個目錄中的所有文件全部複製粘貼到上一層目錄中,這樣就能將整個CoolWeather工程目錄添加到版本控制中去了。

註意:.git是一個隱藏目錄,在複製的時候千萬不要漏掉。另外,上一層目錄中也有一個.gitignore文件,我們直接將其覆蓋即可。複製完之後可以把coolweather目錄刪除掉,最終CoolWeather工程的目錄結構如圖:

image

接下來,我們應該把CoolWeather項目中現有的文件提交到GitHub上面,這就很簡單了,先將所有文件添加到版本控制中,如下所示:

git add .

然後在本地執行提交操作:

git commit -m "First commit."

最後,將提交的內容同步到遠程版本庫,也就是GitHub上面:

git push -u origin master

註意,在最後一步的時候GitHub要求輸入用戶名和密碼來進行身份校驗,這裡輸入我們註冊時填入的用戶名和密碼就可以了,如圖:

image

這樣就已經同步完成了,現在刷新一下酷歐天氣版本庫的主頁,會看到剛纔提交的那些文件已經存在了,如圖:

image

14.3 創建資料庫和表

從本節開始,就要真正地動手編碼了,為了要讓項目能夠有更好的結構,這裡需要在com.coolweather.android包下再新建幾個包,如圖:

image

其中,

  • db包用於存放資料庫模型相關的代碼;
  • gson包用於存放GSON模型相關的代碼;
  • service包用於存放服務相關的代碼;
  • util包用於存放工具相關的代碼。

根據14.1節進行的技術可行性分析,第一階段我們要做的就是創建好資料庫和表,這樣從伺服器獲取到的數據才能夠存儲到本地。關於資料庫和表的創建方式,我們早在第6章中就已經學過了。

簡化資料庫的操作,這裡我準備使用LitePal來管理酷歐天氣的資料庫。首先需要將項目所需的各種依賴庫進行聲明,編輯app/build.gradle文件,在dependencies閉包中添加如下內容:

dependencies {

    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'

    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    implementation 'org.litepal.guolindev:core:3.2.3'
    implementation 'com.squareup.okhttp3:okhttp:4.9.3'
    implementation 'com.google.code.gson:gson:2.9.0'
    implementation 'com.github.bumptech.glide:glide:4.13.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.13.0'
}

LitePal用於對資料庫進行操作,OkHttp用於進行網路請求,GSON用於解析JSON數據,Glide用於載入和展示圖片。

註意:當時,我的下麵代碼Province類在繼承LitePalSupport類時LitePalSupport一直爆紅,加了導包也不行。最後,修改了settings.gradle文件,才OK:

pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        jcenter()
        maven { url 'https://jitpack.io'}
    }
}
rootProject.name = "CoolWeather"
include ':app'

然後,來設計一下資料庫的表結構,表的設計當然是仁者見仁智者見智,並不是說哪種設計就是最規範最完美的。這裡我準備建立3張表:province、city、county,分別用於存放省、市、縣的數據信息。對應到實體類中的話,就應該建立Province、City、County這3個類。

那麼,在db包下新建一個Province類,代碼如下所示:

package com.coolweather.android.db;

import org.litepal.crud.LitePalSupport;

//書中繼承的是DataSupport(已經棄用)
public class Province extends LitePalSupport {
    //id是每個實體類中都應該有的欄位
    private int id;
    //provinceName記錄省的名字
    private String provinceName;
    //provinceCode記錄省的代號
    private int provinceCode;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getProvinceName() {
        return provinceName;
    }

    public void setProvinceName(String provinceName) {
        this.provinceName = provinceName;
    }

    public int getProvinceCode() {
        return provinceCode;
    }

    public void setProvinceCode(int provinceCode) {
        this.provinceCode = provinceCode;
    }
}

接著,在db包下新建一個City類,代碼如下所示:

package com.coolweather.android.db;

import org.litepal.crud.LitePalSupport;

public class City extends LitePalSupport {
    private int id;
    //cityName記錄市的名字
    private String cityName;
    //cityCode記錄市的代號
    private int cityCode;
    //provinceId記錄當前市所屬省的id值
    private int provinceId;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCityName() {
        return cityName;
    }

    public void setCityName(String cityName) {
        this.cityName = cityName;
    }

    public int getCityCode() {
        return cityCode;
    }

    public void setCityCode(int cityCode) {
        this.cityCode = cityCode;
    }

    public int getProvinceId() {
        return provinceId;
    }

    public void setProvinceId(int provinceId) {
        this.provinceId = provinceId;
    }
}

然後,在db包下新建一個County類,代碼如下所示:

package com.coolweather.android.db;

import org.litepal.crud.LitePalSupport;

public class County extends LitePalSupport {
    private int id;
    //countyName記錄縣的名字
    private String countyName;
    //weatherId記錄縣所對應的天氣id
    private String weatherId;
    //cityId記錄當前縣所屬市的id值
    private int cityId;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCountyName() {
        return countyName;
    }

    public void setCountyName(String countyName) {
        this.countyName = countyName;
    }

    public String getWeatherId() {
        return weatherId;
    }

    public void setWeatherId(String weatherId) {
        this.weatherId = weatherId;
    }

    public int getCityId() {
        return cityId;
    }

    public void setCityId(int cityId) {
        this.cityId = cityId;
    }
}

可以看到,實體類的內容都非常簡單,就是聲明瞭一些需要的欄位,並生成相應的getter和setter方法就可以了。接下來需要配置litepal.xml文件。

右擊app/src/main目錄→New→Directory,創建一個assets目錄,然後在assets目錄下再新建一個litepal.xml文件,接著編輯litepal.xml文件中的內容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="cool_weather" />
    <version value="1" />
    <list>
        <mapping class="com.coolweather.android.db.Province"/>
        <mapping class="com.coolweather.android.db.City"/>
        <mapping class="com.coolweather.android.db.County"/>
    </list>
</litepal>

這裡將資料庫名指定成cool_weather,資料庫版本指定成1,並將Province、City和County這3個實體類添加到映射列表當中。

最後,還需要再配置一下LitePalApplication,修改AndroidManifest.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.coolweather.android">

    <application
        android:name="org.litepal.LitePalApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.CoolWeather">
        ...
    </application>

</manifest>

這樣我們就將所有的配置都完成了,資料庫和表會在首次執行任意資料庫操作的時候自動創建。

現在提交一下。首先,將所有新增的文件添加到版本控制中:

git add .

接著,執行提交操作:

git commit -m "加入創建資料庫和表的各項操作。"

最後,將提交同步到GitHub上面:

git push origin master

第一階段完工。

14.4 遍歷全國省市縣數據

在第二階段中,我們準備把遍歷全國省市縣的功能加入,這一階段需要編寫的代碼量比較大。

我們已經知道,全國所有省市縣的數據都是從伺服器端獲取到的,因此這裡和伺服器的交互是必不可少的,所以我們可以在util包下先增加一個HttpUtil類,代碼如下所示:

package com.coolweather.android.util;

import okhttp3.OkHttpClient;
import okhttp3.Request;

public class HttpUtil {
    public static void sendOkHttpRequest(String address,okhttp3.Callback callback) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(address).build();
        client.newCall(request).enqueue(callback);
    }
}

由於OkHttp的出色封裝,這裡和伺服器進行交互的代碼非常簡單,僅僅3行就完成了。

現在,我們發起一條HTTP請求只需要調用sendOkHttpRequest()方法,傳入請求地址,並註冊一個回調來處理伺服器響應就可以了。

另外,由於伺服器返回的省市縣數據都是JSON格式的,所以我們最好再提供一個工具類來解析和處理這種數據。在util包下新建一個Utility類,代碼如下所示:

package com.coolweather.android.util;

import android.text.TextUtils;

import com.coolweather.android.db.City;
import com.coolweather.android.db.County;
import com.coolweather.android.db.Province;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class Utility {
    /**
     * 解析和處理伺服器返回的省級數據
     */
    public static boolean handleProvinceResponse(String response) {
        if (! TextUtils.isEmpty(response)) {
            try {
                JSONArray allProvinces = new JSONArray(response);
                for (int i = 0;i < allProvinces.length();i++) {
                    JSONObject provinceObject = allProvinces.getJSONObject(i);
                    Province province = new Province();
                    province.setProvinceName(provinceObject.getString("name"));
                    province.setProvinceCode(provinceObject.getInt("id"));
                    province.save();
                }
                return true;
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
    /**
     * 解析和處理伺服器返回的市級數據
     */
    public static boolean handleCityResponse(String response,int provinceId) {
        if (! TextUtils.isEmpty(response)) {
            try {
                JSONArray allCities = new JSONArray(response);
                for (int i = 0; i < allCities.length(); i++) {
                    JSONObject cityObject = allCities.getJSONObject(i);
                    City city = new City();
                    city.setCityName(cityObject.getString("name"));
                    city.setCityCode(cityObject.getInt("id"));
                    city.setProvinceId(provinceId);
                    city.save();
                }
                return true;
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
    /**
     * 解析和處理伺服器返回的縣級數據
     */
    public static boolean handleCountyResponse(String response,int cityId) {
        if (! TextUtils.isEmpty(response)) {
            try {
                JSONArray allCounties = new JSONArray(response);
                for (int i = 0; i < allCounties.length(); i++) {
                    JSONObject countyObject = allCounties.getJSONObject(i);
                    County county = new County();
                    county.setCountyName(countyObject.getString("name"));
                    county.setWeatherId(countyObject.getString("weather_id"));
                    county.setCityId(cityId);
                    county.save();
                }
                return true;
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

可以看到,我們提供了handleProvinceResponse()、handleCityResponse()、handleCountyResponse()這3個方法,分別用於解析和處理伺服器返回的省級、市級和縣級數據。處理的方式都是類似的,先使用JSONArray和JSONObject將數據解析出來,然後組裝成實體類對象,再調用save()方法將數據存儲到資料庫當中。

需要準備的工具類就這麼多,現在可以開始寫界面了。由於遍歷全國省市縣的功能我們在後面還會復用,因此就不寫在活動裡面了,而是寫在碎片裡面,這樣需要復用的時候直接在佈局裡面引用碎片就可以了。

在res/layout目錄中新建choose_area.xml佈局,代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#fff">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/title_text"
            android:layout_centerInParent="true"
            android:textColor="#fff"
            android:textSize="20sp"/>
        <Button
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:id="@+id/back_button"
            android:layout_marginLeft="10dp"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:background="@drawable/ic_back"/>
    </RelativeLayout>
    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/list_view"/>
</LinearLayout>

先是定義了一個頭佈局來作為標題欄,將佈局高度設置為actionBar的高度,背景色設置為colorPrimary。然後在頭佈局中放置了一個TextView用於顯示標題內容,放置了一個Button用於執行返回操作,註意我已經提前準備好了一張ic_back.png圖片用於作為按鈕的背景圖。這裡之所以要自己定義標題欄,是因為碎片中最好不要直接使用ActionBar或Toolbar,不然在復用的時候可能會出現一些你不想看到的效果。

接下來,在頭佈局的下麵定義了一個ListView,省市縣的數據就將顯示在這裡。之所以這次使用了ListView,是因為它會自動給每個子項之間添加一條分隔線,而如果使用RecyclerView想實現同樣的功能則會比較麻煩,這裡我們總是選擇最優的實現方案。

接下來也是最關鍵的一步,我們需要編寫用於遍歷省市縣數據的碎片了。新建ChooseAreaFragment繼承自Fragment,代碼如下所示:

package com.coolweather.android;

import android.app.ProgressDialog;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import com.coolweather.android.db.City;
import com.coolweather.android.db.County;
import com.coolweather.android.db.Province;
import com.coolweather.android.util.HttpUtil;
import com.coolweather.android.util.Utility;

import org.litepal.LitePal;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;

public class ChooseAreaFragment extends Fragment {
    public static final int LEVEL_PROVINCE = 0;
    public static final int LEVEL_CITY = 1;
    public static final int LEVEL_COUNTY = 2;
    private ProgressDialog progressDialog;
    private TextView titleText;
    private Button backButton;
    private ListView listView;
    private ArrayAdapter<String> adapter;
    private List<String> dataList = new ArrayList<>();
    /**
     * 省列表
     */
    private List<Province> provinceList;
    /**
     * 市列表
     */
    private List<City> cityList;
    /**
     * 縣列表
     */
    private List<County> countyList;
    /**
     * 選中的省份
     */
    private Province selectedProvince;
    /**
     * 選中的市
     */
    private City selectedCity;
    /**
     * 當前選中的級別
     */
    private int currentLevel;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        //在onCreateView()方法中先是獲取到了一些控制項的實例,然後去初始化了ArrayAdapter,並將它設置為ListView的適配器。
        View view = inflater.inflate(R.layout.choose_area,container,false);
        titleText = (TextView) view.findViewById(R.id.title_text);
        backButton = (Button) view.findViewById(R.id.back_button);
        listView = (ListView) view.findViewById(R.id.list_view);
        adapter = new ArrayAdapter<>(getContext(),android.R.layout.simple_list_item_1,dataList);
        listView.setAdapter(adapter);
        return view;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        //在onActivityCreated()方法中給ListView和Button設置了點擊事件
        super.onActivityCreated(savedInstanceState);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            /**
             * 當你點擊了某個省的時候會進入到ListView的onItemClick()方法中,
             * 這個時候會根據當前的級別來判斷是去調用queryCities()方法還是queryCounties()方法,
             * queryCities()方法是去查詢市級數據,而queryCounties()方法是去查詢縣級數據,
             * 這兩個方法內部的流程和queryProvinces()方法基本相同
             */
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if (currentLevel == LEVEL_PROVINCE) {
                    selectedProvince = provinceList.get(position);
                    queryCities();
                } else if (currentLevel == LEVEL_CITY) {
                    selectedCity = cityList.get(position);
                    queryCounties();
                }
            }
        });
        /**
         * 在返回按鈕的點擊事件里,會對當前ListView的列表級別進行判斷。
         * 如果當前是縣級列表,那麼就返回到市級列表,
         * 如果當前是市級列表,那麼就返回到省級表列表。
         * 當返回到省級列表時,返回按鈕會自動隱藏,從而也就不需要再做進一步的處理了。
         */
        backButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (currentLevel == LEVEL_COUNTY) {
                    queryCities();
                } else if (currentLevel == LEVEL_CITY) {
                    queryProvince();
                }
            }
        });
        //調用了queryProvinces()方法,也就是從這裡開始載入省級數據的。
        queryProvince();
    }

    /**
     * 查詢全國所有的省,優先從資料庫查詢,如果沒有查詢到再去伺服器查詢
     */
    private void queryProvince() {
        //queryProvinces()方法中首先會將頭佈局的標題設置成中國,將返回按鈕隱藏起來,因為省級列表已經不能再返回了。
        titleText.setText("中國");
        backButton.setVisibility(View.GONE);
        //調用LitePal的查詢介面來從資料庫中讀取省級數據,如果讀取到了就直接將數據顯示到界面上,
        //如果沒有讀取到就按照14.1節講述的介面組裝出一個請求地址,然後調用queryFromServer()方法來從伺服器上查詢數據。
        provinceList = LitePal.findAll(Province.class);
        if (provinceList.size() > 0) {
            dataList.clear();
            for (Province province : provinceList) {
                dataList.add(province.getProvinceName());
            }
            adapter.notifyDataSetChanged();
            listView.setSelection(0);
            currentLevel = LEVEL_PROVINCE;
        } else {
            String address = "http://guolin.tech/api/china";
            queryFromService(address,"province");
        }
    }
    /**
     * 查詢全國所有的市,優先從資料庫查詢,如果沒有查詢到再去伺服器查詢
     */
    private void queryCities() {
        titleText.setText(selectedProvince.getProvinceName());
        backButton.setVisibility(View.VISIBLE);
        cityList = LitePal.where("provinceid = ?",String.valueOf(selectedProvince.getId())).find(City.class);
        if (cityList.size() > 0) {
            dataList.clear();
            for (City city : cityList) {
                dataList.add(city.getCityName());
            }
            adapter.notifyDataSetChanged();
            listView.setSelection(0);
            currentLevel = LEVEL_CITY;
        } else {
            int provinceCode = selectedProvince.getProvinceCode();
            String address = "http://guolin.tech/api/china/" + provinceCode;
            queryFromService(address,"city");
        }
    }
    /**
     * 查詢全國所有的縣,優先從資料庫查詢,如果沒有查詢到再去伺服器查詢
     */
    private void queryCounties() {
        titleText.setText(selectedCity.getCityName());
        backButton.setVisibility(View.VISIBLE);
        countyList = LitePal.where("cityid = ?",String.valueOf(selectedCity.getId())).find(County.class);
        if (countyList.size() > 0) {
            dataList.clear();
            for (County county : countyList) {
                dataList.add(county.getCountyName());
            }
            adapter.notifyDataSetChanged();
            listView.setSelection(0);
            currentLevel = LEVEL_COUNTY;
        } else {
            int provinceCode = selectedProvince.getProvinceCode();
            int cityCode = selectedCity.getCityCode();
            String address = "http://guolin.tech/api/china/" + provinceCode + "/" + cityCode;
            queryFromService(address,"county");
        }
    }
    /**
     * 根據傳入的地址和類型從伺服器上查詢省市縣的數據
     * queryFromServer()方法中會調用HttpUtil的sendOkHttpRequest()方法來向伺服器發送請求,
     * 響應的數據會回調到onResponse()方法中,然後去調用Utility的handleProvincesResponse()方法,
     * 來解析和處理伺服器返回的數據,並存儲到資料庫中。
     */
    private void queryFromService(String address,final String type) {
        showProgressDialog();
        HttpUtil.sendOkHttpRequest(address, new Callback() {
            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
                String responseText = response.body().string();
                boolean result = false;
                if ("province".equals(type)) {
                    result = Utility.handleProvinceResponse(responseText);
                } else if ("city".equals(type)) {
                    result = Utility.handleCityResponse(responseText,selectedProvince.getId());
                } else if ("county".equals(type)) {
                    result = Utility.handleCountyResponse(responseText,selectedCity.getId());
                }
                /**
                 * 在解析和處理完數據之後,再次調用了queryProvinces()方法來重新載入省級數據,
                 * 由於queryProvinces()方法牽扯到了UI操作,因此必須要在主線程中調用,
                 * 這裡藉助了runOnUiThread()方法來實現從子線程切換到主線程。
                 * 現在資料庫中已經存在了數據,因此調用queryProvinces()就會直接將數據顯示到界面上了。
                 */
                if (result) {
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            closeProgressDialog();
                            if ("province".equals(type)) {
                                queryProvince();
                            } else if ("city".equals(type)) {
                                queryCities();
                            } else if ("county".equals(type)) {
                                queryCounties();
                            }
                        }
                    });
                }
            }
            @Override
            public void onFailure(@NonNull Call call, @NonNull IOException e) {
                //通過runOnUiThread()方法回到主線程處理邏輯
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        closeProgressDialog();
                        Toast.makeText(getContext(), "載入失敗", Toast.LENGTH_SHORT).show();
                    }
                });
            }
        });
    }

    /**
     * 顯示進度條對話框
     */
    private void showProgressDialog() {
        if (progressDialog == null) {
            progressDialog = new ProgressDialog(getActivity());
            progressDialog.setMessage("正在載入...");
            progressDialog.setCanceledOnTouchOutside(false);
        }
        progressDialog.show();
    }

    /**
     * 關閉進度條
     */
    private void closeProgressDialog() {
        if (progressDialog != null) {
            progressDialog.dismiss();
        }
    }
}

這樣<我們就把遍歷全國省市縣的功能完成了,可是碎片是不能直接顯示在界面上的,因此我們還需要把它添加到活動里才行。修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/choose_area_fragment"
        android:name="com.coolweather.android.db.ChooseAreaFragment"/>
</FrameLayout>

佈局文件很簡單,只是定義了一個FrameLayout,然後將ChooseAreaFragment添加進來,並讓它充滿整個佈局。另外,我們剛纔在碎片的佈局裡面已經自定義了一個標題欄,因此就不再需要原生的ActionBar了,修改res/values/themes.xml中的代碼,如下所示:

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.CoolWeather" parent="Theme.AppCompat.Light.NoActionBar">
        ...
</resources>

接著,要聲明程式所需要的許可權。修改AndroidManifest.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.coolweather.android">
    <uses-permission android:name="android.permission.INTERNET"/>
    ...
</manifest>

由於我們是通過網路介面來獲取全國省市縣數據的,因此必須要添加訪問網路的許可權才行。現在可以運行一下程式了,結果如圖:

image

可以看到,全國所有省級數據都顯示出來了。還可以繼續查看市級數據,比如點擊浙江省,結果如圖:

image

這個時候標題欄上會出現一個返回按鈕,用於返回上一級列表。然後再點擊杭州市查看縣級數據,結果如圖:

image

這樣第二階段的開發工作也都完成了,仍然要把代碼提交一下。

git add .
git commit -m "完成遍歷省市縣三級列表的功能。"
git push origin master

14.5 顯示天氣信息

在第三階段中,我們就要開始去查詢天氣,並且把天氣信息顯示出來了。由於和風天氣返回的JSON數據結構非常複雜,如果還使用JSONObject來解析就會很麻煩,這裡我們就準備藉助GSON來對天氣信息進行解析了。

14.5.1 定義GSON實體類

GSON的用法很簡單,解析數據只需要一行代碼就能完成了,但前提是要先將數據對應的實體類創建好。由於和風天氣返回的數據內容非常多,這裡我們不可能將所有的內容都利用起來,因此我篩選了一些比較重要的數據來進行解析。首先我們回顧一下返回數據的大致格式:

{
    "HeWeather": [
        {
            "status": "ok",
            "basic": {},
            "aqi": {},
            "now": {},
            "suggestion": {},
            "daily_forecast" :[]
        }
    ]
}

其中,basic、aqi、now、suggestion和daily_forecast的內部又都會有具體的內容,那麼我們就可以將這5個部分定義成5個實體類。

下麵開始來一個個看,basic中具體內容如下所示:

"basic": {
    "city":"蘇州",
    “id:"CN101190401",
    "update":{
    	"loc":"2016-08-08 21:58"
	}
}

其中,city表示城市名,id表示城市對應的天氣id, update中的loc表示天氣的更新時間。我們按照此結構就可以在gson包下建立一個Basic類,代碼如下所示:

package com.coolweather.android.gson;

import com.google.gson.annotations.SerializedName;

public class Basic {
    @SerializedName("city")
    public String cityName;
    @SerializedName("id")
    public String weatherId;
    public Update update;
    public class Update {
        @SerializedName("loc")
        public String updateTime;
    }
}

由於JSON中的一些欄位可能不太適合直接作為Java欄位來命名,因此這裡使用了@SerializedName註解的方式來讓JSON欄位和Java欄位之間建立映射關係。

這樣就將Basic類定義好了,其餘的幾個實體類也是類似的,使用同樣的方式來定義就可以了。比如aqi中的具體內容如下如示:

"aqi":{
    "city":{
        "aqi":"44",
        "pm25":"13"
    }
}

那麼,在gson包下新建一個AQI類,代碼如下所示:

package com.coolweather.android.gson;

public class AQI {
    public AQICity city;
    public class AQICity {
        public String aqi;
        public String pm25;
    }
}

now中的具體內容如下所示:

"now":{
    "tmp":"29",
    "cond":{
        "txt":"陣雨"
    }
}

那麼,在gson包下新建一個Now類,代碼如下所示:

package com.coolweather.android.gson;

import com.google.gson.annotations.SerializedName;

public class Now {
    @SerializedName("tmp")
    public String temperature;
    @SerializedName("cond")
    public More more;
    public class More {
        @SerializedName("txt")
        public String info;
    }
}

suggestion中的具體內容如下所示:

"suggestion":{
    "comf":{
        "txt":"白天天氣較熱,雖然有雨,但仍然無法消弱較高氣溫給人們帶來的暑意,這種天氣會讓您感到不是很舒適。"
    },
    "cw":{
        "txt":"不宜洗車,未來24小時內有雨,如果在此期間洗車,雨水和路上的泥水可能弄髒您的愛車。"
    },
    "sport":{
        "txt":"有降雨,且風力較強,推薦您在室內進行低強度運動,若堅持戶外運動,請選擇避雨防風的地點。"
    }
}

那麼,在gson包下新建一個Suggestion類,代碼如下所示:

package com.coolweather.android.gson;

import com.google.gson.annotations.SerializedName;

public class Suggestion {
    @SerializedName("comf")
    public Comfort comfort;
    @SerializedName("cw")
    public CarWash carWash;
    public Sport sport;
    public class Comfort{
        @SerializedName("txt")
        public String info;
    }
    public class CarWash {
        @SerializedName("txt")
        public String info;
    }
    public class Sport {
        @SerializedName("txt")
        public String info;
    }
}

接下來的一項數據就有點特殊了,daily_forecast中的具體內容如下所示:

"daily_forecast":[
    {
        "date":"2016-08-08",
        "cond":{
            "txt_d":"陣雨"
        },
        "tmp":{
            "max":"34",
            "min":"27"
        }
    },
    {
        "date":"2016-08-09",
        "cond":{
            "txt_d":"多雲"
        },
        "tmp":{
            "max":"35",
            "min":"29"
        }
    },
    ...
]

可以看到,daily_forecast中包含的是一個數組,數組中的每一項都代表著未來一天的天氣信息。針對於這種情況,我們只需要定義出單日天氣的實體類就可以了,然後在聲明實體類引用的時候使用集合類型來進行聲明。那麼在gson包下新建一個Forecast類,代碼如下所示:

package com.coolweather.android.gson;

import com.google.gson.annotations.SerializedName;

public class Forecast {
    public String date;
    @SerializedName("tmp")
    public Temperature temperature;
    @SerializedName("cond")
    public More more;
    public class Temperature {
        public String max;
        public String min;
    }
    public class More {
        @SerializedName("txt_d")
        public String info;
    }
}

這樣我們就把basic、aqi、now、suggestion和daily_forecast對應的實體類全部都創建好了,接下來還需要再創建一個總的實例類來引用剛剛創建的各個實體類。在gson包下新建一個Weather類,代碼如下所示:

package com.coolweather.android.gson;

import com.google.gson.annotations.SerializedName;
import java.util.List;

public class Weather {
    public String status;
    public Basic basic;
    public AQI aqi;
    public Now now;
    public Suggestion suggestion;
    @SerializedName("daily_forecast")
    public List<Forecast> forecastList;
}

在Weather類中,我們對Basic、AQI、Now、Suggestion和Forecast類進行了引用。其中,由於daily_forecast中包含的是一個數組,因此這裡使用了List集合來引用Forecast類。

另外,返回的天氣數據中還會包含一項status數據,成功返回ok,失敗則會返回具體的原因,那麼這裡也需要添加一個對應的status欄位。現在所有的GSON實體類都定義好了,接下來我們開始編寫天氣界面。

14.5.2 編寫天氣界面

首先創建一個用於顯示天氣信息的活動。右擊com.coolweather.android包→New→Activity→Empty Activity,創建一個WeatherActivity,並將佈局名指定成activity_weather.xml。

由於所有的天氣信息都將在同一個界面上顯示,因此activity_weather.xml會是一個很長的佈局文件。

那麼,為了讓裡面的代碼不至於混亂不堪,這裡我準備使用3.4.1小節學過的引入佈局技術,即將界面的不同部分寫在不同的佈局文件裡面,再通過引入佈局的方式集成到activity_weather.xml中,這樣整個佈局文件就會顯得非常工整。

右擊res/layout→New→Layout resource file,新建一個title.xml作為頭佈局,代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/title_city"
        android:layout_centerInParent="true"
        android:textColor="#fff"
        android:textSize="20sp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/title_update_time"
        android:layout_marginRight="10dp"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:textColor="#fff"
        android:textSize="16sp"/>
</RelativeLayout>

頭佈局中放置了兩個TextView,一個居中顯示城市名,一個居右顯示更新時間。然後,新建一個now.xml作為當前天氣信息的佈局,代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="15dp">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/degree_text"
        android:layout_gravity="end"
        android:textColor="#fff"
        android:textSize="60sp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/weather_info_text"
        android:layout_gravity="end"
        android:textColor="#fff"
        android:textSize="20sp"/>
</LinearLayout>

當前天氣信息的佈局中也是放置了兩個TextView,一個用於顯示當前氣溫,一個用於顯示天氣概況。然後新建forecast.xml作為未來幾天天氣信息的佈局,代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="15dp"
    android:background="#8000">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="15dp"
        android:text="預報"
        android:textColor="#fff"
        android:textSize="20sp"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:id="@+id/forecast_layout"/>
</LinearLayout>

最外層使用LinearLayout定義了一個半透明的背景,然後使用TextView定義了一個標題,接著又使用一個LinearLayout定義了一個用於顯示未來幾天天氣信息的佈局。不過這個佈局中並沒有放入任何內容,因為這是要根據伺服器返回的數據在代碼中動態添加的。

為此,我們還需要再定義一個未來天氣信息的子項佈局,創建forecast_item.xml文件,代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="15dp">
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:id="@+id/date_text"
        android:layout_weight="2"
        android:textColor="#fff"/>
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:id="@+id/info_text"
        android:layout_weight="1"
        android:gravity="center"
        android:textColor="#fff"/>
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:id="@+id/max_text"
        android:layout_weight="1"
        android:gravity="right"
        android:textColor="#fff"/>
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:id="@+id/min_text"
        android:layout_weight="1"
        android:gravity="right"
        android:textColor="#fff"/>
</LinearLayout>

子項佈局中放置了4個TextView,一個用於顯示天氣預報日期,一個用於顯示天氣概況,另外兩個分別用於顯示當天的最高溫度和最低溫度。然後,新建aqi.xml作為空氣質量信息的佈局,代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="15dp"
    android:background="#8000">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="15dp"
        android:text="空氣質量"
        android:textColor="#fff"
        android:textSize="20sp"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="15dp">
        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:layout_centerInParent="true">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/aqi_text"
                    android:layout_gravity="center"
                    android:textColor="#fff"
                    android:textSize="40sp"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="AQI指數"
                    android:textColor="#fff"/>
            </LinearLayout>
        </RelativeLayout>
        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:layout_centerInParent="true">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:id="@+id/pm25_text"
                    android:layout_gravity="center"
                    android:textColor="#fff"
                    android:textSize="40sp"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="PM2.5指數"
                    android:textColor="#fff"/>
            </LinearLayout>
        </RelativeLayout>
    </LinearLayout>
</LinearLayout>

使用LinearLayout定義了一個半透明的背景,然後使用TextView定義了一個標題。接下來,這裡使用LinearLayout和RelativeLayout嵌套的方式實現了一個左右平分並且居中對齊的佈局,分別用於顯示AQI指數和PM 2.5指數。然後,新建suggestion.xml作為生活建議信息的佈局,代碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="#8000"
    android:layout_margin="15dp">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="15dp"
        android:text="生活建議"
        android:textColor="#fff"
        android:textSize="20sp"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/comfort_text"
        android:layout_margin="15dp"
        android:textColor="#fff"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/car_wash_text"
        android:layout_margin="15dp"
        android:textColor="#fff"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/sport_text"
        android:layout_margin="15dp"
        android:textColor="#fff"/>
</LinearLayout>

同樣也是先定義了一個半透明的背景和一個標題,然後下麵使用了3個TextView分別用於顯示舒適度、洗車指數和運動建議的相關數據。

這樣,我們就把天氣界面上每個部分的佈局文件都編寫好了,接下來的工作就是將它們引入到activity_weather.xml當中,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/design_default_color_primary">
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/weather_layout"
        android:scrollbars="none"
        android:overScrollMode="never">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <include layout="@layout/title"/>
            <include layout="@layout/now"/>
            <include layout="@layout/forecast"/>
            <include layout="@layout/aqi"/>
            <include layout="@layout/suggestion"/>
        </LinearLayout>
    </ScrollView>
</FrameLayout>

可以看到,首先最外層佈局使用了一個FrameLayout,並將它的背景色設置成@color/design_default_color_primary。然後,在FrameLayout中嵌套了一個ScrollView,這是因為天氣界面中的內容比較多,使用ScrollView可以允許我們通過滾動的方式查看屏幕以外的內容。

由於ScrollView的內部只允許存在一個

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 引言 我們在定時任務中經常能接觸到cron表達式,但是在寫cron表達式的時候我們會遇到各種各樣版本的cron表達式,比如我遇到過5位、6位甚至7位的cron表達式,導致我一度搞混這些表達式。更嚴重的是,當我們沒有準確寫出cron表達式時,會出現定時任務一直沒有執行,或者定時任務執行太頻繁的糟糕情況 ...
  • 一、Flink中的狀態 官方文檔 有狀態的計算是流處理框架要實現的重要功能,因為稍複雜的流處理場景都需要記錄狀態,然後在新流入數據的基礎上不斷更新狀態。下麵的幾個場景都需要使用流處理的狀態功能: 數據流中的數據有重覆,想對重覆數據去重,需要記錄哪些數據已經流入過應用,當新數據流入時,根據已流入過的數 ...
  • 這邊文章聊聊自己對數據治理開發實踐的一些思路,就是聊聊怎麼開始去做數據治理這件事情。說起數據治理,有時候雖然看了很多文章,看了很多的介紹,瞭解數據治理的理論,但是實際上需要我們去搞的時候,就會踩很多的坑。這裡記一下自己做數據治理的一些思路,做做筆記,也分享給需要的同學。 當然,想要做數據治理,想要學 ...
  • 在數倉項目中,我們常常會選擇Apache Atlas進行數據的治理。本文結合筆者在生產環境中遇到的常見問題及解決方法,整合出完整的Atlas編譯、部署及使用過程。 ...
  • 1、高可用性的目的是什麼? 高可用性的目標是以最小的停機時間提供連續的服務(唯一真正具有 "零 "停機時間的設備是心臟起搏器和核武器中的安全裝置)。這意味著,如果一個組件發生故障,另一個組件可以立即接管其功能,而不會實質性地中斷對系統用戶的服務。高可用性還要求有能力檢測到一個或多個組件發生故障,然後 ...
  • 上一篇文章我們演示瞭如何《在 S3 備份恢復 RadonDB MySQL 集群數據》,本文將演示在 KubeSphere[1] 中使用 Prometheus[2] + Grafana[3] 構建 MySQL 監控平臺,開啟所需監控指標。 背景 Prometheus 基於文本的暴露格式,已經成為雲原生 ...
  • 聲明:全文來源《mysql SQL必知必會(第3版)》 第一章 瞭解SQL 1.1 資料庫基礎 資料庫(database)保存有組織的數據的容器 表(table)某種特定類型數據的結構化清單。資料庫中的每個表都有一個用來標識自己的名字。此名字是唯一的。 模式(schema)關於資料庫和表的佈局及特性 ...
  • 本文介紹什麼是通配符、如何使用通配符,以及怎樣使用 SQL LIKE 操作符進行通配搜索,以便對數據進行複雜過濾。 一、LIKE 操作符 前面介紹的所有操作符都是針對已知值進行過濾的。不管是匹配一個值還是多個值,檢驗大於還是小於已知值,或者檢查某個範圍的值,其共同點是過濾中使用的值都是已知的。 但是 ...
一周排行
    -Advertisement-
    Play Games
  • 1. 說明 /* Performs operations on System.String instances that contain file or directory path information. These operations are performed in a cross-pla ...
  • 視頻地址:【WebApi+Vue3從0到1搭建《許可權管理系統》系列視頻:搭建JWT系統鑒權-嗶哩嗶哩】 https://b23.tv/R6cOcDO qq群:801913255 一、在appsettings.json中設置鑒權屬性 /*jwt鑒權*/ "JwtSetting": { "Issuer" ...
  • 引言 集成測試可在包含應用支持基礎結構(如資料庫、文件系統和網路)的級別上確保應用組件功能正常。 ASP.NET Core 通過將單元測試框架與測試 Web 主機和記憶體中測試伺服器結合使用來支持集成測試。 簡介 集成測試與單元測試相比,能夠在更廣泛的級別上評估應用的組件,確認多個組件一起工作以生成預 ...
  • 在.NET Emit編程中,我們探討了運算操作指令的重要性和應用。這些指令包括各種數學運算、位操作和比較操作,能夠在動態生成的代碼中實現對數據的處理和操作。通過這些指令,開發人員可以靈活地進行算術運算、邏輯運算和比較操作,從而實現各種複雜的演算法和邏輯......本篇之後,將進入第七部分:實戰項目 ...
  • 前言 多表頭表格是一個常見的業務需求,然而WPF中卻沒有預設實現這個功能,得益於WPF強大的控制項模板設計,我們可以通過修改控制項模板的方式自己實現它。 一、需求分析 下圖為一個典型的統計表格,統計1-12月的數據。 此時我們有一個需求,需要將月份按季度劃分,以便能夠直觀地看到季度統計數據,以下為該需求 ...
  • 如何將 ASP.NET Core MVC 項目的視圖分離到另一個項目 在當下這個年代 SPA 已是主流,人們早已忘記了 MVC 以及 Razor 的故事。但是在某些場景下 SSR 還是有意想不到效果。比如某些靜態頁面,比如追求首屏載入速度的時候。最近在項目中回歸傳統效果還是不錯。 有的時候我們希望將 ...
  • System.AggregateException: 發生一個或多個錯誤。 > Microsoft.WebTools.Shared.Exceptions.WebToolsException: 生成失敗。檢查輸出視窗瞭解更多詳細信息。 內部異常堆棧跟蹤的結尾 > (內部異常 #0) Microsoft ...
  • 引言 在上一章節我們實戰了在Asp.Net Core中的項目實戰,這一章節講解一下如何測試Asp.Net Core的中間件。 TestServer 還記得我們在集成測試中提供的TestServer嗎? TestServer 是由 Microsoft.AspNetCore.TestHost 包提供的。 ...
  • 在發現結果為真的WHEN子句時,CASE表達式的真假值判斷會終止,剩餘的WHEN子句會被忽略: CASE WHEN col_1 IN ('a', 'b') THEN '第一' WHEN col_1 IN ('a') THEN '第二' ELSE '其他' END 註意: 統一各分支返回的數據類型. ...
  • 在C#編程世界中,語法的精妙之處往往體現在那些看似微小卻極具影響力的符號與結構之中。其中,“_ =” 這一組合突然出現還真不知道什麼意思。本文將深入剖析“_ =” 的含義、工作原理及其在實際編程中的廣泛應用,揭示其作為C#語法奇兵的重要角色。 一、下劃線 _:神秘的棄元符號 下劃線 _ 在C#中並非 ...