第十三章主要是編寫了一個功能完善的天氣預(yù)報程序
一、功能需求分析
??在開始編碼之前,我們先對程序進(jìn)行需求分析,首先,我們應(yīng)該認(rèn)為酷歐天氣 app 中應(yīng)該具備以下功能:
- 1.可以羅列出全國所有的省、市、縣
- 2.可以查看全國任意城市的天氣信息
- 3.可以自由地切換城市,去查看其它城市的天氣
- 4.提供手動更新以及后臺自動更新天氣的功能
??需求分析完成后,需要考慮一個問題,我們?nèi)绾蔚玫饺珖∈锌h的數(shù)據(jù)信息,以及如何獲取到每個城市的天氣信息?這理由兩個選擇:彩云天氣以及和風(fēng)天氣,這兩個天氣預(yù)報服務(wù)雖說都是收費(fèi)的,但是他們每天都提供了一定次數(shù)的免費(fèi)天氣預(yù)報請求。其中彩云天氣的數(shù)據(jù)更加實(shí)時和專業(yè),可以將天氣預(yù)報精確到分鐘級別,每天提供1000次免費(fèi)請求;和風(fēng)天氣數(shù)據(jù)相對簡單一些,比較適合學(xué)習(xí),每天提供此3000次免費(fèi)請求,那么我們就用這個次數(shù)多的了。
??想要羅列出中國所有的省份,需要訪問下列地址:http://guolin.tech/api/china,服務(wù)器會返回我們一段 JSON 格式的數(shù)據(jù),其中包含了中國所有的省份名稱以及省份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":"內(nèi)蒙古"},{"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 數(shù)組,數(shù)組中的每個元素都包含了省份 id 和省份名稱,那么如何才能知道某個省內(nèi)有哪些城市呢?只需要在 url 地址后面加上對應(yīng)的省份 id 即可。http://guolin.tech/api/china/32
[{"id":311,"name":"蘭州"},{"id":312,"name":"定西"},{"id":313,"name":"平?jīng)?},
{"id":314,"name":"慶陽"},{"id":315,"name":"武威"},{"id":316,"name":"金昌"},
{"id":317,"name":"張掖"},{"id":318,"name":"酒泉"},{"id":319,"name":"天水"},
{"id":320,"name":"武都"},{"id":321,"name":"臨夏"},{"id":322,"name":"合作"},
{"id":323,"name":"白銀"},{"id":324,"name":"嘉峪關(guān)"}]
??還是一個數(shù)組,并且包含了城市 id 和城市名字,那么如何知道城市下面的縣和區(qū)呢?猜到了吧,就是繼續(xù)在 url 地址后面加上 id
http://guolin.tech/api/china/32/311
[{"id":2332,"name":"蘭州","weather_id":"CN101160101"},
{"id":2333,"name":"皋蘭","weather_id":"CN101160102"},
{"id":2334,"name":"永登","weather_id":"CN101160103"},
{"id":2335,"name":"榆中","weather_id":"CN101160104"}]
??省市區(qū)的問題解決了,那么如何獲取城市對應(yīng)的天氣信息呢?注意上面的 JSON,每個縣或區(qū)都會有一個 weather_id 字段,用這個字段再去訪問和風(fēng)天氣的接口,就能獲取到該地區(qū)具體的天氣信息了。
http://guolin.tech/api/weather?cityid=CN101160101&key=bc0418b57b2d4918819d3974ac1285d9
??其中這個 key 我們需要去和風(fēng)天氣官網(wǎng)上注冊且登錄后,會自動生成,注冊地址:http://guolin.tech/api/weather/register
{
"HeWeather": [{
"basic": {
"cid": "CN101160101",
"location": "蘭州",
"parent_city": "蘭州",
"admin_area": "甘肅",
"cnty": "中國",
"lat": "36.05804062",
"lon": "103.82355499",
"tz": "+8.00",
"city": "蘭州",
"id": "CN101160101",
"update": {
"loc": "2018-04-23 17:47",
"utc": "2018-04-23 09:47"
}
},
"update": {
"loc": "2018-04-23 17:47",
"utc": "2018-04-23 09:47"
},
"status": "ok",
"now": {
"cloud": "100",
"cond_code": "300",
"cond_txt": "陣雨",
"fl": "7",
"hum": "72",
"pcpn": "0.0",
"pres": "1024",
"tmp": "9",
"vis": "10",
"wind_deg": "57",
"wind_dir": "東北風(fēng)",
"wind_sc": "2",
"wind_spd": "10",
"cond": {
"code": "300",
"txt": "陣雨"
}
},
"daily_forecast": [{
"date": "2018-04-23",
"cond": {
"txt_d": "小雨"
},
"tmp": {
"max": "14",
"min": "8"
}
}, {
"date": "2018-04-24",
"cond": {
"txt_d": "多云"
},
"tmp": {
"max": "18",
"min": "8"
}
}, {
"date": "2018-04-25",
"cond": {
"txt_d": "陣雨"
},
"tmp": {
"max": "22",
"min": "9"
}
}, {
"date": "2018-04-26",
"cond": {
"txt_d": "晴"
},
"tmp": {
"max": "24",
"min": "9"
}
}, {
"date": "2018-04-27",
"cond": {
"txt_d": "晴"
},
"tmp": {
"max": "27",
"min": "11"
}
}, {
"date": "2018-04-28",
"cond": {
"txt_d": "晴"
},
"tmp": {
"max": "29",
"min": "13"
}
}, {
"date": "2018-04-29",
"cond": {
"txt_d": "晴"
},
"tmp": {
"max": "29",
"min": "14"
}
}],
"hourly": [{
"cloud": "100",
"cond_code": "305",
"cond_txt": "小雨",
"dew": "3",
"hum": "63",
"pop": "61",
"pres": "1022",
"time": "2018-04-23 19:00",
"tmp": "11",
"wind_deg": "17",
"wind_dir": "東北風(fēng)",
"wind_sc": "1-2",
"wind_spd": "2"
}, {
"cloud": "100",
"cond_code": "305",
"cond_txt": "小雨",
"dew": "3",
"hum": "67",
"pop": "61",
"pres": "1024",
"time": "2018-04-23 22:00",
"tmp": "8",
"wind_deg": "35",
"wind_dir": "東北風(fēng)",
"wind_sc": "1-2",
"wind_spd": "6"
}, {
"cloud": "99",
"cond_code": "305",
"cond_txt": "小雨",
"dew": "3",
"hum": "71",
"pop": "25",
"pres": "1022",
"time": "2018-04-24 01:00",
"tmp": "8",
"wind_deg": "90",
"wind_dir": "東風(fēng)",
"wind_sc": "1-2",
"wind_spd": "7"
}, {
"cloud": "98",
"cond_code": "305",
"cond_txt": "小雨",
"dew": "2",
"hum": "74",
"pop": "20",
"pres": "1022",
"time": "2018-04-24 04:00",
"tmp": "8",
"wind_deg": "96",
"wind_dir": "東風(fēng)",
"wind_sc": "1-2",
"wind_spd": "11"
}, {
"cloud": "75",
"cond_code": "305",
"cond_txt": "小雨",
"dew": "5",
"hum": "72",
"pop": "14",
"pres": "1023",
"time": "2018-04-24 07:00",
"tmp": "8",
"wind_deg": "93",
"wind_dir": "東風(fēng)",
"wind_sc": "1-2",
"wind_spd": "9"
}, {
"cloud": "82",
"cond_code": "305",
"cond_txt": "小雨",
"dew": "3",
"hum": "53",
"pop": "55",
"pres": "1024",
"time": "2018-04-24 10:00",
"tmp": "10",
"wind_deg": "96",
"wind_dir": "東風(fēng)",
"wind_sc": "1-2",
"wind_spd": "6"
}, {
"cloud": "95",
"cond_code": "305",
"cond_txt": "小雨",
"dew": "2",
"hum": "52",
"pop": "6",
"pres": "1021",
"time": "2018-04-24 13:00",
"tmp": "15",
"wind_deg": "37",
"wind_dir": "東北風(fēng)",
"wind_sc": "1-2",
"wind_spd": "2"
}, {
"cloud": "83",
"cond_code": "104",
"cond_txt": "陰",
"dew": "1",
"hum": "53",
"pop": "3",
"pres": "1019",
"time": "2018-04-24 16:00",
"tmp": "17",
"wind_deg": "16",
"wind_dir": "東北風(fēng)",
"wind_sc": "3-4",
"wind_spd": "21"
}],
"aqi": {
"city": {
"aqi": "45",
"pm25": "28",
"qlty": "優(yōu)"
}
},
"suggestion": {
"comf": {
"brf": "較舒適",
"txt": "白天會有降雨,這種天氣條件下,人們會感到有些涼意,但大部分人完全可以接受。",
"type": "comf"
},
"sport": {
"brf": "較不宜",
"txt": "有降水,推薦您在室內(nèi)進(jìn)行健身休閑運(yùn)動;若堅(jiān)持戶外運(yùn)動,須注意保暖并攜帶雨具。",
"type": "sport"
},
"cw": {
"brf": "不宜",
"txt": "不宜洗車,未來24小時內(nèi)有雨,如果在此期間洗車,雨水和路上的泥水可能會再次弄臟您的愛車。",
"type": "cw"
}
}
}]
}
??很復(fù)雜,其實(shí)是多,我們可以在http://www.heweather.com/documents這個網(wǎng)站里查看更加詳細(xì)的文檔說明。
二、將代碼托管到 GitHub 上
?? GitHub 是全球最大的代碼托管網(wǎng)站,主要是借助 Git 來進(jìn)行版本控制的,任何開源軟件都可以免費(fèi)地將代碼提交到 GitHub 上,進(jìn)行代碼托管。其官網(wǎng)地址是:https://github.com/
??具體方法請移步到:http://www.itdecent.cn/p/d02175a0a3ef
三、創(chuàng)建數(shù)據(jù)庫和表
??為了讓項(xiàng)目有個更好的結(jié)構(gòu),我們需要重新建幾個包。

??然后添加一些依賴:
compile 'org.litepal.android:core:1.3.2'
compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'com.google.code.gson:gson:2.7'
compile 'com.github.bumptech.glide:glide:3.7.0'
??然后設(shè)計(jì)數(shù)據(jù)庫的表結(jié)構(gòu),這里建立3張表:province、city、country,分別用于存放省、市、縣的數(shù)據(jù),對應(yīng)到實(shí)體類中分別是:Province、City、County.
/**
* 省份
*/
public class Province extends DataSupport{
private int id;
private String provinceName;//省份的名字
private int provinceCode;//省份的代號
//下面省略了 getter 和 setter 方法
}
/**
* 城市
*/
public class City extends DataSupport{
private int id;
private String cityName;//城市名字
private int cityCode;//城市代號
private int provinceId;//當(dāng)前城市所屬省份的id
//下面省略了 getter 和 setter 方法
}
/**
* 縣或區(qū)
*/
public class County extends DataSupport{
private int id;
private String countyName;//縣或區(qū)名字
private String weatherId;//縣或區(qū)所對應(yīng)的天氣id
private int cityId;//縣或區(qū)所屬城市的id
//下面省略了 getter 和 setter 方法
}
??然后配置 litepal.xml 文件

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

??然后進(jìn)行代碼提交
- git add .
- git commit -m "加入創(chuàng)建數(shù)據(jù)庫和表的各項(xiàng)配置"
-
git push origin master
image.png
四、遍歷全國省市縣數(shù)據(jù)
??在 Util 包下新增一個 HttpUtil 類
/**
* Http工具類
*/
public class HttpUtil {
/**
* 發(fā)送 Http 請求
*/
public static void sendOkHttpRequest(String address,okhttp3.Callback callback){
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder()
.url(address)
.build();
okHttpClient.newCall(request).enqueue(callback);
}
}
??然后在 util 包下建立一個 Utility 類
/**
* JSON解析工具類
*/
public class Utility {
/**
* 解析和處理服務(wù)器返回的省級數(shù)據(jù)
*/
public static boolean handleProvinceResponse(String response) {
if (!TextUtils.isEmpty(response)) {
try {
JSONArray jsonArray = new JSONArray(response);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
Province province = new Province();
province.setProvinceName(jsonObject.getString("name"));
province.setProvinceCode(jsonObject.getInt("id"));
province.save();//保存到數(shù)據(jù)庫
}
return true;
} catch (JSONException e) {
e.printStackTrace();
}
}
return false;
}
/**
* 解析和處理服務(wù)器返回的市級數(shù)據(jù)
*/
public static boolean handleCityResponse(String response, int provinceId) {
if (!TextUtils.isEmpty(response)) {
try {
JSONArray jsonArray = new JSONArray(response);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
City city = new City();
city.setCityName(jsonObject.getString("name"));
city.setCityCode(jsonObject.getInt("id"));
city.setProvinceId(provinceId);
city.save();
}
return true;
} catch (JSONException e) {
e.printStackTrace();
}
}
return false;
}
/**
* 解析和處理服務(wù)器返回的縣級數(shù)據(jù)
*/
public static boolean handleCountyResponse(String response, int cityId) {
if (!TextUtils.isEmpty(response)) {
try {
JSONArray jsonArray = new JSONArray(response);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
County county = new County();
county.setCountyName(jsonObject.getString("name"));
county.setWeatherId(jsonObject.getString("weather_id"));
county.setCityId(cityId);
county.save();
}
return true;
} catch (JSONException e) {
e.printStackTrace();
}
}
return false;
}
}
??由于遍歷全國省市縣的功能我們在后面還會復(fù)用,因此就不寫在活動里面,而是寫在碎片里面,這樣復(fù)用的時候就在布局中直接引用碎片就可以了。
<?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="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="#ffffff"
android:textSize="20sp" />
<Button
android:id="@+id/btn_back"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:background="@drawable/ic_back" />
</RelativeLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
注意:這里之所以選擇自定義標(biāo)題欄,是因?yàn)樗槠凶詈貌灰苯邮褂?ActionBar 或 Toolbar,不然在復(fù)用的時候可能會出現(xiàn)你不想看到的效果。
??接下來編寫用于遍歷省市區(qū)的 Fragment
/**
* 選擇省市區(qū)Fragment
*/
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 static final String SERVICE_URL = "http://guolin.tech/api/china/";
//省列表
private List<Province> provinceList;
//市列表
private List<City> cityList;
//縣列表
private List<County> countyList;
//選中的省份
private Province selectedProvince;
//選中的城市
private City selectedCity;
//選中的級別
private int currentLevel;
private List<String> dataList = new ArrayList<>();
private TextView tvTitle;
private Button btnBack;
private ListView listView;
private ArrayAdapter<String> adapter;
private ProgressDialog progressDialog;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.choose_area, container, false);
tvTitle = view.findViewById(R.id.tv_title);
btnBack = view.findViewById(R.id.btn_back);
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) {
super.onActivityCreated(savedInstanceState);
listView.setOnItemClickListener((parent, view, position, id) -> {
if (currentLevel == LEVEL_PROVINCE) {
selectedProvince = provinceList.get(position);
queryCities();
} else if (currentLevel == LEVEL_CITY) {
selectedCity = cityList.get(position);
queryCounties();
}
});
btnBack.setOnClickListener(v -> {
if (currentLevel == LEVEL_COUNTY) {
queryCities();
} else if (currentLevel == LEVEL_CITY) {
queryProvinces();
}
});
queryProvinces();
}
/**
* 查詢?nèi)珖惺?,?yōu)先從數(shù)據(jù)庫查詢,如果沒有查詢到就再去服務(wù)器上傳查詢
*/
private void queryProvinces() {
tvTitle.setText("中國");
btnBack.setVisibility(View.GONE);
provinceList = DataSupport.findAll(Province.class);
if (provinceList != null && provinceList.size() > 0) {//從數(shù)據(jù)庫中查詢
dataList.clear();
for (Province province : provinceList) {
dataList.add(province.getProvinceName());
}
adapter.notifyDataSetChanged();
listView.setSelection(0);
currentLevel = LEVEL_PROVINCE;
} else {//從服務(wù)器上獲取
String address = SERVICE_URL;
queryFromServer(address, "province");
}
}
/**
* 查詢?nèi)珖惺?,?yōu)先從數(shù)據(jù)庫查詢,如果沒有查詢到就再去服務(wù)器上傳查詢
*/
private void queryCities() {
tvTitle.setText(selectedProvince.getProvinceName());
btnBack.setVisibility(View.VISIBLE);
cityList = DataSupport.where("provinceid = ?", String.valueOf(selectedProvince.getId())).find(City.class);
if (cityList != null && cityList.size() > 0) {
dataList.clear();
for (City city : cityList) {
dataList.add(city.getCityName());
}
adapter.notifyDataSetChanged();
listView.setSelection(0);
currentLevel = LEVEL_CITY;
} else {
String address = SERVICE_URL + selectedProvince.getProvinceCode();
queryFromServer(address, "city");
}
}
/**
* 查詢?nèi)珖锌h,優(yōu)先從數(shù)據(jù)庫查詢,如果沒有查詢到就再去服務(wù)器上傳查詢
*/
private void queryCounties() {
tvTitle.setText(selectedCity.getCityName());
btnBack.setVisibility(View.VISIBLE);
countyList = DataSupport.where("cityid = ?", String.valueOf(selectedCity.getId())).find(County.class);
if (countyList != null && countyList.size() > 0) {
dataList.clear();
for (County county : countyList) {
dataList.add(county.getCountyName());
}
adapter.notifyDataSetChanged();
listView.setSelection(0);
currentLevel = LEVEL_COUNTY;
} else {
String address = SERVICE_URL + selectedProvince.getProvinceCode() + "/" + selectedCity.getCityCode();
queryFromServer(address, "county");
}
}
/**
* 根據(jù)傳入的地址和類型從服務(wù)器上查詢省市縣數(shù)據(jù)
*/
private void queryFromServer(String address, String type) {
showProgressDialog();
HttpUtil.sendOkHttpRequest(address, new Callback() {
@Override
public void onResponse(Call call, 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());
}
if (result) {
getActivity().runOnUiThread(() ->{
closeProgressDialog();
if ("province".equals(type)) {
queryProvinces();
} else if ("city".equals(type)) {
queryCities();
} else if ("county".equals(type)) {
queryCounties();
}
});
}
}
@Override
public void onFailure(Call call, IOException e) {
getActivity().runOnUiThread(() -> {
closeProgressDialog();
Toast.makeText(getContext(), "加載失敗", Toast.LENGTH_SHORT).show();
});
}
});
}
/**
* 顯示進(jìn)度對話框
*/
private void showProgressDialog() {
if(progressDialog == null){
progressDialog = new ProgressDialog(getActivity());
progressDialog.setMessage("正在加載...");
progressDialog.setCanceledOnTouchOutside(false);
}
progressDialog.show();
}
/**
* 關(guān)閉進(jìn)度對話框
*/
private void closeProgressDialog() {
if(progressDialog != null){
progressDialog.dismiss();
}
}
}
??接下來修改活動的布局文件
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/choose_area_fragment"
android:name="com.coolweather.android.coolweather.ChooseAreaFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
??另外,因?yàn)槲覀円呀?jīng)在布局里面自定義了標(biāo)題欄,因此就不需要系統(tǒng)提供的 ActionBar 了。

