第一行代码(十四)

第十三章主要是编写了一个功能完善的天气预报程序

一、功能需求分析

  在开始编码之前,我们先对程序进行需求分析,首先,我们应该认为酷欧天气 app 中应该具备以下功能:

  • 1.可以罗列出全国所有的省、市、县
  • 2.可以查看全国任意城市的天气信息
  • 3.可以自由地切换城市,去查看其它城市的天气
  • 4.提供手动更新以及后台自动更新天气的功能
      需求分析完成后,需要考虑一个问题,我们如何得到全国省市县的数据信息,以及如何获取到每个城市的天气信息?这理由两个选择:彩云天气以及和风天气,这两个天气预报服务虽说都是收费的,但是他们每天都提供了一定次数的免费天气预报请求。其中彩云天气的数据更加实时和专业,可以将天气预报精确到分钟级别,每天提供1000次免费请求;和风天气数据相对简单一些,比较适合学习,每天提供此3000次免费请求,那么我们就用这个次数多的了。
      想要罗列出中国所有的省份,需要访问下列地址:http://guolin.tech/api/china,服务器会返回我们一段 JSON 格式的数据,其中包含了中国所有的省份名称以及省份id,如下所示:
[{"id":1,"name":"北京"},{"id":2,"name":"上海"},{"id":3,"name":"天津"},{"id":4,"name":"重庆"},
{"id":5,"name":"香港"},{"id":6,"name":"澳门"},{"id":7,"name":"台湾"},{"id":8,"name":"黑龙江"},
{"id":9,"name":"吉林"},{"id":10,"name":"辽宁"},{"id":11,"name":"内蒙古"},{"id":12,"name":"河北"},
{"id":13,"name":"河南"},{"id":14,"name":"山西"},{"id":15,"name":"山东"},{"id":16,"name":"江苏"},
{"id":17,"name":"浙江"},{"id":18,"name":"福建"},{"id":19,"name":"江西"},{"id":20,"name":"安徽"},
{"id":21,"name":"湖北"},{"id":22,"name":"湖南"},{"id":23,"name":"广东"},{"id":24,"name":"广西"},
{"id":25,"name":"海南"},{"id":26,"name":"贵州"},{"id":27,"name":"云南"},{"id":28,"name":"四川"},
{"id":29,"name":"西藏"},{"id":30,"name":"陕西"},{"id":31,"name":"宁夏"},{"id":32,"name":"甘肃"},
{"id":33,"name":"青海"},{"id":34,"name":"新疆"}]

  这是一个 JSON 数组,数组中的每个元素都包含了省份 id 和省份名称,那么如何才能知道某个省内有哪些城市呢?只需要在 url 地址后面加上对应的省份 id 即可。http://guolin.tech/api/china/32

[{"id":311,"name":"兰州"},{"id":312,"name":"定西"},{"id":313,"name":"平凉"},
{"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":"嘉峪关"}]

  还是一个数组,并且包含了城市 id 和城市名字,那么如何知道城市下面的县和区呢?猜到了吧,就是继续在 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"}]

  省市区的问题解决了,那么如何获取城市对应的天气信息呢?注意上面的 JSON,每个县或区都会有一个 weather_id 字段,用这个字段再去访问和风天气的接口,就能获取到该地区具体的天气信息了。
http://guolin.tech/api/weather?cityid=CN101160101&key=bc0418b57b2d4918819d3974ac1285d9
  其中这个 key 我们需要去和风天气官网上注册且登录后,会自动生成,注册地址: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": "东北风",
            "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": "东北风",
            "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": "东北风",
            "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": "东风",
            "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": "东风",
            "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": "东风",
            "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": "东风",
            "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": "东北风",
            "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": "东北风",
            "wind_sc": "3-4",
            "wind_spd": "21"
        }],
        "aqi": {
            "city": {
                "aqi": "45",
                "pm25": "28",
                "qlty": "优"
            }
        },
        "suggestion": {
            "comf": {
                "brf": "较舒适",
                "txt": "白天会有降雨,这种天气条件下,人们会感到有些凉意,但大部分人完全可以接受。",
                "type": "comf"
            },
            "sport": {
                "brf": "较不宜",
                "txt": "有降水,推荐您在室内进行健身休闲运动;若坚持户外运动,须注意保暖并携带雨具。",
                "type": "sport"
            },
            "cw": {
                "brf": "不宜",
                "txt": "不宜洗车,未来24小时内有雨,如果在此期间洗车,雨水和路上的泥水可能会再次弄脏您的爱车。",
                "type": "cw"
            }
        }
    }]
}

  很复杂,其实是多,我们可以在http://www.heweather.com/documents这个网站里查看更加详细的文档说明。

