我們已經知道省市縣的數據都是從服務器端獲取到的,因此與服務器的交互是必不可少的,我們再util包下增加一個HttpUtil類,代碼如下所示:
1 public class HttpUtil{ 2 public static void sendOkHttpRequest(String address,okttp3.Callback callback) 3 { 4 OkHttpClient client = new OkHttpClient(); 5 Request request= new Request.Builder().url(address).build(); 6 Client.newcall(request).enqueue(callback); 7 } 8 }
由於Okhttp的出色封裝,這里和服務器進行交互的代碼非常簡單,僅僅3行就完成了。現在我們發起一條HTTP請求只需要調用sendOkHttpRequest()方法,傳入請求地址,並注冊一個回調來處理服務器響應就可以了。
由於服務器返回的省市縣數據都是JSON格式的,所以我們再提供一個工具類來解析和處理這種數據。在util包下新建一個Utility類,代碼如下:
1 public class Utility { 2 3 /** 4 * 解析和處理服務器返回的省級數據 5 */ 6 public static boolean handleProvinceResponse(String response) { 7 if (!TextUtils.isEmpty(response)) { 8 try { 9 JSONArray allProvinces = new JSONArray(response); 10 for (int i = 0; i < allProvinces.length(); i++) { 11 JSONObject provinceObject = allProvinces.getJSONObject(i); 12 Province province = new Province(); 13 province.setProvinceName(provinceObject.getString("name")); 14 province.setProvinceCode(provinceObject.getInt("id")); 15 province.save(); 16 } 17 return true; 18 } catch (JSONException e) { 19 e.printStackTrace(); 20 } 21 } 22 return false; 23 } 24 25 /** 26 * 解析和處理服務器返回的市級數據 27 */ 28 public static boolean handleCityResponse(String response, int provinceId) { 29 if (!TextUtils.isEmpty(response)) { 30 try { 31 JSONArray allCities = new JSONArray(response); 32 for (int i = 0; i < allCities.length(); i++) { 33 JSONObject cityObject = allCities.getJSONObject(i); 34 City city = new City(); 35 city.setCityName(cityObject.getString("name")); 36 city.setCityCode(cityObject.getInt("id")); 37 city.setProvinceId(provinceId); 38 city.save(); 39 } 40 return true; 41 } catch (JSONException e) { 42 e.printStackTrace(); 43 } 44 } 45 return false; 46 } 47 48 /** 49 * 解析和處理服務器返回的縣級數據 50 */ 51 public static boolean handleCountyResponse(String response, int cityId) { 52 if (!TextUtils.isEmpty(response)) { 53 try { 54 JSONArray allCounties = new JSONArray(response); 55 for (int i = 0; i < allCounties.length(); i++) { 56 JSONObject countyObject = allCounties.getJSONObject(i); 57 County county = new County(); 58 county.setCountyName(countyObject.getString("name")); 59 county.setWeatherId(countyObject.getString("weather_id")); 60 county.setCityId(cityId); 61 county.save(); 62 } 63 return true; 64 } catch (JSONException e) { 65 e.printStackTrace(); 66 } 67 } 68 return false; 69 } 70 71 }
可以看到,我們提供了handleProvincesResponse()、handleCitiesResponse()、handleCountiesResponse()這3個方法,分別用於解析和處理服務器返回的省級、市級和縣級數據。處理的方法是類似的,先使用JSONArray和JSONObject將數據解析出來,然后組裝成實體類對象,再調用save()方法將數據存儲到數據庫當中。
工具類准備好了,現在開始寫界面。由於遍歷全國省市縣的功能我們后面還會復用,因此寫在碎片里,這樣復用的時候,直接在布局里引用碎片就可以了。
在res/layout目錄中新建choose_area.xml布局,代碼如下所示:
1 <LinearLayout 2 xmlns:android="http://schemas.android.com/apk/res/android" 3 android:orientation="vertical" 4 android:layout_height="match_parent" 5 android:layout_width="match_parent" 6 android:background="#fff " > 7 <RelativeLayout 8 android:layout_height="?attr/actionBarSize" 9 android:layout_width="match_parent 10 android:background="?attr/colorPrimary""> 11 <TextView 12 android:id="@+id/title_text" 13 android:layout_height="wrap_content" 14 android:layout_width="wrap_content" 15 android:textSize="20sp" 16 android:textColor="#fff" 17 android:layout_centerInParent="true" /> 18 <Button 19 android:id="@+id/back_button" 20 android:layout_height="25dp" 21 android:layout_width="25dp" 22 android:layout_centerVertical="true" 23 android:layout_alignParentLeft="true" 24 android:layout_marginLeft="10dp" 25 android:background="@drawable/ic_back"/> 26 </RelativeLayout> 27 <ListView 28 android:id="@+id/list_view" 29 android:layout_height="match_parent" 30 android:layout_width="match_parent"/> 31 </LinearLayout>
布局文件中的內容並不復雜,我們先定義了一個頭布局作為標題欄,將布局高度設置為actionBar的高度,背景色設置為colorPrimary。然后在頭布局中放置一個TextView用於顯示標題內容,放置了一個Button用於執行返回操作,注意我已經提前准備好了一張ic_back.png圖片作為按鈕的背景圖。這里之所以要自己定義標題欄,是因為碎片中最好不要直接使用ActionBar或Toolbar,不然在復用的時候可能出現一些你不想看到的效果。
接下來在頭布局的下面定義了一個ListView,省市縣的數據就將顯示在這里。之所以這次使用ListView,是因為它會自動給每個子項之間添加一條分割線,而如果使用RecyclerView想實現同樣的功能則會比較麻煩,這里我們總是選擇最優的實現方案。
接下來我們需要編寫用於遍歷省市縣數據的碎片了。新建ChooseAreaFragment繼承自Fragment,代碼如下:
1 public class ChooseAreaFragment extends Fragment { 2 3 private static final String TAG = "ChooseAreaFragment"; 4 5 public static final int LEVEL_PROVINCE = 0; 6 7 public static final int LEVEL_CITY = 1; 8 9 public static final int LEVEL_COUNTY = 2; 10 11 private ProgressDialog progressDialog; 12 13 private TextView titleText; 14 15 private Button backButton; 16 17 private ListView listView; 18 19 private ArrayAdapter<String> adapter; 20 21 private List<String> dataList = new ArrayList<>(); 22 23 /** 24 * 省列表 25 */ 26 private List<Province> provinceList; 27 28 /** 29 * 市列表 30 */ 31 private List<City> cityList; 32 33 /** 34 * 縣列表 35 */ 36 private List<County> countyList; 37 38 /** 39 * 選中的省份 40 */ 41 private Province selectedProvince; 42 43 /** 44 * 選中的城市 45 */ 46 private City selectedCity; 47 48 /** 49 * 當前選中的級別 50 */ 51 private int currentLevel; 52 53 54 @Override 55 public View onCreateView(LayoutInflater inflater, ViewGroup container, 56 Bundle savedInstanceState) { 57 View view = inflater.inflate(R.layout.choose_area, container, false); 58 titleText = (TextView) view.findViewById(R.id.title_text); 59 backButton = (Button) view.findViewById(R.id.back_button); 60 listView = (ListView) view.findViewById(R.id.list_view); 61 adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, dataList); 62 listView.setAdapter(adapter); 63 return view; 64 } 65 66 @Override 67 public void onActivityCreated(Bundle savedInstanceState) { 68 super.onActivityCreated(savedInstanceState); 69 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 70 @Override 71 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 72 if (currentLevel == LEVEL_PROVINCE) { 73 selectedProvince = provinceList.get(position); 74 queryCities(); 75 } else if (currentLevel == LEVEL_CITY) { 76 selectedCity = cityList.get(position); 77 queryCounties(); 78 } else if (currentLevel == LEVEL_COUNTY) { 79 String weatherId = countyList.get(position).getWeatherId(); 80 if (getActivity() instanceof MainActivity) { 81 Intent intent = new Intent(getActivity(), WeatherActivity.class); 82 intent.putExtra("weather_id", weatherId); 83 startActivity(intent); 84 getActivity().finish(); 85 } else if (getActivity() instanceof WeatherActivity) { 86 WeatherActivity activity = (WeatherActivity) getActivity(); 87 activity.drawerLayout.closeDrawers(); 88 activity.swipeRefresh.setRefreshing(true); 89 activity.requestWeather(weatherId); 90 } 91 } 92 } 93 }); 94 backButton.setOnClickListener(new View.OnClickListener() { 95 @Override 96 public void onClick(View v) { 97 if (currentLevel == LEVEL_COUNTY) { 98 queryCities(); 99 } else if (currentLevel == LEVEL_CITY) { 100 queryProvinces(); 101 } 102 } 103 }); 104 queryProvinces(); 105 } 106 107 /** 108 * 查詢全國所有的省,優先從數據庫查詢,如果沒有查詢到再去服務器上查詢。 109 */ 110 private void queryProvinces() { 111 titleText.setText("中國"); 112 backButton.setVisibility(View.GONE); 113 provinceList = DataSupport.findAll(Province.class); 114 if (provinceList.size() > 0) { 115 dataList.clear(); 116 for (Province province : provinceList) { 117 dataList.add(province.getProvinceName()); 118 } 119 adapter.notifyDataSetChanged(); 120 listView.setSelection(0); 121 currentLevel = LEVEL_PROVINCE; 122 } else { 123 String address = "http://guolin.tech/api/china"; 124 queryFromServer(address, "province"); 125 } 126 } 127 128 /** 129 * 查詢選中省內所有的市,優先從數據庫查詢,如果沒有查詢到再去服務器上查詢。 130 */ 131 private void queryCities() { 132 titleText.setText(selectedProvince.getProvinceName()); 133 backButton.setVisibility(View.VISIBLE); 134 cityList = DataSupport.where("provinceid = ?", String.valueOf(selectedProvince.getId())).find(City.class); 135 if (cityList.size() > 0) { 136 dataList.clear(); 137 for (City city : cityList) { 138 dataList.add(city.getCityName()); 139 } 140 adapter.notifyDataSetChanged(); 141 listView.setSelection(0); 142 currentLevel = LEVEL_CITY; 143 } else { 144 int provinceCode = selectedProvince.getProvinceCode(); 145 String address = "http://guolin.tech/api/china/" + provinceCode; 146 queryFromServer(address, "city"); 147 } 148 } 149 150 /** 151 * 查詢選中市內所有的縣,優先從數據庫查詢,如果沒有查詢到再去服務器上查詢。 152 */ 153 private void queryCounties() { 154 titleText.setText(selectedCity.getCityName()); 155 backButton.setVisibility(View.VISIBLE); 156 countyList = DataSupport.where("cityid = ?", String.valueOf(selectedCity.getId())).find(County.class); 157 if (countyList.size() > 0) { 158 dataList.clear(); 159 for (County county : countyList) { 160 dataList.add(county.getCountyName()); 161 } 162 adapter.notifyDataSetChanged(); 163 listView.setSelection(0); 164 currentLevel = LEVEL_COUNTY; 165 } else { 166 int provinceCode = selectedProvince.getProvinceCode(); 167 int cityCode = selectedCity.getCityCode(); 168 String address = "http://guolin.tech/api/china/" + provinceCode + "/" + cityCode; 169 queryFromServer(address, "county"); 170 } 171 } 172 173 /** 174 * 根據傳入的地址和類型從服務器上查詢省市縣數據。 175 */ 176 private void queryFromServer(String address, final String type) { 177 showProgressDialog(); 178 HttpUtil.sendOkHttpRequest(address, new Callback() { 179 @Override 180 public void onResponse(Call call, Response response) throws IOException { 181 String responseText = response.body().string(); 182 boolean result = false; 183 if ("province".equals(type)) { 184 result = Utility.handleProvinceResponse(responseText); 185 } else if ("city".equals(type)) { 186 result = Utility.handleCityResponse(responseText, selectedProvince.getId()); 187 } else if ("county".equals(type)) { 188 result = Utility.handleCountyResponse(responseText, selectedCity.getId()); 189 } 190 if (result) { 191 getActivity().runOnUiThread(new Runnable() { 192 @Override 193 public void run() { 194 closeProgressDialog(); 195 if ("province".equals(type)) { 196 queryProvinces(); 197 } else if ("city".equals(type)) { 198 queryCities(); 199 } else if ("county".equals(type)) { 200 queryCounties(); 201 } 202 } 203 }); 204 } 205 } 206 207 @Override 208 public void onFailure(Call call, IOException e) { 209 // 通過runOnUiThread()方法回到主線程處理邏輯 210 getActivity().runOnUiThread(new Runnable() { 211 @Override 212 public void run() { 213 closeProgressDialog(); 214 Toast.makeText(getContext(), "加載失敗", Toast.LENGTH_SHORT).show(); 215 } 216 }); 217 } 218 }); 219 } 220 221 /** 222 * 顯示進度對話框 223 */ 224 private void showProgressDialog() { 225 if (progressDialog == null) { 226 progressDialog = new ProgressDialog(getActivity()); 227 progressDialog.setMessage("正在加載..."); 228 progressDialog.setCanceledOnTouchOutside(false); 229 } 230 progressDialog.show(); 231 } 232 233 /** 234 * 關閉進度對話框 235 */ 236 private void closeProgressDialog() { 237 if (progressDialog != null) { 238 progressDialog.dismiss(); 239 } 240 } 241 242 }
這個類里的代碼非常多,但邏輯並不復雜。在onCreateView()方法中先是獲取到了一些控件的實例,然后去初始化ArrayAdapter,並將它設置為ListView的適配器。接着在onActivityCreated()方法中給ListView和Button設置了點擊事件,到這初始工作算是完成。
在onAcivityCreated()方法的最后,調用了queryProvinces()方法,也就是從這里開始加載省級數據的。queryProvinces()方法中首先會將頭布局的標題設置成中國,將返回按鈕隱藏起來,因為省級列表已經不能再返回了。然后調用LitePal的查詢接口來從數據庫中讀取省級數據,如果讀取到了就直接將數據顯示到界面上,如果沒有,就組裝出一個請求地址,然后調用queryFromServer()方法來從服務器上查詢數據。
queryFromServer()方法會調用HttpUtil的sendOkHttpRequest()方法來向服務器發送請求,響應的數據會回調到onResponse()方法中,然后我們在這里去調用Utility的handleProvincesResponse()方法來解析和處理服務器返回的數據,並存儲到數據庫中。在解析和處理完數據之后,我們再次調用了queryProvinces()方法來重新加載省級數據,由於queryProvinces()方法牽扯到了UI操作,因此必須要在主線程中調用,,這里借助了runOnUiThread()方法來實現從子線程切換到主線程。現在數據庫中已經存在了數據,因此調用queryProvinces()就會直接將數據顯示到界面上了。
當你點擊了某個省的時候會進入到ListView的onItemClick()方法中,這個時候會根據當前的級別來判斷是去調用queryCities()方法還是queryCounties()方法,queryCities()方法是去查詢市級數據,而queryCounties()方法是去查詢縣級數據。
另外,在返回按鈕的點擊事件里,會對當前ListView的列表級別進行判斷。如果當前是縣級列表,那么就返回到市級列表,如果當前是市級列表,那么就返回到省級列表。當返回到省級列表時,返回按鈕會自動隱藏。
這樣我們就把遍歷省市縣的功能完成了,但碎片不能直接顯示在界面上的,因為我們需要將它添加到活動當中。修改activity_main.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 <fragment 6 android:id="@+id/choose_area_fragment" 7 android:name="com.coolweather.android.ChooseAreaFragment" 8 android:layout_height="match_parent" 9 android:layout_width="match_parent" /> 10 </FrameLayout>
定義了一個FrameLayout,然后將ChooseAreaFragment添加進來,並讓它充滿整個布局。
另外,我們剛才在碎片的布局里面已經自定義了一個標題欄,因此就不再需要原生的AcitonBar了,修改res/values/styles.xml中的代碼,如下所示:
1 <resources> 2 <!-- Base application theme. --> 3 <style parent="Theme.AppCompat.Light.NoActionBar"> 4 .... 5 </style> 6 </resources>
接下來聲明程序所需要的權限。修改AndroidManifest.xml中的代碼,如下所示:
1 <manifest 2 package="com.coolweather.android" 3 xmlns:android="http://schemas.android.com/apk/res/android"> 4 <uses-permission android:name="android.permission.INTERNET"/> 5 .... 6 </manifest>
由於我們是通過網絡接口來獲取全國省市縣數據的,因此必須要添加訪問網絡的權限才行。
下一章節設計並編寫顯示天氣信息的布局和功能。
具體實現步驟連接:
android開發學習之路——天氣預報之技術分析與數據庫(一)
android開發學習之路——天氣預報之遍歷省市縣數據(二)