??然后再清單文件中添加訪問網(wǎng)絡(luò)的權(quán)限,然后我們測試一下。


??成功了,趕快提交一下代碼。
- git add .
- git commit -m "完成遍歷省市縣三級列表的功能"
- git push origin master
五、顯示天氣信息
??我們需要查詢天氣,并且把天氣信息展示出來。由于和風(fēng)天氣返回的 JSON 數(shù)據(jù)結(jié)構(gòu)非常復(fù)雜,使用 JSONObject 來解析會很麻煩,所以我們就借助 GSON 來對天氣信息進(jìn)行解析。
??由于和風(fēng)天氣返回的數(shù)據(jù)內(nèi)容非常多,我們就篩選一些比較重要的數(shù)據(jù)來進(jìn)行解析。
{
"HeWeather": [
{
"basic": {},
"status": "ok",
"now": {},
"daily_forecast": [],
"aqi": {},
"suggestion": {}
}
]
}
??其中每個部分的內(nèi)部都會有具體的內(nèi)容
- basic 中具體內(nèi)容:
"basic": {
"city": "蘭州",
"id": "CN101160101",
"update": {
"loc": "2018-04-23 17:47",
}
}
??在gson包下面建立對應(yīng)的實(shí)體類
注意:可能有些字段不太適合直接作為 Java 字段來命名,因此這里使用了
@SerializedName注解的方式來讓 JSON 字段和 Java 字段之間建立映射關(guān)系。
public class Basiec {
@SerializedName("city")
public String cityName;//城市名稱
@SerializedName("id")
public String weatherId;//城市對應(yīng)的天氣id
public Update update;
public class Update{
@SerializedName("loc")
public String updateTime;
//下面是 getter 和 setter 方法
}
//下面是 getter 和 setter 方法
}
- aqi 中具體內(nèi)容:
"aqi": {
"city": {
"aqi": "45",
"pm25": "28"
}
}
public class AQI {
public AQICity city;
public class AQICity{
public String api;
public String pm25;
//下面是 getter 和 setter 方法
}
//下面是 getter 和 setter 方法
}
- now 中具體內(nèi)容:
"now": {
"tmp": "9",
"cond": {
"txt": "陣雨"
}
}
public class Now {
@SerializedName("tmp")
public String temperature;
@SerializedName("cond")
public More more;
public class More {
@SerializedName("txt")
public String info;
//下面是 getter 和 setter 方法
}
//下面是 getter 和 setter 方法
}
- suggestion 中具體內(nèi)容:
"suggestion": {
"comf": {
"txt": "白天會有降雨,這種天氣條件下,人們會感到有些涼意,但大部分人完全可以接受。"
},
"sport": {
"txt": "有降水,推薦您在室內(nèi)進(jìn)行健身休閑運(yùn)動;若堅(jiān)持戶外運(yùn)動,須注意保暖并攜帶雨具。"
},
"cw": {
"txt": "不宜洗車,未來24小時內(nèi)有雨,如果在此期間洗車,雨水和路上的泥水可能會再次弄臟您的愛車。"
}
}
public class Suggestion {
@SerializedName("comf")
public Comfort comfort;
@SerializedName("cw")
public CarWash carWash;
public Sport sport;
public class Comfort{
@SerializedName("txt")
public String info;
//下面是 getter 和 setter 方法
}
public class CarWash{
@SerializedName("txt")
public String info;
//下面是 getter 和 setter 方法
}
public class Sport{
@SerializedName("txt")
public String info;
//下面是 getter 和 setter 方法
}
//下面是 getter 和 setter 方法
}
- daily_forecast 中具體內(nèi)容:
"daily_forecast": [{
"date": "2018-04-23",
"cond": {
"txt_d": "小雨"
},
"tmp": {
"max": "14",
"min": "8"
}
}, {
"date": "2018-04-24",
"cond": {
"txt_d": "多云"
},
"tmp": {
"max": "18",
"min": "8"
}
},
...]
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;
//下面是 getter 和 setter 方法
}
public class More{
@SerializedName("txt_d")
public String info;
//下面是 getter 和 setter 方法
}
//下面是 getter 和 setter 方法
}
??接下來再創(chuàng)建一個總實(shí)例類,來引用剛剛創(chuàng)建的各個實(shí)體類
public class Weather {
public String status;
public Basiec basic;
public AQI aqi;
public Now now;
public Suggestion suggestion;
@SerializedName("daily_forecast")
public List<Forecast> forecastList;
//下面是 getter 和 setter 方法
}
??接下來,我們需要編寫天氣界面了,新建一個 Activity 叫做 WeatherActivity
??這里由于天氣界面的布局比較復(fù)雜,為了不讓里面的代碼混亂不堪,我們使用引入布局的技巧,將界面的不同部分寫在不同的布局文件里,再通過引入的方式集成到activity_weather.xml中
??首先我們新建一個 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:id="@+id/tv_title_city"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="#ffffff"
android:textSize="20sp" />
<TextView
android:id="@+id/tv_update_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:textColor="#ffffff"
android:textSize="16sp"/>
</RelativeLayout>
??然后新建一個 now.xml 作為當(dāng)前天氣信息的布局
<?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"
android:orientation="vertical">
<TextView
android:id="@+id/tv_degree"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textColor="#ffffff"
android:textSize="60sp"/>
<TextView
android:id="@+id/tv_weather_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textColor="#ffffff"
android:textSize="20sp"/>
</LinearLayout>
??然后新建 forecast.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"
android:background="#8000"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="15dp"
android:text="預(yù)報"
android:textColor="#ffffff"
android:textSize="20sp"/>
<!-- 這里是用于顯示未來幾天天氣信息的布局,根據(jù)服務(wù)器返回的數(shù)據(jù)在
代碼中動態(tài)添加 -->
<LinearLayout
android:id="@+id/ll_forecast"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</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"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="2"
android:textColor="#ffffff" />
<TextView
android:id="@+id/tv_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:gravity="center"
android:textColor="#ffffff" />
<TextView
android:id="@+id/tv_max"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="right"
android:textColor="#ffffff" />
<TextView
android:id="@+id/tv_min"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="right"
android:textColor="#ffffff" />
</LinearLayout>
??新建 aqi.xml 作為空氣質(zhì)量布局
<?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"
android:background="#8000"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="15dp"
android:text="空氣質(zhì)量"
android:textColor="#ffffff"
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:id="@+id/tv_aqi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#ffffff"
android:textSize="40sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="AQI 指數(shù)"
android:textColor="#ffffff"/>
</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:id="@+id/tv_pm25"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#ffffff"
android:textSize="40sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="PM2.5 指數(shù)"
android:textColor="#ffffff"/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
??再新建一個 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:layout_margin="15dp"
android:background="#8000"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="15dp"
android:text="生活建議"
android:textColor="#ffffff"
android:textSize="20sp"/>
<TextView
android:id="@+id/tv_confort"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:textColor="#ffffff"/>
<TextView
android:id="@+id/tv_car_wash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:textColor="#ffffff"/>
<TextView
android:id="@+id/tv_sport"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:textColor="#ffffff"/>
</LinearLayout>
??天氣界面上每個部分的布局都寫好了,接下來就是將他們引入到 activity_weather.xml 中
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary">
<ScrollView
android:id="@+id/scrollview_weather"
android:layout_width="match_parent"
android:layout_height="match_parent"
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>
??接下來只需要將數(shù)據(jù)顯示到界面上就可以了
??首先我們要在 Utility 類中添加一個用于解析天氣 JSON 數(shù)據(jù)的方法
/**
* 將返回的 JSON 數(shù)據(jù)解析成 Weather 實(shí)體類
*/
public static Weather handleWeatherResponse(String response){
try {
JSONObject jsonObject = new JSONObject(response);
JSONArray jsonArray = jsonObject.getJSONArray("HeWeather");
String weatherContent = jsonArray.getJSONObject(0).toString();
return new Gson().fromJson(weatherContent,Weather.class);
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
??接下來是請求數(shù)據(jù)
public class WeatherActivity extends AppCompatActivity {
private ScrollView scrollWeather;
private TextView tvTitleCity;
private TextView tvUpdateTime;
private TextView tvDegree;
private TextView tvWeatherInfo;
private LinearLayout llForecast;
private TextView tvAqi;
private TextView tvPm25;
private TextView tvComfort;
private TextView tvCarWash;
private TextView tvSport;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_weather);
initView();
}
/**
* 初始化控件
*/
private void initView(){
scrollWeather = (ScrollView) findViewById(R.id.scrollview_weather);
tvTitleCity = (TextView) findViewById(R.id.tv_title_city);
tvUpdateTime = (TextView) findViewById(R.id.tv_update_time);
tvDegree = (TextView) findViewById(R.id.tv_degree);
tvWeatherInfo = (TextView) findViewById(R.id.tv_weather_info);
llForecast = (LinearLayout) findViewById(R.id.ll_forecast);
tvAqi = (TextView) findViewById(R.id.tv_aqi);
tvPm25 = (TextView) findViewById(R.id.tv_pm25);
tvComfort = (TextView) findViewById(R.id.tv_comfort);
tvCarWash = (TextView) findViewById(R.id.tv_car_wash);
tvSport = (TextView) findViewById(R.id.tv_sport);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String weatherString = prefs.getString("weather",null);
//有緩存,直接解析天氣數(shù)據(jù)
if(!TextUtils.isEmpty(weatherString)){
Weather weather = Utility.handleWeatherResponse(weatherString);
showWeatherInfo(weather);
}else{
//無緩存從網(wǎng)絡(luò)上獲取
String weatherId = getIntent().getStringExtra("weather_id");
//注意,請求數(shù)據(jù)的時候先將 ScrollView 隱藏掉,否則空數(shù)據(jù)的界面看上去很奇怪
scrollWeather.setVisibility(View.INVISIBLE);
requestWeather(weatherId);
}
}
/**
* 根據(jù)天氣id從服務(wù)器上獲取對應(yīng)的天氣信息
*/
private void requestWeather(String weatherId) {
String weatherUrl = "http://guolin.tech/api/weather?cityid=" + weatherId + "&key=bc0418b57b2d4918819d3974ac1285d9";
HttpUtil.sendOkHttpRequest(weatherUrl, new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
String responseText = response.body().string();
Weather weather = Utility.handleWeatherResponse(responseText);
runOnUiThread(() -> {
if(weather != null && "ok".equals(weather.getStatus())){
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(WeatherActivity.this).edit();
editor.putString("weather",responseText);
editor.apply();
showWeatherInfo(weather);
}else{
Toast.makeText(WeatherActivity.this, "獲取天氣信息失敗", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> Toast.makeText(WeatherActivity.this, "獲取天氣信息失敗", Toast.LENGTH_SHORT).show());
}
});
}
/**
* 展示天氣
*/
private void showWeatherInfo(Weather weather) {
String cityName = weather.basic.cityName;
String updateTime = weather.basic.update.updateTime.split(" ")[1];
String degree = weather.now.temperature + "°C";
String weatherInfo = weather.now.more.info;
tvTitleCity.setText(cityName);
tvUpdateTime.setText(updateTime);
tvDegree.setText(degree);
tvWeatherInfo.setText(weatherInfo);
llForecast.removeAllViews();
/*
這里處理每天的天氣信息,在循環(huán)中動態(tài)加載 forecast_item.xml 布局并設(shè)置相應(yīng)的數(shù)據(jù),然后添加到
父布局當(dāng)中,設(shè)置完成后要把 ScrollView 變得可見
*/
for(Forecast forecast : weather.forecastList){
View view = LayoutInflater.from(this).inflate(R.layout.forecast_item, llForecast, false);
TextView tvDate = view.findViewById(R.id.tv_date);
TextView tvInfo = view.findViewById(R.id.tv_info);
TextView tvMax = view.findViewById(R.id.tv_max);
TextView tvMin = view.findViewById(R.id.tv_min);
tvDate.setText(forecast.date);
tvInfo.setText(forecast.more.info);
tvMax.setText(forecast.temperature.max);
tvMin.setText(forecast.temperature.min);
llForecast.addView(view);
}
if(weather.aqi != null){
tvAqi.setText(weather.aqi.aqiCity.aqi);
tvPm25.setText(weather.aqi.aqiCity.pm25);
}
String comfort = "舒適度:"+weather.suggestion.comfort.info;
String carWash = "洗車指數(shù):"+weather.suggestion.carWash.info;
String sport = "運(yùn)動建議:"+weather.suggestion.sport.info;
tvComfort.setText(comfort);
tvCarWash.setText(carWash);
tvSport.setText(sport);
scrollWeather.setVisibility(View.VISIBLE);
}
}
??然后要完成從省市縣列表到天氣界面的跳轉(zhuǎn),修改 ChooseAreaFragment中的代碼
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
listView.setOnItemClickListener((parent, view, position, id) -> {
if (currentLevel == LEVEL_PROVINCE) {
selectedProvince = provinceList.get(position);
queryCities();
} else if (currentLevel == LEVEL_CITY) {
selectedCity = cityList.get(position);
queryCounties();
} else if(currentLevel == LEVEL_COUNTY){//跳轉(zhuǎn)到天氣信息界面
String weatherId = countyList.get(position).getWeatherId();
Intent intent = new Intent(getActivity(),WeatherActivity.class);
intent.putExtra("weather_id",weatherId);
startActivity(intent);
getActivity().finish();
}
});
btnBack.setOnClickListener(v -> {
if (currentLevel == LEVEL_COUNTY) {
queryCities();
} else if (currentLevel == LEVEL_CITY) {
queryProvinces();
}
});
queryProvinces();
}
??這時,天氣信息界面已經(jīng)展示成功了,那么我們在 MainActivity 中加入一個緩存數(shù)據(jù)的判斷,如果有緩存數(shù)據(jù),就直接展示緩存中的城市天氣信息,否則才展示城市選擇界面。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if(prefs.getString("weather",null) != null){
Intent intent = new Intent(this,WeatherActivity.class);
startActivity(intent);
finish();
}
}
}