二、将代码托管到 GitHub 上

   GitHub 是全球最大的代码托管网站,主要是借助 Git 来进行版本控制的,任何开源软件都可以免费地将代码提交到 GitHub 上,进行代码托管。其官网地址是:https://github.com/
  具体方法请移步到:https://www.jianshu.com/p/d02175a0a3ef

三、创建数据库和表

  为了让项目有个更好的结构,我们需要重新建几个包。


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'

  然后设计数据库的表结构,这里建立3张表:province、city、country,分别用于存放省、市、县的数据,对应到实体类中分别是: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;//当前城市所属省份的id

    //下面省略了 getter 和 setter 方法
}
/**
 * 县或区
 */
public class County extends DataSupport{
    private int id;
    private String countyName;//县或区名字
    private String weatherId;//县或区所对应的天气id
    private int cityId;//县或区所属城市的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

  然后进行代码提交

  • git add .
  • git commit -m "加入创建数据库和表的各项配置"
  • git push origin master


    image.png

四、遍历全国省市县数据

  在 Util 包下新增一个 HttpUtil 类

/**
 * Http工具类
 */

public class HttpUtil {

    /**
     * 发送 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 {


    /**
     * 解析和处理服务器返回的省级数据
     */
    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();//保存到数据库
                }
                return true;
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * 解析和处理服务器返回的市级数据
     */
    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;
    }

    /**
     * 解析和处理服务器返回的县级数据
     */
    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;
    }
}

  由于遍历全国省市县的功能我们在后面还会复用,因此就不写在活动里面,而是写在碎片里面,这样复用的时候就在布局中直接引用碎片就可以了。

<?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>

注意:这里之所以选择自定义标题栏,是因为碎片中最好不要直接使用 ActionBar 或 Toolbar,不然在复用的时候可能会出现你不想看到的效果。

  接下来编写用于遍历省市区的 Fragment

/**
 * 选择省市区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();
    }

    /**
     * 查询全国所有省,优先从数据库查询,如果没有查询到就再去服务器上传查询
     */
    private void queryProvinces() {
        tvTitle.setText("中国");
        btnBack.setVisibility(View.GONE);
        provinceList = DataSupport.findAll(Province.class);
        if (provinceList != null && provinceList.size() > 0) {//从数据库中查询
            dataList.clear();
            for (Province province : provinceList) {
                dataList.add(province.getProvinceName());
            }
            adapter.notifyDataSetChanged();
            listView.setSelection(0);
            currentLevel = LEVEL_PROVINCE;
        } else {//从服务器上获取
            String address = SERVICE_URL;
            queryFromServer(address, "province");
        }
    }

    /**
     * 查询全国所有市,优先从数据库查询,如果没有查询到就再去服务器上传查询
     */
    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");
        }
    }

    /**
     * 查询全国所有县,优先从数据库查询,如果没有查询到就再去服务器上传查询
     */
    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");
        }
    }

    /**
     * 根据传入的地址和类型从服务器上查询省市县数据
     */
    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();
                });
            }
        });
    }

    /**
     * 显示进度对话框
     */
    private void showProgressDialog() {
        if(progressDialog == null){
            progressDialog = new ProgressDialog(getActivity());
            progressDialog.setMessage("正在加载...");
            progressDialog.setCanceledOnTouchOutside(false);
        }
        progressDialog.show();
    }

    /**
     * 关闭进度对话框
     */
    private void closeProgressDialog() {
        if(progressDialog != null){
            progressDialog.dismiss();
        }
    }
}

  接下来修改活动的布局文件

<?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>

  另外,因为我们已经在布局里面自定义了标题栏,因此就不需要系统提供的 ActionBar 了。


image.png

  然后再清单文件中添加访问网络的权限,然后我们测试一下。


device-2018-04-24-175238.png

device-2018-04-24-175401.png

  成功了,赶快提交一下代码。
  • git add .
  • git commit -m "完成遍历省市县三级列表的功能"
  • git push origin master

五、显示天气信息

  我们需要查询天气,并且把天气信息展示出来。由于和风天气返回的 JSON 数据结构非常复杂,使用 JSONObject 来解析会很麻烦,所以我们就借助 GSON 来对天气信息进行解析。
  由于和风天气返回的数据内容非常多,我们就筛选一些比较重要的数据来进行解析。

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

  其中每个部分的内部都会有具体的内容

  • basic 中具体内容:
