《第二行代碼》—— 酷歐天氣的開發

使用的教材是國@郭霖寫的《第二行代碼》

應用名:Cool Weather

一、功能需求及技術可行性分析

1、具備的功能

  • 可以羅列出全國所有的省市縣
  • 可以查看全國任意城市的天氣信息
  • 可以自由地切換城市,去查看其他城市地天氣
  • 提供手動更新以及後臺自動更新天氣的功能

2、技術可行性分析

  1. 如何獲取全國省市縣的信息:

天氣數據:和風天氣的數據接口
省市縣數據:http://guolin.tech/api/china

  1. 編寫步驟
  • 基本配置
  • 創建數據庫和表
  • 遍歷全國省市縣數據
  • 顯示天氣數據
  • 編寫天氣界面
  • 將天氣顯示到界面上
  • 獲取必應天氣壁紙
  • 手動更新天氣
  • 手動切換城市
  • 後臺自動更新天氣
  • 擴展功能【有空再寫】
  • 修改圖標和名稱

二、正式編寫

新建一個空項目 Cool Weather

在這裏插入圖片描述

一、基本配置

配置Git,啓用版本控制
app/build.gradle 依賴庫更新到最新

打開AndroidManifest.xml

  • 設置<application>屬性tools:ignore="GoogleAppIndexingWarning"【應用內打開app的功能,此功能不需要】
  • <application>屬性android:allowBackup設置爲false【不允許應用內數據被取出】

二、創建數據庫和表

爲了讓項目有更好的結構,在com.coolweather.android下新建幾個包
在這裏插入圖片描述

1、添加依賴庫

    implementation 'org.litepal.android:java:3.0.0'     // 數據庫操作
    implementation "com.squareup.okhttp3:okhttp:4.3.1"  // 網絡請求
    implementation 'com.google.code.gson:gson:2.8.6'    // 解析JSON
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' // 加載圖片的庫
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'   // 下拉刷新

2、設計數據庫表的結構

db下建立三張表 provincecitycounty

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;
    }
}
public class City extends LitePalSupport {
    private int id;
    private String cityName;
    private int provinceId;
    private int cityCode;

    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 getProvinceId() {
        return provinceId;
    }

    public void setProvinceId(int provinceId) {
        this.provinceId = provinceId;
    }

    public int getCityCode() {
        return cityCode;
    }

    public void setCityCode(int cityCode) {
        this.cityCode = cityCode;
    }
}
public class County extends LitePalSupport {
    private int id;
    private String countyName;
    private int cityId;
    private String weatherId;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCountyName() {
        return countyName;
    }

    public void setCountyName(String countyName) {
        this.countyName = countyName;
    }

    public int getCityId() {
        return cityId;
    }

    public void setCityId(int cityId) {
        this.cityId = cityId;
    }

    public String getWeatherId() {
        return weatherId;
    }

    public void setWeatherId(String weatherId) {
        this.weatherId = weatherId;
    }
}

3、數據庫版本控制

app/src/main目錄下新建一個文件夾assets,在該目錄下建一個litepal.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="cool_weather" />

    <version value="1" />

    <list>
        <mapping class = "com.coolweather.android.db.Province" />
        <mapping class = "com.coolweather.android.db.City" />
        <mapping class = "com.coolweather.android.db.County" />
    </list>
</litepal>

引入LitePal,修改AndroidManifest:

    <application
        android:name="org.litepal.LitePalApplication"

三、遍歷全國省市縣數據

1、服務器交互

util包下增加一個HttpUtil類,用於服務器交互:

public class HttpUtil {
    public static void sendOkHttpRequest(String address, okhttp3.Callback callback){
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(address).build();
        client.newCall(request).enqueue(callback);
    }
}

2、解析JSON

增加一個Utility類,便於解析json數據:

public class Utility {
    // 解析省級數據
    public static boolean handleProvinceResponse(String response){
        if(!TextUtils.isEmpty(response)){
            try{
                JSONArray allProvinces = new JSONArray(response);
                for (int i = 0; i < allProvinces.length(); i++){
                    JSONObject provinceObject = allProvinces.getJSONObject(i);
                    Province province = new Province();
                    province.setProvinceName(provinceObject.getString("name"));
                    province.setProvinceCode(provinceObject.getInt("id"));
                    province.save();
                }
                return true;
            }catch (JSONException e){
                e.printStackTrace();
            }
        }
        return false;
    }
    
    // 解析市級數據
    public static boolean handleCityResponse(String response, int provinceId){
        if(!TextUtils.isEmpty(response)){
            try {
                JSONArray allCities = new JSONArray(response);
                for(int i = 0; i < allCities.length(); i++){
                    JSONObject cityObject = allCities.getJSONObject(i);
                    City city = new City();
                    city.setCityName(cityObject.getString("name"));
                    city.setCityCode(cityObject.getInt("id"));
                    city.setProvinceId(provinceId);
                    city.save();
                }
                return true;
            }catch (JSONException e){
                e.printStackTrace();
            }
        }
        return false;
    }
    
    // 解析縣級數據
    public static boolean handleCountyResponse(String response, int cityId){
        if(!TextUtils.isEmpty(response)){
            try {
                JSONArray allCounties = new JSONArray(response);
                for(int i = 0; i < allCounties.length(); i++){
                    JSONObject countyObject = allCounties.getJSONObject(i);
                    County county = new County();
                    county.setCityId(cityId);
                    county.setCountyName(countyObject.getString("name"));
                    county.setWeatherId(countyObject.getString("weather_id"));
                    county.save();
                }
                return true;
            }catch (JSONException e){
                e.printStackTrace();
            }
        }
        return false;
    }
}