六、添加必應(yīng)每日一圖
??天氣界面是編寫出來了,不過我們的背景顏色是一個固定的純色,感覺不是很高大上,所以我們決定將背景改成一張可以變化的圖片。
??必應(yīng)是由微軟開發(fā)的搜索引擎網(wǎng)站,它除了搜索功能外,每天都會在首頁展示一張精美的背景圖片。獲取必應(yīng)每日一圖的接口:http://guolin.tech/api/bing_pic
??訪問該接口后,服務(wù)器會返回圖片的連接地址,然后我們再去加載即可。
??修改 activity_weather.xml 中的代碼
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary">
<!-- 背景圖片 -->
<ImageView
android:id="@+id/iv_bing_pic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
<ScrollView
android:id="@+id/scrollview_weather"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
android:overScrollMode="never">
...
</ScrollView>
</FrameLayout>
/**
* 初始化控件
*/
private void initView(){
...
ivBingPic = (ImageView) findViewById(R.id.iv_bing_pic);
...
String bingPic = prefs.getString("bing_pic",null);
//從緩存中讀取,如果沒有,就訪問服務(wù)器
if(!TextUtils.isEmpty(bingPic)){
Glide.with(this).load(bingPic).into(ivBingPic);
}else{
loadBingPic();
}
}
/**
* 從服務(wù)端獲取背景圖片
*/
private void loadBingPic(){
String requestBingPic = "http://guolin.tech/api/bing_pic";
HttpUtil.sendOkHttpRequest(requestBingPic, new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
String bingPic = response.body().string();
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(WeatherActivity.this).edit();
editor.putString("bing_pic",bingPic).apply();
runOnUiThread(() -> Glide.with(WeatherActivity.this).load(bingPic).into(ivBingPic));
}
@Override
public void onFailure(Call call, IOException e) {
}
});
}
/**
* 根據(jù)天氣id從服務(wù)器上獲取對應(yīng)的天氣信息
*/
private void requestWeather(String weatherId) {
...
/*
注意,在每次請求天氣信息的時候也調(diào)用一下獲取圖片的方法,
這樣在每次請求天氣的時候就會同時刷新背景圖片
*/
loadBingPic();
}

