一、項(xiàng)目框架的搭建
1、在git上新建一個(gè)coolweather的倉庫
2、新建一個(gè)Coolweather項(xiàng)目,添加到git版本管理
3、在java中新建幾個(gè)包,db:用于存放數(shù)據(jù)庫模型相關(guān)的代碼,gson:用于存放GSon相關(guān)的代碼,service:用于存放服務(wù)相關(guān)的代碼,util包用于存放工具相關(guān)代碼
4、添加項(xiàng)目依賴的第三方,在app/build.gradle
依賴:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
implementation 'org.litepal.android:core:2.0.0'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.github.bumptech.glide:glide:4.7.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.7.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}
倉庫添加
allprojects {
repositories {
mavenCentral()
google()
jcenter()
}
}
說明:
implementation 'org.litepal.android:core:2.0.0'是數(shù)據(jù)庫操作的框架
implementation 'com.squareup.okhttp3:okhttp:3.10.0'網(wǎng)絡(luò)請求框架
implementation 'com.google.code.gson:gson:2.8.5' JSON解析框架
implementation 'com.github.bumptech.glide:glide:4.7.1' annotationProcessor 'com.github.bumptech.glide:compiler:4.7.1'為加載圖片的框架
二、數(shù)據(jù)庫表的設(shè)計(jì)
1、數(shù)據(jù)獲取
省份獲取接口:
http://guolin.tech/api/china
獲取的數(shù)據(jù)如下:
[{"id":1,"name":"北京"},{"id":2,"name":"上海"}]
省下面對應(yīng)的市獲取接口
http://guolin.tech/api/china/22,其中22為省份的id,數(shù)據(jù)結(jié)構(gòu)如下:
[{"id":191,"name":"長沙"},{"id":192,"name":"湘潭"},{"id":193,"name":"株洲"},{"id":194,"name":"衡陽"},{"id":195,"name":"郴州"},{"id":196,"name":"常德"},{"id":197,"name":"益陽"},{"id":198,"name":"婁底"},{"id":199,"name":"邵陽"},{"id":200,"name":"岳陽"},{"id":201,"name":"張家界"},{"id":202,"name":"懷化"},{"id":203,"name":"永州"},{"id":204,"name":"吉首"}]
獲取區(qū)對應(yīng)的信息
http://guolin.tech/api/china/22/191,其中191為市的id
[{"id":1422,"name":"長沙","weather_id":"CN101250101"},{"id":1423,"name":"寧鄉(xiāng)","weather_id":"CN101250102"},{"id":1424,"name":"瀏陽","weather_id":"CN101250103"},{"id":1425,"name":"馬坡嶺","weather_id":"CN101250104"},{"id":1426,"name":"望城","weather_id":"CN101250105"}]
最后通過weather_id 獲取天氣信息,使用和風(fēng)天氣的api
https://console.heweather.com/register?role=personal,注冊免費(fèi)
天氣信息的獲取
https://free-api.heweather.com/s6/weather/now?location=CN101250101&key=22c4dc1c0a3245c7a97e7c2543b9781a
數(shù)據(jù)如下:
{"HeWeather6":[{"basic":{"cid":"CN101250101","location":"長沙","parent_city":"長沙","admin_area":"湖南","cnty":"中國","lat":"28.19408989","lon":"112.98227692","tz":"+8.00"},"update":{"loc":"2018-07-06 16:49","utc":"2018-07-06 08:49"},"status":"ok","now":{"cloud":"75","cond_code":"104","cond_txt":"陰","fl":"31","hum":"85","pcpn":"0.0","pres":"1000","tmp":"29","vis":"13","wind_deg":"293","wind_dir":"西北風(fēng)","wind_sc":"3","wind_spd":"19"}}]}
2、數(shù)據(jù)庫表實(shí)現(xiàn)
新建Province類 繼承LitePalSupport類
public class Province extends LitePalSupport {
private int id;
private String provinceName;
private int provinceCode;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getProvinceName() {
return provinceName;
}
public void setProvinceName(String provinceName) {
this.provinceName = provinceName;
}
public int getProvinceCode() {
return provinceCode;
}
public void setProvinceCode(int provinceCode) {
this.provinceCode = provinceCode;
}
}
City類
public class City extends LitePalSupport {
private int id;
private String cityName;
private int cityCode;
private int provinceId;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getCityName() {
return cityName;
}
public void setCityName(String cityName) {
this.cityName = cityName;
}
public int getCityCode() {
return cityCode;
}
public void setCityCode(int cityCode) {
this.cityCode = cityCode;
}
public int getProvinceId() {
return provinceId;
}
public void setProvinceId(int provinceId) {
this.provinceId = provinceId;
}
}
Country類
public class Country extends LitePalSupport {
private int id;
private String countryName;
private String weatherId;
private int cityId;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getCountryName() {
return countryName;
}
public void setCountryName(String countryName) {
this.countryName = countryName;
}
public String getWeatherId() {
return weatherId;
}
public void setWeatherId(String weatherId) {
this.weatherId = weatherId;
}
public int getCityId() {
return cityId;
}
public void setCityId(int cityId) {
this.cityId = cityId;
}
}
在app/src/main 目錄在新建assets 文件,在改目錄下新建一個(gè)litepal.xml文件,編輯內(nèi)容如下:
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<!--
Define the database name of your application.
By default each database name should be end with .db.
If you didn't name your database end with .db,
LitePal would plus the suffix automatically for you.
For example:
<dbname value="demo" />
-->
<dbname value="cool_weather" />
<!--
Define the version of your database. Each time you want
to upgrade your database, the version tag would helps.
Modify the models you defined in the mapping tag, and just
make the version value plus one, the upgrade of database
will be processed automatically without concern.
For example:
<version value="1" />
-->
<version value="1" />
<list>
<mapping class="com.example.yangjie.coolweather.db.Province" />
<mapping class="com.example.yangjie.coolweather.db.City" />
<mapping class="com.example.yangjie.coolweather.db.Country" />
</list>
<!--
Define where the .db file should be. "internal" means the .db file
will be stored in the database folder of internal storage which no
one can access. "external" means the .db file will be stored in the
path to the directory on the primary external storage device where
the application can place persistent files it owns which everyone
can access. "internal" will act as default.
For example:
<storage value="external" />
-->
</litepal>
最后需要在配置一下LitePalApplication,修改如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.yangjie.coolweather">
<application
android:name="org.litepal.LitePalApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
3、自定義Fragment
首先定義一個(gè)線性布局如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#fff"
>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
>
<TextView
android:id="@+id/title_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="#fff"
android:textSize="20sp"
android:text="標(biāo)題"
/>
<Button
android:id="@+id/btn_back"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:background="@drawable/btn_back"
/>
</RelativeLayout>
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
這里自定義了導(dǎo)航條,所以需要修改style.xml文件,修改如下:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
新建一個(gè)ChooseAreaFragment繼承Fragment,代碼如下:
public class ChooseAreaFragment extends Fragment {
public static final int LEVEL_PROVINCE = 0;
public static final int LEVEL_CITY = 1;
public static final int LEVEL_COUNTRY = 2;
private static final String TYPE_PROVINCE = "province";
private static final String TYPE_City = "city";
private static final String TYPE_Country = "country";
private ProgressDialog progressDialog;
private TextView titleText;
private Button backButton;
private ListView listView;
private ArrayAdapter<String> adapter;
private List<String> dataList = new ArrayList<>();
private List<Province> provinceList;
private List<City> cityList;
private List<Country> countryList;
private Province selectedProvince;
private City selectedCity;
private int currentLevel;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.choose_area,container,false);
titleText = (TextView)view.findViewById(R.id.title_text);
backButton = (Button)view.findViewById(R.id.btn_back);
listView = (ListView)view.findViewById(R.id.listView);
adapter = new ArrayAdapter<>(getContext(),android.R.layout.simple_list_item_1,dataList);
listView.setAdapter(adapter);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
if (currentLevel == LEVEL_PROVINCE){
selectedProvince = provinceList.get(i);
queryCities();
}else if (currentLevel == LEVEL_CITY){
selectedCity = cityList.get(i);
queryCounties();
}
}
});
backButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (currentLevel == LEVEL_COUNTRY){
queryCities();
}else if (currentLevel == LEVEL_CITY){
queryProinces();
}
}
});
queryProinces();
}
private void queryCounties() {
titleText.setText(selectedCity.getCityName());
backButton.setVisibility(View.VISIBLE);
countryList = LitePal.where("cityid = ?",String.valueOf(selectedCity.getId())).find(Country.class);
if (countryList.size()>0){
dataList.clear();
for (Country country:countryList){
dataList.add(country.getCountryName());
}
adapter.notifyDataSetChanged();;
listView.setSelection(0);
currentLevel = LEVEL_COUNTRY;
}else {
int provinceCode = selectedProvince.getProvinceCode();
int cityCode = selectedCity.getCityCode();
String address = "http://guolin.tech/api/china/" + provinceCode + "/" + cityCode;
queryFromServer(address,TYPE_Country);
}
}
private void queryCities() {
titleText.setText(selectedProvince.getProvinceName());
backButton.setVisibility(View.VISIBLE);
cityList = LitePal.where("provinceid = ?",String.valueOf(selectedProvince.getId())).find(City.class);
if (cityList.size()>0){
dataList.clear();
for (City city:cityList){
dataList.add(city.getCityName());
}
adapter.notifyDataSetChanged();;
listView.setSelection(0);
currentLevel = LEVEL_CITY;
}else {
int provinceCode = selectedProvince.getProvinceCode();
String address = "http://guolin.tech/api/china/" + provinceCode;
queryFromServer(address,TYPE_City);
}
}
private void queryProinces() {
titleText.setText("中國");
backButton.setVisibility(View.GONE);
provinceList = LitePal.findAll(Province.class);
if (provinceList.size()>0){
dataList.clear();
for (Province province:provinceList){
dataList.add(province.getProvinceName());
}
adapter.notifyDataSetChanged();
listView.setSelection(0);
currentLevel = LEVEL_PROVINCE;
}else {
String address = "http://guolin.tech/api/china";
queryFromServer(address,TYPE_PROVINCE);
}
}
private void queryFromServer(String address,final String type){
showProgressDialog();
HttpUtil.sendHttpRequest(address, new Callback() {
@Override
public void onFailure(Call call, IOException e) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
closeProgressDialog();
Toast.makeText(getContext(),"加載失敗",Toast.LENGTH_SHORT).show();;
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//在這里解析數(shù)據(jù)
String responseText = response.body().string();
Log.d(TAG, "onResponse: " + responseText);
boolean result = false;
if (type.equals(TYPE_PROVINCE)){
result = Utility.handleProvinceResponse(responseText);
}else if(type.equals(TYPE_City)){
result = Utility.handleCityResponse(responseText,selectedProvince.getId());
}else if(type.equals(TYPE_Country)){
result = Utility.handleCountryResponse(responseText,selectedCity.getId());
}
if (result){
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
closeProgressDialog();
if (type.equals(TYPE_PROVINCE)){
queryProinces();
}else if(type.equals(TYPE_City)){
queryCities();
}else if(type.equals(TYPE_Country)){
queryCounties();
}
}
});
}
}
});
}
private void showProgressDialog(){
if (progressDialog == null){
progressDialog = new ProgressDialog(getActivity());
progressDialog.setMessage("正在加載");
progressDialog.setCanceledOnTouchOutside(false);
}
progressDialog.show();
}
private void closeProgressDialog(){
if (progressDialog!=null){
progressDialog.dismiss();
}
}
}
在activity_main.xml文件中添加Fragment布局如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/choose_area_fragment"
android:name="com.example.yangjie.coolweather.ChooseAreaFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>
三、展示天氣數(shù)據(jù)
效果如下:

將上圖的界面拆分成三個(gè)布局:1、導(dǎo)航部分(展示城市和更新時(shí)間)
2、現(xiàn)在天氣信息 3、未來三天信息展示
1、導(dǎo)航部分,新建一個(gè)title.xml,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/relativeLayout">
<Button
android:id="@+id/nav_button"
android:layout_width="32dp"
android:layout_height="30dp"
android:background="@drawable/home"
android:layout_marginBottom="8dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title_city"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginLeft="180dp"
android:layout_marginStart="180dp"
android:layout_marginTop="8dp"
android:text="北京"
android:textColor="#fff"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title_update_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:textColor="#fff"
android:textSize="16sp"
android:text="2018-7-10"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</android.support.constraint.ConstraintLayout>
2、天氣信息部分
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_margin="15dp"
>
<TextView
android:id="@+id/tmp_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textColor="#fff"
android:textSize="60sp"
/>
<TextView
android:id="@+id/weather_info_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textSize="20sp"
android:textColor="#fff"
/>
</LinearLayout>
3、未來天氣布局forcast.xml代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_margin="15dp"
android:background="#8000"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="15dp"
android:textColor="#fff"
android:text="預(yù)報(bào)"
android:textSize="20sp"
/>
<LinearLayout
android:id="@+id/forecast_layout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
</LinearLayout>
</LinearLayout>
forecast_item.xml文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="15dp"
>
<TextView
android:id="@+id/info_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:textColor="#fff"
/>
<TextView
android:id="@+id/tem_min"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:textColor="#fff"
/>
<TextView
android:id="@+id/tmp_max"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:textColor="#fff"
/>
<TextView
android:id="@+id/date_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="2"
android:textColor="#fff"
/>
</LinearLayout>