3、選擇地區的佈局

choose_area.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#fff">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/title_text"
            android:layout_centerInParent="true"
            android:textColor="#fff"
            android:textSize="20sp"/>

        <Button
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:id="@+id/back_button"
            android:layout_marginLeft="10dp"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:background="@drawable/ic_back"
            android:layout_marginStart="10dp"
            android:layout_alignParentStart="true" />

    </RelativeLayout>

    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/list_view"/>

</LinearLayout>

4、遍歷省市縣的碎片

public class ChooseAreaFragment extends Fragment {
    public static final int LEVEL_PROVINCE = 0;
    public static final int LEVEL_CITY = 1;
    public static final int LEVEL_COUNTY = 2;

    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<County> countyList;
    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 = view.findViewById(R.id.title_text);
        backButton = view.findViewById(R.id.back_button);
        listView = view.findViewById(R.id.list_view);
        assert getContext()!= null;
        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<?> parent, View view, int position, long id) {
                if(currentLevel == LEVEL_PROVINCE){
                    selectedProvince = provinceList.get(position);
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    selectedCity = cityList.get(position);
                    queryCounties();
                }
            }
        });
        backButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(currentLevel == LEVEL_COUNTY){
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    queryProvinces();
                }
            }
        });
        queryProvinces();
    }

    /*c查詢全國的所有的省,優先從數據庫查詢,沒有則到服務器上查詢*/
    private void queryProvinces(){
        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, "province");
        }
    }

    /*查詢省內所有的市,優先從數據庫查詢*/
    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, "city");
        }
    }

    /*查詢選中市內的所有縣,優先從數據庫查詢*/
    private void queryCounties(){
        titleText.setText(selectedCity.getCityName());
        backButton.setVisibility(View.VISIBLE);
        countyList = LitePal.where("cityId = ?", String.valueOf(selectedCity.getId())).find(County.class);
        if (countyList.size() > 0){
            dataList.clear();
            for (County county : countyList){
                dataList.add(county.getCountyName());
            }
            adapter.notifyDataSetChanged();
            listView.setSelection(0);
            currentLevel = LEVEL_COUNTY;
        }else {
            int provinceCode = selectedProvince.getProvinceCode();
            int cityCode = selectedCity.getCityCode();
            String address = "http://guolin.tech/api/china/" + provinceCode + "/" +cityCode;
            queryFromServer(address, "county");
        }
    }

    private void queryFromServer(String address, final String type){
        showProgressDialog();
        HttpUtil.sendOkHttpRequest(address, new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                // 回到主線程處理邏輯
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        closeProgressDialog();
                        Toast.makeText(getContext(), "加載失敗", Toast.LENGTH_SHORT).show();
                    }
                });
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                String responseText = response.body().string();
                boolean result = false;
                if ("province".equals(type)){
                    result = Utility.handleProvinceResponse(responseText);
                }else if("city".equals(type)){
                    result = Utility.handleCityResponse(responseText, selectedProvince.getId());
                }else if("county".equals(type)){
                    result = Utility.handleCountyResponse(responseText, selectedCity.getId());
                }
                if (result){
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            closeProgressDialog();
                            if ("province".equals(type)){
                                queryProvinces();
                            }else if("city".equals(type)){
                                queryCities();
                            }else if("county".equals(type)){
                                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();
        }
    }
}

5、佈局中引入碎片

主佈局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="com.coolweather.android.ChooseAreaFragment"
        android:id="@+id/choose_area_fragment"/>

</FrameLayout>

去除原生標題欄,修改res/values/styles.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>

6、聲明權限

<uses-permission android:name="android.permission.INTERNET"/>

如果是安卓Q版本,還要給<application>設置屬性

android:usesCleartextTraffic="true"

7、展示

在這裏插入圖片描述

四、顯示天氣數據

1、基本格式

天氣數據返回的內容非常多,但解析數據只要一行代碼,但前提是將數據對應的實體類建立好,查詢開發文檔,得到三個請求分別爲:
請求參數爲now

{
	"HeWeather6": [
		{
			"basic": {},
			"update": {},
			"status": "ok",
			"now": {},
		},
	]
}

請求參數爲forecast:

{
	"HeWeather6": [
		{
			"basic": {},
			"update": {},
			"status": "ok",
			"daily_forecast": []
		},
	]
}

請求參數爲life_style:

{
	"HeWeather6": [
		{
			"basic": {},
			"update": {},
			"status": "ok",
			"lifestyle": []
		},
	]
}

2、建立關係

basic示例:

"basic": {
			"cid": "CN101010100",
			"location": "北京",
			"parent_city": "北京",
			"admin_area": "北京",
			"cnty": "中國",
			"lat": "39.90498734",
			"lon": "116.4052887",
			"tz": "+8.00"
		}

對應Basic類:

public class Basic {
    @SerializedName("location")
    public String cityName;

    @SerializedName("cid")
    public String weatherId;
}

update示例:

"update": {
			"loc": "2019-06-05 22:39",
			"utc": "2019-06-05 14:39"
		}

對應Update類:

public class Update {
    @SerializedName("loc")
    public String updateTime;
}

以此類推:

public class Now {
    @SerializedName("tmp")
    public String temperature;

    @SerializedName("cond_txt")
    public String info;

    @SerializedName("wind_dir")
    public String directionOfWind;

    @SerializedName("wind_sc")
    public String powerOfWind;

    @SerializedName("hum")
    public String humidity;

    @SerializedName("vis")
    public String visibility;
}

public class Forecast {
    public String date;

    @SerializedName("sr")
    public String sunrise;

    @SerializedName("ss")
    public String sunset;

    public String tmp_max;
    public String tmp_min;