??背景圖片出來了,但是有一點(diǎn)問題,背景圖并沒有和狀態(tài)欄融合在一起,在這我們不打算借助 Design Support 庫來完成,而是一種更加簡單的方式。修改 WeatherActivity 的代碼
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*
由于是 Android5.0 以上才支持的,所以我們加一個判斷
接著我們拿到當(dāng)前活動的 DecorView,在調(diào)用 setSystemUiVisibility() 方法,
來改變系統(tǒng) UI 的顯示
*/
if(Build.VERSION.SDK_INT >= 21){
View decorView = getWindow().getDecorView();
//這里設(shè)置的參數(shù)的意思是表示活動的布局會顯示在狀態(tài)欄上面
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
//將狀態(tài)欄設(shè)置成透明色
getWindow().setStatusBarColor(Color.TRANSPARENT);
}
setContentView(R.layout.activity_weather);
initView();
}

??狀態(tài)欄和背景圖融合到一起了,但是天氣界面的頭布局和狀態(tài)欄緊貼在一起了,不太好看。這是因?yàn)橄到y(tǒng)狀態(tài)欄已經(jīng)成為我們布局的一部分,沒有單獨(dú)為它留出空間,這是需要借助
android:fitsSystemWindows屬性就可以了,修改 acivity_weather.xml 中的代碼
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary">
<!-- 背景圖片 -->
<ImageView
android:id="@+id/iv_bing_pic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
<ScrollView
android:id="@+id/scrollview_weather"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
android:overScrollMode="never">
<!-- 在這里將 fitSystemWindows 屬性設(shè)置為true -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:orientation="vertical">
...
</LinearLayout>
</ScrollView>
</FrameLayout>

