酷歐天氣的開(kāi)發(fā)

簡(jiǎn)介

參考《第一行代碼》,開(kāi)發(fā)出一款全國(guó)省市縣的天氣預(yù)報(bào)app.

創(chuàng)建數(shù)據(jù)庫(kù)和表

使用LitePal對(duì)數(shù)據(jù)庫(kù)進(jìn)行操作,創(chuàng)建三個(gè)實(shí)體類分別是Province、City和County。

1. 添加依賴項(xiàng)
compile 'org.litepal.android:core:1.3.2'
2. 創(chuàng)建實(shí)體類
package com.example.stardream.coolweather.db;

import org.litepal.crud.DataSupport;

/**
 * Created by StarDream on 2018/8/22.
 */
//LitePal中的每一個(gè)實(shí)體類都應(yīng)該繼承DataSupport
public class Province extends DataSupport {
    private int id;  //實(shí)體類具有的id
    private String provinceName;  //省份的名字
    private int provinceCode;  //省的代號(hào)

    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實(shí)體類和County實(shí)體類同理。每個(gè)實(shí)體類代表一張表,實(shí)體類中的屬性代表表中的每一列。

3. 配置litepal.xml文件
<litepal>
    <!--dbname 為數(shù)據(jù)庫(kù)的名字-->
    <dbname value="cool_weather"/>
    <!--數(shù)據(jù)庫(kù)版本指定為1-->
    <version value="1"/>
    <!--將實(shí)體類添加到映射列表中-->
    <list>
        <mapping class="com.example.stardream.coolweather.db.Province"/>
        <mapping class="com.example.stardream.coolweather.db.City"/>
        <mapping class="com.example.stardream.coolweather.db.County"/>
    </list>
</litepal>
4. 修改AndroidManifest.xml文件

將項(xiàng)目的application配置為org.litepal.LitePalApplication

android:name="org.litepal.LitePalApplication"

關(guān)于LitePal的具體內(nèi)容詳見(jiàn)LitePal詳解

遍歷全國(guó)省市縣數(shù)據(jù)

1. 客戶端與服務(wù)器的交互

package com.example.stardream.coolweather.util;

import okhttp3.OkHttpClient;
import okhttp3.Request;

/**
 * Created by StarDream on 2018/8/22.
 */
//采用OkHttp與服務(wù)器進(jìn)行通信
public class HttpUtil {
    //與服務(wù)器進(jìn)行交互發(fā)起一條http請(qǐng)求只需要調(diào)用sendOkHttpRequest()即可
    //傳入要請(qǐng)求的地址,注冊(cè)一個(gè)回調(diào)來(lái)處理服務(wù)器的響應(yīng)
    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);
    }
}

發(fā)起http請(qǐng)求只需調(diào)用sendOkHttprequest()這個(gè)方法,傳入一個(gè)地址,并且注冊(cè)一個(gè)回調(diào)來(lái)處理服務(wù)器的響應(yīng)。

2. 處理服務(wù)器返回的Json格式的數(shù)據(jù)

新建一個(gè)Utility類處理和解析Json數(shù)據(jù)。

package com.example.stardream.coolweather.util;

import android.text.TextUtils;

import com.example.stardream.coolweather.db.City;
import com.example.stardream.coolweather.db.County;
import com.example.stardream.coolweather.db.Province;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * Created by StarDream on 2018/8/22.
 */

public class Utility {
    //處理和解析省份的數(shù)據(jù)
    public static boolean hanldeProvinceResponse(String response){
        if(!TextUtils.isEmpty(response)){
            try{
                //json的對(duì)象的數(shù)組,用來(lái)接收傳回的多個(gè)省份的數(shù)據(jù)
                JSONArray allProvinces = new JSONArray(response);
                for(int i=0;i<allProvinces.length();i++){
                    //取出每一個(gè)省份
                    JSONObject provinceObject = allProvinces.getJSONObject(i);
                    Province province = new Province();
                    //解析出省份的id并將其賦值給province對(duì)象
                    province.setProvinceCode(provinceObject.getInt("id"));
                    //解析出省份的name并將其賦值給province對(duì)象
                    province.setProvinceName(provinceObject.getString("name"));
                    //將這一個(gè)省份保存到表中
                    province.save();
                }
                //處理成功返回真
                return true;
            }catch(JSONException e){
                e.printStackTrace();
            }
        }
        //處理失敗返回假
        return false;
    }
    //處理和解析市的數(shù)據(jù)
    public static boolean handleCityResponse(String response,int provinceId){
        if(!TextUtils.isEmpty(response)){
            try{
                JSONArray allCity = new JSONArray(response);
                for(int i=0;i<allCity.length();i++){
                    JSONObject cityObject = allCity.getJSONObject(i);
                    City city = new City();
                    city.setCityCode(cityObject.getInt("id"));
                    city.setCityName(cityObject.getString("name"));
                    city.setProvinceId(provinceId);
                    city.save();
                }

            }catch(JSONException e){
                e.printStackTrace();
            }
            return true;
        }
        return false;
    }
    //處理和解析縣的數(shù)據(jù)
    public static boolean handleCountyResponse(String response,int cityId){
        if(!TextUtils.isEmpty(response)){
            try{
                JSONArray allCounty = new JSONArray(response);
                for(int i=0;i<allCounty.length();i++){
                    JSONObject countyObject = allCounty.getJSONObject(i);
                    County county = new County();
                    county.setCountyName(countyObject.getString("name"));
                    county.setCityId(cityId);
                    county.setWeatherId(countyObject.getInt("weather_id"));
                    county.save();
                }
            }catch (JSONException e){
                e.printStackTrace();
            }
            return true;
        }
        return false;
    }
}

省級(jí)、市級(jí)和縣級(jí)對(duì)服務(wù)器發(fā)來(lái)的數(shù)據(jù)的處理解析方式都是類似的,使用JSONArrayJSONObject進(jìn)行解析,然后組裝成實(shí)體類對(duì)象,再調(diào)用save()方法存儲(chǔ)到數(shù)據(jù)庫(kù)中。