    @SerializedName("con_txt_d")
    public String weatherDay;

    @SerializedName("con_txt_n")
    public String weatherNight;

    @SerializedName("pop")
    public String precipitationProbability;
}
public class LifeStyle {
    public String type;
    public String brf;
    public String txt;
}

最後用一個Weather類把所有類串起來:

public class Weather {
    public Basic basic;
    public Update update;
    public String status;
    public Now now;

    @SerializedName("daily_forecast")
    public List<Forecast> forecastList;

    @SerializedName("lifestyle")
    public List<LifeStyle> lifeStyleList;

}

五、編寫天氣界面

所有的天氣信息將在同一個界面顯示,新建一個WeatherActivity,佈局指定爲activity_weather.xml

1、頭佈局

新建頭佈局title.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/title_city"
        android:layout_centerInParent="true"
        android:textColor="#fff"
        android:textSize="20sp"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/title_update_time"
        android:layout_marginRight="10dp"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:textColor="#fff"
        android:textSize="16sp"
        android:layout_alignParentEnd="true"
        android:layout_marginEnd="10dp"
        tools:ignore="RelativeOverlap" />

</RelativeLayout>

2、當前天氣佈局

再來個now.xml作爲當前天氣信息的佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="15dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end"
        android:textColor="#fff"
        android:textSize="60sp"
        android:id="@+id/degree_text"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/weather_info_text"
        android:layout_gravity="end"
        android:textSize="20sp"
        android:textColor="#fff"/>

</LinearLayout>

3、未來天氣佈局

未來天氣的佈局forecast.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    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:text="預報"
        android:textColor="#fff"
        android:textSize="20sp"
        android:layout_marginStart="15dp"
        tools:ignore="HardcodedText" />
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:id="@+id/forecast_layout">
    </LinearLayout>

</LinearLayout>

4、未來天氣子項佈局

未來天氣的子項佈局forecast_item

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="15dp">

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:id="@+id/date_text"
        android:layout_weight="2"
        android:textColor="#fff"/>

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:id="@+id/info_text"
        android:layout_gravity="center_vertical"
        android:gravity="center"
        android:textColor="#fff"/>

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:id="@+id/max_text"
        android:layout_weight="1"
        android:layout_gravity="center_vertical"
        android:gravity="right"
        android:textColor="#fff"
        tools:ignore="RtlHardcoded" />
    
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:id="@+id/min_text"
        android:layout_weight="1"
        android:layout_gravity="center_vertical"
        android:gravity="right"
        android:textColor="#fff"
        tools:ignore="RtlHardcoded" />


</LinearLayout>

5、空氣質量佈局

空氣質量的佈局aqi.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    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:text="空氣質量"
        android:textColor="#fff"
        android:textSize="20sp"
        android:layout_marginStart="15dp"
        tools:ignore="HardcodedText" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="15dp">

        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <LinearLayout
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:id="@+id/hum_text"
                    android:textColor="#fff"
                    android:textSize="40sp"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="空氣溼度"
                    android:textColor="#fff"
                    tools:ignore="HardcodedText" />
            </LinearLayout>

        </RelativeLayout>

        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <LinearLayout
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:textColor="#fff"
                    android:textSize="40sp"
                    android:id="@+id/wind_text"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="風向"
                    android:textColor="#fff"
                    tools:ignore="HardcodedText" />                

            </LinearLayout>

        </RelativeLayout>

    </LinearLayout>

</LinearLayout>

6、生活建議佈局

然後是生活建議suggestions.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    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:text="生活建議"
        android:textColor="#fff"
        android:textSize="20sp"
        tools:ignore="HardcodedText"
        android:layout_marginStart="15dp" />
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="15dp"
        android:textColor="#fff"
        android:id="@+id/comfort_text"/>    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="15dp"
        android:textColor="#fff"
        android:id="@+id/car_wash_text"/>    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="15dp"
        android:textColor="#fff"
        android:id="@+id/sport_text"/>

</LinearLayout>

7、合併佈局

最後把它們引入到activity_weather.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars = "none"
        android:overScrollMode="never"
        android:id="@+id/weather_layout">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <include layout="@layout/title"/>
            <include layout="@layout/now" />
            <include layout="@layout/forecast" />
            <include layout="@layout/aqi"/>
            <include layout="@layout/suggestions" />


        </LinearLayout>

    </ScrollView>

</FrameLayout>

六、將天氣顯示到界面上

1、解析服務器JSON數據

Utility類中添加一個用於解析天氣 JSON 數據的方法:

public class Utility {
。。。。。。

