酷欧天气的开发

简介

参考《第一行代码》,开发出一款全国省市县的天气预报app.

创建数据库和表

使用LitePal对数据库进行操作,创建三个实体类分别是Province、City和County。

1. 添加依赖项
compile 'org.litepal.android:core:1.3.2'
2. 创建实体类
package com.example.stardream.coolweather.db;

import org.litepal.crud.DataSupport;

/**
 * Created by StarDream on 2018/8/22.
 */
//LitePal中的每一个实体类都应该继承DataSupport
public class Province extends DataSupport {
    private int id;  //实体类具有的id
    private String provinceName;  //省份的名字
    private int provinceCode;  //省的代号

    public int getId() {
        return id;
    }

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

    public String getProvinceName() {
        return provinceName;
    }

    public void setProvinceName(String provinceName) {
        this.provinceName = provinceName;
    }

    public int getProvinceCode() {
        return provinceCode;
    }

    public void setProvinceCode(int provinceCode) {
        this.provinceCode = provinceCode;
    }
}

City实体类和County实体类同理。每个实体类代表一张表,实体类中的属性代表表中的每一列。

3. 配置litepal.xml文件
<litepal>
    <!--dbname 为数据库的名字-->
    <dbname value="cool_weather"/>
    <!--数据库版本指定为1-->
    <version value="1"/>
    <!--将实体类添加到映射列表中-->
    <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文件

将项目的application配置为org.litepal.LitePalApplication

android:name="org.litepal.LitePalApplication"

关于LitePal的具体内容详见LitePal详解

遍历全国省市县数据

1. 客户端与服务器的交互

package com.example.stardream.coolweather.util;

import okhttp3.OkHttpClient;
import okhttp3.Request;

/**
 * Created by StarDream on 2018/8/22.
 */
//采用OkHttp与服务器进行通信
public class HttpUtil {
    //与服务器进行交互发起一条http请求只需要调用sendOkHttpRequest()即可
    //传入要请求的地址,注册一个回调来处理服务器的响应
    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);
    }
}

发起http请求只需调用sendOkHttprequest()这个方法,传入一个地址,并且注册一个回调来处理服务器的响应。

2. 处理服务器返回的Json格式的数据

新建一个Utility类处理和解析Json数据。

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 {
    //处理和解析省份的数据
    public static boolean hanldeProvinceResponse(String response){
        if(!TextUtils.isEmpty(response)){
            try{
                //json的对象的数组,用来接收传回的多个省份的数据
                JSONArray allProvinces = new JSONArray(response);
                for(int i=0;i<allProvinces.length();i++){
                    //取出每一个省份
                    JSONObject provinceObject = allProvinces.getJSONObject(i);
                    Province province = new Province();
                    //解析出省份的id并将其赋值给province对象
                    province.setProvinceCode(provinceObject.getInt("id"));
                    //解析出省份的name并将其赋值给province对象
                    province.setProvinceName(provinceObject.getString("name"));
                    //将这一个省份保存到表中
                    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 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;
    }
    //处理和解析县的数据
    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;
    }
}

省级、市级和县级对服务器发来的数据的处理解析方式都是类似的,使用JSONArrayJSONObject进行解析,然后组装成实体类对象,再调用save()方法存储到数据库中。

3. 遍历省市县

遍历省市县功能.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">
    <!--布局高度为actionBar高度,背景色为colorPrimary-->
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary">
   <!--用于显示标题内容-->
        <TextView
            android:id="@+id/title_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:textColor="#fff"
            android:textSize="20sp"/>
        <!--返回按钮-->
        <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"/>
        <!--省市县的数据显示在这里-->
        <!--listView会自动给每个子项之间增加一条分割线-->
        <ListView
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            android:id="@+id/list_view/>
    </RelativeLayout>
</LinearLayout>

一般情况下标题栏可以采用ActionBar,但是碎片中最好不用ActionBar或Toolbar,否则会有问题。

遍历省市县碎片

1. 新建一个ChooseAreaFragment用于展示查询界面和实现基本查询功能,定义一些量值。

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<>();//动态数组
    //省列表
    private List<Province> provinceList;
    //市列表
    private List<City> cityList;
    //县列表
    private List<County> countyList;
    //选中的省份
    private Province selectedProvince;
    //选中的城市
    private City selectedCity;
    //当前选中的级别
    private int currentLevel;
}

2. onCreateView()方法中获取到一些控件的实例,初始化了ArrayAdapter,将其设置为ListView的适配器。

   @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        /*
        * 【LayoutInflater】其实是在res/layout/下找到xml布局文件,并且将其实例化,
        * 对于一个没有被载入或者想要动态载入的界面,都需要使用LayoutInflater.inflate()来载入;
        * */
        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()方法中设置ListView和Button的点击事件,在这里完成了基本的初始化操作,调用queryProvinces()方法,加载省级数据。

    @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);
                    //显示出省份对应下city的界面
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    //记住选中的City
                    selectedCity = cityList.get(position);
                    //切换到相应的county界面
                    queryCounties();
                }
            }
        });
        //为返回按钮注册监听事件
        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();
                }
            }
        });
        //初始状态下显示province
        queryProvinces();
    }

