第一行代碼(十四)

第十三章主要是編寫了一個功能完善的天氣預(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),我們需要重新建幾個包。


image.png

??然后添加一些依賴:

    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 文件


image.png
<?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>
image.png

??然后進(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 了。


image.png

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


device-2018-04-24-175238.png

device-2018-04-24-175401.png

??成功了,趕快提交一下代碼。
  • 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();
        }
    }
}
天氣信息圖1.png
天氣信息圖2.png

六、添加必應(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();
    }
背景圖片天氣信息.png

??背景圖片出來了,但是有一點(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)欄圖1.png

??狀態(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>
沉浸式狀態(tài)欄圖2.png

??完美,趕緊提交一下代碼,喝杯咖啡。

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);
                });
            }
        });
        ...
    }
下拉刷新.png

切換城市

??還記得我們前面把遍歷全國省市區(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);
                }
            }
        });

        ...
    }
側(cè)邊欄切換城市圖1.png

??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>
側(cè)邊欄切換城市圖2.png

??完美,趕緊提交一下代碼,上個廁所。

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

下一篇文章:http://www.itdecent.cn/p/9bad6c987206

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

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,034評論 25 709
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,304評論 4 61
  • 虛構(gòu)故事整體是進(jìn)步的,使智人從人類的進(jìn)化中脫穎而出,實(shí)現(xiàn)了大規(guī)模的合作,后來農(nóng)業(yè)文明產(chǎn)生,逐漸產(chǎn)生了貨幣,國家,再...
    木魚飛閱讀 906評論 1 0
  • 2018,作為最后一批的90后,我成年了。心情略微的復(fù)雜,長大了,不能在只想著玩玩玩;不能在橫沖直撞,不計(jì)后果;不...
    是栗栗呀閱讀 545評論 0 2
  • 改個標(biāo)題吸睛,我也是拼了 前情提要: 有一天服部全藏穿越到了我家,還堵了我的馬桶。凸! -2- 我胡亂抹了一把臉,...
    隔壁的森林閱讀 501評論 1 0

友情鏈接更多精彩內(nèi)容