    // 將返回的數據解析成實體類
    public static Weather handleWeatherResponse(String response, Weather weather, String cate){
        try{
            JSONObject jsonObject = new JSONObject(response);
            JSONArray jsonArray = jsonObject.getJSONArray("HeWeather6");
            String weatherContent = jsonArray.getJSONObject(0).toString();
            if ("now".equals(cate)){
                Now nowWeather = new Gson().fromJson(weatherContent, Now.class);
                weather.basic = nowWeather.basic;
                weather.update = nowWeather.update;
                weather.now = nowWeather.now;
                weather.status = nowWeather.status;
                return weather;
            }else if("forecast".equals(cate)){
                Forecast forecastWeather = new Gson().fromJson(weatherContent, Forecast.class);
                weather.forecastList = forecastWeather.forecastList;
                weather.status = forecastWeather.status;
            }else if("lifestyle".equals(cate)){
                LifeStyle lifeStyleWeather = new Gson().fromJson(weatherContent, LifeStyle.class);
                weather.lifestyleList = lifeStyleWeather.lifestyleList;
                weather.status = lifeStyleWeather.status;
            }
            return weather;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

2、在活動中處理數據

public class WeatherActivity extends AppCompatActivity {
    private ScrollView weatherLayout;
    private TextView titleCity;
    private TextView titleUpdateTime;
    private TextView degreeText;
    private TextView weatherInfoText;
    private LinearLayout forecastLayout;
    private TextView humText;
    private TextView windText;
    private TextView comfortText;
    private TextView drsgText;
    private TextView sportText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_weather);

        // 初始化控件
        weatherLayout = findViewById(R.id.weather_layout);
        titleCity = findViewById(R.id.title_city);
        titleUpdateTime = findViewById(R.id.title_update_time);
        weatherInfoText = findViewById(R.id.weather_info_text);
        forecastLayout = findViewById(R.id.forecast_layout);
        humText = findViewById(R.id.hum_text);
        windText = findViewById(R.id.wind_text);
        comfortText = findViewById(R.id.comfort_text);
        drsgText = findViewById(R.id.drsg_text);
        sportText = findViewById(R.id.sport_text);
        degreeText = findViewById(R.id.degree_text);

        Weather weather = new Weather();
        SharedPreferences prefs = getApplicationContext().getSharedPreferences("weather_data", MODE_PRIVATE);
        String nowString = prefs.getString("now", null);
        String forecastString = prefs.getString("forecast", null);
        String lifestyleString = prefs.getString("lifestyle", null);
        if(TextUtils.isEmpty(nowString) || TextUtils.isEmpty(forecastString) || TextUtils.isEmpty(lifestyleString)){
            // 無緩存時去服務器查詢天氣
            String weatherId = getIntent().getStringExtra("weather_id");
            weatherLayout.setVisibility(View.INVISIBLE);
            requestWeather(weatherId, weather, "now");
        }else{
            // 有緩存時直接讀取數據
            weather = Utility.handleWeatherResponse(nowString, weather, "now");
            weather = Utility.handleWeatherResponse(forecastString, weather, "forecast");
            weather = Utility.handleWeatherResponse(lifestyleString, weather, "lifestyle");
            assert weather != null;
            showWeatherInfo(weather);
        }
    }
    // 構造Url
    private String reUrl(String cate, String weatherId){
        String key = "cd9034ce70ad477b8454b6638261f0a5";
        return "https://free-api.heweather.net/s6/weather/" + cate + "?location=" + weatherId + "&key=" + key;
    }


    // 根據天氣 id 查詢城市天氣
    public void requestWeather(final String weatherId, final Weather weather,final String cate){
        String requestUrl = reUrl(cate, weatherId);
        Log.d("UUU", requestUrl);
        HttpUtil.sendOkHttpRequest(requestUrl, new Callback() {
            @Override
            public void onFailure(@NotNull final Call call, @NotNull IOException e) {
                e.printStackTrace();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        switch (cate) {
                            case "now":
                                Toast.makeText(WeatherActivity.this, "獲取今天天氣失敗", Toast.LENGTH_SHORT).show();
                                break;
                            case "lifestyle":
                                Toast.makeText(WeatherActivity.this, "獲取生活指數失敗", Toast.LENGTH_SHORT).show();
                                break;
                            case "forecast":
                                Toast.makeText(WeatherActivity.this, "獲取預報天氣失敗", Toast.LENGTH_SHORT).show();
                                break;
                        }
                    }
                });
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                final String responseText = response.body().string();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if ((weather != null && "ok".equals(weather.status)) || ("now".equals(cate))){
                            SharedPreferences.Editor editor = getApplicationContext().getSharedPreferences("weather_data", MODE_PRIVATE).edit();
                            Weather weather_new = Utility.handleWeatherResponse(responseText, weather, cate);
                            editor.putString(cate, responseText);
                            editor.apply();
                            switch (cate) {
                                case "now":
                                    requestWeather(weatherId, weather_new, "forecast");
                                    break;
                                case "forecast":
                                    requestWeather(weatherId, weather_new, "lifestyle");
                                    break;
                                case "lifestyle":
                                    assert weather_new != null;
                                    showWeatherInfo(weather_new);
                                    break;
                            }
                        }else {
                            Toast.makeText(WeatherActivity.this, "請求到的數據出錯", Toast.LENGTH_SHORT).show();
                        }
                    }
                });
            }
        });
    }

    // 處理並展示實體類 Weather 的數據
    private void showWeatherInfo(Weather weather){
        String cityName = weather.basic.cityName;
        String updateTime = weather.update.updateTime;
        String degree = weather.now.temperature + "℃";
        String weatherInfo = weather.now.info;

        titleCity.setText(cityName);
        titleUpdateTime.setText(updateTime);
        degreeText.setText(degree);
        weatherInfoText.setText(weatherInfo);
        forecastLayout.removeAllViews();
        for (Forecast.forecast forecast: weather.forecastList){
            View view= LayoutInflater.from(this).inflate(R.layout.forecast_item, forecastLayout, false);
            TextView dateText = view.findViewById(R.id.date_text);
            TextView infoText = view.findViewById(R.id.info_text);
            TextView maxText = view.findViewById(R.id.max_text);
            TextView minText = view.findViewById(R.id.min_text);
            dateText.setText(forecast.date);
            if (forecast.day.equals(forecast.night)){
                infoText.setText(forecast.day);
            }else {
                infoText.setText(String.format("%s轉%s", forecast.day, forecast.night));
            }
            maxText.setText(forecast.tmp_max);
            minText.setText(forecast.tmp_min);
            forecastLayout.addView(view);
        }
        if(weather.now != null){
            humText.setText(weather.now.humidity);
            windText.setText(weather.now.directionOfWind);
        }
        for(LifeStyle.lifestyle lifestyle: weather.lifestyleList){
            if ("comf".equals(lifestyle.type)){
                comfortText.setText(String.format("舒適度:%s", lifestyle.txt));
            }
            if ("drsg".equals(lifestyle.type)){
                drsgText.setText(String.format("穿衣指數:%s", lifestyle.txt));
            }
            if ("sport".equals(lifestyle.type)){
                sportText.setText(String.format("運動建議:%s", lifestyle.txt));
            }
        }
        weatherLayout.setVisibility(View.VISIBLE);
    }
}