4. 在queryProvinces()方法中,设置头布局的标题,返回按钮。调用LitePal查询结构读取省级数据,若读取到则将其显示在界面上,若没有则调用queryServer()方法从服务器查询数据。queryCities()方法和queryCounty()方法同理。

  • 查询省级数据
/*查询全国所有的省,先从数据库查,没有的话去服务器查询
    * */
    private void queryProvinces(){
        //设置标题栏
        titleText.setText("中国");
        //隐藏返回按钮
        backButton.setVisibility(View.GONE);
        //在数据库中查询所有省份
        provinceList = DataSupport.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");
        }
    }
  • 查询市级数据
/*查询对应省的City数据,优先从数据库查,若没有,则去服务器查询
    * */
    private void queryCities(){
        //设置标题栏
        titleText.setText(selectedProvince.getProvinceName());
        //设置返回按钮可见
        backButton.setVisibility(View.VISIBLE);
        //在数据库中查询对应的City数据
        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");
        }
    }
  • 查询县级数据
 /*查询选中的市内的所有县,优先从数据库查,若没有则去服务器查询
    * */
    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()方法中,调用HttpUtil中的sendOkHttpRequest()方法向服务器发送请求,响应的数据回调到onResponse()方法中,然后调用Utility中的handleProvincesResponse()方法解析和处理服务器返回的数据,然后将其存储到数据库中。最后再次调用queryProvinces()方法重新加载省级数据,因为queryProvinces()涉及UI操作,则须在主线程中调用,因此借助runOnUiThread()方法从子线程切换到主线程

  • 问题:queryFromServer()在哪里说明是是在子线程中执行的???
   /*根据传入的地址和类型从服务器上获取数据
    * */
    private void queryFromServer(String address,final String type){
        //未查出之前显示出进度条框
        showProgressDialog();
        HttpUtil.sendOkHttpRequest(address, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                //通过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. 涉及到的进度条框

  • 进度条框的显示
    //显示进度条框
    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();
        }
    }

将碎片添加到活动

1. 修改activity_main.xml中的代码

定义一个FrameLayout,将ChooseAreaFragment加入,并让其充满整个布局。

 <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. 权限问题

在AndroidManifest.xml中添加网络权限。

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

显示天气信息

因为和风天气返回的天气信息的JSON数据结构非常复杂,若使用JSONObject解析比较麻烦,因此使用GSON对天气信息进行解析。
在这里应该加入JSONObject和GSON解析和处理数据的区别

1. 定义GSON实体类

返回天气信息的格式:


返回数据的格式

要为basic、aqi、now、suggestion和daily_forecase定义实体类。