??完美,趕緊提交一下代碼,喝杯咖啡。
git add.
git commit -m "加入顯示天氣信息的功能"
git push origin master
七、手動更新天氣和切換城市
手動更新天氣
??手動更新天氣,我們使用下拉刷新的方式來更新,修改 activity_weather.xml中的代碼。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary">
...
<!-- 下拉刷新 -->
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scrollview_weather"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:scrollbars="none">
...
</ScrollView>
</android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>
??然后修改 WeatherActivity 中的代碼
/**
* 初始化控件
*/
private void initView(){
...
swipeRefresh = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh);
swipeRefresh.setColorSchemeResources(R.color.colorPrimary);
...
String weatherId;
//有緩存,直接解析天氣數(shù)據(jù)
if(!TextUtils.isEmpty(weatherString)){
Weather weather = Utility.handleWeatherResponse(weatherString);
weatherId = weather.basic.weatherId;
showWeatherInfo(weather);
}else{
//無緩存從網(wǎng)絡(luò)上獲取
weatherId = getIntent().getStringExtra("weather_id");
//注意,請求數(shù)據(jù)的時候先將 ScrollView 隱藏掉,否則空數(shù)據(jù)的界面看上去很奇怪
scrollWeather.setVisibility(View.INVISIBLE);
requestWeather(weatherId);
}
...
swipeRefresh.setOnRefreshListener(() -> requestWeather(weatherId));
}
/**
* 根據(jù)天氣id從服務(wù)器上獲取對應(yīng)的天氣信息
*/
private void requestWeather(String weatherId) {
String weatherUrl = "http://guolin.tech/api/weather?cityid=" + weatherId + "&key=bc0418b57b2d4918819d3974ac1285d9";
HttpUtil.sendOkHttpRequest(weatherUrl, new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
...
runOnUiThread(() -> {
...
swipeRefresh.setRefreshing(false);
});
}
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> {
Toast.makeText(WeatherActivity.this, "獲取天氣信息失敗", Toast.LENGTH_SHORT).show();
swipeRefresh.setRefreshing(false);
});
}
});
...
}