3. 遍歷省市縣

遍歷省市縣功能.xml

因?yàn)楸闅v省市縣的功能會(huì)用到多次,因此將其寫為碎片的形式而不是寫在活動(dòng)里面,這樣復(fù)用的時(shí)候可以直接在布局文件中調(diào)用碎片。

<?xml version="1.0" encoding="utf-8"?>
<!--頭布局作為標(biāo)題欄-->
<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">
    <!--布局高度為actionBar高度,背景色為colorPrimary-->
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary">
   <!--用于顯示標(biāo)題內(nèi)容-->
        <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"/>
        <!--返回按鈕-->
        <Button
            android:id="@+id/back_button"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_marginLeft="10dp"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:background="@drawable/ic_back"/>
        <!--省市縣的數(shù)據(jù)顯示在這里-->
        <!--listView會(huì)自動(dòng)給每個(gè)子項(xiàng)之間增加一條分割線-->
        <ListView
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            android:id="@+id/list_view/>
    </RelativeLayout>
</LinearLayout>

一般情況下標(biāo)題欄可以采用ActionBar,但是碎片中最好不用ActionBar或Toolbar,否則會(huì)有問(wèn)題。

遍歷省市縣碎片

1. 新建一個(gè)ChooseAreaFragment用于展示查詢界面和實(shí)現(xiàn)基本查詢功能,定義一些量值。

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;//適配器,與ListView配合使用
    private List<String> dataList = new ArrayList<>();//動(dòng)態(tài)數(shù)組
    //省列表
    private List<Province> provinceList;
    //市列表
    private List<City> cityList;
    //縣列表
    private List<County> countyList;
    //選中的省份
    private Province selectedProvince;
    //選中的城市
    private City selectedCity;
    //當(dāng)前選中的級(jí)別
    private int currentLevel;
}

2. onCreateView()方法中獲取到一些控件的實(shí)例,初始化了ArrayAdapter,將其設(shè)置為L(zhǎng)istView的適配器。

   @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        /*
        * 【LayoutInflater】其實(shí)是在res/layout/下找到xml布局文件,并且將其實(shí)例化,
        * 對(duì)于一個(gè)沒(méi)有被載入或者想要?jiǎng)討B(tài)載入的界面,都需要使用LayoutInflater.inflate()來(lái)載入;
        * */
        View view = inflater.inflate(R.layout.choose_area,container,false);
        titleText = (TextView) view.findViewById(R.id.title_text);
        backButton = (Button) view.findViewById(R.id.back_button);
        listView = (ListView) view.findViewById(R.id.list_view);
        adapter = new ArrayAdapter<>(getContext(),android.R.layout.simple_list_item_1,dataList);
        //載入listView
        listView.setAdapter(adapter);
        return view;
    }

3. 在onActivityCreated()方法中設(shè)置ListView和Button的點(diǎn)擊事件,在這里完成了基本的初始化操作,調(diào)用queryProvinces()方法,加載省級(jí)數(shù)據(jù)。

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //對(duì)列表設(shè)置監(jiān)聽(tīng)事件
        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);
                    //顯示出省份對(duì)應(yīng)下city的界面
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    //記住選中的City
                    selectedCity = cityList.get(position);
                    //切換到相應(yīng)的county界面
                    queryCounties();
                }
            }
        });
        //為返回按鈕注冊(cè)監(jiān)聽(tīng)事件
        backButton.setOnClickListener(new View.OnClickListener(){
            public void onClick(View v){
                //若在county切換到City
                if(currentLevel == LEVEL_COUNTY){
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    //若在City切換到province
                    queryProvinces();
                }
            }
        });
        //初始狀態(tài)下顯示province
        queryProvinces();
    }

4. 在queryProvinces()方法中,設(shè)置頭布局的標(biāo)題,返回按鈕。調(diào)用LitePal查詢結(jié)構(gòu)讀取省級(jí)數(shù)據(jù),若讀取到則將其顯示在界面上,若沒(méi)有則調(diào)用queryServer()方法從服務(wù)器查詢數(shù)據(jù)。queryCities()方法和queryCounty()方法同理。

  • 查詢省級(jí)數(shù)據(jù)
/*查詢?nèi)珖?guó)所有的省,先從數(shù)據(jù)庫(kù)查,沒(méi)有的話去服務(wù)器查詢
    * */
    private void queryProvinces(){
        //設(shè)置標(biāo)題欄
        titleText.setText("中國(guó)");
        //隱藏返回按鈕
        backButton.setVisibility(View.GONE);
        //在數(shù)據(jù)庫(kù)中查詢所有省份
        provinceList = DataSupport.findAll(Province.class);
        if(provinceList.size()>0){
            dataList.clear();
            for(Province province:provinceList){
                dataList.add(province.getProvinceName());
            }
            //更新適配器中的內(nèi)容,變?yōu)槭》輸?shù)據(jù)
            adapter.notifyDataSetChanged();
            listView.setSelection(0);
            currentLevel = LEVEL_PROVINCE;
        }else{
            //從服務(wù)器中查詢
            String address = "http://guolin.tech/api/china";
            queryFromServer(address,"province");
        }
    }
  • 查詢市級(jí)數(shù)據(jù)