"basic": {
            "city": "兰州",
            "id": "CN101160101",
            "update": {
                "loc": "2018-04-23 17:47",
            }
        }

  在gson包下面建立对应的实体类

注意:可能有些字段不太适合直接作为 Java 字段来命名,因此这里使用了 @SerializedName 注解的方式来让 JSON 字段和 Java 字段之间建立映射关系。

public class Basiec {

    @SerializedName("city")
    public String cityName;//城市名称
    @SerializedName("id")
    public String weatherId;//城市对应的天气id

    public Update update;

    public class Update{

        @SerializedName("loc")
        public String updateTime;

        //下面是 getter 和 setter 方法
    }

    //下面是 getter 和 setter 方法
}
  • aqi 中具体内容:
        "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 中具体内容:
        "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 中具体内容:
"suggestion": {
            "comf": {
                "txt": "白天会有降雨,这种天气条件下,人们会感到有些凉意,但大部分人完全可以接受。"
            },
            "sport": {
                "txt": "有降水,推荐您在室内进行健身休闲运动;若坚持户外运动,须注意保暖并携带雨具。"
            },
            "cw": {
                "txt": "不宜洗车,未来24小时内有雨,如果在此期间洗车,雨水和路上的泥水可能会再次弄脏您的爱车。"
            }
        }
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 中具体内容:
"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 方法
}

  接下来再创建一个总实例类,来引用刚刚创建的各个实体类

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
  这里由于天气界面的布局比较复杂,为了不让里面的代码混乱不堪,我们使用引入布局的技巧,将界面的不同部分写在不同的布局文件里,再通过引入的方式集成到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 作为当前天气信息的布局

<?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="预报"
        android:textColor="#ffffff"
        android:textSize="20sp"/>

    <!-- 这里是用于显示未来几天天气信息的布局,根据服务器返回的数据在
         代码中动态添加 -->
    <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 作为空气质量布局

<?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"/>

    <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 指数"
                    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 指数"
                    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>

  接下来只需要将数据显示到界面上就可以了
  首先我们要在 Utility 类中添加一个用于解析天气 JSON 数据的方法

    /**
     * 将返回的 JSON 数据解析成 Weather 实体类
     */
    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;
    }

  接下来是请求数据

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);
        //有缓存,直接解析天气数据
        if(!TextUtils.isEmpty(weatherString)){
            Weather weather = Utility.handleWeatherResponse(weatherString);
            showWeatherInfo(weather);
        }else{
            //无缓存从网络上获取
            String weatherId = getIntent().getStringExtra("weather_id");
            //注意,请求数据的时候先将 ScrollView 隐藏掉,否则空数据的界面看上去很奇怪
            scrollWeather.setVisibility(View.INVISIBLE);
            requestWeather(weatherId);
        }
    }

    /**
     * 根据天气id从服务器上获取对应的天气信息
     */
    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();
        /*
            这里处理每天的天气信息,在循环中动态加载 forecast_item.xml 布局并设置相应的数据,然后添加到
            父布局当中,设置完成后要把 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 = "洗车指数:"+weather.suggestion.carWash.info;
        String sport = "运动建议:"+weather.suggestion.sport.info;
        tvComfort.setText(comfort);
        tvCarWash.setText(carWash);
        tvSport.setText(sport);
        scrollWeather.setVisibility(View.VISIBLE);
    }
}

  然后要完成从省市县列表到天气界面的跳转,修改 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){//跳转到天气信息界面
                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();
    }

  这时,天气信息界面已经展示成功了,那么我们在 MainActivity 中加入一个缓存数据的判断,如果有缓存数据,就直接展示缓存中的城市天气信息,否则才展示城市选择界面。

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

六、添加必应每日一图

  天气界面是编写出来了,不过我们的背景颜色是一个固定的纯色,感觉不是很高大上,所以我们决定将背景改成一张可以变化的图片。
  必应是由微软开发的搜索引擎网站,它除了搜索功能外,每天都会在首页展示一张精美的背景图片。获取必应每日一图的接口:http://guolin.tech/api/bing_pic
  访问该接口后,服务器会返回图片的连接地址,然后我们再去加载即可。
  修改 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);
        //从缓存中读取,如果没有,就访问服务器
        if(!TextUtils.isEmpty(bingPic)){
            Glide.with(this).load(bingPic).into(ivBingPic);
        }else{
            loadBingPic();
        }
    }
    /**
     * 从服务端获取背景图片
     */
    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) {

            }
        });
    }
    /**
     * 根据天气id从服务器上获取对应的天气信息
     */
    private void requestWeather(String weatherId) {
        ...
        /*
            注意,在每次请求天气信息的时候也调用一下获取图片的方法,
            这样在每次请求天气的时候就会同时刷新背景图片
         */
        loadBingPic();
    }