切換城市
??還記得我們前面把遍歷全國省市區(qū)的數(shù)據(jù)這個功能放到了一個碎片里面么,這時候就派上用場了,我們只需要在天氣界面的布局中引入這個碎片,就可以快速集成切換城市的功能了。這里,我們希望使用側(cè)邊欄的功能來實(shí)現(xiàn)切換城市。修改 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">
<Button
android:id="@+id/btn_nav"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:background="@drawable/ic_home" />
...
</RelativeLayout>
??添加完 Button 后,我們緊接著修改 activity_weather.xml 布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary">
...
<android.support.v4.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 下拉刷新 -->
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
</android.support.v4.widget.SwipeRefreshLayout>
<fragment
android:id="@+id/choose_area_fragment"
android:name="com.coolweather.android.coolweather.ChooseAreaFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"/>
</android.support.v4.widget.DrawerLayout>
</FrameLayout>
/**
* 初始化控件
*/
private void initView(){
...
btnNav = (Button) findViewById(R.id.btn_nav);
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
btnNav.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START));
...
}
??我們還需要處理切換城市后的邏輯,這個工作必須要在 ChooseAreaFragment 中進(jìn)行。
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
listView.setOnItemClickListener((parent, view, position, id) -> {
if (currentLevel == LEVEL_PROVINCE) {
...
} else if (currentLevel == LEVEL_CITY) {
...
} else if(currentLevel == LEVEL_COUNTY){//跳轉(zhuǎn)到天氣信息界面
String weatherId = countyList.get(position).getWeatherId();
if(getActivity() instanceof MainActivity){
Intent intent = new Intent(getActivity(),WeatherActivity.class);
intent.putExtra("weather_id",weatherId);
startActivity(intent);
getActivity().finish();
}else if(getActivity() instanceof WeatherActivity){
WeatherActivity activity = (WeatherActivity) getActivity();
activity.drawerLayout.closeDrawers();
activity.swipeRefresh.setRefreshing(true);
activity.requestWeather(weatherId);
}
}
});
...
}