3、從省市縣列表跳轉到天氣頁面

修改ChooseAreaFragment.java

	@Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if(currentLevel == LEVEL_PROVINCE){
                    selectedProvince = provinceList.get(position);
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    selectedCity = cityList.get(position);
                    queryCounties();
                }else if(currentLevel == LEVEL_COUNTY){
                    String weatherId = countyList.get(position).getWeatherId();
                    Intent intent = new Intent(getActivity(), WeatherActivity.class);
                    intent.putExtra("weather_id", weatherId);
                    startActivity(intent);
                    getActivity().finish();
                }
            }
        });
        backButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(currentLevel == LEVEL_COUNTY){
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    queryProvinces();
                }
            }
        });
        queryProvinces();
    }

4、選擇啓動頁面

MainActivity判斷緩存數據來決定::

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        SharedPreferences prefs = getApplicationContext().getSharedPreferences("weather_data", MODE_PRIVATE);
        if (prefs.getString("lifestyle", null) != null){
            Intent intent = new Intent(this, WeatherActivity.class);
            startActivity(intent);
            finish();
        }
    }
}

5、展示

在這裏插入圖片描述

七、獲取必應天氣壁紙

http://guolin.tech/api/bing_pic這個接口可以得到每日壁紙的url

1、天氣佈局修改

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/bing_pic_img"
        android:scaleType="centerCrop"/>

    <ScrollView
    ......

2、加載每日壁紙

WeatherActivity.java

public class WeatherActivity extends AppCompatActivity {
    private ScrollView weatherLayout;
    private TextView titleCity;
    private TextView titleUpdateTime;
    private TextView degreeText;
    private TextView weatherInfoText;
    private LinearLayout forecastLayout;
    private TextView humText;
    private TextView windText;
    private TextView comfortText;
    private TextView drsgText;
    private TextView sportText;
    private ImageView bingPicImg;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_weather);

        // 初始化控件
        weatherLayout = findViewById(R.id.weather_layout);
        titleCity = findViewById(R.id.title_city);
        titleUpdateTime = findViewById(R.id.title_update_time);
        weatherInfoText = findViewById(R.id.weather_info_text);
        forecastLayout = findViewById(R.id.forecast_layout);
        humText = findViewById(R.id.hum_text);
        windText = findViewById(R.id.wind_text);
        comfortText = findViewById(R.id.comfort_text);
        drsgText = findViewById(R.id.drsg_text);
        sportText = findViewById(R.id.sport_text);
        degreeText = findViewById(R.id.degree_text);
        bingPicImg = findViewById(R.id.bing_pic_img);


        Weather weather = new Weather();
        SharedPreferences prefs = getApplicationContext().getSharedPreferences("weather_data", MODE_PRIVATE);
        String bingPic = prefs.getString("bing_pic", null);
        if(bingPic != null){
            Glide.with(this).load(bingPic).into(bingPicImg);
        }else {
            loadBingPic();
        }
        String nowString = prefs.getString("now", null);
        String forecastString = prefs.getString("forecast", null);
        String lifestyleString = prefs.getString("lifestyle", null);
        if(TextUtils.isEmpty(nowString) || TextUtils.isEmpty(forecastString) || TextUtils.isEmpty(lifestyleString)){
            // 無緩存時去服務器查詢天氣
            String weatherId = getIntent().getStringExtra("weather_id");
            weatherLayout.setVisibility(View.INVISIBLE);
            requestWeather(weatherId, weather, "now");
        }else{
            // 有緩存時直接讀取數據
            weather = Utility.handleWeatherResponse(nowString, weather, "now");
            weather = Utility.handleWeatherResponse(forecastString, weather, "forecast");
            weather = Utility.handleWeatherResponse(lifestyleString, weather, "lifestyle");
            assert weather != null;
            showWeatherInfo(weather);
        }
    }

    // 加載每日一圖
    private void loadBingPic() {
        String requestBingPic = "http://guolin.tech/api/bing_pic";
        HttpUtil.sendOkHttpRequest(requestBingPic, new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                Toast.makeText(WeatherActivity.this, "壁紙加載出錯", Toast.LENGTH_SHORT).show();
                e.printStackTrace();
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                final String bingPic = response.body().string();
                SharedPreferences.Editor editor = getApplicationContext().getSharedPreferences("weather_data", MODE_PRIVATE).edit();
                editor.putString("bing_pic", bingPic);
                editor.apply();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Glide.with(WeatherActivity.this).load(bingPic).into(bingPicImg);
                    }
                });
            }
        });
    }

    // 構造Url
    private String reUrl(String cate, String weatherId){
        String key = "cd9034ce70ad477b8454b6638261f0a5";
        return "https://free-api.heweather.net/s6/weather/" + cate + "?location=" + weatherId + "&key=" + key;
    }


    // 根據天氣 id 查詢城市天氣
    public void requestWeather(final String weatherId, final Weather weather,final String cate){
        String requestUrl = reUrl(cate, weatherId);
        HttpUtil.sendOkHttpRequest(requestUrl, new Callback() {
            @Override
            public void onFailure(@NotNull final Call call, @NotNull IOException e) {
                e.printStackTrace();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        switch (cate) {
                            case "now":
                                Toast.makeText(WeatherActivity.this, "獲取今天天氣失敗", Toast.LENGTH_SHORT).show();
                                break;
                            case "lifestyle":
                                Toast.makeText(WeatherActivity.this, "獲取生活指數失敗", Toast.LENGTH_SHORT).show();
                                break;
                            case "forecast":
                                Toast.makeText(WeatherActivity.this, "獲取預報天氣失敗", Toast.LENGTH_SHORT).show();
                                break;
                        }
                    }
                });
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                final String responseText = response.body().string();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if ((weather != null && "ok".equals(weather.status)) || ("now".equals(cate))){
                            SharedPreferences.Editor editor = getApplicationContext().getSharedPreferences("weather_data", MODE_PRIVATE).edit();
                            Weather weather_new = Utility.handleWeatherResponse(responseText, weather, cate);
                            editor.putString(cate, responseText);
                            editor.apply();
                            switch (cate) {
                                case "now":
                                    requestWeather(weatherId, weather_new, "forecast");
                                    break;
                                case "forecast":
                                    requestWeather(weatherId, weather_new, "lifestyle");
                                    break;
                                case "lifestyle":
                                    assert weather_new != null;
                                    showWeatherInfo(weather_new);
                                    break;
                            }
                        }else {
                            Toast.makeText(WeatherActivity.this, "請求到的數據出錯", Toast.LENGTH_SHORT).show();
                        }
                    }
                });
            }
        });
    }

    // 處理並展示實體類 Weather 的數據
    private void showWeatherInfo(Weather weather){
        loadBingPic();
        String cityName = weather.basic.cityName;
        String updateTime = weather.update.updateTime;
        String degree = weather.now.temperature + "℃";
        String weatherInfo = weather.now.info;

        titleCity.setText(cityName);
        titleUpdateTime.setText(updateTime);
        degreeText.setText(degree);
        weatherInfoText.setText(weatherInfo);
        forecastLayout.removeAllViews();
        for (Forecast.forecast forecast: weather.forecastList){
            View view= LayoutInflater.from(this).inflate(R.layout.forecast_item, forecastLayout, false);
            TextView dateText = view.findViewById(R.id.date_text);
            TextView infoText = view.findViewById(R.id.info_text);
            TextView maxText = view.findViewById(R.id.max_text);
            TextView minText = view.findViewById(R.id.min_text);
            dateText.setText(forecast.date);
            if (forecast.day.equals(forecast.night)){
                infoText.setText(forecast.day);
            }else {
                infoText.setText(String.format("%s轉%s", forecast.day, forecast.night));
            }
            maxText.setText(forecast.tmp_max);
            minText.setText(forecast.tmp_min);
            forecastLayout.addView(view);
        }
        if(weather.now != null){
            humText.setText(weather.now.humidity);
            windText.setText(weather.now.directionOfWind);
        }
        for(LifeStyle.lifestyle lifestyle: weather.lifestyleList){
            if ("comf".equals(lifestyle.type)){
                comfortText.setText(String.format("舒適度:%s", lifestyle.txt));
            }
            if ("drsg".equals(lifestyle.type)){
                drsgText.setText(String.format("穿衣指數:%s", lifestyle.txt));
            }
            if ("sport".equals(lifestyle.type)){
                sportText.setText(String.format("運動建議:%s", lifestyle.txt));
            }
        }
        weatherLayout.setVisibility(View.VISIBLE);
    }
}

