android开发学习之路——天气预报之遍历省市县数据(二)
我们已经知道省市县的数据都是从服务器端获取到的,因此与服务器的交互是必不可少的,我们再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开发学习之路——天气预报之遍历省市县数据(二)