??wtf?透明的?怎么辦?簡單,只要給 ListView 設(shè)置一個背景色就可以了
??修改 choose_area.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="match_parent"
android:textColor="#ffffff"
android:orientation="vertical">
...
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff">
</ListView>
</LinearLayout>

??完美,趕緊提交一下代碼,上個廁所。
git add .
git commit -m "新增切換城市和手動更新天氣的功能"
git push origin master
八、后臺自動更新天氣
??為了讓我們 app 更加智能,這里加入后臺自動更新天氣的功能,這樣就可以保證用戶每次打開軟件時看到的都是最新的天氣信息。
??首先新建一個服務(wù),名為:AutoUpdateService
public class AutoUpdateService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
updateWeather();
updateBingPic();
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
int anHour = 8 * 60 * 60 * 1000;//8小時的毫秒數(shù)
long trigger = SystemClock.elapsedRealtime() + anHour;
Intent i = new Intent(this,AutoUpdateService.class);
PendingIntent pendingIntent = PendingIntent.getService(this,0,i,0);
manager.cancel(pendingIntent);
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,trigger,pendingIntent);
return super.onStartCommand(intent, flags, startId);
}
/**
* 更新天氣信息
*/
private void updateWeather() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String weatherString = prefs.getString("weather", null);
if (weatherString != null) {
//有緩存直接解析天氣數(shù)據(jù)
Weather weather = Utility.handleWeatherResponse(weatherString);
String weatherId = weather.basic.getWeatherId();
String weatherUrl = "http://guolin.tech/api/weather?cityid=" + weatherId + "&key=bc0418b57b2d4918819d3974ac1285d9";
HttpUtil.sendOkHttpRequest(weatherUrl, new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
String responseText = response.body().string();
Weather weather = Utility.handleWeatherResponse(responseText);
if(weather != null){
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(AutoUpdateService.this).edit();
editor.putString("weather",responseText).apply();
}
}
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
});
}
}
/**
* 更新必應(yīng)每日一圖
*/
private void updateBingPic() {
String requestBingPic = "http://guolin.tech/api/bing_pic";
HttpUtil.sendOkHttpRequest(requestBingPic, new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
String bingPic = response.body().string();
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(AutoUpdateService.this).edit();
editor.putString("bing_pic",bingPic);
editor.apply();
}
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
});
}
}
??在 WeatherActivity 里面去激活這個服務(wù)
/**
* 展示天氣
*/
private void showWeatherInfo(Weather weather) {
if (weather != null && "ok".equals(weather.status)) {
String cityName = weather.basic.cityName;
String updateTime = weather.basic.update.updateTime.split(" ")[1];
String degree = weather.now.temperature + "°C";
String weatherInfo = weather.now.more.info;
tvTitleCity.setText(cityName);
tvUpdateTime.setText(updateTime);
tvDegree.setText(degree);
tvWeatherInfo.setText(weatherInfo);
llForecast.removeAllViews();
...
Intent intent = new Intent(this, AutoUpdateService.class);
startService(intent);
} else {
Toast.makeText(this, "獲取天氣信息失敗", Toast.LENGTH_SHORT).show();
}
}
??又完成了一個功能,提交一下。
git add .
git commit -m "增加后臺自動更新天氣的功能"
git push origin master
九、修改圖標(biāo)和名稱
??使用 AndroidStudio 自動生成的圖標(biāo)不太好看,我們需要換一張圖標(biāo)。
??理論上來將,我們應(yīng)該給這個圖標(biāo)提供幾種不同分辨率的版本,然后分別放入響應(yīng)分辨率的 mipmap 目錄下,但是為了方便起見,我們就用一張圖片 logo.png,并且將我們程序的名稱修改成酷歐天氣,修改清單文件。
<application
android:name="org.litepal.LitePalApplication"
android:allowBackup="true"
android:icon="@mipmap/logo"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
</application>
<resources>
<string name="app_name">酷歐天氣</string>
</resources>
??提交代碼,下班走人。
git add .
git commit -m "修改程序圖標(biāo)和名稱"
git push origin master