3、狀態欄融合

public class WeatherActivity extends AppCompatActivity {
。。。。。。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 背景和狀態欄融合
        if (Build.VERSION.SDK_INT >= 21){
            View decorView = getWindow().getDecorView();
            decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            getWindow().setStatusBarColor(Color.TRANSPARENT);
        }
        setContentView(R.layout.activity_weather);

4、狀態欄不融合,只透明

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
......

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:fitsSystemWindows="true">
            ......

5、展示

在這裏插入圖片描述

八、手動更新天氣

1、使用下拉控件

    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh"
        android:layout_height="match_parent"
        android:layout_width="match_parent">

        <ScrollView>
           ......
        </ScrollView>
    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

2、加入邏輯

public class WeatherActivity extends AppCompatActivity {
......

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 背景和狀態欄融合
        if (Build.VERSION.SDK_INT >= 21){
            View decorView = getWindow().getDecorView();
            decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            getWindow().setStatusBarColor(Color.TRANSPARENT);
        }
        setContentView(R.layout.activity_weather);
        swipeRefreshLayout = findViewById(R.id.swipe_refresh);
        swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary);

        // 初始化控件
        weatherLayout = findViewById(R.id.weather_layout);
        titleCity = findViewById(R.id.title_city);
        titleUpdateTime = findViewById(R.id.title_update_time);
        weatherInfoText = findViewById(R.id.weather_info_text);
        forecastLayout = findViewById(R.id.forecast_layout);
        humText = findViewById(R.id.hum_text);
        windText = findViewById(R.id.wind_text);
        comfortText = findViewById(R.id.comfort_text);
        drsgText = findViewById(R.id.drsg_text);
        sportText = findViewById(R.id.sport_text);
        degreeText = findViewById(R.id.degree_text);
        bingPicImg = findViewById(R.id.bing_pic_img);