/*查詢對(duì)應(yīng)省的City數(shù)據(jù),優(yōu)先從數(shù)據(jù)庫(kù)查,若沒(méi)有,則去服務(wù)器查詢
    * */
    private void queryCities(){
        //設(shè)置標(biāo)題欄
        titleText.setText(selectedProvince.getProvinceName());
        //設(shè)置返回按鈕可見(jiàn)
        backButton.setVisibility(View.VISIBLE);
        //在數(shù)據(jù)庫(kù)中查詢對(duì)應(yīng)的City數(shù)據(jù)
        cityList = DataSupport.findAll(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{
            String address = "http://guolin.tech/api/china/"+selectedProvince.getProvinceCode();
            queryFromServer(address,"city");
        }
    }
  • 查詢縣級(jí)數(shù)據(jù)
 /*查詢選中的市內(nèi)的所有縣,優(yōu)先從數(shù)據(jù)庫(kù)查,若沒(méi)有則去服務(wù)器查詢
    * */
    private void queryCounties(){
        titleText.setText(selectedCity.getCityName());
        backButton.setVisibility(View.VISIBLE);
        countyList = DataSupport.findAll(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{
            String address = "http://guolin.tech/api/china/"+
                    selectedProvince.getProvinceCode()+"/"+selectedCity.getCityCode();
            queryFromServer(address,"county");
        }
    }

5. 在queryFromServer()方法中,調(diào)用HttpUtil中的sendOkHttpRequest()方法向服務(wù)器發(fā)送請(qǐng)求,響應(yīng)的數(shù)據(jù)回調(diào)到onResponse()方法中,然后調(diào)用Utility中的handleProvincesResponse()方法解析和處理服務(wù)器返回的數(shù)據(jù),然后將其存儲(chǔ)到數(shù)據(jù)庫(kù)中。最后再次調(diào)用queryProvinces()方法重新加載省級(jí)數(shù)據(jù),因?yàn)?strong>queryProvinces()涉及UI操作,則須在主線程中調(diào)用,因此借助runOnUiThread()方法從子線程切換到主線程。

  • 問(wèn)題:queryFromServer()在哪里說(shuō)明是是在子線程中執(zhí)行的???
   /*根據(jù)傳入的地址和類型從服務(wù)器上獲取數(shù)據(jù)
    * */
    private void queryFromServer(String address,final String type){
        //未查出之前顯示出進(jìn)度條框
        showProgressDialog();
        HttpUtil.sendOkHttpRequest(address, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                //通過(guò)runOnUiThread回到主線程處理邏輯
                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 {
                String responseText = response.body().string();
                boolean result = false;
                if(type.equals("province")){
                    result = Utility.hanldeProvinceResponse(responseText);
                }else if(type.equals("city")){
                    result = Utility.handleCityResponse(responseText,selectedProvince.getId());
                }else if(type.equals("county")){
                    result = Utility.handleCountyResponse(responseText,selectedCity.getId());
                }
                if(result){
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            closeProgressDialog();
                            if(type.equals("province")){
                                queryProvinces();
                            }else if(type.equals("city")){
                                queryCities();
                            }else if(type.equals("county")){
                                queryCounties();
                            }
                        }
                    });
                }


            }
        });
    }

6. 涉及到的進(jìn)度條框

  • 進(jìn)度條框的顯示
    //顯示進(jìn)度條框
    private void showProgressDialog(){
        if(progressDialog == null){
            progressDialog = new ProgressDialog(getActivity());
            progressDialog.setMessage("正在加載…");
            progressDialog.setCanceledOnTouchOutside(false);
        }
        progressDialog.show();
    }
  • 進(jìn)度條框的關(guān)閉
    //關(guān)閉進(jìn)度框
    private void closeProgressDialog(){
        if(progressDialog != null){
            progressDialog.dismiss();
        }
    }

將碎片添加到活動(dòng)

1. 修改activity_main.xml中的代碼

定義一個(gè)FrameLayout,將ChooseAreaFragment加入,并讓其充滿整個(gè)布局。

 <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <fragment
            android:id="@+id/choose_area_fragment"
            android:name="com.example.stardream.coolweather.activity.ChooseAreaFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>

2. 刪除原生的ActionBar

在style.xml中,

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">

3. 權(quán)限問(wèn)題

在AndroidManifest.xml中添加網(wǎng)絡(luò)權(quán)限。

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

顯示天氣信息

因?yàn)楹惋L(fēng)天氣返回的天氣信息的JSON數(shù)據(jù)結(jié)構(gòu)非常復(fù)雜,若使用JSONObject解析比較麻煩,因此使用GSON對(duì)天氣信息進(jìn)行解析。
在這里應(yīng)該加入JSONObject和GSON解析和處理數(shù)據(jù)的區(qū)別

1. 定義GSON實(shí)體類

返回天氣信息的格式:


返回?cái)?shù)據(jù)的格式

要為basic、aqi、now、suggestion和daily_forecase定義實(shí)體類。

basic
basic具體內(nèi)容

在gson包下建立Basic類

package com.example.stardream.coolweather.gson;
import com.google.gson.annotations.SerializedName;
/**
 * Created by StarDream on 2018/8/24.
 */
/*由于JSON中的一些字段不太適合直接作為Java字段命名,
這里使用@SerializedName朱姐的方式讓JSON字段和java字段建立映射關(guān)系
* */
public class Basic {
    //"city"與cityName建立映射關(guān)系
    @SerializedName("city")
    public String cityName;
    
    //"id"與weatherId建立映射關(guān)系
    @SerializedName("id")
    public String weatherId;
    
    @SerializedName("update")
    public Update update;
    public class Update{
        //"loc"與updateTime建立映射關(guān)系
        @SerializedName("loc")
        public String updateTime;
    }
}
aqi
aqi具體內(nèi)容
package com.example.stardream.coolweather.gson;
import com.google.gson.annotations.SerializedName;
/**
 * Created by StarDream on 2018/8/24.
 */
public class AQI {
    public AQICity city;
    public class AQICity{
        @SerializedName("aqi")
        String aqi;
        
        @SerializedName("pm25")
        String pm25;
    }
}

為什么這里的“aqi”與“pm25”沒(méi)有使用SerilizedName???

now
now具體內(nèi)容
package com.example.stardream.coolweather.gson;

import com.google.gson.annotations.SerializedName;

/**
 * Created by StarDream on 2018/8/24.
 */

public class Now {
    @SerializedName("tmp")
    public String temperature;
    
    @SerializedName("cond")
    public More more;
    public class More{
        @SerializedName("txt")
        public String info;
    }
}
suggestion
suggestion
package com.example.stardream.coolweather.gson;

import com.google.gson.annotations.SerializedName;

/**
 * Created by StarDream on 2018/8/24.
 */

public class Suggestion {
    @SerializedName("comf")
    public Comfort comfort;

    @SerializedName("cw")
    public CarWash carWash;

