android开发学习之路——天气预报之显示天气信息(三)
由于和风天气返回的JSON数据结构非常复杂,我们借助GSON来对天气信息进行解析。
(一)定义GSON实体类
GSON的用法比较简单。先将数据对应的实体类创建好。由于和风天气返回的数据非常多,作者筛选了一些比较重要的数据来进行解析。
先回顾一下返回数据的大致格式:
1 { 2 "HeWeather":[ 3 { 4 "status":"ok", 5 "basic":{}, 6 "api":{}, 7 "now":{}, 8 "suggestion":{}, 9 "daily_forecast":[] 10 } 11 ] 12 }
其中,basic、api、now、suggetion和daily_forecast的内部又都会有具体的内容,那么我们就可以将这5个部分定义成5个实体类。
下面我们就可以将这5个部分定义成5个实体类。
basic中具体内容如下所示:
1 "basic":{ 2 "city":"苏州”, 3 “id”:“CN101190401", 4 "update”{ 5 “loc”:“2016-08-08 21:58” 6 } 7 }
其中,city表示城市名,id表示城市对应的天气id,update中的loc表示天气的更新时间。我们按照此结构就可以在gson包下建立一个Basic类,代码如下:
1 public class Basic{ 2 @SerializedName("city") 3 public String cityName; 4 @SerializedName("id") 5 public String weatherId; 6 public Update update; 7 public class Update{ 8 @SerializedName("loc") 9 public String updateTime; 10 } 11 }
由于JSON中的一些字段可能不太适合直接作为Java字段来命名,因此这里使用了@SerializedName注解的方式来让JSON字段和Java字段之间建立映射关系。
这样我们就将Basic类定义好了,其余的几个实体类也是类似的,我们使用同样的方式来定义。api中的具体内容如下:
1 api":{ 2 "city":{ 3 "api":"44", 4 "pm25":"13" 5 } 6 }
在gson包下新建一个AQI类,代码如下:
1 public class AQI{ 2 public AQICity city; 3 public class AQICity{ 4 public String api; 5 public String pm25; 6 } 7 }
now中的具体内容如下所示:
1 "now":{ 2 "tmp":"29", 3 "cond":{ 4 "txt":"阵雨“ 5 } 6 }
在gson包下新建一个Now类,代码如下:
1 public class Now{ 2 @SerializedName("tmp") 3 public String temperature; 4 @SerializedName("cond") 5 public More more; 6 public class More{ 7 @SerializedName("txt") 8 public String info; 9 } 10 }
suggestion中的具体内容如下所示:
1 "suggestion":{ 2 "comf":{ 3 "txt":"白天天气较热,虽然有雨,但仍然无法削弱较高气温给人们带来的 暑意,这种天气会让您感到不很舒适。" 4 }, 5 "cw":{ 6 "txt":"不宜洗车,未来24小时内有雨,如果在此期间洗车,雨水和路上的泥水..." 7 }, 8 "sport":{ 9 "txt":"有降水,且风力较强,建议...." 10 } 11 }
在gson包下新建一个Suggestion类,代码如下:
1 public class Suggestion { 2 3 @SerializedName("comf") 4 public Comfort comfort; 5 6 @SerializedName("cw") 7 public CarWash carWash; 8 9 public Sport sport; 10 11 public class Comfort { 12 13 @SerializedName("txt") 14 public String info; 15 16 } 17 18 public class CarWash { 19 20 @SerializedName("txt") 21 public String info; 22 23 } 24 25 public class Sport { 26 27 @SerializedName("txt") 28 public String info; 29 30 } 31 32 }
到目前为止都还比较简单,不过接下来的一项数据就有点特殊了,daily_forecast中的具体内容如下所示:
1 "daily_forecast":{ 2 { 3 date":"2016-08-08", 4 "cond":{ 5 "txt_d":"阵雨" 6 }, 7 "tmp":{ 8 "max":"34", 9 "min":"27" 10 } 11 } 12 { 13 date":"2016-08-09", 14 "cond":{ 15 "txt_d":"多云" 16 }, 17 "tmp":{ 18 "max":"35", 19 "min":"29" 20 } 21 }, 22 .... 23 }
可以看到,daily_forecast中包含的是一个数组,数组中的每一项都代表着未来一天的天气信息。因此,我们需要定义单日天气的实体类,然后在声明实体类引用的时候使用集合类型来进行声明。
在gson包下新建一个Forecast类,代码如下所示:
1 public class Forecast { 2 3 public String date; 4 5 @SerializedName("tmp") 6 public Temperature temperature; 7 8 @SerializedName("cond") 9 public More more; 10 11 public class Temperature { 12 13 public String max; 14 15 public String min; 16 17 } 18 19 public class More { 20 21 @SerializedName("txt_d") 22 public String info; 23 24 } 25 26 }
这样我们就把basic、aqi、now、suggestion和daily_forecast对应的实体类全部都创建好了,接下来再创建一个总的实例类来引用刚刚创建的各种实体类。再gson包下新建一个weather类,代码如下:
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; }
在Weather类中,我们对Basic、AQI、Now、Suggestion和Forecast类进行引用。其中由于daily_forecast中包含的是一个数组,因此用List集合来引用Forecas类。
另外,返回的天气数据中还会包含一项status数据,成功返回ok,失败则会返回具体原因。
现在所有GSON实体类都定义好饿,接下来开始编写天气界面。
(二)编写天气界面
先创建一个用于显示天气信息的活动。右击com.coolweather.android包-New-Activity-Empty Activity,创建一个WeatherActivity,并将布局名指定成activity_weather.xml。
由于所有的天气信息都将在同一个界面上显示,因此activity_weather.xml会是一个很长的布局文件。那么为了让里面的代码不至于那么混乱,这里使用引入布局技巧。即将界面的不同部分写在不同的布局文件里面,再通过引入布局的方式集成到activity_weather.xml中,这样整个布局文件就会显得非常工整。
右击res/layout-New-Layout resource file,新建一个title.xml作为头布局,代码如下所示:
1 <RelativeLayout 2 xmlns:android="http://schemas.android.com/apk/res/android 3 android:layout_height="?attr/actionBarSize" 4 android:layout_width="match_parent""> 5 <TextView 6 android:id="@+id/title_city" 7 android:layout_height="wrap_content" 8 android:layout_width="wrap_content" 9 android:textSize="20sp" 10 android:textColor="#fff" 11 android:layout_centerInParent="true"/> 12 <TextView 13 android:id="@+id/title_update_time" 14 android:layout_height="wrap_content" 15 android:layout_width="wrap_content" 16 android:layout_centerVertical="true" 17 android:textSize="16sp" 18 android:textColor="#fff" 19 android:layout_alignParentRight="true" 20 android:layout_marginRight="10dp"/> 21 </RelativeLayout>
这段代码在头布局中放置了两个TextView,一个居中显示城市名,一个居右显示更新时间。
新建一个now.xml作为当前天气信息的布局,代码如下:
1 <LinearLayout 2 xmlns:android="http://schemas.android.com/apk/res/android" 3 android:orientation="vertical" 4 android:layout_height="wrap_content" 5 android:layout_width="match_parent" 6 android:layout_margin="15dp" > 7 <TextView 8 android:id="@+id/degree_text" 9 android:layout_height="wrap_content" 10 android:layout_width="wrap_content" 11 android:textSize="60sp" 12 android:textColor="#fff" 13 android:layout_gravity="end" /> 14 <TextView 15 android:id="@+id/weather_info_text" 16 android:layout_height="wrap_content" 17 android:layout_width="wrap_content" 18 android:textSize="20sp" 19 android:textColor="#fff" 20 android:layout_gravity="end" /> 21 </LinearLayout>
当前天气信息的布局中放置两个TextView,一个用于显示当前气温,一个用于显示天气概况。
新建forecast.xml作为未来几天天气信息的布局,代码如下:
1 <LinearLayout 2 xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_height="wrap_content" 4 android:layout_width="match_parent" 5 android:orientation="vertical" 6 android:background="#8000" 7 android:layout_margin="15dp" > 8 <TextView 9 android:layout_height="wrap_content" 10 android:layout_width="wrap_content" 11 android:layout_marginTop="15dp" 12 android:layout_marginLeft="15dp" 13 android:textSize="20sp" 14 android:textColor="#fff" 15 android:text="预报" /> 16 <LinearLayout 17 android:id="@+id/forecast_layout" 18 android:orientation="vertical" 19 android:layout_height="wrap_content" 20 android:layout_width="match_parent" > 21 </LinearLayout> 22 </LinearLayout>
这里最外层使用LinearLayout定义了一个半透明的背景,然后使用TextView定义了一个标题,接着又使用一个Linearlayout定义了一个用于显示未来几天天气信息的布局。不过这个布局中并没有放入任何内容,因为这是要根据服务器返回的数据在代码中动态添加的。
为此,我们还需要再定义一个未来天气信息的子项布局,创建forecast_item.xml文件,代码如下所示:
1 <LinearLayout 2 xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_margin="15dp" 4 android:layout_height="wrap_content" 5 android:layout_width="match_parent" > 6 <TextView 7 android:id="@+id/date_text" 8 android:layout_height="wrap_content" 9 android:layout_width="0dp" 10 android:textColor="#fff" android:layout_weight="2" 11 android:layout_gravity="center_vertical" /> 12 <TextView 13 android:id="@+id/info_text" 14 android:layout_height="wrap_content" 15 android:layout_width="0dp" 16 android:textColor="#fff" 17 android:layout_weight="1" 18 android:layout_gravity="center_vertical" 19 android:gravity="center"/> 20 <TextView 21 android:id="@+id/max_text" 22 android:layout_height="wrap_content" 23 android:layout_width="0dp" android:textColor="#fff" 24 android:layout_weight="1" 25 android:layout_gravity="center" 26 android:gravity="right"/> 27 <TextView 28 android:id="@+id/min_text" 29 android:layout_height="wrap_content" 30 android:layout_width="0dp" 31 android:textColor="#fff" 32 android:layout_weight="1" 33 android:layout_gravity="center" 34 android:gravity="right"/> 35 </LinearLayout>
子项布局中放置了4个TextView,一个用于显示天气预报,一个用于显示天气概况,另外两个分别用于显示当前的最高温度和最低温度。
新建api.xml作为空气质量信息的布局,代码如下所示:
1 <LinearLayout 2 xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_height="wrap_content" 4 android:layout_width="match_parent" 5 android:orientation="vertical" 6 android:background="#8000" 7 android:layout_margin="15dp"> 8 <TextView 9 android:layout_height="wrap_content" 10 android:layout_width="wrap_content" 11 android:layout_marginTop="15dp" 12 android:layout_marginLeft="15dp" 13 android:textSize="20sp" 14 android:textColor="#fff" 15 android:text="空气质量" /> 16 <LinearLayout 17 android:layout_margin="15dp" 18 android:layout_height="wrap_content" 19 android:layout_width="match_parent"> 20 <RelativeLayout 21 android:layout_height="match_parent" 22 android:layout_width="0dp" 23 android:layout_weight="1"> 24 <LinearLayout 25 android:layout_height="wrap_content" 26 android:layout_width="match_parent" 27 android:orientation="vertical" 28 android:layout_centerInParent="true"> 29 <TextView 30 android:id="@+id/aqi_text" 31 android:layout_height="wrap_content" 32 android:layout_width="wrap_content" 33 android:textSize="40sp" 34 android:textColor="#fff" 35 android:layout_gravity="center" /> 36 <TextView 37 android:layout_height="wrap_content" 38 android:layout_width="wrap_content" 39 android:layout_gravity="center" 40 android:textColor="#fff" 41 android:text="AQI指数" /> 42 </LinearLayout> 43 </RelativeLayout> 44 <RelativeLayout 45 android:layout_height="match_parent" 46 android:layout_width="0dp" 47 android:layout_weight="1"> 48 <LinearLayout 49 android:layout_height="wrap_content" 50 android:layout_width="match_parent" 51 android:orientation="vertical" 52 android:layout_centerInParent="true"> 53 <TextView 54 android:id="@+id/pm25_text 55 android:layout_height="wrap_content" 56 android:layout_width="wrap_content" 57 android:layout_gravity="center" 58 android:textSize="40sp" 59 android:textColor="#fff""/> 60 <TextView 61 android:layout_height="wrap_content" 62 android:layout_width="wrap_content" 63 android:layout_gravity="center" 64 android:textColor="#fff" 65 android:text="PM2.5指数"/> 66 </LinearLayout> 67 </RelativeLayout> 68 </LinearLayout> 69 </LinearLayout>
这个布局看上去有点长,但很好理解。首先跟前面一样的,使用LinearLayout定义一个半透明的背景,然后使用TextView定义了一个标题。接下来,这里使用LinearLayout和RelativeLayout嵌套的方式实现了一个左右平分并且居中对齐的布局,分别用于显示AQI指数和PM2.5指数。
新建suggestion.xml作为生活建议信息的布局,代码如下:
1 <LinearLayout 2 xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_margin="15dp" 4 android:layout_height="wrap_content" 5 android:layout_width="match_parent" 6 android:orientation="vertical" 7 android:background="#8000" > 8 <TextView 9 android:layout_height="wrap_content" 10 android:layout_width="wrap_content" 11 android:layout_marginTop="15dp" 12 android:layout_marginLeft="15dp" 13 android:textSize="20sp" 14 android:textColor="#fff" android:text="生活建议" /> 15 <TextView 16 android:id="@+id/comfort_text" 17 android:layout_margin="15dp" 18 android:layout_height="wrap_content" 19 android:layout_width="wrap_content" 20 android:textColor="#fff" /> 21 <TextView 22 android:id="@+id/car_wash_text" 23 android:layout_margin="15dp" 24 android:layout_height="wrap_content" 25 android:layout_width="wrap_content" 26 android:textColor="#fff"/> 27 <TextView 28 android:id="@+id/sport_text"w 29 android:layout_margin="15dp" 30 android:layout_height="wrap_content" 31 android:layout_width="wrap_content" 32 android:textColor="#fff" /> 33 </LinearLayout>
这里同样也是定义了一个半透明的背景和一个标题,然后下面使用3个TextView分别用于显示舒适度、洗车指数、和运动建议的相关数据。
这样我们就把天气界面上每个部分的布局文件都编写好了,接下来的工作就是将它们引入到activity_weather.xml当中,如下所示:
1 <FrameLayout 2 xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_height="match_parent" 4 android:layout_width="match_parent" 5 android:background="@color/colorPrimary"> 6 <ScrollView 7 android:id="@+id/weather_layout" 8 android:layout_height="match_parent" 9 android:layout_width="match_parent" 10 android:overScrollMode="never" 11 android:scrollbars="none"> 12 <LinearLayout 13 android:layout_height="wrap_content" 14 android:layout_width="match_parent" 15 android:fitsSystemWindows="true" 16 android:orientation="vertical"> 17 <include layout="@layout/title"/> 18 <include layout="@layout/now"/> 19 <include layout="@layout/forecast"/> 20 <include layout="@layout/aqi"/> 21 <include layout="@layout/suggestion"/> 22 </LinearLayout> 23 </ScrollView> 24 </FrameLayout>
可以看到,首先最外层布局使用了一个FrameLayout,并将它的背景色设置成colorPrimary。然后在FrameLayout中嵌套了一个ScrollView,这是因为天气界面中的内容比较多,使用ScrollWiew可以允许我们通过滚动的方式查看屏幕以外的内容。
由于ScrollView的内部只允许存在一个直接子布局,因此这里又嵌套了一个垂直方向的LinearLayout,然后在LinearLayout中将刚才定义的布局逐个引入。
这样,我们天气界面编写完成了,接下来编写业务逻辑,将天气显示到界面上。
(三)将天气显示到界面上
首先在Utility类中添加一个用于解析天气JSON数据的方法,如下所示:
public class Utility{ .... /** * 将返回的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(Exception e){ e.printStackTrace(); } return null; } }
可以看到,handleWeatherResponse()方法中先是通过JSONObject和JSONArray将天气数据中的主题内容解析出来,即如下内容:
{ "status":"ok:, "basic":{}, "aqi":{}, "now":{}, "suggestion":{}, "daily_forecast":[] }
由于我们之前已经按照上面的数据格式定义过相应的GSON实体类,因此只需要通过调用fromJson()方法就可以直接将JSON数据转换成Weather对象了。
接下来的工作是我们如何在活动中去请求天气数据,以及将数据显示到界面上。修改WeatherActivity中的代码,如下所示:
1 public class WeatherActivity extends AppCompatActivity { 2 3 private ScrollView weatherLayout; 4 5 private TextView titleCity; 6 7 private TextView titleUpdateTime; 8 9 private TextView degreeText; 10 11 private TextView weatherInfoText; 12 13 private LinearLayout forecastLayout; 14 15 private TextView aqiText; 16 17 private TextView pm25Text; 18 19 private TextView comfortText; 20 21 private TextView carWashText; 22 23 private TextView sportText; 24 25 @Override 26 protected void onCreate(Bundle savedInstanceState) { 27 super.onCreate(savedInstanceState); 28 setContentView(R.layout.activity_weather); 29 // 初始化各控件 30 weatherLayout = (ScrollView) findViewById(R.id.weather_layout); 31 titleCity = (TextView) findViewById(R.id.title_city); 32 titleUpdateTime = (TextView) findViewById(R.id.title_update_time); 33 degreeText = (TextView) findViewById(R.id.degree_text); 34 weatherInfoText = (TextView) findViewById(R.id.weather_info_text); 35 forecastLayout = (LinearLayout) findViewById(R.id.forecast_layout); 36 aqiText = (TextView) findViewById(R.id.aqi_text); 37 pm25Text = (TextView) findViewById(R.id.pm25_text); 38 comfortText = (TextView) findViewById(R.id.comfort_text); 39 carWashText = (TextView) findViewById(R.id.car_wash_text); 40 sportText = (TextView) findViewById(R.id.sport_text); 41 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 42 String weatherString = prefs.getString("weather", null); 43 final String weatherId; 44 if (weatherString != null) { 45 // 有缓存时直接解析天气数据 46 Weather weather = Utility.handleWeatherResponse(weatherString); 47 showWeatherInfo(weather); 48 } else { 49 // 无缓存时去服务器查询天气 50 String weatherId = getIntent().getStringExtra("weather_id") 51 weatherLayout.setVisibility(View.INVISIBLE); 52 requestWeather(weatherId); 53 } 54 } 55 56 /** 57 * 根据天气id请求城市天气信息。 58 */ 59 public void requestWeather(final String weatherId) { 60 String weatherUrl = "http://guolin.tech/api/weather?cityid=" + weatherId + "&key=bc0418b57b2d4918819d3974ac1285d9"; 61 HttpUtil.sendOkHttpRequest(weatherUrl, new Callback() { 62 @Override 63 public void onResponse(Call call, Response response) throws IOException { 64 final String responseText = response.body().string(); 65 final Weather weather = Utility.handleWeatherResponse(responseText); 66 runOnUiThread(new Runnable() { 67 @Override 68 public void run() { 69 if (weather != null && "ok".equals(weather.status)) { 70 SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(WeatherActivity.this).edit(); 71 editor.putString("weather", responseText); 72 editor.apply(); 73 showWeatherInfo(weather); 74 } else { 75 Toast.makeText(WeatherActivity.this, "获取天气信息失败", Toast.LENGTH_SHORT).show(); 76 } 77 } 78 }); 79 } 80 81 @Override 82 public void onFailure(Call call, IOException e) { 83 e.printStackTrace(); 84 runOnUiThread(new Runnable() { 85 @Override 86 public void run() { 87 Toast.makeText(WeatherActivity.this, "获取天气信息失败", Toast.LENGTH_SHORT).show(); 88 swipeRefresh.setRefreshing(false); 89 } 90 }); 91 } 92 }); 93 } 94 /** 95 * 处理并展示Weather实体类中的数据。 96 */ 97 private void showWeatherInfo(Weather weather) { 98 String cityName = weather.basic.cityName; 99 String updateTime = weather.basic.update.updateTime.split(" ")[1]; 100 String degree = weather.now.temperature + "℃"; 101 String weatherInfo = weather.now.more.info; 102 titleCity.setText(cityName); 103 titleUpdateTime.setText(updateTime); 104 degreeText.setText(degree); 105 weatherInfoText.setText(weatherInfo); 106 forecastLayout.removeAllViews(); 107 for (Forecast forecast : weather.forecastList) { 108 View view = LayoutInflater.from(this).inflate(R.layout.forecast_item, forecastLayout, false); 109 TextView dateText = (TextView) view.findViewById(R.id.date_text); 110 TextView infoText = (TextView) view.findViewById(R.id.info_text); 111 TextView maxText = (TextView) view.findViewById(R.id.max_text); 112 TextView minText = (TextView) view.findViewById(R.id.min_text); 113 dateText.setText(forecast.date); 114 infoText.setText(forecast.more.info); 115 maxText.setText(forecast.temperature.max); 116 minText.setText(forecast.temperature.min); 117 forecastLayout.addView(view); 118 } 119 if (weather.aqi != null) { 120 aqiText.setText(weather.aqi.city.aqi); 121 pm25Text.setText(weather.aqi.city.pm25); 122 } 123 String comfort = "舒适度:" + weather.suggestion.comfort.info; 124 String carWash = "洗车指数:" + weather.suggestion.carWash.info; 125 String sport = "运行建议:" + weather.suggestion.sport.info; 126 comfortText.setText(comfort); 127 carWashText.setText(carWash); 128 sportText.setText(sport); 129 weatherLayout.setVisibility(View.VISIBLE); 130 } 131 132 }
这个活动中的代码比较长,我们一步步梳理下。在onCreate()方法中先是去获取一些控件的实例,然后会尝试从本地缓存中读取天气数据。第一次是没有缓存的,因此会从Intent中取出天气id,并调用requestWeather()方法来从服务器请求天气数据。注意,请求数据的时候先将ScrollView进行隐藏,不然空数据的界面看上去会很奇怪。
requestWeather()方法中先是使用了参数中传入的天气id,并调用requestWeather()方法来从服务器请求天气数据。注意,请求数据的时候先将ScollView进行隐藏,不然空数据的界面看上去会很奇怪。
requestWeather()方法中先是使用了参数中传入的天气id和我们之前申请好的APIKey拼装出一个接口地址,接着调用HttpUtil.sendOkHttpRequest()方法来向该地址发出请求,服务器会将相应城市的天气信息以JSON格式返回。然后我们在onResponse()回调中先调用Utility.handleWeatherResponse()方法将返回的JSON数据转换成Weather对象,再将当前线程切换到主线程。然后进行判断,如果服务器返回的status状态是ok,就说明请求成功了,此时将返回的数据缓存到SharedPreferences当中,并调用showWeatherInfo()方法来进行内容显示。
showWeatherInfo()方法中的逻辑就比较简单了,其实就是从weather对象中获取数据,然后显示到相应的控件上。注意在未来几天天气预报的部分我们使用for循环来处理每天的天气信息,在循环中动态加载forecast_item.xml布局并设置相应的数据,然后添加到父布局当中。设置完了所有数据之后,记得要将ScrollView重新变成可见的。
这样我们就将首次进入WeatherActivity时的逻辑全部梳理完了,那么当下一次再进入WeatherActivity时,由于缓存已经存在了,因此会直接解析并显示天气数据,而不会再次发起网络请求了。
处理完了WeatherActivity中的逻辑,接下来我们要做的,就是如何从省市县列表界面跳转到天气界面了,修改ChooseAreaFragment中的代码,如下所示:
1 public class ChooseAreaFragment extends Fragment { 2 ... 3 @Override 4 public void onActivityCreated(Bundle savedInstanceState) { 5 super.onActivityCreated(savedInstanceState); 6 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 7 @Override 8 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 9 if (currentLevel == LEVEL_PROVINCE) { 10 selectedProvince = provinceList.get(position); 11 queryCities(); 12 } else if (currentLevel == LEVEL_CITY) { 13 selectedCity = cityList.get(position); 14 queryCounties(); 15 } else if (currentLevel == LEVEL_COUNTY) { 16 String weatherId = countyList.get(position).getWeatherId(); 17 if (getActivity() instanceof MainActivity) { 18 Intent intent = new Intent(getActivity(), WeatherActivity.class); 19 intent.putExtra("weather_id", weatherId); 20 startActivity(intent); 21 getActivity().finish(); 22 } 23 } 24 }); 25 ... 26 } 27 ... 28 }
这里在onItemClick()方法中加入了一个if判断,如果当前级别时LEVEL_COUNTY,就启动WeatherActivity,并把当前选中县的天气id传递过去。
另外,我们还需要在MainActivity中加入一个缓存数据的判断才行。修改MainActivity中的代码,如下所示:
1 public class MainActivity extends AppCompatActivity{ 2 @Override 3 protected void onCreate(Bundle savedInstanceState){ 4 super.onCreate(savedInstanceState); 5 setContentView(R.layout.activity_main); 6 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 7 if(prefs.getString("weather",null) != null){ 8 Intent intent = new Intent(this,WeatherActivity.class); 9 startActivity(intent); 10 finish(); 11 } 12 } 13 }
可以看到,这里在onCreate()方法的一开始先从SharedPreferences文件中读取缓存数据,如果不为null就说明之前已经请求过天气数据了,那就直接跳转到WeatherActivity即可。
(四)获取必应每日一图
现在我们已经把天气界面编写得不错了,不过和市场上的天气如见的界面相比还是又一定差距。出色的天气软件不会使用一个固定的背景色。因此我们添加更换背景的功能。这里我们使用一个巧妙的方法。
必应时一个由微软开发的搜索引擎网站,它每天都会在首页展示一张精美的背景图片。如果我们使用它们来作为天气界面的背景图,不仅使界面更美观,而且解决一成不变的问题。
为此,作者准备了一个获取必应每日一图的接口:http://guolin.tech/api/bing_pic。
访问这个接口,服务器会返回今日的必应背景图连接:
http://cn.bing.com/az/hprichbg/rb/ChicagoHarborLH_ZH-CN9974330969_1920x1080.jpg.
然后我们再使用Glide去加载这张图片就可以了。
首先修改activity_weather.xml中的代码,如下所示:
1 <FrameLayout 2 xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:background="@color/colorPrimary"> 6 <ImageView 7 android:id="@+id/bing_pic_img" 8 android:layout_width="match_parent" 9 android:layout_height="match_parent" 10 android:scaleType="centerCrop"/> 11 <ScrollView 12 android:id="@+id/weather_layout" 13 android:layout_width="match_parent" 14 android:layout_height="match_parent" 15 android:scrollbars="none" 16 android:overScrollMode="never"> 17 18 .... 19 20 </ScrollView> 21 </FrameLayout>
这里我们在FrameLayout中添加了一个ImageView,并且将它的宽和高都设置成match_parent。由于FrameLayout默认情况下会将控件都放置在左上角,因此ScrollView会完全覆盖住ImageView,从而ImageView也就成为背景图片了。
接着修改WeatherActivity中的代码,如下所示:
public class WeatherActivity extends AppCompatActivity { private ImageView bingPicImg; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_weather); // 初始化各控件 bingPicImg = (ImageView) ... String bingPic = prefs.getString("bing_pic", null); if (bingPic != null) { Glide.with(this).load(bingPic).into(bingPicImg); } else { loadBingPic(); } } /** * 根据天气id请求城市天气信息。 */ public void requestWeather(final String weatherId) { ... 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 { final String bingPic = response.body().string(); SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(WeatherActivity.this).edit(); editor.putString("bing_pic", bingPic); editor.apply(); runOnUiThread(new Runnable() { @Override public void run() { Glide.with(WeatherActivity.this).load(bingPic).into(bingPicImg); } }); } @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } }); } ... }
可以看到,首先在onCreate()方法中获取了新增控件ImageView的实例,然后尝试从SharedPreferences中读取缓存的背景图片。如果有缓存的话就直接使用Glide来加载这张图片,如果没有的话就调用loadBingPic()方法去请求今日的必应背景图。
loadBingPic()方法中的逻辑就非常简单了,先是调用了HttpUtil.sendOkHttpRequest()方法获取到必应背景图的连接,然后将这个链接缓存到SharedPreferences当中,再将当前线程切换到主线程,最后使用Glide来加载这张图片就可以了。另外需要注意,在requestWeather()方法的最后也需要调用一下loadBingPic()方法,这样在每次请求天气信息的时候同时也会刷新背景图片。这样每天都会是不同的图片。
不过这样背景图和状态栏没有融合到一起,这里我们使用一种简单的实现方式。修改WeatherActivity中的代码,如下所示:
1 public class WeatherActivity extends AppCompatActivity { 2 3 @Override 4 protected void onCreate(Bundle savedInstanceState) { 5 super.onCreate(savedInstanceState); 6 if (Build.VERSION.SDK_INT >= 21) { 7 View decorView = getWindow().getDecorView(); 8 decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 9 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 10 getWindow().setStatusBarColor(Color.TRANSPARENT); 11 } 12 setContentView(R.layout.activity_weather); 13 ... 14 } 15 ... 16 }
由于这个功能是Android5.0及以上的系统才支持的,因此我们先在代码中做了一个系统版本号的判断,只有当版本号大于或等于21,也就是5.0及以上系统时才会执行后面的代码。
接着我们调用getWindow().getDecorView()方法拿到当前活动的DecorView,在调用它的setSystemUiVisibility()方法来改变系统UI的显示,这里传入View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN和View.SYSTEM_UI_FLAG_LAYOUT_STABLE就表示活动的布局会显示在状态栏上面,最后调用一下setStatusBarColor()方法将状态栏设置成透明色即可。但仅仅这些代码,天气界面的头布局几乎和系统状态栏紧贴到一起,这是由于系统状态栏已经成为我们布局的一部分,因此没有单独为它留出空间。当然这个问题也是非常好解决的。借助android:fitsSystemWindows属性就可以了。修改activity_weather.xml中的代码,如下所示:
1 <FrameLayout 2 xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_height="match_parent" 4 android:layout_width="match_parent" 5 android:background="@color/colorPrimary"> 6 <ScrollView 7 android:id="@+id/weather_layout" 8 android:layout_height="match_parent" 9 android:layout_width="match_parent" 10 android:overScrollMode="never" 11 android:scrollbars="none"> 12 <LinearLayout 13 android:layout_height="wrap_content" 14 android:layout_width="match_parent" 15 android:fitsSystemWindows="true" 16 android:orientation="vertical" 17 android:fitsSystemWindows="true"> 18 ... 19 </LinearLayout> 20 </ScrollView> 21 </FrameLayout>
这里在ScrollView的LinearLayout中增加了android:fitsSystemWindows属性,设置成true就表示会为系统状态栏留出空间。
下一章节开发手动更新天气和切换城市的功能。
具体实现步骤连接:
android开发学习之路——天气预报之技术分析与数据库(一)
android开发学习之路——天气预报之遍历省市县数据(二)
android开发学习之路——天气预报之手动更新天气和切换城市(四)
android开发学习之路——天气预报之后台自动更新天气(五)