第014天:实战开发酷欧天气APP

2023-10-09 06:30
文章标签 实战 app 开发 天气 014 酷欧

本文主要是介绍第014天:实战开发酷欧天气APP,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

        我们将要在本章中编写一个功能较为完整的天气预报程序,学习了这么久的Android开发,现在终于到了考核验收的时候了。那么第一步我们需要给这个软件起个好听的名字,这里就叫它酷欧天气吧,英文名就叫作CoolWeathero确定了名字之后,下面就可以开始动手了。

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

        在开始编码之前,我们需要先对程序进行需求分析,想一想酷欧天气中应该具备哪些功能。 将这些功能全部整理岀来之后,我们才好动手去一一实现。这里我认为酷欧天气中至少应该具备以下功能:

□可以罗列岀全国所有的省、市、县;

可以查看全国任意城市的天气信息;

□可以自由地切换城市,去查看其他城市的天气;

提供手动更新以及后台自动更新天气的功能。

        虽然看上去只有4个主要的功能点,但如果想要全部实现这些功能却需要用到UI网络、 数据存储、服务等技术,因此还是非常考验你的综合应用能力的。不过好在这些技术在前面的章 节中我们全部都学习过了,只要你学得用心,相信完成这些功能对你来说并不难。

        分析完了需求之后,接下来就要进行技术可行性分析了。首先需要考虑的一个问题就是,我 们如何才能得到全国省市县的数据信息,以及如何才能获取到每个城市的天气信息。比较遗憾的 是,现在网上免费的天气预报接口已经越来越少,很多之前可以使用的接口都慢慢关闭掉了,包 括本书第1版中使用的中国天气网的接口。因此,这次我也是特意用心去找了一些更加稳定的天 气预报服务,比如彩云天气以及和风天气都非常不错。这两个天气预报服务虽说都是收费的,但 它们每天都提供了一定次数的免费天气预报请求。其中彩云天气的数据更加实时和专业,可以将 天气预报精确到分钟级,每天提供1000次免费请求;和风天气的数据相对简单一些,比较适合 新手学习,每天提供3000次免费请求。那么简单起见,这里我们就使用和风天气来作为天气预 报的数据来源,每天3000次的免费请求对于学习而言已经是相当充足了。

        解决了天气数据的问题,接下来还需要解决全国省市县数据的问题。同样,现在网上也没有 一个稳定的接口可以使用,那么为了方便你的学习,我专门架设了一台服务器用于提供全国所有 省市县的数据信息,从而帮你把道路都铺平了。

        那么下面我们来看一下这些接口的具体用法。比如要想罗列出中国所有的省份,只需访问如 下地址:http://guolin.tech/api/china服务器会返回我们一段JSON格式的数据,其中包含了中国所有的省份名称以及省份id,如 下所示:

        可以看到,这是一个JSON数组,数组中的每一个元素都代表着一个省份。其中,北京的id 1,上海的id2。那么如何才能知道某个省内有哪些城市呢?其实也很简单,比如江苏的id 16,访问如下地址即可:http://guolin.tech/api/china/16也就是说,只需要将省份id添加到url地址的最后面就可以了,现在服务器返回的数据如下:

        这样我们就得到江苏省内所有城市的信息了,可以看到,现在返回的数据格式和刚才查看省 份信息时返回的数据格式是一样的。相信此时你已经可以举一反三了,比如说苏州的id116, 那么想要知道苏州市下又有哪些县和区的时候,只需访问如下地址:http://guolin.tech/api/china/16/116这次服务器返回的数据如下:

 

        通过这种方式,我们就能把全国所有的省、市、县都罗列出来了。那么解决了省市县数据的 获取,我们又怎样才能查看到具体的天气信息呢?这就必须要用到每个地区对应的天气id 了。 观察上面返回的数据,你会发现每个县或区都会有一个weather_id,拿着这个id再去访问和风天 气的接口,就能够获取到该地区具体的天气信息了。

        下面我们来看一下和风天气的接口该如何使用。首先你需要注册一个自己的账号,注册地址 是http://guolin.tech/api/weather/registero注册好了之后使用这个账号登录,就能看到自己的API Key,以及每天剩余的访问次数了,如图14.1所示。

        有了 API Key,再配合刚才的weather_id,我们就能获取到任意城市的天气信息了。比如说 苏州的weather_idCN101190401,那么访问如下接口即可查看苏州的天气信息:

http://guolin.tech/api/weather?cityid=CN101190401&key=bc0418b57b2d4918819d3974acl2 85d9

        其中,cityid部分填入的就是待查看城市的weather_id, key部分填入的就是我们申请到的 API Keyo这样,服务器就会把苏州详细的天气信息以JSON格式返回给我们了。不过,由于返 回的数据过于复杂,这里我做了一下精简处理,如下所示:

"HeWeather":[
{
"status": "ok",
"basic": {},
"aqi": {},
"now": {},
"suggestion": {}, "daily-forecast":[]
}
]
}

        返回数据的格式大体上就是这个样子了,其中status代表请求的状态,ok表示成功o basic 中会包含城市的一些基本信息,aqi中会包含当前空气质量的情况,now中会包含当前的天气信 息,suggestion中会包含一些天气相关的生活建议,daily foecast中会包含未来几天的天气 信息。访问http://guolin.tech/api/weather/doc这个网址可以查看更加详细的文档说明。这个网址可以查看更加详细的文档说明。http://guolin.tech/api/weather/doc这个网址可以查看更加详细的文档说明。

        数据都能获取到了之后,接下来就是JSON解析的工作了,这对于你来说应该很轻松了吧?确定了技术完全可行之后,接下来就可以开始编码了。不过别着急,我们准备让酷欧天气成为一个开源软件,并使用GitHub来进行代码托管,因此先让我们进入到本书最后一次的Git时间。

14.2 Git时间——将代码托管到GitHub

        经过前面几章的学习,相信你已经可以非常熟练地使用Git 了。本节依然是Git时间,这次 我们将会把酷欧天气的代码托管到GitHub上面。

        GitHub是全球最大的代码托管网站,主要是借助Git来进行版本控制的。任何开源软件都可 以免费地将代码提交到GitHub上,以零成本的代价进行代码托管。GitHub的官网地址是 https://github.com/。官网的首页如图14.2所示。官网的首页如图14.2所示。

https://github.com/。官网的首页如图14.2所示。

        首先你需要有一个GitHub账号才能使用GitHub的代码托管功能,点击Sign up for GitHub 按钮进行注册,然后填入用户名、邮箱和密码,如图14.3所示。

        点击Create an account按钮来创建账户,接下来会让你选择个人计划,收费计划有创建私 人版本库的权限,而我们的酷欧天气是开源软件,所以这里选择免费计划就可以了,如图14.4 所示。

        接着点击Continue按钮会进入一个问卷调查界面,如图14.5所示。

         如果你对这个有兴趣就填写一下,没兴趣的话直接点击最下方的skip this step跳过就可以了 o 这样我们就把账号注册好了,会自动跳转到GitHub的个人主页,如图14.6所示。

        接下来就可以点击Start a project按钮来创建一个版本库了。由于我们是刚刚注册的账号,在 创建版本库之前还需要做一下邮箱验证,验证成功之后就能开始创建了。这里将版本库命名为 coolweather,然后选择添加一个Android项目类型的.gitignore文件,并使用Apache License 2.0 来作为酷欧天气的开源协议,如图14.7所示。

         接着点击Create repository按钮,coolweather这个版本库就创建完成了,如图14.8所示。版 本库主页地址是 https://github.com/guolindev/coolweather

 

        可以看到,GitHub已经自动帮我们创建了.gitignoreLICENSEREADME.md3个文件, 其中编辑README.md文件中的内容可以修改酷欧天气版本库主页的描述。

        创建好了版本库之后,我们就需要创建酷欧天气这个项目了。在Android Studio中新建一个 Android项目,项目名叫作 CoolWeather,包名叫作 com.coolweather.android,如图 14.9所示。

         之后的步骤不用多说,一直点击Next就可以完成项目的创建,所有选项都使用默认的就好。

        接下来的一步非常重要,我们需要将远程版本库克隆到本地。首先必须知道远程版本库的 Git地址,点击Clone or download按钮就能够看到了,如图14.10所示。

        点击右边的复制按钮可以将版本库的Git地址复制到剪贴板,酷欧天气版本库的Git地址是 https://github. com/guolindev/coolweather.git o

        然后打开Git Bash并切换到CoolWeather的工程目录下,如图14.11所示。

接着输入 git clone GitHub - guolindev/coolweather:酷欧天气是一款基于Android端开源的天气预报软件来把远程版本库克隆到本地, 如图14.12所示。

         看到图中所给的文字提示就表示克隆成功了 ,并且.gitignore. LICENSE和README.md3 个文件也已经被复制到了本地,可以进入到coolweather目录,并使用Is-al命令查看一下,如 图14.13所示。

        现在我们需要将这个目录中的所有文件全部复制粘贴到上一层目录中,这样就能将整个 CoolWeatherI程目录添加到版本控制中去了。注意.git是一个隐藏目录,在复制的时候千万不要 漏掉。另外,上一层目录中也有一个.gitignore文件,我们直接将其覆盖即可。复制完之后可以把 coolweather目录删除掉,最终CoolWeatherI程的目录结构如图14.14所示。

         接下来我们应该把CoolWeather项目中现有的文件提交到GitHub上面,这就很简单了,先将 所有文件添加到版本控制中,如下所示:

git add .

然后在本地执行提交操作:

git commit -m "First commit."

最后将提交的内容同步到远程版本库,也就是GitHub ±面:

git push origin master

        注意,在最后一步的时候GitHub要求输入用户名和密码来进行身份校验,这里输入我们注 册时填入的用户名和密码就可以了,如图14.15所示。

        这样就已经同步完成了,现在刷新一下酷欧天气版本库的主页,你会看到刚才提交的那些文 件已经存在了,如图14.16所示。