    @SerializedName("sport")
    public Sport sport;

    public class Comfort{
        @SerializedName("txt")
        public String info;
    }
    public class CarWash{
        @SerializedName("txt")
        public String info;
    }
    public class Sport{
        @SerializedName("txt")
        public String info;
    }
}
daily_forecast
daily_forecast具體內(nèi)容

daily_forecase內(nèi)包含的是一個(gè)數(shù)組,只定義出單日天氣的實(shí)體類,在聲明實(shí)體類引用的時(shí)候使用集合類型來(lái)進(jìn)行聲明。
package com.example.stardream.coolweather.gson;
import com.google.gson.annotations.SerializedName;
/**

  • Created by StarDream on 2018/8/24.
    */

public class Forecast {
@SerializedName("date")
public String date;

@SerializedName("tmp")
public Temperature temperature;

@SerializedName("cond")
public More more;

public class Temperature{
    @SerializedName("max")
    public String max;
    
    @SerializedName("min")
    public String min;
}
public class More{
    @SerializedName("txt_d")
    public String info;
}

}

總實(shí)體類Weather

創(chuàng)建一個(gè)總的實(shí)體類來(lái)引用印上的各個(gè)實(shí)體類。

package com.example.stardream.coolweather.gson;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
 * Created by StarDream on 2018/8/24.
 */
public class Weather {
    public String status;
    public Basic basic;
    public AQI aqi;
    public Now now;
    public Suggestion suggestion;
    //由于daily_forecase中包含的是一個(gè)數(shù)組,
    //這里使用List集合來(lái)引用Forecast類
    @SerializedName("daily_forecast")
    public List<Forecast> forecastList;
}

2. 編寫天氣界面

天氣信息界面,activity_weather.xml

為了讓代碼相對(duì)整齊,采用引用布局技術(shù),將界面的不同部分寫在不同的文件中,再通過(guò)引入布局的方式集成到activity_weather.xml中。

title.xml頭布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize">
    <TextView
        android:id="@+id/title_city"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textColor="#fff"
        android:textSize="20sp"/>
    <TextView
        android:id="@+id/title_update_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="10dp"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:textColor="#fff"
        android:textSize="16sp"/>

</RelativeLayout>

其中,一個(gè)居中顯示城市名,一個(gè)居右顯示更新時(shí)間。

now.xml當(dāng)前天氣信息布局

<?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:id="@+id/degree_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:textColor="#fff"
        android:textSize="20sp"/>

</LinearLayout>

其中,放置了兩個(gè)textView,一個(gè)用來(lái)顯示氣溫,另外一個(gè)用于顯示天氣概況。

forecast.xml未來(lái)幾天天氣信息的布局

<?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"
    android:background="#8000">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="15dp"
        android:text="預(yù)報(bào)"
        android:textColor="#fff"
        android:textSize="20sp"/>
    <LinearLayout
        android:id="@+id/forecast_layout"
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:layout_height="wrap_content">
    </LinearLayout>

</LinearLayout>

布局使用了半透明背景,TextView定義了標(biāo)題“預(yù)報(bào)”,使用LinearLayout定義了一個(gè)顯示未來(lái)幾天天氣的布局,但未放入內(nèi)容,要根據(jù)服務(wù)器返回的數(shù)據(jù)在代碼中動(dòng)態(tài)添加。

forecast_item.xml未來(lái)天氣信息的子項(xiàng)布局

<?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="wrap_content"
    android:layout_margin="15dp">
    <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"/>
    <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:gravity="center"
        android:textColor="#fff"/>
    <TextView
        android:id="@+id/max_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_weight="1"
        android:gravity="right"
        android:textColor="#fff"/>
    <TextView
        android:id="@+id/min_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_weight="1"
        android:gravity="left"
        android:textColor="#fff"/>
    

</LinearLayout>

子項(xiàng)布局中放置了4個(gè)textView,分別用于顯示天氣預(yù)報(bào)日期,天氣概況,最高溫度和最低溫度。

aqi空氣質(zhì)量信息的布局

<?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"
    android:background="#8000">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="15dp"
        android:text="空氣質(zhì)量"
        android:textColor="#fff"
        android:textSize="20sp"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="15dp">
        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1">
            <LinearLayout
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true">
                <TextView
                    android:id="@+id/aqi_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:textColor="#fff"
                    android:textSize="40sp"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:textColor="#fff"
                    android:text="AQI指數(shù)"/>
            </LinearLayout>
        </RelativeLayout>
        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1">
            <LinearLayout
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true">
                <TextView
                    android:id="@+id/pm25_text"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:textColor="#fff"
                    android:textSize="40sp"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="PM2.5指數(shù)"
                    android:textColor="#fff"/>
            </LinearLayout>
        </RelativeLayout>
    </LinearLayout>

</LinearLayout>

使用半透明背景,最上方定義了“空氣質(zhì)量”標(biāo)題,然后實(shí)現(xiàn)了左右平分且居中對(duì)齊的布局,分別用于顯示AQI指數(shù)和PM2.5指數(shù)。

suggestion.cml作為生活建議信息的布局

<?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="15sp"
    android:background="#8000">
    <TextView
        android:layout_marginLeft="15dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="15dp"
        android:text="生活建議"
        android:textColor="#fff"
        android:textSize="20sp"/>
    <TextView
        android:id="@+id/comfort_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="15dp"
        android:textColor="#fff"/>
    <TextView
        android:id="@+id/car_wash_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="15dp"
        android:textColor="#fff"/>
    <TextView
        android:id="@+id/sport_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="15dp"
        android:textColor="#fff"/>

</LinearLayout>

同樣使用半透明背景和一個(gè)標(biāo)題,用三個(gè)textView顯示舒適度、洗車指數(shù)和運(yùn)動(dòng)建議的相關(guān)數(shù)據(jù)。