basic
basic具体内容

在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字段建立映射关系
* */
public class Basic {
    //"city"与cityName建立映射关系
    @SerializedName("city")
    public String cityName;
    
    //"id"与weatherId建立映射关系
    @SerializedName("id")
    public String weatherId;
    
    @SerializedName("update")
    public Update update;
    public class Update{
        //"loc"与updateTime建立映射关系
        @SerializedName("loc")
        public String updateTime;
    }
}
aqi
aqi具体内容
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”没有使用SerilizedName???

now
now具体内容
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具体内容

daily_forecase内包含的是一个数组,只定义出单日天气的实体类,在声明实体类引用的时候使用集合类型来进行声明。
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;
}

}

总实体类Weather

创建一个总的实体类来引用印上的各个实体类。

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中包含的是一个数组,
    //这里使用List集合来引用Forecast类
    @SerializedName("daily_forecast")
    public List<Forecast> forecastList;
}

2. 编写天气界面

天气信息界面,activity_weather.xml

为了让代码相对整齐,采用引用布局技术,将界面的不同部分写在不同的文件中,再通过引入布局的方式集成到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>

其中,一个居中显示城市名,一个居右显示更新时间。

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: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>

其中,放置了两个textView,一个用来显示气温,另外一个用于显示天气概况。

forecast.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"
    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"/>
    <LinearLayout
        android:id="@+id/forecast_layout"
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:layout_height="wrap_content">
    </LinearLayout>

</LinearLayout>

布局使用了半透明背景,TextView定义了标题“预报”,使用LinearLayout定义了一个显示未来几天天气的布局,但未放入内容,要根据服务器返回的数据在代码中动态添加。

forecast_item.xml未来天气信息的子项布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="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>

子项布局中放置了4个textView,分别用于显示天气预报日期,天气概况,最高温度和最低温度。

aqi空气质量信息的布局

<?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="空气质量"
        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指数"/>
            </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指数"
                    android:textColor="#fff"/>
            </LinearLayout>
        </RelativeLayout>
    </LinearLayout>

</LinearLayout>

使用半透明背景,最上方定义了“空气质量”标题,然后实现了左右平分且居中对齐的布局,分别用于显示AQI指数和PM2.5指数。

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>

同样使用半透明背景和一个标题,用三个textView显示舒适度、洗车指数和运动建议的相关数据。

将以上布局文件引入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,然后嵌套了一个ScrollView,可以滚动查看屏幕之外的内容。因为ScrollView内部只允许存在一个子布局,因此嵌入垂直方向的Lin,将其余所有布局引入。

将天气显示在界面

解析天气JSON数据

    //将返回的JSON数据解析成Weather实体类
    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数据的方法,handleWeatherResponse()方法中先通过JSONObject和JSONArray将天气中的主体内容解析出来,之后可通过调用dromJson()方法将JSON转换成Weather对象。

编写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()方法

获取控件的实例,从本地缓存读取天气数据,若没有,则会从Intent中读出天气Id,然后调用requestWeahter()方法请求服务器上的数据。
注意:这里缓存中保存数据采用SharedPreferences的方式,具体用法见 SharedPreferences存储

@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){
            //若有缓存直接解析天气数据
            Weather weather = Utility.handleWeatherResponse(weatherString);
            showWeatherInfo(weather);
        }else{
            //无缓存时去服务器查询天气
            String weatherId = getIntent().getStringExtra("weather_id");
            weatherLayout.setVisibility(View.INVISIBLE);
            requestWeather(weatherId);
        }
    }

向服务器请求天气信息

   /*
    * 根据天气的Id向服务器请求天气信息*/
    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拼装出接口地址,然后调用HttpUtil.sendOkHttpRequest()方法向该地址发送请求,服务器会以JSON格式返回天气信息。然后在onResponse()回调中调用Utility.handleWeatherResponse()将返回的JSON数据转换成Weather对象,将线程切换到主线程。若服务器返回的status状态是ok,则将返回数据缓存到SharedPreferences中,并进行显示。