14.3创建数据库和表

        从本节开始,我们就要真正地动手编码了,为了要让项目能够有更好的结构,这里需要在 com.coolweather.android包下再新建儿个包,如图14.17所示。

         其中db包用于存放数据库模型相关的代码,gson包用于存放GSON模型相关的代码,service 包用于存放服务相关的代码,util包用于存放工具相关的代码。

        根据14.1节进行的技术可行性分析,第一阶段我们要做的就是创建好数据库和表,这样从 服务器获取到的数据才能够存储到本地。关于数据库和表的创建方式,我们早在第6章中就已经 学过了。那么为了简化数据库的操作,这里我准备使用LitePal来管理酷欧天气的数据库。

        首先需要将项目所需的各种依赖库进行声明,编辑app/build.gradle文件,在dependencies闭 包中添加如下内容:

dependencies {

compile fileTree(dir: 'libs', include: [jar'])

compile 1 com.android.support:appcompat-v7:24.2.1'

testCompile 1junit:junit:4.12'

compile 'org.litepal.android:core:1.3.2*

compile 'comsquareup.okhttp3:okhttp:3.4.1*

compile 'com.google.codegson:gson:2.7'

compile 'com.github.bumptech.glide:glide:3.7.0'

}

        这里声明的4个库我们之前都是使用过的,LitePal用于对数据库进行操作,OkHttp用于进 行网络请求,GSON用于解析JSON数据,Glide用于加载和展示图片。酷欧天气将会对这几个 库进行综合运用,这里直接一次性将它们都添加进来。

        然后我们来设计一下数据库的表结构,表的设计当然是仁者见仁智者见智,并不是说哪种设 计就是最规范最完美的。这里我准备建立3张表:province. citycounty,分另U用于存放省、市、 县的数据信息。对应到实体类中的话,就应该建立Province. CityCounty3个类。

那么,在db包下新建一个Province类,代码如下所示:

public class Province extends DataSupport {

private int id;

private String provinceName;

private int provinceCode;

return id;

public void setld(int id) { this.id = id;

}

public String getProvinceName() {

return provinceName;

}

public void setProvinceName(String provinceName) {

this.provinceName = provinceName;

}

public int getProvinceCode() {

return provinceCode;

}

public void setProvinceCode(int provinceCode) {

this.provinceCode = provinceCode;

}

}

        其中,id是每个实体类中都应该有的字段,provinceName记录省的名字,provinceCode 记录省的代号。另外,LitePal中的每一个实体类都是必须要继承自DataSupport类的。

        接着在db包下新建一个City类,代码如下所示:

public class City extends DataSupport {

private int id;

private String cityName;

private int cityCode;

private int provinceld;

public int getld() {

return id;

}

public void setld(int id) {

this.id = id;

}

public String getCityName() {

return cityName;

}

public void setCityName(String cityName) {

this.cityName = cityName;

public int getCityCode() { return cityCode;

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

}

public int getProvinceId() {

return provinceld;

}

public void setProvinceId(int provinceld) { this.provinceld = provinceld;

}

}

        其中,cityName记录市的名字,cityCode记录市的代号,provinceld记录当前市所属省 的id值。

然后在db包下新建一个County类,代码如下所示:

public class County extends DataSupport {

private int id;

private String countyName;

private String weatherld;

private int cityld;

public int getld() {

return id;

public void setld(int id) { this.id = id;

public String getCountyName() { return countyName;

}

public void setCountyName(St ring countyName) { this.countyName = countyName;

public String getWeatherld() { return weatherld;

public void setWeathe rid(St ring weatherld) { this.weatherld = weatherld;

}

public int getCityId() (

return cityld;

}

public void setCityId(int cityld) {

this.cityld = cityld;

}

}

        其中,countyName记录县的名字,weatherld记录县所对应的天气id, cityld记录当前 县所属市的id值。

        可以看到,实体类的内容都非常简单,就是声明了一些需要的字段,并生成相应的getter setter方法就可以了。

        接下来需要配置litepal.xml文件。右击app/src/main目录>New—>Directory,创建一个assets 目录,然后在assets目录下再新建一个litepal.xml文件,接着编辑1 itepal.xml文件中的内容,如 下所示:

<litepal>

<dbname value="cool_weather" />

<version value="l" />

<list>

<mapping class="com.coolweather.android.db.Province" />

<mapping class="com.coolweather.android.db.City" />

<mapping class="com.coolweather.android.db.County" />

</list>

</litepal>

        这里我们将数据库名指定成cool weather,数据库版本指定成1,并将Province. CityCounty3个实体类添加到映射列表当中。

        最后还需要再配置一下LitePalApplication,修改AndroidManifest.xm 1中的代码,如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.coolweather.android">

<application

android: name=a,org  litepal  LitePalApplication"

android:allowBackup="true"

android: icon="(amipmap/ic_launcher"

android:label="(astring/app name"

android:supportsRtl="true"

android:theme="@style/AppTheme">

</application>

</manifest>

        这样我们就将所有的配置都完成了,数据库和表会在首次执行任意数据库操作的时候自动 创建。

        好了,第一阶段的代码写到这里就差不多了,我们现在提交一下。首先将所有新增的文件添 加到版本控制中:

git add .

        接着执行提交操作:

git commit -m “加入创建数据库和表的各项配置。"

        最后将提交同步到GitHub上面:

git push origin master

        OK!第一阶段完工,下面让我们赶快进入到第二阶段的开发工作中吧。

14.4遍历全国省市县数据

        在第二阶段中,我们准备把遍历全国省市县的功能加入,这一阶段需要编写的代码量比较大, 你一定要跟上脚步。

        我们已经知道,全国所有省市县的数据都是从服务器端获取到的,因此这里和服务器的交互 是必不可少的,所以我们可以在util包下先增加一个Httputil类,代码如下所示:

public class HttpUtil {

public static void sendOkHttpRequest(String address, okhttp3.Callback callback) ( OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder().url(address).build(); client.newCall(request).enqueue(callback);

}

}

        由于OkHttp的出色封装,这里和服务器进行交互的代码非常简单,仅仅3行就完成了。现 在我们发起一条HTTP请求只需要调用sendOkHttpRequest ()方法,传入请求地址,并注册一 个回调来处理服务器响应就可以了。

        另外,由于服务器返回的省市县数据都是JSON格式的,所以我们最好再提供一个工具类来 解析和处理这种数据。在util包下新建一个Utility类,代码如下所示:

public class Utility {

/**

*解析和处理服务器返回的省级数据

*/

public static boolean handleProvinceResponse(String response) ( if (!TextUtils.isEmpty(response)) {

try {

JSONArray allProvinces = new JSONArray(response);

for (int i = 0; i < allProvinces.length(); i++) (

JSONObject provinceobject = allProvinces.getJSONObject(i); Province province = new Province();

province.setProvinceName(provinceObject.getString("name")); province.setProvinceCode(provinceObject.getlnt("id")); province.save();

}

return true;

} catch (JSONException e) { e.printStackTrace();

}

}

return false;

}

/**

*解析和处理服务器返回的市级数据

*/

public static boolean handleCityResponse(String response, int provinceld) { if (JTextUtils.isEmpty(response)) {

try (

JSONArray allCities = new JSONArray(response); for (int i = 0; i < allCities.length(); i++) { JSONObject cityObject = allCities.getJSONObject(i); City city = new City();

city.setCityName(cityObject.getString("name"));

city.setCityCode(cityObject.getInt("id"));

city.setProvinceld(provinceld);

city.save();

}

return true;

} catch (JSONException e) { e.printStackTrace();

}

}

return false;

}

/**

*解析和处理服务器返回的县级数据

*/

public static boolean handleCountyResponse(String response, int cityld) { if (ITextUtils.isEmpty(response)) {

try {

JSONArray allCounties = new JSONArray(response);

for (int i = 0; i < allCounties.length(); i++) { JSONObject countyObject = allCounties.getJSONObject(i); County county = new County();

county.setCountyName(countyObject.getString("name")); county.setWeatherld(countyObject.getString("weatherid")); county.setCityld(cityld);

county.save();

}

return true;

} catch (JSONException e) {

e.printStackTrace();

}

}

return false;

}

}

        可以看到,我们提供了 handleProvincesResponse()handleCitiesResponse()handleCountiesResponseO3个方法,分别用于解析和处理服务器返回的省级、市级和县级 数据。处理的方式都是类似的,先使用JSONArrayJSONObject将数据解析出来,然后组装成 实体类对象,再调用save()方法将数据存储到数据库当中。由于这里的JSON数据结构比较简 单,我们就不使用GSON来进行解析了。

        需要准备的工具类就这么多,现在可以开始写界面了。由于遍历全国省市县的功能我们在后 面还会复用,因此就不写在活动里面了,而是写在碎片里面,这样需要复用的时候直接在布局里 面引用碎片就可以了。

res/layout目录中新建choose area.xml布局,代码如下所示:

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:background="#fff">

<RelativeLayout

android:layout_width="match_parent"

android:layout_height="?attr/actionBarSize"

and roid :backg round=,,?attr/colorPrimary,,>

<TextView

android:id="@+id/title_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content" android:layout_centerInParent="true" android:textCoTor="#fff" android:textSize="20sp"/>

<Button
android :id=,,@+id/back_button"

android:layout_width="25dp"

android:layout_height="25dp"

android:layout_marginLeft="10dp"

android:layoutalignParentLeft="true"

android:layout_centerVertical="true" android:background="@drawable/ic_back"/> </RelativeLayout>

<ListView

android:id="@+id/list_view" android:layout_width="match_parent" android:layout_height="matchparent"/>

</LinearLayout>

        布局文件中的内容并不复杂,我们先是定义了一个头布局来作为标题栏,将布局高度设置为 actionBar的高度,背景色设置为colorPrimary然后在头布局中放置了一个TextView用于显示标 题内容,放置了一个Button用于执行返回操作,注意我已经提前准备好了一张ic_back.png图片 用于作为按钮的背景图。这里之所以要自己定义标题栏,是因为碎片中最好不要直接使用 ActionBarToolbar,不然在复用的时候可能会出现一些你不想看到的效果。

        接下来在头布局的下面定义了一个ListView,省市县的数据就将显示在这里。之所以这次使 用了 ListView,是因为它会自动给每个子项之间添加一条分隔线,而如果使用RecyclerView想实 现同样的功能则会比较麻烦,这里我们总是选择最优的实现方案。

        接下来也是最关键的一步,我们需要编写用于遍历省市县数据的碎片了。新建Choose- AreaFragment继承自Fragment,代码如下所示:

public class ChooseAreaFragment extends Fragment {

private ProgressDialog progressDialog;

private TextView titleText;

private Button backButton;

private Listview listview;

private ArrayAdapter<String> adapter;

private List<String> dataList = new ArrayList<>();

/**

*省列表

*/

private List<Province> provinceList;

/**

*市列表

*/

private List<City> cityList;

/**

*县列表

*/

private List<County> countyList;

/**

*选中的省份

*/

private Province selectedProvince;

/**

*选中的城市

*/

private City selectedCity;

/**

*当前选中的级别

*/

private int currentLevel;

@0verride

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedlnstanceState) {

View view = inflater.inflate(R.layout.choosearea, container, false); titleText = (Textview) view.findViewByld(R.id.titletext); backButton = (Button) view.findViewByld(R.id.backbutton); listview = (ListView) view.findViewByld(R.id.listview);

adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_ iteml, dataList);

listview.setAdapter(adapter);

return view;

}

©Override

public void onActivityCreated(Bundle savedlnstanceState) { super.onActivityCreated(savedlnstanceState); listview.setOnItemClickListener(new AdapterView.OnltemClickListenerf) { ^Override

public void onItemClick(AdapterView<?> parentz View view, int position, long id) ( if (currentLevel == LEVEL_PROVINCE) {

selectedProvince = provinceList.get(position); queryCities();

} else if (currentLevel == LEVELCITY) < selectedCity = cityList.get(position); queryCounties();

}

}

})

backButton.setOnClickListener(new View.OnClickListener() { ^Override public void onClick(View v) {

if (currentLevel == LEVEL_COUNTY) { queryCities();

} else if (currentLevel == LEVEL_CITY) { queryProvinces();

}

}

})

queryProvinces();

} /**

*查询全国所有的省,优先从数据库查询,如果没有查询到再去服务器上查询 */

private void queryProvinces() { titleText.setText(”中); backButton.setVisibility(View.GONE); provinceList = DataSupport.findAll(Province.class); if (provinceList.size() > 0) { dataList.clear();

for (Province province : provinceList) { dataList.add(province.getProvinceName());

} adapter.notifyDataSetChanged(); listview.setSelection(O); currentLevel = LEVELPROVINCE;

else {

String address = "http://guolin.tech/api/china"; queryFromSeever(address, "province");

/**

*查询选中省内所有的市,优先从数据库查询,如果没有查询到再去服务器上查询

*/

private void queryCities() {

titleText.setText(selectedProvince.getProvinceName()); backButton.setvisibility(View.VISIBLE);

cityList = DataSupport.where("provinceid = ?", St ring.valueOf(selected Province.getld())).find(City.class);

if (cityList.size() > 0) {

dataList.clear();

for (City city : cityList) {

dataList.add(city.getCityName());

}

adapter.notifyDataSetChanged(); listview.setSelection(O); currentLevel = LEVEL_CITY;

) else {

int provinceCode = selectedProvince.getProvinceCode();

String address = "http://guolin.tech/api/china/u + provinceCode; queryFromSeever(address, "city");

*查询选中市内所有的县,优先从数据库查询,如果没有查询到再去服务器上查询

*/

private void queryCounties() {

titleText.setText(selectedCity.getCityName()); backButton.setvisibility(View.VISIBLE);

countyList = DataSupport.where("cityid = ?", St ring.valueOf(selectedCity. getld())).find(County.class);

if (countyList.size() > 0) { dataList.clear(); for (County county : countyList) { dataList.add(county.getCountyName());

}

adapter.notifyDataSetChangedf);

listview.setSelection(O); currentLevel = LEVEL_COUNTY;

} else {

int provinceCode = selectedProvince.getProvinceCodef);

int cityCode = selectedCity.getCityCode();

String address = "http://guolin.tech/api/china/" + provinceCode + "/" + cityCode;

que ryF romSe rve r(add res s, "county");

*根据传入的地址和类型从服务器上查询省市县数据

*/

private void que ryF romSe rve r(St ring address, final String type) { showProgressDialog(); HttpUtil.sendOkHttpRequest(address, new Callback。 {

(aOverride

public void onResponse(Call call, Response response) throws lOException { 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.getld());

}

if (result) {

getActivity().runOnUiThread(new Runnable() {

^Override

public void run() { closeProgressDialog(); if ("province".equals(type)) (

queryProvinces();

} else if ("city".equals(type)) { queryCities();

} else if ("county".equals(type)) { queryCounties();

}

}

})

)

}

(QOverride

public void onFailure(Call call, lOException e) {

//通过runOnlliThread()方法回到主线程处理逻辑

getActivity(). runOnlliThread(new Runnable() {

^Override

public void run() { closeProgressDialog(); Toast.makeText(getContext(),"加载失败“,Toast.LENGTH SHORT). show();

}

})

}

})

}

/**

*显示进度对话框

*/

private void showProgressDialog() (

if (progressDialog == null) { progressDialog = new ProgressDialog(getActivity()); progressDialog. setMessage("正在加载 progressDialog.setCanceledOnTouchOutside(false);

} progressDialog.show();

}

/**

*关闭进度对话框

*/

private void closeProgressDialog() {

if (progressDialog != null) { progressDialog.dismiss();

}

}

        这个类里的代码虽然非常多,可是逻辑却不复杂,我们来慢慢理一下。在onCreateViewO 方法中先是获取到了一些控件的实例,然后去初始化了 ArrayAdapter,并将它设置为List View的 适配器。接着在0nActivityCreated()方法中给ListViewButton设置了点击事件,到这里我 们的初始化工作就算是完成了。

        在0nActivityCreated()方法的最后,调用了 queryProvinces ()方法,也就是从这里开 始加载省级数据的。queryProvinces()方法中首先会将头布局的标题设置成中国,将返回按钮 隐藏起来,因为省级列表已经不能再返回了。然后调用LitePal的查询接口来从数据库中读取省 级数据,如果读取到了就直接将数据显示到界面上,如果没有读取到就按照14.1节讲述的接口 组装出一个请求地址,然后调用queryFromServerO^法来从服务器上查询数据。

        queryFromServerO方法中会调用HttpUtilsendOkHttpRequest ()方法来向服务器发送 请求,响应的数据会回调到onResponse()方法中,然后我们在这里去调用UtilityhandleProvincesResponse()方法来解析和处理服务器返回的数据,并存储到数据库中。接下 来的一步很关键,在解析和处理完数据之后,我们再次调用了 queryProvinces()方法来重新加 载省级数据,由于queryProvinces()方法牵扯到了 UI操作,因此必须要在主线程中调用,这 里借助了 runOnUiThreadO方法来实现从子线程切换到主线程。现在数据库中已经存在了数据, 因此调用queryProvinces ()就会直接将数据显示到界面上了。

        当你点击了某个省的时候会进入到ListViewonItemClick()方法中,这个时候会根据当 前的级别来判断是去调用queryCities()方法还是queryCounties ()方法,queryCities ()方 法是去查询市级数据,而queryCounties()方法是去查询县级数据,这两个方法内部的流程和 queryProvinces()方法基本相同,这里就不重复讲解了。

        另外还有一点需要注意,在返回按钮的点击事件里,会对当前ListView的列表级别进行判断。 如果当前是县级列表,那么就返回到市级列表,如果当前是市级列表,那么就返回到省级表列表。 当返回到省级列表时,返回按钮会自动隐藏,从而也就不需要再做进一步的处理了。

        这样我们就把遍历全国省市县的功能完成了,可是碎片是不能直接显示在界面上的,因此我 们还需要把它添加到活动里才行。修改activity_main.xml中的代码,如下所示:

<FrameLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_parent">

<fragment

android:id="@+id/choose_areafragment" android:name="com.coolweather.android.ChooseAreaFragment" android:layout_width="match_parent" android:layout_height="match_parent" />

</FrameLayout>

        布局文件很简单,只是定义了一个FrameLayout,然后将ChooseAreaFragment添加进来,并 让它充满整个布局。

        另外,我们刚才在碎片的布局里面已经自定义了一个标题栏,因此就不再需要原生的 ActionBar 了,修改 res/values/styles.xml 中的代码,如下所示:

<resources>

<!-- Base application theme.-->

<style name="AppThemeH parent="Theme.AppCompat.Light.NoActionBar">

</style>

</resources>

        现在第二阶段的开发工作也完成得差不多了,我们可以运行一下来看看效果。不过在运行之 前还有一件事没有做,那就是声明程序所需要的权限。修改AndroidManifest.xml中的代码,如下 所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.coolweather.androidn>

<uses-permission android:name=**android.permission.INTERNET" />

</manifest>

        由于我们是通过网络接口来获取全国省市县数据的,因此必须要添加访问网络的权限才行。 现在可以运行一下程序了,结果如图14.18所示。

 

        可以看到,全国所有省级数据都显示出来了。我们还可以继续查看市级数据,比如点击江苏 省,结果如图14.19所示。

        这个时候标题栏上会出现一个返回按钮,用于返回上一级列表。

然后再点击苏州市查看县级数据,结果如图14.20所示。

好了,这样第二阶段的开发工作也都完成了,我们仍然要把代码提交一下。

git add .

git commit -m "完成遍历省市县三级列表的功能。"

git push origin master

到目前为止进度算是相当不错啊,那么我们就趁热打铁,来进行第三阶段的开发工作。

14.5显示天气信息

        在第三阶段中,我们就要开始去查询天气,并且把天气信息显示出来了。由于和风天气返回 的JSON数据结构非常复杂,如果还使用JSONObject来解析就会很麻烦,这里我们就准备借助 GSON来对天气信息进行解析了。

14.5.1定义GSON实体类

        GSON的用法很简单,解析数据只需要一行代码就能完成了,但前提是要先将数据对应的实 体类创建好。由于和风天气返回的数据内容非常多,这里我们不可能将所有的内容都利用起来, 因此我筛选了一些比较重要的数据来进行解析。

        首先我们回顾一下返回数据的大致格式:

{

"HeWeather":[

{

"status": "ok",

"basic": {},

"aqi": {},

"now": {},

"suggestion": {}, "dailyforecast":[]

}

]

}

        其中,basicaqinowsuggestiondaily_forecast的内部又都会有具体的内容, 那么我们就可以将这5个部分定义成5个实体类。

下面开始来一个个看,basic中具体内容如下所示:

,•basic'*:{

“city” :” 苏州“,

,,id,,:"CN101190401,,/

"update":{

”loc””2016M8M8 21:58H

)

}

        其中,city表示城市名,id表示城市对应的天气id, update中的loc表示天气的更新时 间。我们按照此结构就可以在gson包下建立一个Basic类,代码如下所示:

public class Basic {

@SerializedName("city")

public String cityName;

@SerializedName("id")

public String weatherld;

public Update update;

public class Update {

@SerializedName("loc")

public String updateTime;

}

}

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

        这样我们就将Basic类定义好了,还是挺容易理解的吧?其余的几个实体类也是类似的, 我们使用同样的方式来定义就可以了。比如api中的具体内容如下如示:

“aqi”{

"city":{

"aqi" :,,44",

}

}

那么,在gson包下新建一个AQI类,代码如下所示:

public class AQI {

public AQICity city;

public class AQICity {

public String aqi;

public String pm25;

}

}

now中的具体内容如下所示:

"now"{

,'cond":{

"txt":"阵雨"

}

}

那么,在gson包下新建一个Now类,代码如下所示:

public class Now {

@SerializedName("tmp") public String temperature;

@SerializedName("cond")

public More more;

public class More {

@SerializedName("txt") public String info;

}

suggestion中的具体内容如下所示:

"suggestion":{

"comf **: (

"txt":"白天天气较热,虽然有雨,但仍然无法削弱较高气温给人们带来的暑意, 这种天气会让您感到不很舒适。”

},

"cw":{

"txt" 不宜洗车,未来24小时内有雨,如果在此期间洗车,雨水和路上的泥水 可能会再次弄脏您的爱车。"

},

"sport":{

有降水,且风力较强,推荐您在室内进行低强度运动;若坚持户外运动, 请选择避雨防风的地点。"

}

那么,在gson包下新建一个Suggestion类,代码如下所示:

public class Suggestion {

@SerializedName("comf")

public Comfort comfort;

@SerializedName("cw")

public CarWash carWash;

public Sport sport;

public class Comfort {

@SerializedName("txt") public String info;

}

public class CarWash {

@SerializedName("txt") public String info;

}

public class Sport {

(aSerializedName( "txt") public String info;

}

}

        到目前为止都还比较简单,不过接下来的一项数据就有点特殊了,daily_foecast中的具 体内容如下所示:

"dailyforecast":[

{-

date””2016-08-08”

"cond":{

阵雨”

}, ~

"trnp":{

"max":"34%

}

},

{

"cond":{

"txtd*':"多云”

}, ~

“max”“35”

“min””29”

}

},

}

        可以看到,daily_foecast中包含的是一个数组,数组中的每一项都代表着未来一天的天 气信息°针对于这种情况,我们只需要定义出单日天气的实体类就可以了,然后在声明实体类引 用的时候使用集合类型来进行声明。

那么在gson包下新建一个Forecast类,代码如下所示:

public class Forecast {

public String date;

(aSeriatizedName( "tmp")

public Temperature temperature;

@Se rializedName (" cond '*)

public More more;

public class Temperature (

public String max;

public String min;

}

public class More {

@SerializedName("txtd")

public String info;

}

}

        这样我们就把basic, aqinowsuggestiondaily_foecast对应的实体类全部都创 建好了,接下来还需要再创建一个总的实例类来引用刚刚创建的各个实体类。在gson包下新建 一个Weather类,代码如下所示:

public class Weather {

public String status;

public Basic basic;

public AQI aqi;

public Now now;

public Suggestion suggestion;

(QSe rializedName ("dailyfo recast") public List<Forecast> forecastList;

}

        在 Weather 类中,我们对 BasicAQINowSuggestion Forecast 类进行了引用其 中,由于daily forecast中包含的是一个数组,因此这里使用了 List集合来引用Forecast类。 另外,返回的天气数据中还会包含一项status数据,成功返回ok,失败则会返回具体的原因,那 么这里也需要添加一个对应的status字段。

        现在所有的GSON实体类都定义好了,接下来我们开始编写天气界面。

14.5.2编写天气界面

        首先创建一个用于显示天气信息的活动。右击。0111(001\¥11°11(11可(1包->NewTActivity—> Empty Activity,创建一个 WeatherActivity,并将布局名指定成 activity weather.xmlo

        由于所有的天气信息都将在同一个界面上显示,因此activity weather.xml会是一个很长的布 局文件。那么为了让里面的代码不至于混乱不堪,这里我准备使用3.4.1小节学过的引入布局技 术,即将界面的不同部分写在不同的布局文件里面,再通过引入布局的方式集成到activity, weather.xml中,这样整个布局文件就会显得非常工整。

右击res/layout—>New—^Layout resource file,新建一个title.xml作为头布局,代码如下所示:

<RelativeLayout

xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize">

<TextView

android: id="(a+id/title_city"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_centerInParent="true"

android:textColor=n#fff"

android:textSize="20sp" />

<TextView

android:id="@+id/title_update_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="10dp" android: layout_aHgnParentRight="true" android:layout_centerVertical="true" android:textColor="#fff" android:textSize="16sp"/>

</RelativeLayout>

        这段代码还是比较简单的,头布局中放置了两个TextView, 一个居中显示城市名,一个居右 显示更新时间。

        然后新建一个now.xml作为当前天气信息的布局,代码如下所示:

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/and roid" android:orientation="vertical"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layoutmargin="15dp">

<TextView

android:id="@+id/degreetext"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_gravity="end"

android:textColor="#fff"

android:textSize="60sp" />

<TextView

android :id=,,(a+id/weather_info_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layoutg ravity=u end"

android:textColor="#fff"

android:textSize="20sp" />

</LinearLayout>

        当前天气信息的布局中也是放置了两个TextView, 一个用于显示当前气温,一个用于显示天 气概况。

        然后新建forecast.xml作为未来几天天气信息的布局,代码如下所示:

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical"

android: layoiit_width="match_parent"

android:layout_height="wrap_content"

android: l.ayout_margin=,l15dp"

android:background="#8000">

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginLeft="i5dp"

android:layout_marginTop="15dp"

android :text="^ 报”

android:textColor="#fff"

android:textSize="20sp"/>

<LinearLayout

android:id="@+id/forecast_layout"

android:orientation="vertical"

android:layout_width="match_parent"

and roid: 'layout_height="wrap_content ">

</LinearLayout>

</LinearLayout>

        这里最外层使用LinearLayout定义了一个半透明的背景,然后使用TextView定义了一个标 题,接着又使用一个LinearLayout定义了一个用于显示未来几天天气信息的布局。不过这个布局 中并没有放入任何内容,因为这是要根据服务器返回的数据在代码中动态添加的。

        为此,我们还需要再定义一个未来天气信息的子项布局,创建forecast item.xml文件,代码 如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_margin="15dp">

<TextView

android: id="(a+id/date_text"

android:tayout_width="0dp"

android:layout_height="wrap_content"

android:layoutgravity="cente r_ve rtical"

android:layout_weight="2"

android:textColor="#fff"/>

<TextView

android: id="(a+id/info_text"

android :tayout_width=,,0dp"

android:layout_height="wrap_content"

android:layoutgravity="center_vertical"

android:layout_weight="1"

android:gravity="center" android:textColor="#fff"/>

<TextView

android:id="@+id/max_text"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layoutgravity="center"

and roid :layout_weight=,,l,,

android:gravity="right" android: textColor=,,#fff,,/>

<TextView

android: id="(a+id/min_text"

android: layout_width=*,0dp"

android:layout_height="wrap_content"

android:layout_gravity="center"

android:layout_weight="l"

android:gravity=H right"

android:textColor="#fff"/>

</LinearLayout>

        子项布局中放置了 4TextView, 一个用于显示天气预报日期,一个用于显示天气概况,另 外两个分别用于显示当天的最高温度和最低温度。

        然后新建aqi.xml作为空气质量信息的布局,代码如下所示:

<LinearLayout

xmlns:and roid="http://schemas.android.com/apk/res/android" android:orientation=,•vertical"

android: layout_width=,,match_parent" android: layout_height="wrap_content,1 android:layout_margin="15dp"

android:background="#8000">

<TextView

android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout marginLeft="15dp" android:layout_marginTop="15dp" android :text="^ 气质量” android:textColor="#fff" android: textSize=,,20sp"/>

<LinearLayout

android: ■Layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="15dp">

<RelativeLayout

android :layout_width=,,0dp"

android:layout_height="match_parent"

android:layout_weight="1">

<LinearLayout

android:orientation="vertical" android: layout_width=,,match_parent" android:layout_height="wrap_content" android:layoutcenterlnPa rent="t rue">

<TextView

android:id="@+id/aqi_text" android :layout_width=,,wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:textCoTor="#fff"

android:textSize="40sp"

/>

<TextView

android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android :text="AQI 指数” android:textColor="#fff"/>

</LinearLayout>

</RelativeLayout>

<RelativeLayout

android:layout_width="Odp" android:layout_height="match_parent"

android:layout_weight="l">

<LinearLayout

android:orientation="vertical"

android:layout_width="match_parentH

android :layout_height=,,wrap_content"

and roid: layoutcenterlnPa rent=,,true">

<TextView

and roid: id="(a+id/pm25_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layoutgravity="center"

android:textCoTor="#fff"

android:textSize="40sp"

/>

<TextView

android:layout_width="wrap_content"

and roid :layout_height="wrap_content,,

android:layout_gravity="center"

android:text="PM2.5 指数”

android:textColor="#fff"

/>

</LinearLayout>

</RelativeLayout>

</LinearLayout>

</LinearLayout>

        这个布局中的代码虽然看上去有点长,但是并不复杂。首先前面都是一样的,使用 LinearLayout定义了一个半透明的背景,然后使用TextView定义了一个标题。接下来,这里使用 LinearLayoutRelativeLayout嵌套的方式实现了一个左右平分并且居中对齐的布局,分别用于 显示AQI指数和PM2.5指数。相信你只要仔细看一看,这个布局还是很好理解的。

然后新建suggestion.xml作为生活建议信息的布局,代码如下所示:

<LinearLayout

xmlns:and roid="http://schemas.android.com/apk/res/android"

android:orientation="vertical"

android:layout_width="match_pa rent"

android:layout_height="wrap_content"

android:layout_margin="15dp"

android:background="#8000">

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android :layout_marginLeft=,'15dp"

android :layout_marginTop=,,15dp"

and roid: text="± 活建议"

android:textColor="#fff"

android:textSize="20sp"/>

<TextView

android: id=,,(a+id/comfort_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_margin="15dp"

android:textCoTor="#fff" />

<TextView

android: id="(a+icl/car_wash_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_margin="15dp"

android:textCoTor="#fff" />

<TextView

android :id=,,@+id/sport_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_margin="15dp"

and roid:textCoTor="#fff" />

</LinearLayout>

        这里同样也是先定义了一个半透明的背景和一个标题,然后下面使用了 3TextView分别 用于显示舒适度、洗车指数和运动建议的相关数据。

        这样我们就把天气界面上每个部分的布局文件都编写好了,接下来的工作就是将它们引入到 activity weather.xml 当中,如下所示:

<FrameLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_pa rent"

android: background=,,@color/colorPriniary,,>

<ScrollView

and roid: id=”@+id/weather_layoiit"

and roid: layout_width=,,match_pa rent"

android:layout_height="match_parent"

android:scrollbars="none"

android:overScrollMode="never">

<LinearLayout

android:orientation="vertical"

android:layout_width="match_pa rent"

android:layout_height="wrap_content"> <include layout="@layout/title" />

<include layout="@layout/now" />

<include layout="@layout/forecast" />

<include layout="@layout/aqi" />

<include layout="@layout/suggestion" />

</LinearLayout>

</ScrollView>

</FrameLayout>

        可以看到,首先最外层布局使用了一个FrameLayout,并将它的背景色设置成colorPrimary0 然后在FrameLayout中嵌套了一个ScrollView,这是因为天气界面中的内容比较多,使用 ScrollView可以允许我们通过滚动的方式查看屏幕以外的内容。

        由于ScrollView的内部只允许存在一个直接子布局,因此这里又嵌套了一个垂直方向的 LinearLayout,然后在LinearLayout中将刚才定义的所有布局逐个引入。

        这样我们就将天气界面编写完成了,接下来开始编写业务逻辑,将天气显示到界面上。

14.5.3将天气显示到界面上

        首先需要在Utility类中添加一个用于解析天气JSON数据的方法,如下所示:

public class Utility {

/**

*将返回的JSON数据解析成Weather实体类

*/

public static Weather handleWeatherResponse(String response) { try {

JSONObject jsonObject = new JSONObject(response);

JSONArray jsonArray = j sonObject.getJSONArray("HeWeather"); String weatherContent = jsonArray.getJSONObject(O).toStringO; return new Gson().fromJson(weatherContent, Weather.class);

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

        可以看到,handleWeatherResponse()方法中先是通过JSONObjectJSONArray将天气 数据中的主体内容解析出来,即如下内容:

"status": "ok", "basic": {}, "aqi": {},

"now": {},

"suggestion": {},

"daily forecast":[]

} ~

        然后由于我们之前已经按照上面的数据格式定义过相应的GSON实体类,因此只需要通过 调用fromJsonO方法就能直接将JSON数据转换成Weather对象了。

接下来的工作是我们如何在活动中去请求天气数据,以及将数据展示到界面上。修改

WeatherActivity中的代码,如下所示:

public class WeatherActivity extends AppCompatActivity {

private Scrollview weatherLayout;

private Textview titleCity;

private Textview titleUpdateTime;

private Textview degreeText;

private Textview weatherlnfoText;

private LinearLayout forecastLayout;

private Textview aqiText;

private Textview pm25Text;

@Override protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState);

setContentView(R.layout.activityweather);

//初始化各控件

weatherLayout = (Scrollview) findViewByld(R.id.weatherlayout); titleCity = (TextView) findViewById(R.id.titlecity); titleUpdateTime = (TextView) findViewByld(R.id.titleupdatetime); degreeText = (TextView) findViewByld(R.id.degree text); weatherlnfoText = (TextView) findViewByld(R.id.weather info text); forecastLayout = (LinearLayout) findViewById(R.id.forecastlayout); aqiText = (TextView) findViewByld(R.id.aqitext); pm25Text = (TextView) findViewByld(R.id.pm25_text); comfortText = (TextView) findViewByld(R.id.comfort text); carWashText = (TextView) findViewByld(R.id.car wash text); sportText = (TextView) findViewById(R.id.sporttext);

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences (this);

String weatherstring = prefs.getString("weather", null); if (weatherstring != null) {

//有缓存时直接解析天气数据

Weather weather = Utility.handleWeatherResponse(weatherString); showWeatherlnfo(weather);

} else {

//无缓存时去服务器查询天气

String weatherld = getlntent().getStringExtra("weatherid"); weatherLayout.setvisibility(View.INVISIBLE);

requestweather(weatherld);

)

}

/**

*根据天气id请求城市天气信息

*/

public void requestweather(final String weatherld) {

String weatherllrl = "http://guolin.tech/api/weather?cityid=" + weatherld + "&key=bc0418b57b2d4918819d3974acl285d9”

HttpUtil.sendOkHttpRequest(weatherllrl, new Callback。{ @0verride public void onResponse(Call call, Response response) throws lOException { final String responseText = response.body().st ring();

final Weather weather = Utility.handleWeatherResponse(responseText); runOnlliThread (new Runnable() {

(QOverride

public void run() {

if (weather != null && "ok".equals(weather.status)) { SharedPreferences.Editor editor = PreferenceManager. getDefaultSharedPreferences(WeatherActivity.this). edit();

editor.putString("weather", responseText);

editor.apply(); showWeatherlnfo(weather);

} else {

Toast.makeText (WeatherActivity. this,"获取天气信息失败”, Toast.LENGTH_SHORT).show();

}

}

})

}

^Override

public void onFailure(Call call, lOException e) {

e.printStackTrace(); runOnlliThread(new Runnable() {

^Override

public void run() {

Toast.makeText (WeatherActivity. this,"获取天气信息失败”, Toast,LENGTH_SHORT),show();

}

})

}

})

}

*处理并展示Weather实体类中的数据

*/

private void showWeatherlnfo(Weather weather) {

String cityName = weather.basic.cityName;

String updateTime = weather.basic.update.updateTime.split(" ")[1]; String degree = weather.now.temperature + 也";

String weatherinfo = weather.now.more.info; titleCity.setText(cityName);

titleUpdateTime.setText(updateTime); degreeText.setText(degree);

weatherlnfoText.setText(weatherinfo); forecastLayout. removeAHViews ();

for (Forecast forecast : weather.forecastList) {

View view = Layoutlnflater.from(this).inflate(R.layout.forecast_ item, forecastLayout, false);

Textview dateText = (Textview) view.findViewByld(R.id.datetext); Textview infoText = (Textview) view.findViewByld(R.id.infotext); Textview maxText = (Textview) view.findViewByld(R.id.maxtext); TextView minText = (Textview) view.findViewByld(R.id.mintext); dateText.setText(forecast.date);

infoText.setText(forecast.more.info); maxText.setText(forecast.tempenature.max); minText.setText(forecast.temperature.min); forecastLayout.addView(view);

}

if (weather.aqi != null) { aqiText.setText(weathe r.aqi.city.aqi); pm25Text.setText(weather.aqi.city.pm25);

这个活动中的代码也比较长,我们还是一步步梳理下。在onCreateO方法中仍然先是去获 取一些控件的实例,然后会尝试从本地缓存中读取天气数据。那么第一次肯定是没有缓存的,因 此就会从Intent中取出天气id,并调用requestWeather()方法来从服务器请求天气数据。注意, 请求数据的时候先将ScrollView进行隐藏,不然空数据的界面看上去会很奇怪。

requestWeatherO方法中先是使用了参数中传入的天气id和我们之前申请好的API Key拼装 出一个接口地址,接着调用Httplltil. sendOkHttpRequest ()方法来向该地址发出请求,月艮务器 会将相应城市的天气信息以JSON格式返回。然后我们在onResponseO回调中先调用Utility. handleWeatherResponseO方法将返回的JSON数据转换成Weather对象,再将当前线程切换到 主线程。然后进行判断,如果服务器返回的status状态是ok,就说明请求天气成功了,此时将返 回的数据缓存到SharedPreferences当中,并调用showWeatherlnfo()方法来进行内容显示。 showWeatherlnfoO方法中的逻辑就比较简单了,其实就是从Weather对象中获取数据, 然后显示到相应的控件上。注意在未来几天天气预报的部分我们使用了一个for循环来处理每天 的天气信息,在循环中动态加载forecast_item.xml布局并设置相应的数据,然后添加到父布局当 中。设置完了所有数据之后,记得要将ScrollView重新变成可见。

这样我们就将首次进入WeatherActivity时的逻辑全部梳理完了,那么当下一次再进入 WeatherActivity时,由于缓存已经存在了,因此会直接解析并显示天气数据,而不会再次发起网 络请求了。

处理完了 WeatherActivity中的逻辑,接下来我们要做的,就是如何从省市县列表界面跳转到 天气界面了,修改ChooseAreaFragment中的代码,如下所示:

public class ChooseAreaFragment extends Fragment {

(QOverride

public void onActivityCreated(Bundle savedlnstanceState) { super.onActivityCreated(savedlnstanceState); listview.setOnItemClickListener(new Adapterview.OnItemClickListener() { (QOverride

public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (currentLevel == LEVELPROVINCE) {

selectedProvince = provinceList.get(position); queryCities();

} else if (currentLevel == LEVELCITY) { selectedCity = cityList.get(position); queryCounties();

) else if (currentLevel == LEVEL_COUNTY) {

String weatherld =. countyList.get(position).getWeatherld(); Intent intent = new Intent(getActivity(), WeatherActivity. class);

intent.putExtra("weather^id", weatherld); startActivity(intent); getActivity().finish();

}

}

})

}

)

非常简单,这里在onItemClick()方法中加入了一个if判断,如果当前级别是LEVEL_ COUNTY,就启动WeatherActivity,并把当前选中县的天气id传递过去。

另外,我们还需要在MainActivity中加入一个缓存数据的判断才行。修改MainActivity中的 代码,如下所示:

public class MainActivity extends AppCompatActivity {

@0verride

protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain);

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences (this);

if (prefs.getString("weather", null) != null) {

Intent intent = new Intent(this, WeatherActivity.class); sta rtActivity(intent); finishO;

}

}

}

可以看到,这里在onCreate()方法的一开始先从SharedPreferences文件中读取缓存数据, 如果不为null就说明之前已经请求过天气数据了,那么就没必要让用户再次选择城市,而是直 接跳转到WeatherActivity即可。

好了,现在重新运行一下程序,然后选择江苏-苏州-昆山,结果如图14.21所示。

然后我们还可以向下滑动查看更多天气信息,如图14.22所示。

 14.5.4获取必应每日一图

        虽说现在我们已经把天气界面编写得非常不错了,不过和市场上的一些天气软件的界面相 比,仍然还是有一定差距的。出色的天气软件不会像我们现在这样使用一个固定的背景色,而是 会根据不同的城市或者天气情况展示不同的背景图片。

        当然实现这个功能并不复杂,最重要的是需要有服务器的接口支持。不过我实在是没有精力 去准备这样一套完善的服务器接口,那么为了不让我们的天气界面过于单调,这里我准备使用一 个巧妙的办法。

        必应想必你肯定不会陌生,这是一个由微软开发的搜索引擎网站。这个网站除了提供强大的 搜索功能之外,还有一个非常有特色的地方,就是它每天都会在首页展示一张精美的背景图片, 如图14.23所示。

         由于这些图片都是由必应精挑细选出来的,并且每天都会变化,如果我们使用它们来作为天 气界面的背景图,不仅可以让界面变得更加美观,而且解决了界面一成不变、过于单调的问题。

为此我专门准备了一个获取必应每日一图的接口 : 我的微信公众号

访问这个接口,服务器会返回今日的必应背景图链接:

http://cn.bing.com/az/hprichbg/rb/ ChicagoHarborLH_ZH-CN9974330969_1920xl080.jpg

然后我们再使用Glide去加载这张图片就可以了。

总体思路就是这么简单,下面开始来动手实现吧。首先修改activity_weather.xm 1中的代码, 如下所示:

<FrameLayout

xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"

android:layout_height="match_parent" android: background="@color/colorPrimary,,>

<ImageView

android:id="@+id/bing_pic_img" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" />

<ScrollView

android:id="@+id/weathelayout"

android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="none"

android:overScrollMode="never">

</ScrollView>

</FrameLayout>

这里我们在FrameLayout中添加了一个Image View,并且将它的宽和高都设置成match_ parento由于FrameLayout默认情况下会将控件都放置在左上角,因此ScrollView会完全覆盖住 Image View,从而Image View也就成为背景图片了。

接着修改WeatherActivity中的代码,如下所示:

public class WeatherActivity extends AppCompatActivity {

private Imageview bingPidmg;

^Override

protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); setContentView(R.layout.activityweather);

//初始化各控件

bingPidmg = (ImageView) findViewById(R.id.bing_pic_img);

String bingPic = prefs.getString("bing__pic", null); if (bingPic != null) {

Glide.with(this).load(bingPic).into(bingPidmg); } else {

loadBingPic();

}

}

/**

*根据天气id请求城市天气信息

*/

public void requestweather(final String weatherld) {

loadBingPicO;

/**

*加载必应每日一图

*/

private void LoadBingPic() {

String requestBingPic = "http://guolin.tech/api/bing_pic"; HttpUtil.sendOkHttpRequest(requestBingPicF new Callbackf) { ^Override public void onResponse(Call call., Response response) throws lOException { final String bingPic = response.body().stringO; SharedPreferences.Editor editor = PreferenceManager. getDefaultSharedPreferences(WeatherActivity.this).edit(); editor.putString("bing_pic", bingPic);

editor.apply(); runOnUiThread(new RunnableO { (dOverride public void run() {

Glide.with(WeatherActivity.this).load(bingPic).into (bingPidmg);

^Override public void e.printStackTrace();

} }) }

        可以看到,首先在onCreate()方法中获取了新增控件ImageView的实例,然后尝试从 SharedPreferences中读取缓存的背景图片。如果有缓存的话就直接使用Glide来加载这张图片, 如果没有的话就调用loadBingPic()方法去请求今日的必应背景图。

        loadBingPic()方法中的逻辑就非常简单了,先是调用了 HttpUtil.sendOkHttpRequest() 方法获取到必应背景图的链接,然后将这个链接缓存到SharedPreferences当中,再将当前线程切 换到主线程,最后使用Glide来加载这张图片就可以了。另外需要注意,在requestWeatherO 方法的最后也需要调用一下loadBingPic()方法,这样在每次请求天气信息的时候同时也会刷 新背景图片。现在重新运行一下程序,效果如图14.24所示。

        怎么样?虽说只是换了一张背景图而已,但是整个界面的视觉体验就完全不一样了,瞬间提 升了好几个档次。而且我们的背景图并不是一成不变的,每天都会是不同的图片,永远给人一种 耳目一新的感觉。

        不过如果你仔细观察图14.24,你会发现背景图并没有和状态栏融合到一起,这样的话视觉 体验就还是没有达到最佳的效果。虽说我们在12.7.2小节已经学习过如何将背景图和状态栏融 合到一起,但当时是借助Design Support库完成的,而我们这个项目中并没有引入Design Support 库。

        当然如果还是模仿12.7.2小节的做法,引入Design Support ,然后嵌套CoordinatorLayout AppBarLayout. CollapsingToolbarLayout等布局,也能实现背景图和状态栏融合到一起的效果, 不过这样做就过于麻烦了,这里我准备教你另外一种更简单的实现方式。修改WeatherActivity 中的代码,如下所示:

public class WeatherActivity extends AppCompatActivity {

@0verride protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState);

if (Build.VERSION.SDK_INT >= 21) {

View decorView = getWindow().getDecorView(); decorView.setSystemUiVisibilityf View.SYSTEM_UI_FLAG_LAYOUT__FULLSCREEN I View. SYSTEM_UI_FLAG_LAYOUT_STABLE);

getWindowO.setStatusBarColor(Color.TRANSPARENT); }

setContentView(R.layout.activityweather);

}

}

        由于这个功能是Android 5.0及以上的系统才支持的,因此我们先在代码中做了一个系统版 本号的判断,只有当版本号大于或等于21,也就是5.0及以上系统时才会执行后面的代码。

        接着我们调用了 getWindow() ,getDecorView()方法拿到当前活动的DecorView,再调用它 的setSystemUiVisibility()方法来改变系统 UI的显示,这里传入View.SYSTEM_UI_ FLAG_LAYOUT_FULLSCREEN View. SYSTEM_UI_FLAG_LAYOUT_STABLE 就表示活动的布局会显 示在状态栏上面,最后调用一下setStatusBarColor()方法将状态栏设置成透明色。

        仅仅这些代码就可以实现让背景图和状态栏融合到一起的效果了。不过,如果运行一下程序, 你会发现还是有些问题,天气界面的头布局几乎和系统状态栏紧贴到一起了,如图14.25所示。

        这是由于系统状态栏已经成为我们布局的一部分,因此没有单独为它留出空间。当然,这个 问题也是非常好解决的,借助android:fitsSystemWindov/s属性就可以了。修改activityweather.xml中的代码,如下所示:

<FrameLayout

xmlns:android="http://schemas.android.com/apk/res/android" android: layout_width=,,match_parent"

android:layout_height="match_pa rent"

android:background="@color/colorPrimary">

vScrollView

android:id="@+id/weather_layout" android:layout_width="match_parent" android:layout_height="match_parent" android: scroHbars="none"

android:overScrollMode="never">

<LinearLayout

android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" android: fitsSystemWindows="true,,>

</LinearLayout>

</ScrollView>

</FrameLayout>

        这里在 ScrollView LinearLayout 中增加了 android: fitsSystemWindows 属性,设置成 true就表示会为系统状态栏留出空间。现在重新运行一下代码,效果如图14.26所示。

0K,这样第三阶段的开发工作也都完成了,我们把代码提交一下。

git add .

git commit -m "加入显示天气信息的功能。"

git push origin master

14.6手动更新天气和切换城市

        经过第三阶段的开发,现在酷欧天气的主体功能已经有了,不过你会发现目前存在着一个比 较严重的bug,就是当你选中了某一个城市之后,就没法再去查看其他城市的天气了,即使退出 程序,下次进来的时候还会直接跳转到Weather Activity o

        因此,在第四阶段中我们要加入切换城市的功能,并且为了能够实时获取到最新的天气,我 们还会加入手动更新天气的功能。

14.6.1手动更新天气

        先来实现一下手动更新天气的功能。由于我们在上一节中对天气信息进行了缓存,目前每次 展示的都是缓存中的数据,因此现在非常需要一种方式能够让用户手动更新天气信息。

        至于如何触发更新事件呢?这里我准备采用下拉刷新的方式,正好我们之前也学过下拉刷新 的用法,实现起来会比较简单。

首先修改activity_weather.xml中的代码,如下所示:

<FrameLayout

xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="matchparent" android: layout_height=,,match_parent" android: background=,,(acolor/colorPrimary">

ondroid support.v4.widget.SwipeRefreshLayout android:id="@+id/swipe_refresh" and roid: l.ayout__width=,,match_pa rent" android:layout_height="match_parent">

<ScrollView

android: id="(a+id/weather_'layout" android:layout_width="matchparent" android:layout_height="match_parent" android: scroflbars="none"

android:overScrollMode="never">

</ScrollView>

</android. support.v4 widget.SwipeRefreshLayout>

</FrameLayout>

        可以看到,这里在ScrollView的外面又嵌套了一层SwipeRefreshLayout,这样ScrollView就 自动拥有下拉刷新功能了。

        然后修改WeatherActivity中的代码,加入更新天气的处理逻辑,如下所示:

public class WeatherActivity extends AppCompatActivity {

public SwipeRefreshLayout swipeRefresh;

(QOverride

protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState);

swipeRefresh = (SwipeRefreshLayout) findViewByld(R.id.swipe__refresh); swipeRefresh.setColorSchemeResources(R.color.colorPrimary); SharedPreferences prefs = PreferenceManager.

getDefaultSharedPreferences(this);

String weatherString = prefs.getString("weather", null); final String weatherld;

if (weatherString != null) {

//有缓存时直接解析天气数据

Weather weather = Utility.handleWeatherResponse(weatherString); weatherld = weather.basic.weatherld;

showWeatherlnfo(weather);

} else {

//无缓存时去服务器查询天气

weatherld = getlntent().getSt ringExt ra("weather_id"); weatherLayout.setVisibility(View.INVISIBLE); requestweather(weatherld);

}

swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.

OnRefreshListener() {

QOverride

public void onRefresh() { requestweather(weatherld);

} })

}

/**

*根据天气id请求城市天气信息

*/

public void requestWeather(final String weatherld) {

String weatherUrl = "http://guolin.tech/api/weather?cityid=" + weatherld + ,,&key=bc0418b57b2d4918819d3974acl285d9,';

HttpUtil. sendOkHttpRequest (weatherllrl, new Callback!) { (QOverride

public void onResponse(Call call, Response response) throws lOException {

runOnUiThread(new Runnable() {

^Override

public void run() (

if (weather != null && "ok".equals(weather.status)) { SharedPreferences.Editor editor = PreferenceManager. getDefaultSharedPreferences(WeatherActivity.

this).edit();

editor.putString("weather", responseText); editor.apply();

showWeatherlnfo(weather);

} else {

Toast.makeText (WeatherActivity. this,"获取天气信息失败", Toast,LENGTH_SHORT).show();

}

swipeRefresh.setRefreshing(false);

}

})

}

^Override

public void onFailure(Call call, lOException e) {

e.printStackTrace();

runOnUiThread(new Runnable() {

^Override

public void run() {

Toast.makeText(WeatherActivity.this,"获取天气信息失败” Toast.LENGTHSHORT).show();

swipeRefresh.setRefreshing(false);

)

})

}

})

loadBingPic();

        修改的代码并不算多,首先在onCreate()方法中获取到了 SwipeRefreshLayout的实例,然 后调用setColorSchemeResources()方法来设置下拉刷新进度条的颜色,这里我们就使用主题 中的colorPrimary作为进度条的颜色了。接着定义了一个weatherld变量,用于记录城市的天气 id,然后调用setOnRef reshListener()方法来设置一个下拉刷新的监听器,当触发了下拉刷新 操作的时候,就会回调这个监听器的onRefresh()方法,我们在这里去调用requestWeather() 方法请求天气信息就可以了。

        另外不要忘记,当请求结束后,还需要调用SwipeRefreshLayoutsetRef reshing ()方法

        并传入false,用于表示刷新事件结束,并隐藏刷新进度条。

        现在重新运行一下程序,并在屏幕的主界面向下拖动,效果如图14.27所示。

        更新完天气信息之后,下拉进度条会自动消失。

14.6.2切换城市

        完成了手动更新天气的功能,接下来我们继续实现切换城市功能。

        既然是要切换城市,那么就肯定需要遍历全国省市县的数据,而这个功能我们早在14.4节 就已经完成了,并且当时考虑为了方便后面的复用,特意选择了在碎片当中实现。因此,我们其 实只需要在天气界面的布局中引入这个碎片,就可以快速集成切换城市功能了。

        虽说实现原理很简单,但是显然我们也不可能让引入的碎片把天气界面遮挡住,这又该怎么 办呢?还记得12.3节学过的滑动菜单功能吗?将碎片放入到滑动菜单中真是再合适不过了,正 常情况下它不占据主界面的任何空间,想要切换城市的时候只需要通过滑动的方式将菜单显示出 来就可以了。

        下面我们就按照这种思路来实现。首先按照Material Design的建议,我们需要在头布局中加 入一个切换城市的按钮,不然的话用户可能根本就不知道屏幕的左侧边缘是可以拖动的。修改 title.xml中的代码,如下所示:

<RelativeLayout

xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"

android:layout_height="?att r/actionBa rSize">

<Button

android: id=,,@+id/nav_buttonu

android: ■layout__width=,,30dp"

android:layout_height="30dp" android: Layout_marginLef t=',10dp" android:layout_alignPaentLeft=”true" android :Layout__centerVertical="t rue" android: background=,,@drawable/ic_home" />

</RelativeLayout>

        这里添加了一个Button作为切换城市的按钮,并且让它居左显示。另外,我提前准备好了 一张图片来作为按钮的背景图。

接着修改activity weather.xml布局来加入滑动菜单功能,如下所示:

<FrameLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android: ■Layout_width="match_parent" android: layout_height=,,match_parent" android: background="(acolor/colorPrimary">

<android support.v4.widget.DrawerLayout android: i d="@+id/d rawe r__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.SwipeRef reshLayout>

<fragment

android:id="@+id/choose_area_fragment"

android: name=licom  coolweather. android  ChooseAreaFragment11 android:Layout_width="match_parent" android :layout_height=,,match_parent" android:layout_g ravity="start"

/> ~

</android, support.v4.widget.DrawerLayout>

</FrameLayout>

可以看到,我们在 SwipeRefreshLayout 的外面又嵌套了一层 DrawerLayouto DrawerLayout 中的第一个子控件用于作为主屏幕中显示的内容,第二个子控件用于作为滑动菜单中显示的内 容,因此这里我们在第二个子控件的位置添加了用于遍历省市县数据的碎片。

        接下来需要在WeatherActivity中加入滑动菜单的逻辑处理,修改WeatherActivity中的代码, 如下所示:

public class WeatherActivity extends AppCompatActivity {

public DrawerLayout drawerLayout;

private Button navButton;

^Override

protected void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState);

drawerLayout = (DrawerLayout) findViewById(R.id.drawer__layout); navButton = (Button) findViewByld(R.id.nav__button);

navButton.setOnClickListener(new View.OnClickListener() { @0verride public void onCLick(View v) {

drawerLayout.openDrawer(GravityCompat.START);

}

})

}

}

        很简单,首先在onCreate()方法中获取到新增的DrawerLayoutButton的实例,然后在 Button的点击事件中调用DrawerLayoutopenDrawer()方法来打开滑动菜单就可以了。

        不过现在还没有结束,因为这仅仅是打开了滑动菜单而已,我们还需要处理切换城市后的逻 辑才行。这个工作就必须要在ChooseAreaFragment中进行了,因为之前选中了某个城市后是跳 转到WeatherActivity的,而现在由于我们本来就是在WeatherActivity当中的,因此并不需要跳转, 只是去请求新选择城市的天气信息就可以了。

        那么很显然这里我们需要根据ChooseAreaFragment的不同状态来进行不同的逻辑处理,修 改ChooseAreaFragment中的代码,如下所示:

public class ChooseAreaFragment extends Fragment {

(QOverride public void onActivityCreated(Bundle savedlnstanceState) { super.onActivityCreated(savedlnstanceState);

listView.setOnItemClickListener(new Adapterview.OnItemClickListener() { ^Override

public void onItemClick(AdapterView<?> parent, View view, int position, long id) ( if (currentLevel == LEVEL_PROVINCE) {

selectedProvince = provinceList.get(position); queryCities();

} else if (currentLevel == LEVELCITY) { selectedCity = cityList.get(position); queryCounties();

} else if (currentLevel == LEVELCOUNTY) {

String weatherld = countyList.get(position).getWeatherId();

if (getActivity() instanceof MainActivity) {

Intent intent = new Intent(getActivity(), WeatherActivity. class);

intent.putExtra("weatherid", weatherld); startActivity(intent);

getActivity(),finish();

} else if (getActivityO instanceof WeatherActivity) { WeatherActivity activity = (WeatherActivity) getActivityO; activity.drawerLayout  closeDrawers(); activity.swipeRefresh setRefreshing(true); activity.requestweather(weatherld);

}

}

}

})

}

        这里我使用了一个Java中的小技巧,instanceof关键字可以用来判断一个对象是否属于某 个类的实例。我们在碎片中调用getActivityO方法,然后配合instanceof关键字,就能轻 松判断出该碎片是在MainActivity当中,还是在WeatherActivity当中。如果是在MainActivity当 中,那么处理逻辑不变。如果是在WeatherActivity当中,那么就关闭滑动菜单,显示下拉刷新进 度条,然后请求新城市的天气信息。

        这样我们就把切换城市的功能全部完成了,现在可以重新运行一下程序,效果如图14.28所示。

        可以看到,标题栏上多出了一个用于切换城市的按钮。点击该按钮,或者在屏幕的左侧边缘 进行拖动,就能让滑动菜单界面显示出来了,如图14.29所示。

         然后我们就可以在这里切换其他城市了。选中城市之后滑动菜单会自动关闭,并且主界面上 的天气信息也会更新成你选择的那个城市。

        这样,第四阶段的开发任务也完成了。当然,仍然不要忘记提交代码。

git add .

git commit -m ”新增切换城市和手动更新天气的功能。"

git push origin master

14.7后台自动更新天气

        为了要让酷欧天气更加智能,在第五阶段我们准备加入后台自动更新天气的功能,这样就可 以尽可能地保证用户每次打开软件时看到的都是最新的天气信息。

        要想实现上述功能,就需要创建一个长期在后台运行的定时任务,这个功能肯定是难不倒你 的,因为我们在13.5节中就已经学习过了。

        首先在 service 包下新建一个月艮务,右击 com.coolweather.android.service—>New—^Service—> Service,创建一个AutoUpdateService,并将ExportedEnabled这两个属性都勾中。然后修改 AutoUpdateService中的代码,如下所示:

public class AutoUpdateService extends Service {

^Override

public IBinder onBind(Intent intent) { return null;

}

^Override

public int onStartCommand(Intent intent, int flags, int startld) ( updateWeather(); updateBingPic();

AlarmManager manager = (AlarmManager) getSystemService(ALARMSERVICE); int anHour = 8 * 60 * 60 * 1000; // 这是 8 小时的毫秒数

long triggerAtTime = SystemClock.elapsedRealtime() + anHour; Intent i = new Intent(this, AutoUpdateService.class);

Pendingintent pi = Pendingintent.getService(this, 0# i, 0); manager.cancel(pi);

manager.set(AlarmManager.ELAPSEDREALTIMEWAKEUP, triggerAtTime, pi); return super.onStartCommand(intent, flags, startld);

}

*更新天气信息

*/

private void updateWeather(){

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); String weatherstring = prefs.getString("weather", null);

if (weatherstring != null) (

//有缓存时直接解析天气数据

Weather weather = Utility.handleWeatherResponse(weatherString);

String weatherld = weather.basic.weatherld;

String weatherllrl = "http://guolin.tech/api/weather?cityid=" +

weatherld + ,,&key=bc0418b57b2d4918819d3974acl285d9n;

HttpUtil.sendOkHttpRequest(weatherllrl, new Callback。{ ^Override

public void onResponse(Call call, Response response) throws lOException {

String responseText = response.body().stringO;

Weather weather = Utility.handleWeatherResponse(responseText); if (weather != null && "ok".equals(weather.status)) ( SharedPreferences.Editor editor = PreferenceManager. getDefaultSharedPreferences (AutollpdateService.this). edit();

editor.putString("weather", responseText); editor.apply();

}

} ^Override

public void onFailure(Call call, lOException e) ( e.printStackT race();

}

})

/**

*更新必应每日一图

*/

private void updateBingPic() {

String requestBingPic = "http://guolin.tech/api/bing_pic"; HttpUtil.sendOkHttpRequest(requestBingPic, new Callback。 { (QOverride

public void onResponse(Call call, Response response) throws lOException { String bingPic = response.body().string();

SharedPreferences.Editor editor = PreferenceManager.getDefault

Sha redPreferences(AutoUpdateService.this).edit(); editor.putString("bing pic", bingPic); editor.apply();

} ^Override

public void onFailurefCall call, lOException e) {

e.printStackTrace();

}

})

}

}

        可以看到,在onStartCommandO方法中先是调用了 updateWeather()方法来更新天气, 然后调用了 updateBingPicO方法来更新背景图片。这里我们将更新后的数据直接存储到 SharedPreferences文件中就可以了,因为打开WeatherActivity的时候都会优先从SharedPreferences 缓存中读取数据。

        之后就是我们学习过的创建定时任务的技巧了,为了保证软件不会消耗过多的流量,这里将 时间间隔设置为8小时,8小时后AutoUpdateReceiveronStartCommand ()方法就会重新执行, 这样也就实现后台定时更新的功能了。

        不过,我们还需要在代码某处去激活AutoUpdateService这个服务才行。修改WeatherActivity 中的代码,如下所示:

public class WeatherActivity extends AppCompatActivity {

/**

*处理并展示Weather实体类中的数据。

*/

private void showWeatherlnfo(Weather weather) (

if (weather != null && "ok".equals(weather.status)) (

Intent intent = new Intent(this, AutoUpdateService.class); startservice(intent);

} else (

Toast.makeText (WeatherActivity. this,"获取天气信息失败”Toast. LENGTH_ SHORT).show(); ~

}

}

}

        可以看到,这里在showWeather()方法的最后加入启动AutoUpdateService这个服务的代码, 这样只要一旦选中了某个城市并成功更新天气之后,AutoUpdateService就会一直在后台运行,并 保证每8小时更新一次天气。

现在可以再提交一下代码:

git add .

git commit -m "増加后台自动更新天气的功能。"

git push origin master

14.8修改图标和名称

        目前的酷欧天气看起来还不太像是一个正式的软件,为什么呢?因为都还没有一个像样的图 标呢。一直使用Android Studi。自动生成的图标确实不太合适,是时候需要换一下了。

        这里我事先准备好了一张图片来作为软件图标,由于我也不是搞美术的,因此图标设计得非 常简单,如图14.30所示。

        理论上来讲,我们应该给这个图标提供几种不同分辨率的版本,然后分别放入到相应分辨率 的mipmap目录下,这里简单起见,我就都使用同一张图了。将这张图片命名成logo.png,放入 到所有以mipmap开头的目录下,然后修改AndroidManifest.xml中的代码,如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.coolweather.android">

<uses-permission android:name="android.permission.INTERNET" />

<application

android:name="org.litepal.LitePalApplication"

android:allowBackup="true"

android: icon=,l@mipmap/'logo,>

and roid: label="(ast ring/appname" android:supportsRtl="true" and roid: theme="@sty'le/AppTheme">

</application>

</manifest>

        这里将<application>标签的android:icon属性指定成@mipmap/logo就可以修改程序图 标了。接下来我们还需要修改一下程序的名称,打开res/values/string.xml文件,其中app name 对应的就是程序名称,将它修改成酷欧天气即可,如下所示:

<resources>

<string name="app_name”>酷欧天气v/string

</resources>

现在重新运行一遍程序,这时观察酷欧天气的桌面图标,如图14.31所示。

 

14.31手机桌面图标 养成良好的习惯,仍然不要忘记提交代码。

git add .

git commit -m "修改程序图标和名称。" git push origin master

这样我们就终于大功告成了 !

14.9你还可以做的事情

        经过五个阶段的开发,酷欧天气已经是一个完善、成熟的软件了吗?嘿嘿,还差得远呢!现 在的酷欧天气只能说是具备了一些最基本的功能,和那些商用的天气软件比起来还有很大的差 距,因此你仍然还有非常巨大的发挥空间来对它进行完善。

        比如说以下功能是你可以考虑加入到酷欧天气中的。

口增加设置选项,让用户选择是否允许后台自动更新天气,以及设定更新的频率。

口优化软件界面,提供多套与天气对应的图片,让程序可以根据不同的天气自动切换背景 图。

□允许选择多个城市,可以同时观察多个城市的天气信息,不用来回切换。

提供更加完整的天气信息,目前我们只使用了和风天气返回的一小部分数据而已。

另外,由于酷欧天气的源码已经托管在了 GitHub上面,如果你想在现有代码的基础上继续 对这个项目进行完善,就可以使用GitHubFork功能。

首先登录你自己的GitHub账号,然后打开酷欧天气版本库的主页:https://github.com/guolindev/ coolweather,这时在页面头部的最右侧会有一个Fork按钮,如图14.32所示。

        点击一下Fork按钮就可以将酷欧天气这个项目复制一份到你的账号下,再使用git clone 命令将它克隆到本地,然后你就可以在现有代码的基础上随心所欲地添加任何功能并提交了。

这篇关于第014天:实战开发酷欧天气APP的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/170963

相关文章

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

在Java中使用ModelMapper简化Shapefile属性转JavaBean实战过程

《在Java中使用ModelMapper简化Shapefile属性转JavaBean实战过程》本文介绍了在Java中使用ModelMapper库简化Shapefile属性转JavaBean的过程,对比... 目录前言一、原始的处理办法1、使用Set方法来转换2、使用构造方法转换二、基于ModelMapper

Java实战之自助进行多张图片合成拼接

《Java实战之自助进行多张图片合成拼接》在当今数字化时代,图像处理技术在各个领域都发挥着至关重要的作用,本文为大家详细介绍了如何使用Java实现多张图片合成拼接,需要的可以了解下... 目录前言一、图片合成需求描述二、图片合成设计与实现1、编程语言2、基础数据准备3、图片合成流程4、图片合成实现三、总结前

基于Python开发PPTX压缩工具

《基于Python开发PPTX压缩工具》在日常办公中,PPT文件往往因为图片过大而导致文件体积过大,不便于传输和存储,所以本文将使用Python开发一个PPTX压缩工具,需要的可以了解下... 目录引言全部代码环境准备代码结构代码实现运行结果引言在日常办公中,PPT文件往往因为图片过大而导致文件体积过大,

nginx-rtmp-module构建流媒体直播服务器实战指南

《nginx-rtmp-module构建流媒体直播服务器实战指南》本文主要介绍了nginx-rtmp-module构建流媒体直播服务器实战指南,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. RTMP协议介绍与应用RTMP协议的原理RTMP协议的应用RTMP与现代流媒体技术的关系2

使用DeepSeek API 结合VSCode提升开发效率

《使用DeepSeekAPI结合VSCode提升开发效率》:本文主要介绍DeepSeekAPI与VisualStudioCode(VSCode)结合使用,以提升软件开发效率,具有一定的参考价值... 目录引言准备工作安装必要的 VSCode 扩展配置 DeepSeek API1. 创建 API 请求文件2.

C语言小项目实战之通讯录功能

《C语言小项目实战之通讯录功能》:本文主要介绍如何设计和实现一个简单的通讯录管理系统,包括联系人信息的存储、增加、删除、查找、修改和排序等功能,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录功能介绍:添加联系人模块显示联系人模块删除联系人模块查找联系人模块修改联系人模块排序联系人模块源代码如下

Golang操作DuckDB实战案例分享

《Golang操作DuckDB实战案例分享》DuckDB是一个嵌入式SQL数据库引擎,它与众所周知的SQLite非常相似,但它是为olap风格的工作负载设计的,DuckDB支持各种数据类型和SQL特性... 目录DuckDB的主要优点环境准备初始化表和数据查询单行或多行错误处理和事务完整代码最后总结Duck

基于Python开发电脑定时关机工具

《基于Python开发电脑定时关机工具》这篇文章主要为大家详细介绍了如何基于Python开发一个电脑定时关机工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 简介2. 运行效果3. 相关源码1. 简介这个程序就像一个“忠实的管家”,帮你按时关掉电脑,而且全程不需要你多做