將以上布局文件引入activity_weather.xml中

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_weather"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    tools:context="com.example.stardream.coolweather.activity.WeatherActivity">
    <ScrollView
        android:id="@+id/weahter_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scrollbars="none"
        android:overScrollMode="never">
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <include layout="@layout/title"/>
            <include layout="layout/now"/>
            <include layout="@layout/forecast"/>
            <include layout="@layout/aqi"/>
            <include layout="@layout/suggestion"/>
        </LinearLayout>
    </ScrollView>

</FrameLayout>

最外層布局使用了FrameLayout,然后嵌套了一個(gè)ScrollView,可以滾動(dòng)查看屏幕之外的內(nèi)容。因?yàn)?strong>ScrollView內(nèi)部只允許存在一個(gè)子布局,因此嵌入垂直方向的Lin,將其余所有布局引入。

將天氣顯示在界面

解析天氣JSON數(shù)據(jù)

    //將返回的JSON數(shù)據(jù)解析成Weather實(shí)體類
    public static Weather handleWeatherResponse(String response){
        try{
            JSONObject jsonObject = new JSONObject(response);
            JSONArray jsonArray = jsonObject.getJSONArray("HeWeahter");
            String weatherContent = jsonArray.getJSONObject(0).toString();
            return new Gson().fromJson(weatherContent,Weather.class);
        }catch (Exception ex){
            ex.printStackTrace();
        }
        return null;
    }

在Utility中添加解析JSON數(shù)據(jù)的方法,handleWeatherResponse()方法中先通過(guò)JSONObject和JSONArray將天氣中的主體內(nèi)容解析出來(lái),之后可通過(guò)調(diào)用dromJson()方法將JSON轉(zhuǎn)換成Weather對(duì)象。

編寫WeatherActivity()中的代碼

定義控件的變量
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 aqiText;
    private TextView pm25Text;
    private TextView comfortText;
    private TextView carWashText;
    private TextView sportText;
……
}
onCreate()方法

獲取控件的實(shí)例,從本地緩存讀取天氣數(shù)據(jù),若沒(méi)有,則會(huì)從Intent中讀出天氣Id,然后調(diào)用requestWeahter()方法請(qǐng)求服務(wù)器上的數(shù)據(jù)。
注意:這里緩存中保存數(shù)據(jù)采用SharedPreferences的方式,具體用法見(jiàn) SharedPreferences存儲(chǔ)

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_weather);
        //初始化控件
        weatherLayout = (ScrollView)findViewById(R.id.weahter_layout);
        titleCity = (TextView)findViewById(R.id.title_city);
        titleUpdateTime = (TextView)findViewById(R.id.title_update_time);
        degreeText = (TextView)findViewById(R.id.degree_text);
        weatherInfoText = (TextView)findViewById(R.id.weather_info_text);
        forecastLayout = (LinearLayout)findViewById(R.id.forecast_layout);
        aqiText = (TextView)findViewById(R.id.aqi_text);
        pm25Text = (TextView)findViewById(R.id.pm25_text);
        comfortText = (TextView)findViewById(R.id.comfort_text);
        carWashText = (TextView)findViewById(R.id.car_wash_text);
        sportText = (TextView)findViewById(R.id.sport_text);
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        String weatherString = prefs.getString("weather",null);
        if(weatherString != null){
            //若有緩存直接解析天氣數(shù)據(jù)
            Weather weather = Utility.handleWeatherResponse(weatherString);
            showWeatherInfo(weather);
        }else{
            //無(wú)緩存時(shí)去服務(wù)器查詢天氣
            String weatherId = getIntent().getStringExtra("weather_id");
            weatherLayout.setVisibility(View.INVISIBLE);
            requestWeather(weatherId);
        }
    }

向服務(wù)器請(qǐng)求天氣信息

   /*
    * 根據(jù)天氣的Id向服務(wù)器請(qǐng)求天氣信息*/
    public void requestWeather(final String weatherId){
        String weatherUrl = "http://guolin.tech/api/weather?cityid="+
                weatherId+"&key=";
        HttpUtil.sendOkHttpRequest(weatherUrl, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
                runOnUiThread(new Runnable(){
                    public void run(){
                        Toast.makeText(WeatherActivity.this,"獲取天氣信息失敗",Toast.LENGTH_SHORT).show();
                    }
                });
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                final String responseText = response.body().string();
                final Weather weather = Utility.handleWeatherResponse(responseText);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if(weather !=null && "ok".equals(weather.status)){
                            SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(WeatherActivity.this).edit();
                            editor.putString("weather",responseText);
                            editor.apply();
                            showWeatherInfo(weather);
                        }else{
                            Toast.makeText(WeatherActivity.this,"獲取天氣信息失敗",Toast.LENGTH_SHORT).show();
                        }
                    }
                });

            }
        });
    }

requestWeather()方法先使用傳入的天氣id和APIKEY拼裝出接口地址,然后調(diào)用HttpUtil.sendOkHttpRequest()方法向該地址發(fā)送請(qǐng)求,服務(wù)器會(huì)以JSON格式返回天氣信息。然后在onResponse()回調(diào)中調(diào)用Utility.handleWeatherResponse()將返回的JSON數(shù)據(jù)轉(zhuǎn)換成Weather對(duì)象,將線程切換到主線程。若服務(wù)器返回的status狀態(tài)是ok,則將返回?cái)?shù)據(jù)緩存到SharedPreferences中,并進(jìn)行顯示。

showWeatherInfo()顯示天氣信息