showWeatherInfo()显示天气信息

/*处理冰战士Weather实体类中的数据
    * */
    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 = "洗车指数:"+weather.suggestion.carWash.info;
        String sport = "运动建议:"+weather.suggestion.sport.info;
        comfortText.setText(comfort);
        carWashText.setText(carwash);
        sportText.setText(sport);
        weatherLayout.setVisibility(View.VISIBLE);
    }

从Weather对象获取数据,然后显示到相应的空间上。

从县列表跳转到天气界面

public class ChooseAreaFragment extends Fragment {
……
    @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);
                    //显示出省份对应下city的界面
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    //记住选中的City
                    selectedCity = cityList.get(position);
                    //切换到相应的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();

                }
            }
        });
        //为返回按钮注册监听事件
        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();
                }
            }
        });
        //初始状态下显示province
        queryProvinces();
    }
……
}
  在点击县级列表之后,将选中的县的天气id传递出去,启动WeatherActivity。
#### 修改MainActivity
  若在缓存中存在天气信息时,不再进行选择而是直接跳转到天气界面。

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();
}
···
若缓存中有图片,则直接调用Glide方式取出,若没有,向服务器请求。

loadBingPic()方法
    /*
    * 加载必应图片,每日一图*/
    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();

每次请求天气信息时也会刷新背景图片

解决背景图片和状态栏没有融合的问题

在onCreate()方法中

 //Android5.0及以上系统才支持,即版本号大于等于21
        if(Build.VERSION.SDK_INT>=21){
            //调用getWindow().getDecorView()拿到当前活动的DecorView
            View decorView = getWindow().getDecorView();
            //改变系统的UI显示,传入的两个值表示活动的布局会显示在状态栏上面
            decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            //将状态栏设置成透明色
            getWindow().setStatusBarColor(Color.TRANSPARENT);
        }

头布局和系统状态栏紧贴到了一起,修改activity_weather.xml中的代码,在ScrollView的LinearLayout中增加了android:fitsSystemWindows属性,设置成true表示会为系统状态栏留出空间。

<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>

手动更新天气和切换城市

手动更新天气

采用下拉刷新方式手动更新天气

修改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;
加载控件,设置下拉进度条颜色
swipeRefresh = (SwipeRefreshLayout)findViewById(R.id.swipe_refresh);
        swipeRefresh.setColorSchemeResources(R.color.colorPrimary);
//定义一个weatherId
        final String weatherId;
实现更新
    if(weatherString != null){
            //若有缓存直接解析天气数据
            Weather weather = Utility.handleWeatherResponse(weatherString);
            //若有缓存得到weatherId
            weatherId = weather.basic.weatherId;
            showWeatherInfo(weather);
        }else{
            //无缓存时去服务器查询天气
            weatherId = getIntent().getStringExtra("weather_id");
            weatherLayout.setVisibility(View.INVISIBLE);
            requestWeather(weatherId);
        }
        swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener(){
            public void onRefresh(){
                requestWeather(weatherId);
            }
        });
隐藏刷新进度条
 /*
    * 根据天气的Id向服务器请求天气信息*/
    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();
                        //刷新事件结束,隐藏刷新进度条
                        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();
                        }
                        //刷新事件结束,隐藏刷新进度条
                        swipeRefresh.setRefreshing(false);
                    }
                });

            }
        });
        loadBingPic();//每次请求天气信息时会刷新背景图片
    }

切换城市

在title.xml标题栏设置按钮

<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布局加入滑动菜单功能

<?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为系统状态栏留出空间-->
    <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中的第一个子控件用于作为主屏幕中显示的内容;
第二个子控件用于作为滑动菜单中显示的内容,添加了用于遍历省市县数据的碎片。

修改WeatherActivity中的代码加入滑动菜单的逻辑控制