        Weather weather = new Weather();
        SharedPreferences prefs = getApplicationContext().getSharedPreferences("weather_data", MODE_PRIVATE);
        String bingPic = prefs.getString("bing_pic", null);
        if(bingPic != null){
            Glide.with(this).load(bingPic).into(bingPicImg);
        }else {
            loadBingPic();
        }
        String nowString = prefs.getString("now", null);
        String forecastString = prefs.getString("forecast", null);
        String lifestyleString = prefs.getString("lifestyle", null);
        if(TextUtils.isEmpty(nowString) || TextUtils.isEmpty(forecastString) || TextUtils.isEmpty(lifestyleString)){
            // 無緩存時去服務器查詢天氣
            mWeatherId = getIntent().getStringExtra("weather_id");
            weatherLayout.setVisibility(View.INVISIBLE);
            requestWeather(mWeatherId, weather, "now");
        }else{
            // 有緩存時直接讀取數據
            weather = Utility.handleWeatherResponse(nowString, weather, "now");
            weather = Utility.handleWeatherResponse(forecastString, weather, "forecast");
            weather = Utility.handleWeatherResponse(lifestyleString, weather, "lifestyle");
            mWeatherId = weather.basic.weatherId;
            assert weather != null;
            showWeatherInfo(weather);
        }
        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                requestWeather(mWeatherId, new Weather(), "now");
            }
        });
    }

    // 加載每日一圖
    private void loadBingPic() {
        String requestBingPic = "http://guolin.tech/api/bing_pic";
        HttpUtil.sendOkHttpRequest(requestBingPic, new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                Toast.makeText(WeatherActivity.this, "壁紙加載出錯", Toast.LENGTH_SHORT).show();
                e.printStackTrace();
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                final String bingPic = response.body().string();
                SharedPreferences.Editor editor = getApplicationContext().getSharedPreferences("weather_data", MODE_PRIVATE).edit();
                editor.putString("bing_pic", bingPic);
                editor.apply();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Glide.with(WeatherActivity.this).load(bingPic).into(bingPicImg);
                    }
                });
            }
        });
    }

    // 構造Url
    private String reUrl(String cate, String weatherId){
        String key = "cd9034ce70ad477b8454b6638261f0a5";
        return "https://free-api.heweather.net/s6/weather/" + cate + "?location=" + weatherId + "&key=" + key;
    }


    // 根據天氣 id 查詢城市天氣
    public void requestWeather(final String weatherId, final Weather weather,final String cate){
        String requestUrl = reUrl(cate, weatherId);
        Log.d("???", requestUrl);
        HttpUtil.sendOkHttpRequest(requestUrl, new Callback() {
            @Override
            public void onFailure(@NotNull final Call call, @NotNull IOException e) {
                e.printStackTrace();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        switch (cate) {
                            case "now":
                                Toast.makeText(WeatherActivity.this, "獲取今天天氣失敗", Toast.LENGTH_SHORT).show();
                                break;
                            case "lifestyle":
                                Toast.makeText(WeatherActivity.this, "獲取生活指數失敗", Toast.LENGTH_SHORT).show();
                                break;
                            case "forecast":
                                Toast.makeText(WeatherActivity.this, "獲取預報天氣失敗", Toast.LENGTH_SHORT).show();
                                break;
                        }
                        swipeRefreshLayout.setRefreshing(false);
                    }
                });
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                final String responseText = response.body().string();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if ((weather != null && "ok".equals(weather.status)) || ("now".equals(cate))){
                            SharedPreferences.Editor editor = getApplicationContext().getSharedPreferences("weather_data", MODE_PRIVATE).edit();
                            Weather weather_new = Utility.handleWeatherResponse(responseText, weather, cate);
                            editor.putString(cate, responseText);
                            editor.apply();
                            switch (cate) {
                                case "now":
                                    requestWeather(weatherId, weather_new, "forecast");
                                    break;
                                case "forecast":
                                    requestWeather(weatherId, weather_new, "lifestyle");
                                    break;
                                case "lifestyle":
                                    assert weather_new != null;
                                    showWeatherInfo(weather_new);
                                    mWeatherId = weather.basic.weatherId;
                                    swipeRefreshLayout.setRefreshing(false);
                                    break;
                            }
                        }else {
                            swipeRefreshLayout.setRefreshing(false);
                            Toast.makeText(WeatherActivity.this, "請求到的數據出錯", Toast.LENGTH_SHORT).show();
                        }
                    }
                });
            }
        });
    }

    // 處理並展示實體類 Weather 的數據
    private void showWeatherInfo(Weather weather){
        loadBingPic();
        String cityName = weather.basic.cityName;
        String updateTime = weather.update.updateTime;
        String degree = weather.now.temperature + "℃";
        String weatherInfo = weather.now.info;

        titleCity.setText(cityName);
        titleUpdateTime.setText(updateTime);
        degreeText.setText(degree);
        weatherInfoText.setText(weatherInfo);
        forecastLayout.removeAllViews();
        for (Forecast.forecast forecast: weather.forecastList){
            View view= LayoutInflater.from(this).inflate(R.layout.forecast_item, forecastLayout, false);
            TextView dateText = view.findViewById(R.id.date_text);
            TextView infoText = view.findViewById(R.id.info_text);
            TextView maxText = view.findViewById(R.id.max_text);
            TextView minText = view.findViewById(R.id.min_text);
            dateText.setText(forecast.date);
            if (forecast.day.equals(forecast.night)){
                infoText.setText(forecast.day);
            }else {
                infoText.setText(String.format("%s轉%s", forecast.day, forecast.night));
            }
            maxText.setText(forecast.tmp_max);
            minText.setText(forecast.tmp_min);
            forecastLayout.addView(view);
        }
        if(weather.now != null){
            humText.setText(weather.now.humidity);
            windText.setText(weather.now.directionOfWind);
        }
        for(LifeStyle.lifestyle lifestyle: weather.lifestyleList){
            if ("comf".equals(lifestyle.type)){
                comfortText.setText(String.format("舒適度:%s", lifestyle.txt));
            }
            if ("drsg".equals(lifestyle.type)){
                drsgText.setText(String.format("穿衣指數:%s", lifestyle.txt));
            }
            if ("sport".equals(lifestyle.type)){
                sportText.setText(String.format("運動建議:%s", lifestyle.txt));
            }
        }
        weatherLayout.setVisibility(View.VISIBLE);
    }
}