/*處理冰戰(zhàn)士Weather實(shí)體類中的數(shù)據(jù)
    * */
    private void showWeatherInfo(Weather weather){
        String cityName = weather.basic.cityName;
        String updateTime = weather.basic.update.updateTime.split(" ")[1];
        String degree = weather.now.temperature+"℃";
        String weatherInfo = weather.now.more.info;
        titleCity.setText(cityName);
        titleUpdateTime.setText(updateTime);
        degreeText.setText(degree);
        weatherInfoText.setText(weatherInfo);
        forecastLayout.removeAllViews();
        for(Forecast forecast:weather.forecastList){
            View view = LayoutInflater.from(this).inflate(R.layout.forecas_item,forecastLayout,false);
            TextView dateText = (TextView)view.findViewById(R.id.date_text);
            TextView infoText = (TextView)view.findViewById(R.id.info_text);
            TextView maxText = (TextView)view.findViewById(R.id.max_text);
            TextView minText = (TextView)view.findViewById(R.id.min_text);
            dateText.setText(forecast.date);
            infoText.setText(forecast.more.info);
            maxText.setText(forecast.temperature.max);
            minText.setText(forecast.temperature.min);
            forecastLayout.addView(view);
        }
        if(weather.aqi != null){
            aqiText.setText(weather.aqi.city.aqi);
            pm25Text.setText(weather.aqi.city.pm25);
        }
        String comfort = "舒適度:"+weather.suggestion.comfort.info;
        String carwash = "洗車指數(shù):"+weather.suggestion.carWash.info;
        String sport = "運(yùn)動(dòng)建議:"+weather.suggestion.sport.info;
        comfortText.setText(comfort);
        carWashText.setText(carwash);
        sportText.setText(sport);
        weatherLayout.setVisibility(View.VISIBLE);
    }

從Weather對(duì)象獲取數(shù)據(jù),然后顯示到相應(yīng)的空間上。

從縣列表跳轉(zhuǎn)到天氣界面

public class ChooseAreaFragment extends Fragment {
……
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //對(duì)列表設(shè)置監(jiān)聽(tīng)事件
        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);
                    //顯示出省份對(duì)應(yīng)下city的界面
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    //記住選中的City
                    selectedCity = cityList.get(position);
                    //切換到相應(yīng)的county界面
                    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();

                }
            }
        });
        //為返回按鈕注冊(cè)監(jiān)聽(tīng)事件
        backButton.setOnClickListener(new View.OnClickListener(){
            public void onClick(View v){
                //若在county切換到City
                if(currentLevel == LEVEL_COUNTY){
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    //若在City切換到province
                    queryProvinces();
                }
            }
        });
        //初始狀態(tài)下顯示province
        queryProvinces();
    }
……
}
  在點(diǎn)擊縣級(jí)列表之后,將選中的縣的天氣id傳遞出去,啟動(dòng)WeatherActivity。
#### 修改MainActivity
  若在緩存中存在天氣信息時(shí),不再進(jìn)行選擇而是直接跳轉(zhuǎn)到天氣界面。

package com.example.stardream.coolweather.activity;

import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import com.example.stardream.coolweather.R;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    if(prefs.getString("weather",null) != null){
        Intent intent = new Intent(this,WeatherActivity.class);
        startActivity(intent);
        finish();
    }
}

}

###獲取每日一圖改變天氣背景
####修改activity_weather.xml中的代碼

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_weather"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
tools:context="com.example.stardream.coolweather.activity.WeatherActivity">

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

<ScrollView
    android:id="@+id/weahter_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:scrollbars="none"
    android:overScrollMode="never">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <include layout="@layout/title"/>
        <include layout="layout/now"/>
        <include layout="@layout/forecast"/>
        <include layout="@layout/aqi"/>
        <include layout="@layout/suggestion"/>
    </LinearLayout>
</ScrollView>

</FrameLayout>

  增加ImageView作為背景圖片。
####修改WeatherActivity中的代碼
##### ImageView控件的定義

private ImageView bingPicImg;

##### 在onCreate()中

bingPicImg = (ImageView)findViewById(R.id.bing_pic_img);
String bingPic = prefs.getString("bing_pic",null);
if(bingPic !=null){
Glide.with(this).load(bingPic).into(bingPicImg);
}else{
loadBingPic();
}
···
若緩存中有圖片,則直接調(diào)用Glide方式取出,若沒(méi)有,向服務(wù)器請(qǐng)求。

loadBingPic()方法
    /*
    * 加載必應(yīng)圖片,每日一圖*/
    private void loadBingPic(){
        String requestBingPic = "http://guolin.tech/api/bing_pic";
        HttpUtil.sendOkHttpRequest(requestBingPic, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
            }

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

            }
        });
    }
在requestWeather()方法中
loadBingPic();

每次請(qǐng)求天氣信息時(shí)也會(huì)刷新背景圖片

解決背景圖片和狀態(tài)欄沒(méi)有融合的問(wèn)題

在onCreate()方法中

 //Android5.0及以上系統(tǒng)才支持,即版本號(hào)大于等于21
        if(Build.VERSION.SDK_INT>=21){
            //調(diào)用getWindow().getDecorView()拿到當(dāng)前活動(dòng)的DecorView
            View decorView = getWindow().getDecorView();
            //改變系統(tǒng)的UI顯示,傳入的兩個(gè)值表示活動(dòng)的布局會(huì)顯示在狀態(tài)欄上面
            decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            //將狀態(tài)欄設(shè)置成透明色
            getWindow().setStatusBarColor(Color.TRANSPARENT);
        }

頭布局和系統(tǒng)狀態(tài)欄緊貼到了一起,修改activity_weather.xml中的代碼,在ScrollView的LinearLayout中增加了android:fitsSystemWindows屬性,設(shè)置成true表示會(huì)為系統(tǒng)狀態(tài)欄留出空間。

<LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true">
            <include layout="@layout/title"/>
            <include layout="layout/now"/>
            <include layout="@layout/forecast"/>
            <include layout="@layout/aqi"/>
            <include layout="@layout/suggestion"/>
        </LinearLayout>

手動(dòng)更新天氣和切換城市

手動(dòng)更新天氣

采用下拉刷新方式手動(dòng)更新天氣

修改activity_weather.xml

<!--SwipeRefreshLayout具有下拉刷新功能-->
    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <ScrollView
        ……
    </ScrollView>
    </android.support.v4.widget.SwipeRefreshLayout>

修改WeatherActivity中的代碼

定義刷新控件
private SwipeRefreshLayout swipeRefresh;
加載控件,設(shè)置下拉進(jìn)度條顏色
swipeRefresh = (SwipeRefreshLayout)findViewById(R.id.swipe_refresh);
        swipeRefresh.setColorSchemeResources(R.color.colorPrimary);
//定義一個(gè)weatherId
        final String weatherId;
