可能很多人会问:之前已经写过一篇博文来介绍怎么做一款简单的新闻APP(http://blog.csdn.net/yiwei12/article/details/71249628),为什么还要专门一篇来介绍怎么做一款天气 APP,毕竟网络请求和数据处理都是大同小异的。如果真的要说差别的话,前一篇只是具备了一些基本的功能,来说明怎么请求和处理返回的数据,但还不足与在日常生活中使用?这一篇实践是来做一款日常可用的天气 APP - 彼时天气
—- 说明: 彼时天气仿照魅族 Flyme 天气设计
—- 在 coolWeather 的基础上进行处理
总体思路
这就是总体的设计思路,至于后面其他的功能:选择地区,更新频率等功能可以之后再说
运行GIF
之所以大幅度提前展示 GIF 图,方便对后面布局部分有更好的理解
步骤
- 声明权限
- 依赖库
- 网络请求
- 网络解析
- 界面布局
- 最后
- 完整代码下载地址(github)
声明权限
因为我们需要用到百度SDK的定位服务,所以需要先下载百度中包含基础定位的 SDK(http://lbsyun.baidu.com/sdk/download),解压后,将其中的 .jar 文件移动到 libs 文件夹中,在 main 目录下新建一个 jniLibs,将剩下的几个文件夹复制到里面,点击 Sync 按钮进行同步。 在 AndroidManifest 文件中声明权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
基本上都是 百度SDK 所需要的权限,其中部分权限是需要进行运行时处理的。同时还需要添加:
<meta-data
android:name="com.baidu.lbsapi.API_KEY"
android:value="SmwjePIXo1eeRGbjw8QKrbncWfgi5V0f" />
<service
android:name="com.baidu.location.f"
android:enabled="true"
android:process=":remote" />
value 中填写自己申请获取的 Key,其他的格式都是固定的,这样我们就可以调用 基础定位 中的功能了
依赖库
compile 'org.litepal.android:core:1.4.1' // 数据库框架
compile 'com.squareup.okhttp3:okhttp:3.4.1' // 网络请求
compile 'com.google.code.gson:gson:2.7' // 网络解析
compile 'com.github.bumptech.glide:glide:3.8.0' // 图片加载
compile 'com.android.support:cardview-v7:24.2.1' // 卡片式布局
compile 'com.android.support:design:24.2.1' // Material Design中用到的依赖库
compile 'net.danlew:android.joda:2.9.9' // 时间处理
网络请求
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);
}
}
网路解析
本次的数据来源是和风天气(https://www.heweather.com/documents/api/v5/weather),O(∩_∩)O哈哈~第一行代码的同学有没有很熟悉
参数
请求地址
{
"HeWeather5": [
{
"alarms": [
{
"level": "蓝色",
"stat": "预警中",
"title": "山东省青岛市气象台发布大风蓝色预警",
"txt": "青岛市气象台2016年08月29日15时24分继续发布大风蓝色预警信号:预计今天下午到明天,我市北风风力海上6到7级阵风9级,陆地4到5阵风7级,请注意防范。",
"type": "大风"
}
],
"aqi": {
"city": {
"aqi": "60",
"co": "0",
"no2": "14",
"o3": "95",
"pm10": "67",
"pm25": "15",
"qlty": "良", //共六个级别,分别:优,良,轻度污染,中度污染,重度污染,严重污染
"so2": "10"
}
},
"basic": {
"city": "青岛",
"cnty": "中国",
"id": "CN101120201",
"lat": "36.088000",
"lon": "120.343000",
"prov": "山东" //城市所属省份(仅限国内城市)
"update": {
"loc": "2016-08-30 11:52",
"utc": "2016-08-30 03:52"
}
},
"daily_forecast": [
{
"astro": {
"mr": "03:09",
"ms": "17:06",
"sr": "05:28",
"ss": "18:29"
},
"cond": {
"code_d": "100",
"code_n": "100",
"txt_d": "晴",
"txt_n": "晴"
},
"date": "2016-08-30",
"hum": "45",
"pcpn": "0.0",
"pop": "8",
"pres": "1005",
"tmp": {
"max": "29",
"min": "22"
},
"vis": "10",
"wind": {
"deg": "339",
"dir": "北风",
"sc": "4-5",
"spd": "24"
}
}
],
"hourly_forecast": [
{
"cond": {
"code": "100",
"txt": "晴"
},
"date": "2016-08-30 12:00",
"hum": "47",
"pop": "0",
"pres": "1006",
"tmp": "29",
"wind": {
"deg": "335",
"dir": "西北风",
"sc": "4-5",
"spd": "36"
}
}
],
"now": {
"cond": {
"code": "100",
"txt": "晴"
},
"fl": "28",
"hum": "41",
"pcpn": "0",
"pres": "1005",
"tmp": "26",
"vis": "10",
"wind": {
"deg": "330",
"dir": "西北风",
"sc": "6-7",
"spd": "34"
}
},
"status": "ok",
"suggestion": {
"comf": {
"brf": "较舒适",
"txt": "白天天气晴好,您在这种天气条件下,会感觉早晚凉爽、舒适,午后偏热。"
},
"cw": {
"brf": "较不宜",
"txt": "较不宜洗车,未来一天无雨,风力较大,如果执意擦洗汽车,要做好蒙上污垢的心理准备。"
},
"drsg": {
"brf": "热",
"txt": "天气热,建议着短裙、短裤、短薄外套、T恤等夏季服装。"
},
"flu": {
"brf": "较易发",
"txt": "虽然温度适宜但风力较大,仍较易发生感冒,体质较弱的朋友请注意适当防护。"
},
"sport": {
"brf": "较适宜",
"txt": "天气较好,但风力较大,推荐您进行室内运动,若在户外运动请注意防风。"
},
"trav": {
"brf": "适宜",
"txt": "天气较好,风稍大,但温度适宜,是个好天气哦。适宜旅游,您可以尽情地享受大自然的无限风光。"
},
"uv": {
"brf": "强",
"txt": "紫外线辐射强,建议涂擦SPF20左右、PA++的防晒护肤品。避免在10点至14点暴露于日光下。"
}
}
}
]
}
我们后面申请数据采用的参数都为 城市名称,从返回示例中我们可以看出,结构和我们上次新闻API 返回的结构是有差异的,多嵌套了一层,不过 GSON 解析的方式还是一样的。在包名下新建一个 gson 文件夹,在里面新建对应数据的实体类:
AQI.class
public class AQI {
public AQICity city;
public class AQICity{
public String aqi;
public String pm25;
public String co;
public String o3;
public String pm10;
public String so2;
}
}
Basic.class
public class Basic {
@SerializedName("city")
public String cityName;
@SerializedName("id")
public String weatherId;
public Update update;
public class Update{
public String loc;
}
}
Forecast.class
public class Forecast {
public String date;
@SerializedName("tmp")
public Temperature temperature;
@SerializedName("cond")
public More more;
public class More{
@SerializedName("txt_d")
public String info;
@SerializedName("code_d")
public int code;
}
public class Temperature{
public String max;
public String min;
}
}
Hourly.class
public class Hourly {
public Cond cond;
public class Cond{
public String code;
public String txt;
}
public String date;
public String tmp;
}
Now.class
public class Now {
@SerializedName("tmp")
public String temperature;
@SerializedName("cond")
public More more;
public class More{
@SerializedName("txt")
public String info;
}
}
Suggestion.class
public class Suggestion {
@SerializedName("comf")
public Comfort comfort;
@SerializedName("cw")
public CarWash carWash;
public Sport sport;
@SerializedName("drsg")
public Clothes clothes;
@SerializedName("flu")
public Cold cold;
public UV uv;
public class UV{
@SerializedName("txt")
public String info;
@SerializedName("brf")
public String sign;
}
public class Cold{
@SerializedName("txt")
public String info;
@SerializedName("brf")
public String sign;
}
public class Clothes{
@SerializedName("txt")
public String info;
@SerializedName("brf")
public String sign;
}
public class Comfort{
@SerializedName("txt")
public String info;
@SerializedName("brf")
public String sign;
}
public class CarWash{
@SerializedName("txt")
public String info;
@SerializedName("brf")
public String sign;
}
public class Sport{
@SerializedName("txt")
public String info;
@SerializedName("brf")
public String sign;
}
}
最后就是返回数据的对应实体类 Weather.class
public class Weather {
public String status;
public Basic basic;
public AQI aqi;
public Now now;
public Suggestion suggestion;
@SerializedName("daily_forecast")
public List<Forecast> forecastList;
@SerializedName("hourly_forecast")
public List<Hourly> hourlyList;
}
在包名下新建目录 util , 在其中新建类:Utility.class
public class Utility {
/**
*
* 处理得到的 weather 数据,转化为 weather 对象
*/
public static Weather handleWeatherResponse(String response){
try{
JSONObject jsonObject = new JSONObject(response);
JSONArray jsonArray = jsonObject.getJSONArray("HeWeather5");
String weatherContent = jsonArray.getJSONObject(0).toString();
return new Gson().fromJson(weatherContent, Weather.class);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
界面布局
相信大家都看了运行的 GIF 图,可以看到主界面的布局还是比较繁琐的,所以引入布局不失为一个好的选择,主界面布局主要分为以下几个部分:
weather_title(标题栏)
weather_now(当前天气信息)
weather_hourly(小时天气预报)
weather_forecast(未来几天的天气预报)
weather_aqi(空气质量)
weather_suggestion(生活建议)
以下是各部分的代码:
weather_title:
<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorWhite"
app:popupTheme="@style/ToolbarPopupTheme"
app:subtitleTextColor="@color/colorFont">
<TextView
android:id="@+id/title_city"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textColor="@color/colorFont"
android:textSize="20sp"/>
</android.support.v7.widget.Toolbar>
Toolbar 中间放置了 title_city ,用来显示当前的城市名
weather_now
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="@color/colorWhite">
<RelativeLayout
android:id="@+id/weather_now_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorWhite">
<TextView
android:id="@+id/degree_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:textColor="@color/colorFont"
android:textSize="120sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/degree_text"
android:layout_alignTop="@+id/degree_text"
android:text="°"
android:textColor="@color/colorFont"
android:textSize="120sp"
/>
<TextView
android:id="@+id/weather_info_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/degree_text"
android:layout_centerHorizontal="true"
android:textColor="@color/colorFont"
android:textSize="20sp"/>
<TextView
android:id="@+id/update_time_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/degree_text"
android:layout_centerHorizontal="true"
android:textColor="@color/colorFont"
android:textSize="12sp"/>
</RelativeLayout>
</RelativeLayout>
在视图中间,从上至下放置了三个TextView 控件:weather_info_text(天气状况),degree_text(天气温度),update_time_text(数据更新时间),另外一个“°”符号放在温度的右上角
weather_hourly:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/colorWhite">
<android.support.v7.widget.RecyclerView
android:id="@+id/weather_hourly"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
</LinearLayout>
为了支持小时天气预报部分可以直接水平滑动(不过免费用户可以得到的数据量好像不需要滑动 2333333),放置了一个 RecyclerView
weather_hourly_item
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="80dp"
android:layout_height="100dp"
android:padding="10dp">
<TextView
android:id="@+id/hour_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorFont"
android:layout_centerInParent="true"
android:textSize="14sp"/>
<TextView
android:id="@+id/hour_degree"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorFont"
android:textSize="14sp"
android:layout_below="@+id/hour_text"
android:layout_centerHorizontal="true"
android:layout_marginTop="4dp"/>
<TextView
android:id="@+id/hout_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorFont"
android:textSize="14sp"
android:layout_above="@+id/hour_text"
android:layout_centerHorizontal="true"
android:layout_marginBottom="4dp"/>
</RelativeLayout>
小时天气预报的每个子项中,从上到下放置了三个TextView控件:hour_degree(天气温度),hour_text(天气描述),hout_time(时间)
weather_forecast:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorWhite">
<LinearLayout
android:id="@+id/forecast_layout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</LinearLayout>
</LinearLayout>
这里采用的还是原方案,直接设置一个 LinearLayout 布局,后面直接在其中添加子布局,当然,大家也可以选用一个 ListView 来显示内容
weather_forecast_item
<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="@+id/data_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:gravity=