由於和風天氣返回的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開發學習之路——天氣預報之后台自動更新天氣(五)