實(shí)現(xiàn)更新
    if(weatherString != null){
            //若有緩存直接解析天氣數(shù)據(jù)
            Weather weather = Utility.handleWeatherResponse(weatherString);
            //若有緩存得到weatherId
            weatherId = weather.basic.weatherId;
            showWeatherInfo(weather);
        }else{
            //無(wú)緩存時(shí)去服務(wù)器查詢天氣
            weatherId = getIntent().getStringExtra("weather_id");
            weatherLayout.setVisibility(View.INVISIBLE);
            requestWeather(weatherId);
        }
        swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener(){
            public void onRefresh(){
                requestWeather(weatherId);
            }
        });
隱藏刷新進(jìn)度條
 /*
    * 根據(jù)天氣的Id向服務(wù)器請(qǐng)求天氣信息*/
    public void requestWeather(final String weatherId){
        String weatherUrl = "http://guolin.tech/api/weather?cityid="+
                weatherId+"&key=";
        HttpUtil.sendOkHttpRequest(weatherUrl, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
                runOnUiThread(new Runnable(){
                    public void run(){
                        Toast.makeText(WeatherActivity.this,"獲取天氣信息失敗",Toast.LENGTH_SHORT).show();
                        //刷新事件結(jié)束,隱藏刷新進(jìn)度條
                        swipeRefresh.setRefreshing(false);
                    }
                });
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                final String responseText = response.body().string();
                final Weather weather = Utility.handleWeatherResponse(responseText);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if(weather !=null && "ok".equals(weather.status)){
                            SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(WeatherActivity.this).edit();
                            editor.putString("weather",responseText);
                            editor.apply();
                            showWeatherInfo(weather);
                        }else{
                            Toast.makeText(WeatherActivity.this,"獲取天氣信息失敗",Toast.LENGTH_SHORT).show();
                        }
                        //刷新事件結(jié)束,隱藏刷新進(jìn)度條
                        swipeRefresh.setRefreshing(false);
                    }
                });

            }
        });
        loadBingPic();//每次請(qǐng)求天氣信息時(shí)會(huì)刷新背景圖片
    }

切換城市

在title.xml標(biāo)題欄設(shè)置按鈕

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

修改activity_weather.xml布局加入滑動(dòng)菜單功能

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_weather"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    tools:context="com.example.stardream.coolweather.activity.WeatherActivity">

    <ImageView
        android:id="@+id/bing_pic_img"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"/>
    <!--SwipeRefreshLayout具有下拉刷新功能-->
    <!--fitsSystemWindows為系統(tǒng)狀態(tài)欄留出空間-->
    <android.support.v4.widget.DrawerLayout
        android:id="@+id/drawer_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <ScrollView
        android:id="@+id/weahter_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scrollbars="none"
        android:overScrollMode="never">
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true">
            <include layout="@layout/title"/>
            <include layout="layout/now"/>
            <include layout="@layout/forecast"/>
            <include layout="@layout/aqi"/>
            <include layout="@layout/suggestion"/>
        </LinearLayout>
    </ScrollView>
    </android.support.v4.widget.SwipeRefreshLayout>
    <fragment
        android:id="@+id/choose_area_fragment"
        android:name="com.example.stardream.coolweather.activity.ChooseAreaFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"/>
    </android.support.v4.widget.DrawerLayout>

</FrameLayout>

DrawerLayout中的第一個(gè)子控件用于作為主屏幕中顯示的內(nèi)容;
第二個(gè)子控件用于作為滑動(dòng)菜單中顯示的內(nèi)容,添加了用于遍歷省市縣數(shù)據(jù)的碎片。

修改WeatherActivity中的代碼加入滑動(dòng)菜單的邏輯控制

定義Button和DrawerLayout
public DrawerLayout drawerLayout;
    private Button navButton;
onCreate()
drawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);
        navButton = (Button)findViewById(R.id.nav_button);
        navButton.setOnClickListener(new View.OnClickListener(){
            public void onClick(View v){
                drawerLayout.openDrawer(GravityCompat.START);
            }
        });

在onCreate()中獲取新增的DrawerLayout和Button的實(shí)例,然后在Button點(diǎn)擊事件中調(diào)用DrawerLayout的openDrawer()方法打開(kāi)活動(dòng)菜單即可。

請(qǐng)求新選擇的城市的天氣信息

因?yàn)樵瓉?lái)的跳轉(zhuǎn)是從MainActivity中跳轉(zhuǎn)過(guò)去的,現(xiàn)在就在WeatherActivity中,所以就關(guān)閉滑動(dòng)菜單,顯示下拉刷新進(jìn)度條,請(qǐng)求新城市的天氣信息。
在ChooseAreaFragment中的onActivityCreated()中,

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);
                    //顯示出省份對(duì)應(yīng)下city的界面
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    //記住選中的City
                    selectedCity = cityList.get(position);
                    //切換到相應(yīng)的county界面
                    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.swipeRefresh.setRefreshing(true);
                        activity.requestWeather(weatherId);
                    }


                }
            }
        });

后臺(tái)自動(dòng)更新天氣

要想自動(dòng)更新天氣,需要?jiǎng)?chuàng)建一個(gè)長(zhǎng)期在后臺(tái)運(yùn)行額定時(shí)任務(wù),因此新建一個(gè)服務(wù)AutoUpdateService。

onStartCommand()方法

package com.example.stardream.coolweather.service;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.IBinder;
import android.os.SystemClock;
import android.preference.PreferenceManager;

import com.example.stardream.coolweather.gson.Weather;
import com.example.stardream.coolweather.util.HttpUtil;
import com.example.stardream.coolweather.util.Utility;

import java.io.IOException;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;

public class AutoUpdateService extends Service {
    public AutoUpdateService() {
    }

    @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 = 8*60*60*1000;//8個(gè)小時(shí)的毫秒數(shù)
        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);
    }

在onStartCommand()方法中先調(diào)用了updateWeather()方法更新天氣,調(diào)用updateBingPic()方法更新背景圖片,將更新時(shí)間設(shè)置為8小時(shí),定時(shí)鬧鐘見(jiàn)AlarmManager用法