3、展示

在這裏插入圖片描述

九、手動切換城市

1、導航按鈕

在屏幕左邊先加個按鈕:
title.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize">

    <Button
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_marginLeft="10dp"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:id="@+id/nav_button"
        android:background="@drawable/home"
        android:layout_alignParentStart="true"
        android:layout_marginStart="10dp" />
.........

2、滑動菜單

activity_weather.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
......

    <androidx.drawerlayout.widget.DrawerLayout
        android:id="@+id/drawer_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
            android:id="@+id/swipe_refresh"
            android:layout_height="match_parent"
            android:layout_width="match_parent">

            <ScrollView
......

                <LinearLayout
......

                </LinearLayout>

            </ScrollView>
        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
        
        <fragment
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/choose_area_fragment"
            android:name="com.coolweather.android.ChooseAreaFragment"
            android:layout_gravity="start"/>
        
    </androidx.drawerlayout.widget.DrawerLayout>

</FrameLayout>

3、導航按鈕,滑動菜單 聯結

public class WeatherActivity extends AppCompatActivity {
......
    public DrawerLayout drawerLayout;
    private Button navButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
......

        // 初始化控件
......
        drawerLayout = findViewById(R.id.drawer_layout);
        navButton = findViewById(R.id.nav_button);

        navButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                drawerLayout.openDrawer(GravityCompat.START);
            }
        });

4、邏輯處理

修改碎片ChooseAreaFragment:

......

	@Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if(currentLevel == LEVEL_PROVINCE){
                    selectedProvince = provinceList.get(position);
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    selectedCity = cityList.get(position);
                    queryCounties();
                }else if(currentLevel == LEVEL_COUNTY){
                    String weatherId = countyList.get(position).getWeatherId();
                    if(getActivity() instanceof MainActivity){
                        Intent intent = new Intent(getActivity(), WeatherActivity.class);
                        intent.putExtra("weather_id", weatherId);
                        startActivity(intent);
                        getActivity().finish();
                    }else if (getActivity() instanceof  WeatherActivity){
                        WeatherActivity activity = (WeatherActivity) getActivity();
                        activity.drawerLayout.closeDrawers();
                        activity.swipeRefreshLayout.setRefreshing(true);
                        activity.requestWeather(weatherId, new Weather(), "now");
                    }

                }
            }
        });

......

5、展示

在這裏插入圖片描述

十、後臺自動更新天氣

1、新建服務

新建一個服務AutoUpdateService

ublic class AutoUpdateService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        updateWeather();
        updateBingPic();
        AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
        int anHour = 2 * 60 * 60 * 1000;    // 每兩小時更新一次
        long triggerAtTime = SystemClock.elapsedRealtime()+anHour;
        Intent i = new Intent(this, AutoUpdateService.class);
        PendingIntent pi = PendingIntent.getService(this, 0, i ,0);
        manager.cancel(pi);
        manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pi);
        return super.onStartCommand(intent, flags, startId);
    }

    private void updateWeather() {
        final SharedPreferences prefs = getSharedPreferences("weather_data", MODE_PRIVATE);
        String nowString = prefs.getString("now", null);
        if(nowString != null){
            String weatherId = Utility.handleWeatherResponse(nowString, new Weather(), "now").basic.weatherId;
            savedData(reUrl("now", weatherId), "now");
            savedData(reUrl("forecast", weatherId), "forecast");
            savedData(reUrl("lifestyle", weatherId), "lifestyle");
        }
    }
    // 存放數據
    private void savedData(String weatherUrl, final String cate){
        HttpUtil.sendOkHttpRequest(weatherUrl, new Callback() {
            @Override
            public void onFailure(@NotNull final Call call, @NotNull IOException e) {
                e.printStackTrace();
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                String responseText = response.body().string();
                SharedPreferences.Editor editor = getSharedPreferences("weather_data", MODE_PRIVATE).edit();
                editor.putString(cate, responseText);
                editor.apply();
            }
        });
    }
    
    // 構造Url
    private String reUrl(String cate, String weatherId){
        String key = "cd9034ce70ad477b8454b6638261f0a5";
        return "https://free-api.heweather.net/s6/weather/" + cate + "?location=" + weatherId + "&key=" + key;
    }

    private void updateBingPic() {
        String requestBingPic = "http://guolin.tech/api/bing_pic";
        HttpUtil.sendOkHttpRequest(requestBingPic, new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                e.printStackTrace();
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                String bingPic = response.body().string();
                SharedPreferences.Editor editor = getSharedPreferences("weather_data", MODE_PRIVATE).edit();
                editor.putString("bing_pic", bingPic);
                editor.apply();
            }
        });
    }
}

2、啓動服務

    // 處理並展示實體類 Weather 的數據
    private void showWeatherInfo(Weather weather){
    ......
        weatherLayout.setVisibility(View.VISIBLE);
        Intent intent = new Intent(this, AutoUpdateService.class);
        startService(intent);
    }

十一、修改圖標和名稱

1、修改圖標

修改<application>的屬性:

android:icon="@mipmap/logo"

然後將不同分辨率的logo.png放在所有的mipmap目錄下,圖標就修改好了

2、修改應用名

修改res/values/string.xml

<resources>
    <string name="app_name">酷歐天氣</string>
</resources>

即可
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章