背景图片天气信息.png

  背景图片出来了,但是有一点问题,背景图并没有和状态栏融合在一起,在这我们不打算借助 Design Support 库来完成,而是一种更加简单的方式。修改 WeatherActivity 的代码

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        /*
            由于是 Android5.0 以上才支持的,所以我们加一个判断
            接着我们拿到当前活动的 DecorView,在调用 setSystemUiVisibility() 方法,
            来改变系统 UI 的显示

         */
        if(Build.VERSION.SDK_INT >= 21){
            View decorView = getWindow().getDecorView();
            //这里设置的参数的意思是表示活动的布局会显示在状态栏上面
            decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            //将状态栏设置成透明色
            getWindow().setStatusBarColor(Color.TRANSPARENT);
        }
        setContentView(R.layout.activity_weather);
        initView();
    }

沉浸式状态栏图1.png

  状态栏和背景图融合到一起了,但是天气界面的头布局和状态栏紧贴在一起了,不太好看。这是因为系统状态栏已经成为我们布局的一部分,没有单独为它留出空间,这是需要借助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 属性设置为true -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            android:orientation="vertical">
            ...
        </LinearLayout>

    </ScrollView>

</FrameLayout>
沉浸式状态栏图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;
        //有缓存,直接解析天气数据
        if(!TextUtils.isEmpty(weatherString)){
            Weather weather = Utility.handleWeatherResponse(weatherString);
            weatherId = weather.basic.weatherId;
            showWeatherInfo(weather);
        }else{
            //无缓存从网络上获取
            weatherId = getIntent().getStringExtra("weather_id");
            //注意,请求数据的时候先将 ScrollView 隐藏掉,否则空数据的界面看上去很奇怪
            scrollWeather.setVisibility(View.INVISIBLE);
            requestWeather(weatherId);
        }

        ...

        swipeRefresh.setOnRefreshListener(() -> requestWeather(weatherId));
    }
    /**
     * 根据天气id从服务器上获取对应的天气信息
     */
    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

切换城市

  还记得我们前面把遍历全国省市区的数据这个功能放到了一个碎片里面么,这时候就派上用场了,我们只需要在天气界面的布局中引入这个碎片,就可以快速集成切换城市的功能了。这里,我们希望使用侧边栏的功能来实现切换城市。修改 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 中进行。

    @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){//跳转到天气信息界面
                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);
                }
            }
        });

        ...
    }
侧边栏切换城市图1.png

  wtf?透明的?怎么办?简单,只要给 ListView 设置一个背景色就可以了
  修改 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>
侧边栏切换城市图2.png

  完美,赶紧提交一下代码,上个厕所。

git add .
git commit -m "新增切换城市和手动更新天气的功能"
git push origin master

八、后台自动更新天气

  为了让我们 app 更加智能,这里加入后台自动更新天气的功能,这样就可以保证用户每次打开软件时看到的都是最新的天气信息。
  首先新建一个服务,名为: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小时的毫秒数
        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) {
            //有缓存直接解析天气数据
            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();
                }
            });
        }
    }

    /**
     * 更新必应每日一图
     */
    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 里面去激活这个服务

    /**
     * 展示天气
     */
    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

九、修改图标和名称

  使用 AndroidStudio 自动生成的图标不太好看,我们需要换一张图标。
  理论上来将,我们应该给这个图标提供几种不同分辨率的版本,然后分别放入响应分辨率的 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 "修改程序图标和名称"
git push origin master

下一篇文章:https://www.jianshu.com/p/9bad6c987206

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,085评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,096评论 4 62
  • 虚构故事整体是进步的,使智人从人类的进化中脱颖而出,实现了大规模的合作,后来农业文明产生,逐渐产生了货币,国家,再...
    木鱼飞阅读 833评论 1 0
  • 2018,作为最后一批的90后,我成年了。心情略微的复杂,长大了,不能在只想着玩玩玩;不能在横冲直撞,不计后果;不...
    是栗栗呀阅读 463评论 0 2
  • 改个标题吸睛,我也是拼了 前情提要: 有一天服部全藏穿越到了我家,还堵了我的马桶。凸! -2- 我胡乱抹了一把脸,...
    隔壁的森林阅读 444评论 1 0