updateWeather()方法

  /*更新天氣信息
    * */
    private void updateWeather(){
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        String weatherString = prefs.getString("weather",null);
        if(weatherString !=null){
            //有緩存時(shí)直接解析天氣數(shù)據(jù)
            Weather weather = Utility.handleWeatherResponse(weatherString);
            String weatherId = weather.basic.weatherId;

            String weatherUrl = "http://guolin.tech/api/weather?cityid="+
                    weatherId+"&key=";
            HttpUtil.sendOkHttpRequest(weatherUrl, new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    e.printStackTrace();
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    String responseText = response.body().string();
                    Weather weather = Utility.handleWeatherResponse(responseText);
                    if(weather !=null && "ok".equals(weather.status)){
                        SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(AutoUpdateService.this).edit();
                        editor.putString("weather",responseText);
                        editor.apply();
                    }

                }
            });

        }
    }

updateBingPic()方法

 /*更新必應(yīng)每日一圖
    * */
    private void updateBingPic(){
        String requestBingPic ="http://guolin.tech/api/bing_pic";
        HttpUtil.sendOkHttpRequest(requestBingPic, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String bingPic = response.body().string();
                SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(AutoUpdateService.this).edit();
                editor.putString("bing_pic",bingPic);
                editor.apply();
            }
        });
    }

修改WeatherActivity

在showWeather()方法的最后加入啟動(dòng)AutoUpdateService這個(gè)服務(wù)的代碼,一旦選中某個(gè)城市并成功更新天氣之后,AutoUpdateService就會(huì)一直在后臺(tái)運(yùn)行,并保證每8小時(shí)更新一次天氣。

private void showWeatherInfo(Weather weather){
        ……
        }
        if(weather.aqi != null){
            aqiText.setText(weather.aqi.city.aqi);
            pm25Text.setText(weather.aqi.city.pm25);
            Intent intent = new Intent(this, AutoUpdateService.class);
            startService(intent);
        }
       ……
    }

修改圖標(biāo)和名稱

在AndroidManifest.xml修改圖標(biāo)

android:icon="@mipmap/logo"

在strings.xml修改app名稱

<string name="app_name" translatable="false">我的天氣</string>

終于學(xué)(照貓畫虎)完了這個(gè)天氣預(yù)報(bào),可是還沒(méi)有正常運(yùn)行,據(jù)說(shuō)是和風(fēng)天氣接口過(guò)期了,汗顏??赡苓€要重新找新的API調(diào)用。


emmmmm……我來(lái)為其正名,接口沒(méi)問(wèn)題,是自己的原因,天氣數(shù)據(jù)是可以成功調(diào)出來(lái)的,現(xiàn)在界面處理方面還有點(diǎn)問(wèn)題,debug中……


我發(fā)現(xiàn)這本書(shū)的代碼還是有問(wèn)題的,如下:

  • 1.ListView與ArrayAdapter的使用不當(dāng),每當(dāng)適配器中的內(nèi)容發(fā)生變化時(shí),要再一次載入listView。
adapter = new ArrayAdapter<>(getContext(),android.R.layout.simple_list_item_1,dataList);
            //載入listView
            listView.setAdapter(adapter);
  • 2.每一次在數(shù)據(jù)庫(kù)中查詢是否存在省市縣以及天氣的數(shù)據(jù),若存在則直接取出顯示出來(lái)。這個(gè)完全沒(méi)有考慮存在于數(shù)據(jù)庫(kù)中的數(shù)據(jù)是否是選中的省市縣,不然會(huì)導(dǎo)致選中的省市縣和顯示出來(lái)的不相符,因此要添加限制條件。整個(gè)這一塊的代碼都很混亂。。。在經(jīng)歷心情第一喪之后終于調(diào)出來(lái)了,細(xì)節(jié)修改的地方忘記了,核心在這里。
queryCities()
//在數(shù)據(jù)庫(kù)中查詢對(duì)應(yīng)的City數(shù)據(jù)
        //原來(lái)代碼的問(wèn)題時(shí)把所有City的數(shù)據(jù)取出來(lái)了,然后就發(fā)生混亂
        //應(yīng)該取出的是選中省份的city
        cityList = DataSupport.where("provinceId = ?",String.valueOf(selectedProvince.getId())).find(City.class);
queryCounties()
//在數(shù)據(jù)庫(kù)中查詢對(duì)應(yīng)的county數(shù)據(jù)
        //原來(lái)代碼的問(wèn)題時(shí)把所有County的數(shù)據(jù)取出來(lái)了,然后就發(fā)生混亂
        //應(yīng)該取出的是選中city的county
        countyList = DataSupport.where("cityId = ?",String.valueOf(selectedCity.getId())).find(County.class);

界面展示

Icon

主界面

省級(jí)—甘肅

市級(jí)—臨夏

最關(guān)鍵的界面圖片總是上傳失敗,不知道是什么原因。。。不傳了,真是想吐槽這個(gè)上傳圖片功能。
詳細(xì)代碼請(qǐng)見(jiàn)CoolWeather源代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,023評(píng)論 25 709
  • 我在等風(fēng)也等你 可是 只等到了風(fēng)卻沒(méi)有你 我向街口看看 來(lái)來(lái)往往的人 唯獨(dú)沒(méi)有你 你看 陽(yáng)光正好 微風(fēng)不燥 怎么就...
    烤魚(yú)咯閱讀 264評(píng)論 0 0
  • 2017年12月20日農(nóng)歷11月3日,星期六,天氣睛 1.扎根三年,堅(jiān)持早起YY頻道學(xué)習(xí)第046天。 2.扎根三年...
    _紫霞閱讀 277評(píng)論 0 0
  • 1 二十多歲的時(shí)候,我最喜歡的兩首歌,一首是鄭秀文的《感情線上》,一首是王菲的《給自己的情書(shū)》,經(jīng)年播放,從來(lái)不厭...
    蔡尖尖閱讀 502評(píng)論 0 3

友情鏈接更多精彩內(nèi)容