基于AndroidStudio环境开发的天气app
- 本天气app使用AndroidStudio这个IDE工具在Windows10系统下进行开发。
由于需要源码的人特别多,我特地花时间修复了旧的天气接口无法使用的问题并开源了源码,需要的自取。
主要实现了:
1、定位城市天气显示;
2、城市编辑功能(增、删、改、查)以及对应天气显示信息的改变;
3、天气信息的Widget窗口显示(城市的编辑功能可以远程的更新Widget窗口信息的显示)
4、下拉刷新、天气显示界面左右滑动、城市拖拽等小模块
@[toc]
一、 开发需求分析
1、开发环境搭建
强烈建议使用AndroidStudio做为开发工具,eclipse加上Android SDK进行关联,在eclipse里面再安装 Android Development Tools(ADT)虽然能够进行Android开发,但是环境配置麻烦且效率更低。AndroidStudio现在已经基本成熟稳定,而且AndroidStudio附带Sdk,jdk安装很方便,使用体验很好。
安装步骤:
(1)下载。建议在官网下载,因为国内很多映射文件多少都是有点年代的,还是下载最新的比较好,也免去更新的麻烦。不过国内由于网络限制,上Android官网需要翻墙。AndroidStudio下载地址:https://developer.android.google.cn/studio/index.html里面是最新版本的AndroidStudio。
(2)安装,下载完成之后没有特别要求的话默认选择一直next就好了,一般的话只需要改下安装路径,避免C盘空间不够。因为AndroidStudio自带了JDK和Android SDK,所以Android完成后就可以直接进行开发了。
2、城市信息获取的api
城市信息这里我直接使用的是Android前辈搭建的一个服务器获取的,数据链接是http://guolin.tech/api/china,访问返回的是JSON数据类型的省份信息(JSON数据类型的解析后面会再详细说明),需要返回城市时只需要在本链接后加上“/对应省份id”即可获取到相应的城市信息,县市信息也是一样的,原链接加上“/对应省份id/对应城市id”即可。
这里其实也可以从其它天气服务商提供的api接口获取城市信息。
3、天气信息获取的api
天气信息的获取我使用的是和风天气提供的免费的api,和风天气每天有提供4000次免费的基础天气查询,用来做开发测试是足够用的了。而且和风天气api接口返回的JSON数据类型也比较简单,作为Android初学者做项目是比较好的。想使用该接口只需要简单注册一个账号就可以了(对返回数据的处理我后面再详细说明)。老手的话可以在网上搜索别的服务商提供的免费接口,现在网上的免费接口少了很多,不过有还是有的。
4、定位信息获取的api
我这里使用的是百度提供的免费api接口http://lbsyun.baidu.com/apiconsole/key,因为Android原生定位API在国产手机中一般被阉割了,或者国内网络限制的原因,使用Android原生定位API一般是很难获取到定位信息的,跟手机厂商和网络环境都有关系。所以这边为了避免这种情况的不确定因素,我选择了使用百度提供的免费地位接口,在国内,百度和高德定位服务做得都还是不错的。使用百度定位api接口同样需要注册一个百度开发者账号,因为这不是本篇文章的重点。这边具体的操作就不再说明了。
二、 系统设计分析
1、天气信息界面显示设计
首先先上效果图:
接下来我介绍一下天气显示信息中用到的一些设计:
首先是功能实现上的:
1)首先背景图片是每天会更新的,是从必应网上获取到的背景图片。
2)下拉刷新功能。
3)天气显示信息左右活动切换已选择要显示的城市。
4)通过点击右上角的编辑按钮进入城市管理功能。
5)导航组件功能。
6)小时天气小时超出屏幕宽度时的当前页面左右滑动。
其次是具体显示上的(分为一个城市的天气信息一个页面,每个页面又有七个模块)我们从上往下分析:
1)最上部分是城市名的显示和编辑按钮。
2)然后是导航原点显示。
3)其次是当前温度,当天天气和当天最低最高温的显示。(1)(2)部分都是用户比较关心的问题,所以我们放在最前面。
4)接下来是将来的小时预告,由于和风天气返回的数据只有当天每三小时的天气预告,所以这边的显示实现得比较差,不过我这里做的是兼容可以扩展的,不管数据多少都可以显示。如果将来需要更改数据源,这里的操作将非常简单 。
5)接着显示的是接下来几天的天气的大体介绍,这里显示的数据同样受限于获取到的数据。
6)再接着是一些生活指数的显示,由于我艺术细胞不太好,所以这里的图片显示有点丑。。你们可以根据自己的喜好去更改图片。
7)最后就是一些生活建议的显示了。
2、已选择城市信息界面显示设计
先上图吧
这里主要是有点击编辑前后的区别
下面我们来一一说明:
点击编辑前
布局主要分成三个部分:
1、最上方的:
*左侧返回按钮,回到天气显示界面
*中间固定的“城市管理”四个字
*右侧的编辑按钮,点击之后就可以对城市进行增、删、和更改位置了
2、中间部分:
*中间部分是已选择城市信息的显示
3、最下方部分:
*最下面是一个添加城市的按钮,点击之后进入城市添加功能
点击编辑后
1、最上方的:
*左侧取消按钮,即放弃本次编辑后的结果,回到非编辑界面
*中间固定的“城市管理”四个字
*右侧的保存按钮,即保存本次编辑的结果并回到非编辑界面
2、中间部分:
*中间部分是已选择城市信息的显示,与编辑前不同的是增加了左侧拖动改变顺序的按钮和右侧的删除城市按钮
3、最下方部分:
*最下面是一个添加城市的按钮,点击之后进入城市添加功能
所用到的功能点
1、dragListView:可拖拽的listview
2、Android自带数据库
3、重叠按钮的实现
以上功能模块下面我都会一一说明
3、添加城市信息界面显示设计
先上图:
说明
这里的实现比较简单,就是使用ListView去显示省、市、县三个级别的城市,根据选择的城市去网络或者本地加载数据,然后显示。
4、Widget设计
同样先上图
说明
这里的实现显示上比较简单,但是功能逻辑和实现上相对复杂。
显示上的设计
1、背景图片:widget的背景图片同样是网络上下载并且每天会自动更换的,不同的是为了保证用户滑动界面时的流畅性,这里做了图片缩放处理之后再显示。
2、中间固定文字“当前天气”
3、下面是一个ListView用来显示简略的已选择城市的信息
功能上的设计
1、服务listView改变的server进程
2、contentProvider提供跨进程间的数据通信
3、图片下载的异步线程和图片缩放实现
4、异步线程与UI线程通过handler实现通信
5、界面转换设计
有界面转换实现的:
1、点开app进入到城市天气信息显示界面
2、点击编辑按钮进入到城市管理界面
3、城市管理界面中点击添加按钮进入到城市添加界面
4、城市管理界面中点击返回按钮回到城市天气信息显示界面
5、添加城市界面中添加完成或者点击返回按钮回到城市天气信息显示界面
6、系统总体和局部流程设计(流程图)
由于时间原因,这边就先绘制一个流程图了,别的流程图等后面有时间了再绘制
三、 系统功能模块实现(代码部分)
前面介绍了那么多,现在终于到了重点了,前面讲述的功能我在这里都将为大家一一说明。
首先给大家看一下工程目录的截图:
项目总体流程思路
接下来我根据项目的实现过程来给大家介绍整个项目的总体流程
1、天气app最重要的是获取城市列表和天气信息,所以首先要解决的问题是在网络上找到合适的api接口,并根据服务商提供的数据转换成自己需要显示的数据。
2、有了需要的显示信息之后,我们需要自己去设计怎么显示,怎么让用户去有一个好的体验。我的设计是在使用三个Activity去和用户交互,参照我的项目截图,其中WeatherActivity作为启动活动,用于显示天气信息,提供的是多页带导航栏可左右滑动的效果。ChooseAreaActivity是管理城市的活动,用于添加、删除、改变要显示天气信息的城市列表。AddCountyActivity是用于添加城市的活动。
3、实现了这些基本的城市管理和天气显示之后,接下来就是进阶功能了,首先我们实现百度定位功能,根据定位结果加载当前城市天气。
4、实现widget功能。
这个项目总体的思路就是这样的,接下来我们一步一步的去说明
城市和天气信息获取模块
1、获取城市信息
数据链接http://guolin.tech/api/china,访问返回的是JSON数据类型的省份信息,需要返回城市时只需要在本链接后加上“/对应省份id”即可获取到相应的城市信息,县市信息也是一样的,原链接加上“/对应省份id/对应城市id”即可。
大家点击网址可以得到这样的响应:
[图片上传失败...(image-ac752a-1561597085468)]
这里得到的是一个JSON数据,以下是对它的解析代码:
JSONArray jsonArray = new JSONArray(response);
for (int i = 0; i < jsonArray.length(); ++i) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
Province province = new Province();
province.setProvinceCode(jsonObject.getInt("id"));
province.setProvinceName(jsonObject.getString("name"));
province.save();
}
这里的后几行代码是我使用LItepal存储数据到数据库的操作,response变量就是访问网址得到的原JSON数据。
网上关于JSON的解析方法很多,这边不再说得过多。
这边还需要说明的是怎么去网上获取JSON数据。
首先要说明的是网络操作是不能在UI线程里进行的,否则会程序崩溃。所以这里必须用的异步线程去处理网络加载的问题,并且在加载线程事使用一个进度条来给予用户交互。
以下是网络加载的代码:
```java
public static void sendRequestOkHttpForGet(final String adress,final MyCallBack myCallBack) {
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
try {
URL url = new URL(adress);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
InputStream in = connection.getInputStream();
String response = convertStreamToString(in);
//回调接口函数,让主线程处理
//成功
myCallBack.onResponse(response);
} catch (MalformedURLException e) {
e.printStackTrace();
//失败
myCallBack.onFailure(e);
} catch (ProtocolException e) {
e.printStackTrace();
myCallBack.onFailure(e);
} catch (IOException e) {
e.printStackTrace();
myCallBack.onFailure(e);
} finally {
if (null != connection) {
connection.disconnect();
}
}
}
}).start();
其中第一个参数是要访问的网络的地址,第二个参数是一个回调接口。
public interface MyCallBack {
void onFailure(IOException e);
void onResponse(String response) throws IOException;
}
第一个参数没有什么好说的(就是我们刚刚使用的 ),第二个参数是异步线程经常会用到的一个和主线程交互的手段。在调用函数时传入一个回调接口的指针,当异步线程完成相应的耗时操作之后,再使用该指针调用回调函数即可实现异步线程与主线程的交互了。
城市列表的信息的获取到这里就算结束了。
2、获取天气信息
获取天气信息的网络操作是和获取城市信息的操作是一样的,使用上面那个网络异步函数即可,如果觉得不好,也可以使用网络开源项目包装的网络访问接口,比如说OKHttp。不同的是天气信息的JSON数据要比城市信息的JSON数据复杂得多。
这里提供连接给大家感受一下深圳天气
这里是用Chrome的JSON-handle解析之后的结果。可以看到还是比较复杂的。所以这里我们采用GSON方式来解析JSON,方便我们后面对数据的操作。
GSON方式是把JSON数据解析成相应的对象的一种方式,主要步骤如下:
1、根据JSON数据建立不同的类,JSON数据的每一个结点对应一个类,并且根据不同的结点的复杂程度选择是否还要使用内部类。
2、@SerializedName("JSON中的结点名")需要转换成的节点名;
使用关键字把一些JSON数据中意义晦涩的名词转换成类中名字可以见名知意的属性。
3、JSON数据转换成对象实例
JSONObject jsonObject = new JSONObject(response);
JSONArray jsonArray = jsonObject.getJSONArray("HeWeather5");
String weateherContent = jsonArray.getJSONObject(0).toString();
return new Gson().fromJson(weateherContent, 类名.class);
最后,我们把得到的对象的数据对应的添加到要显示的活动的布局当中就可以了。
城市和天气信息显示模块
3、天气信息的显示
这里相对麻烦一点,因为天气信息的显示中我们做了比较多的功能
获取背景图片和图片的更新
这里我使用的是必应主页提供的背景图片作为天气信息显示的背景图片http://guolin.tech/api/bing_pic这个链接是获取必应每日背景图片下载链接的,可以通过该链接获取图片下载地址,然后再去下载。
由于下载图片是耗时的网络操作,所以我们这里需要使用一个异步线程去下载图片,然后在下载好之后再通知UI线程去加载。
具体代码:
public void updateBingPic() {
String requestBingPic = "http://guolin.tech/api/bing_pic";
OkHttp.sendRequestOkHttpForGet(requestBingPic, new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@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);
editor.apply();
}
});
}
这个是获取图片下载地址的代码,变量bingPic的内容就是下载链接
if (bingPic != null) {
Glide.with(WeatherActivity.this).load(bingPic).into(bingPicIv);
}
当它不为空时,我们使用Glide去下载并加载图片到天气显示背景。Glide 是 Google 员工的开源项目, Google I/O 上被推荐使用Glide具有获取、解码和展示视频剧照、图片、动画等功能,它还有灵活的API,这些API使开发者能够将Glide应用在几乎任何网络协议栈里。创建Glide的主要目的有两个,一个是实现平滑的图片列表滚动效果,另一个是支持远程图片的获取、大小调整和展示。
天气显示信息左右活动切换已选择要显示的城市(ViewPager)
ViewPager是android扩展包v4包中的类,主要功能是实现view页面的左右切换。在本项目中,就是一个view包含一个城市的天气信息,然后view又加入到ViewPager中。
这里说一下ViewPager的使用步骤,ViewPager的实现与ListView有很多相似之处,主要步骤如下:
1、创建或设置数据源。
2、根据数据源创建或配置好相应的适配器。
3、在布局文件中加入ViewPager控件,并在程序给控件设置步骤2中的适配器。
4、给控件添加监听器。
PS:其实Android中很多包含多View的控件都是通过以上步骤实现的,很相似,只要我们认真的掌握了其中的一种,那么别的也就很容易去上手了。
下拉刷新功能
本项目中的下拉刷新功能是使用SwipeRefreshLayout控件实现的,实现的步骤很简单:
1、在布局文件中实现下拉刷新功能的地方添加android.support.v4.widget.SwipeRefreshLayout控件,这里推荐使用v4包,因为能够支持低版本的Android手机。
2、在程序中定义并设置相应属性(样式等等)和监听器。
3、设置事件的相应响应和启动下拉刷新和结束下拉刷新。
通过点击右上角的编辑按钮进入城市管理功能
这里的实现就很基础了,简单讲一下步骤:
1、在布局文件定义按钮
2、在程序中找到按钮并设置监听器
3、在响应事件中做进入城市功能活动的逻辑
导航组件功能
本项目的导航栏功能是用Selector实现,Selector主要是用来改变各种view控件的默认背景的。实现步骤如下:
1、xml文件定义
?xml version="1.0" encoding="utf-8" ?>
selector xmlns:android="http://schemas.android.com/apk/res/android">
!-- 默认时的背景图片-->
item android:drawable="@drawable/pic1" />
!-- 没有焦点时的背景图片 -->
item android:state_window_focused="false"
android:drawable="@drawable/pic1" />
!-- 非触摸模式下获得焦点并单击时的背景图片 -->
item android:state_focused="true" android:state_pressed="true" android:drawable= "@drawable/pic2" />
!-- 触摸模式下单击时的背景图片-->
item android:state_focused="false" android:state_pressed="true" android:drawable="@drawable/pic3" />
!--选中时的图片背景-->
item android:state_selected="true" android:drawable="@drawable/pic4" />
!--获得焦点时的图片背景-->
item android:state_focused="true" android:drawable="@drawable/pic5" />
/selector>
2、使用
LinearLayout layout = (LinearLayout)findViewById(R.id.vp_guide_layout);
LinearLayout.LayoutParams mParams = new LinearLayout.LayoutParams(20, 20);
mParams.setMargins(0, 0, 0, 0);//设置小圆点左右之间的间隔
guideShapeViewArrayList.clear();
layout.removeAllViews();
ImageView imageView = new ImageView(this);
imageView.setLayoutParams(mParams);
imageView.setImageResource(R.drawable.guide_shape_select);
小时天气小时超出屏幕宽度时的当前页面左右滑动(RecycleListView)
RecycleListView是Android官方出品的一个可以代替甚至超越ListView的东西。RecycleListView的实现比不优化的ListView麻烦一些,但是功能上比ListView要更强大,因为他的显示不仅可以竖屏,还可以横屏。
实现步骤:
1、准备数据源
2、根据数据源设置适配器
static class ViewHolder extends RecyclerView.ViewHolder {
TextView hourlyTimeTV;
ImageView hourlyWeatherImageV;
TextView hourlyTemperatureTV;
public ViewHolder(View view){
super(view);
hourlyTimeTV = (TextView) view.findViewById(R.id.hourly_time_tv);
hourlyWeatherImageV = (ImageView) view.findViewById(R.id.hourly_weather_iv);
hourlyTemperatureTV = (TextView) view.findViewById(R.id.hourly_temperature_tv);
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.hourly_forecast_item, parent, false);
ViewHolder holder = new ViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
HourlyWeather hourlyWeather = hourlyWeatherList.get(position);
holder.hourlyTimeTV.setText(hourlyWeather.hourlyTime + "时");
holder.hourlyWeatherImageV.setImageBitmap(hourlyWeather.hourlyImageBit);
holder.hourlyTemperatureTV.setText(hourlyWeather.hourlyTemperature + "º");
}
@Override
public int getItemCount() {
return hourlyWeatherList.size();
}
要实现RecyclerView.Adapter主要是要实现三个函数
onCreateViewHolder()
onBindViewHolder()
getItemCount()
3、在布局文件定义RecycleView控件,并在代码中为控件设置以上适配器。
4、选择是否要设置监听器。
有没有发现和ListView,ViewPager的实现步骤很相似呢。
布局圆角功能
布局圆角主要是为了让布局中的控件看起来美观一些。
实现很简单
1、在drawable中定义xml文件
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#8000" />
<corners
<android:topLeftRadius="10dp"
<android:topRightRadius="10dp"
<android:bottomRightRadius="10dp"
<android:bottomLeftRadius="10dp"/>
</shape>
2、在需要引入圆角的布局文件中引入本配置作为背景
android:background="@drawable/corners_bg"
4、城市信息的显示
这里相对麻烦一点,因为城市信息的显示中我们做了比较多的功能,下面线总体介绍项目使用到的功能模块,然后再一一说明:
1、活动切换按钮,这里就不再重复说明了。
2、添加城市。
3、可拖拽的ListView(DragListView)的城市信息实现
本模块我们主要讲解DragListview的实现:
这里实现的主要功能有:删除城市、城市排序切换。
实现步骤:
1、准备数据源
2、设置适配器
public class CountiesAdapter extends BaseAdapter {
private Context context;
//适配器的数据源 selectedCityList
private List<SelectedCounty> items;
public CountiesAdapter(Context context,List<SelectedCounty> selectedCityList){
this.context = context;
this.items = selectedCityList;
LogUtil.d(TAG, "CountiesAdapter: selectedCityList size:" + selectedCityList.size());
LogUtil.d(TAG, "CountiesAdapter: selectedCityList items size:" + items.size());
}
@Override
public int getCount() {
return items.size();
}
@Override
public Object getItem(int arg0) {
return items.get(arg0);
}
@Override
public long getItemId(int arg0) {
return arg0;
}
public void remove(int arg0) {//删除指定位置的item
items.remove(arg0);
this.notifyDataSetChanged();//不要忘记更改适配器对象的数据源
}
public void insert(SelectedCounty item, int arg0) {
items.add(arg0, item);
this.notifyDataSetChanged();
}
public void change(List<SelectedCounty> selectedCityList) {
items = selectedCityList;
this.notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
SelectedCounty item = (SelectedCounty)getItem(position);
ViewHolder viewHolder;
if(null == convertView){
viewHolder = new ViewHolder();
convertView = LayoutInflater.from(context).inflate(R.layout.drag_listview_item, null);
viewHolder.dragMoveIv = (ImageView) convertView.findViewById(R.id.drag_move_iv);
viewHolder.dragCountyNameTv = (TextView) convertView.findViewById(R.id.drag_county_name_tv);
viewHolder.drag_click_remove = (ImageView) convertView.findViewById(R.id.drag_click_remove);
moveImageViewList.add(viewHolder.dragMoveIv);
deleteImageViewList.add(viewHolder.drag_click_remove);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
//是否点击了edit按钮,
if (isEditClick) {
if (!WeatherActivity.isLocationCountyRemove && null != WeatherActivity.locationCountyWeatherId && 0 == position ) {
viewHolder.drag_click_remove.setVisibility(View.GONE);
} else {
viewHolder.dragMoveIv.setVisibility(View.VISIBLE);
viewHolder.drag_click_remove.setVisibility(View.VISIBLE);
}
}else {
viewHolder.dragMoveIv.setVisibility(View.GONE);
viewHolder.drag_click_remove.setVisibility(View.GONE);
}
viewHolder.dragCountyNameTv.setText(item.getCountyName());
return convertView;
}
class ViewHolder {
ImageView dragMoveIv;
TextView dragCountyNameTv;
ImageView drag_click_remove;
}
}
别的地方和ListView是一样的,不同的是多了一个remove和insert函数
public void remove(int arg0) {//删除指定位置的item
items.remove(arg0);
this.notifyDataSetChanged();//不要忘记更改适配器对象的数据源
}
public void insert(SelectedCounty item, int arg0) {
items.add(arg0, item);
this.notifyDataSetChanged();
}
3、控件绑定适配器
这里也有区别:首先要定义两个函数:
//监听器在手机拖动停下的时候触发
private DragSortListView.DropListener onDrop =
new DragSortListView.DropListener() {
@Override
public void drop(int from, int to) {//from to 分别表示 被拖动控件原位置 和目标位置
//如果定位城市存在,则去除定位城市的操作
if (!WeatherActivity.isLocationCountyRemove && null != WeatherActivity.locationCountyWeatherId) {
if (0 == from || 0 == to) {
return;
}
}
if (from != to) {
SelectedCounty item = (SelectedCounty)countiesAdapter.getItem(from);//得到listview的适配器
countiesAdapter.remove(from);//在适配器中”原位置“的数据。
countiesAdapter.insert(item, to);//在目标位置中插入被拖动的控件。
isSwapCounty = true;
}
}
};
//删除监听器,点击左边差号就触发。删除item操作。
private DragSortListView.RemoveListener onRemove =
new DragSortListView.RemoveListener() {
@Override
public void remove(int which) {
delCountyId.add(selectedCityList.get(which).getId());
delCountyIndex.add(which);
countiesAdapter.remove(which);
Log.d(TAG, "onClick: remove position:" + which);
}
};
然后绑定适配器时这两个函数也一起绑定
//水平滑动显示
hourlyRecycler = (RecyclerView) currentView.findViewById(R.id.hourly_recycler);
layoutManager = new LinearLayoutManager(currentView.getContext());
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
hourlyRecycler.setLayoutManager(layoutManager);
hourlyWeatherAdapter = new HourlyWeatherAdapter(hourlyWeatherList);
hourlyRecycler.setAdapter(hourlyWeatherAdapter);
有问题或要 者建议的可以在评论留言,需要源码的也可以留言,我看到了都会及时回复的。
未完待续。。。。。。