定义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的实例,然后在Button点击事件中调用DrawerLayout的openDrawer()方法打开活动菜单即可。

请求新选择的城市的天气信息

因为原来的跳转是从MainActivity中跳转过去的,现在就在WeatherActivity中,所以就关闭滑动菜单,显示下拉刷新进度条,请求新城市的天气信息。
在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);
                    //显示出省份对应下city的界面
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    //记住选中的City
                    selectedCity = cityList.get(position);
                    //切换到相应的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);
                    }


                }
            }
        });

后台自动更新天气

要想自动更新天气,需要创建一个长期在后台运行额定时任务,因此新建一个服务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个小时的毫秒数
        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()方法中先调用了updateWeather()方法更新天气,调用updateBingPic()方法更新背景图片,将更新时间设置为8小时,定时闹钟见AlarmManager用法

updateWeather()方法

  /*更新天气信息
    * */
    private void updateWeather(){
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        String weatherString = prefs.getString("weather",null);
        if(weatherString !=null){
            //有缓存时直接解析天气数据
            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()方法

 /*更新必应每日一图
    * */
    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()方法的最后加入启动AutoUpdateService这个服务的代码,一旦选中某个城市并成功更新天气之后,AutoUpdateService就会一直在后台运行,并保证每8小时更新一次天气。

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);
        }
       ……
    }

修改图标和名称

在AndroidManifest.xml修改图标

android:icon="@mipmap/logo"

在strings.xml修改app名称

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

终于学(照猫画虎)完了这个天气预报,可是还没有正常运行,据说是和风天气接口过期了,汗颜。可能还要重新找新的API调用。


emmmmm……我来为其正名,接口没问题,是自己的原因,天气数据是可以成功调出来的,现在界面处理方面还有点问题,debug中……


我发现这本书的代码还是有问题的,如下:

  • 1.ListView与ArrayAdapter的使用不当,每当适配器中的内容发生变化时,要再一次载入listView。
adapter = new ArrayAdapter<>(getContext(),android.R.layout.simple_list_item_1,dataList);
            //载入listView
            listView.setAdapter(adapter);
  • 2.每一次在数据库中查询是否存在省市县以及天气的数据,若存在则直接取出显示出来。这个完全没有考虑存在于数据库中的数据是否是选中的省市县,不然会导致选中的省市县和显示出来的不相符,因此要添加限制条件。整个这一块的代码都很混乱。。。在经历心情第一丧之后终于调出来了,细节修改的地方忘记了,核心在这里。
queryCities()
//在数据库中查询对应的City数据
        //原来代码的问题时把所有City的数据取出来了,然后就发生混乱
        //应该取出的是选中省份的city
        cityList = DataSupport.where("provinceId = ?",String.valueOf(selectedProvince.getId())).find(City.class);
queryCounties()
//在数据库中查询对应的county数据
        //原来代码的问题时把所有County的数据取出来了,然后就发生混乱
        //应该取出的是选中city的county
        countyList = DataSupport.where("cityId = ?",String.valueOf(selectedCity.getId())).find(County.class);

界面展示

Icon

主界面

省级—甘肃

市级—临夏

最关键的界面图片总是上传失败,不知道是什么原因。。。不传了,真是想吐槽这个上传图片功能。
详细代码请见CoolWeather源代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,498评论 25 707
  • 我在等风也等你 可是 只等到了风却没有你 我向街口看看 来来往往的人 唯独没有你 你看 阳光正好 微风不燥 怎么就...
    烤鱼咯阅读 245评论 0 0
  • 2017年12月20日农历11月3日,星期六,天气睛 1.扎根三年,坚持早起YY频道学习第046天。 2.扎根三年...
    _紫霞阅读 234评论 0 0
  • 1 二十多岁的时候,我最喜欢的两首歌,一首是郑秀文的《感情线上》,一首是王菲的《给自己的情书》,经年播放,从来不厌...
    蔡尖尖阅读 456评论 0 3