Android 实践:做一款可用的天气 APP

3,437 阅读38分钟
原文链接: blog.csdn.net

可能很多人会问:之前已经写过一篇博文来介绍怎么做一款简单的新闻APP(blog.csdn.net/yiwei12/art…),为什么还要专门一篇来介绍怎么做一款天气 APP,毕竟网络请求和数据处理都是大同小异的。如果真的要说差别的话,前一篇只是具备了一些基本的功能,来说明怎么请求和处理返回的数据,但还不足与在日常生活中使用?这一篇实践是来做一款日常可用的天气 APP - 彼时天气

—- 说明: 彼时天气仿照魅族 Flyme 天气设计
—- 在 coolWeather 的基础上进行处理


总体思路


Created with Raphaël 2.1.0开始有无缓存?直接加载缓存文件有无网络?通过百度SDK 获取位置OKHttp向接口请求对应位置的天气信息GSON 对返回的数据进行解析将返回的数据显示并写入缓存文件直接退出yesnoyesno

这就是总体的设计思路,至于后面其他的功能:选择地区,更新频率等功能可以之后再说


运行GIF


这里写图片描述

之所以大幅度提前展示 GIF 图,方便对后面布局部分有更好的理解


步骤


  1. 声明权限
  2. 依赖库
  3. 网络请求
  4. 网络解析
  5. 界面布局
  6. 最后
  7. 完整代码下载地址(github)

声明权限


因为我们需要用到百度SDK的定位服务,所以需要先下载百度中包含基础定位的 SDK(lbsyun.baidu.com/sdk/downloa…),解压后,将其中的 .jar 文件移动到 libs 文件夹中,在 main 目录下新建一个 jniLibs,将剩下的几个文件夹复制到里面,点击 Sync 按钮进行同步。 在 AndroidManifest 文件中声明权限:

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

基本上都是 百度SDK 所需要的权限,其中部分权限是需要进行运行时处理的。同时还需要添加:

    <meta-data
            android:name="com.baidu.lbsapi.API_KEY"
            android:value="SmwjePIXo1eeRGbjw8QKrbncWfgi5V0f" />

        <service
            android:name="com.baidu.location.f"
            android:enabled="true"
            android:process=":remote" />

value 中填写自己申请获取的 Key,其他的格式都是固定的,这样我们就可以调用 基础定位 中的功能了


依赖库


    compile 'org.litepal.android:core:1.4.1'               // 数据库框架
    compile 'com.squareup.okhttp3:okhttp:3.4.1'            // 网络请求
    compile 'com.google.code.gson:gson:2.7'                // 网络解析
    compile 'com.github.bumptech.glide:glide:3.8.0'        // 图片加载
    compile 'com.android.support:cardview-v7:24.2.1'       // 卡片式布局
    compile 'com.android.support:design:24.2.1'            // Material Design中用到的依赖库
    compile 'net.danlew:android.joda:2.9.9'                // 时间处理

网络请求


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

网路解析


本次的数据来源是和风天气(www.heweather.com/documents/a…),O(∩_∩)O哈哈~第一行代码的同学有没有很熟悉

这里写图片描述
参数

这里写图片描述
请求地址

{
    "HeWeather5": [
        {
            "alarms": [
                {
                    "level": "蓝色",
                    "stat": "预警中",
                    "title": "山东省青岛市气象台发布大风蓝色预警",
                    "txt": "青岛市气象台2016年08月29日15时24分继续发布大风蓝色预警信号:预计今天下午到明天,我市北风风力海上6到7级阵风9级,陆地4到5阵风7级,请注意防范。",
                    "type": "大风"
                }
            ],
            "aqi": {
                "city": {
                    "aqi": "60",
                    "co": "0",
                    "no2": "14",
                    "o3": "95",
                    "pm10": "67",
                    "pm25": "15",
                    "qlty": "良",  //共六个级别,分别:优,良,轻度污染,中度污染,重度污染,严重污染
                    "so2": "10"
                }
            },
            "basic": {
                "city": "青岛",
                "cnty": "中国",
                "id": "CN101120201",
                "lat": "36.088000",
                "lon": "120.343000",
                "prov": "山东"  //城市所属省份(仅限国内城市)
                "update": {
                    "loc": "2016-08-30 11:52",
                    "utc": "2016-08-30 03:52"
                }
            },
            "daily_forecast": [
                {
                    "astro": {
                        "mr": "03:09",
                        "ms": "17:06",
                        "sr": "05:28",
                        "ss": "18:29"
                    },
                    "cond": {
                        "code_d": "100",
                        "code_n": "100",
                        "txt_d": "晴",
                        "txt_n": "晴"
                    },
                    "date": "2016-08-30",
                    "hum": "45",
                    "pcpn": "0.0",
                    "pop": "8",
                    "pres": "1005",
                    "tmp": {
                        "max": "29",
                        "min": "22"
                    },
                    "vis": "10",
                    "wind": {
                        "deg": "339",
                        "dir": "北风",
                        "sc": "4-5",
                        "spd": "24"
                    }
                }
            ],
            "hourly_forecast": [
                {
                    "cond": {
                        "code": "100",
                        "txt": "晴"
                    },
                    "date": "2016-08-30 12:00",
                    "hum": "47",
                    "pop": "0",
                    "pres": "1006",
                    "tmp": "29",
                    "wind": {
                        "deg": "335",
                        "dir": "西北风",
                        "sc": "4-5",
                        "spd": "36"
                    }
                }
            ],
            "now": {
                "cond": {
                    "code": "100",
                    "txt": "晴"
                },
                "fl": "28",
                "hum": "41",
                "pcpn": "0",
                "pres": "1005",
                "tmp": "26",
                "vis": "10",
                "wind": {
                    "deg": "330",
                    "dir": "西北风",
                    "sc": "6-7",
                    "spd": "34"
                }
            },
            "status": "ok",
            "suggestion": {
                "comf": {
                    "brf": "较舒适",
                    "txt": "白天天气晴好,您在这种天气条件下,会感觉早晚凉爽、舒适,午后偏热。"
                },
                "cw": {
                    "brf": "较不宜",
                    "txt": "较不宜洗车,未来一天无雨,风力较大,如果执意擦洗汽车,要做好蒙上污垢的心理准备。"
                },
                "drsg": {
                    "brf": "热",
                    "txt": "天气热,建议着短裙、短裤、短薄外套、T恤等夏季服装。"
                },
                "flu": {
                    "brf": "较易发",
                    "txt": "虽然温度适宜但风力较大,仍较易发生感冒,体质较弱的朋友请注意适当防护。"
                },
                "sport": {
                    "brf": "较适宜",
                    "txt": "天气较好,但风力较大,推荐您进行室内运动,若在户外运动请注意防风。"
                },
                "trav": {
                    "brf": "适宜",
                    "txt": "天气较好,风稍大,但温度适宜,是个好天气哦。适宜旅游,您可以尽情地享受大自然的无限风光。"
                },
                "uv": {
                    "brf": "强",
                    "txt": "紫外线辐射强,建议涂擦SPF20左右、PA++的防晒护肤品。避免在10点至14点暴露于日光下。"
                }
            }
        }
    ]
}

返回类型示例

我们后面申请数据采用的参数都为 城市名称,从返回示例中我们可以看出,结构和我们上次新闻API 返回的结构是有差异的,多嵌套了一层,不过 GSON 解析的方式还是一样的。在包名下新建一个 gson 文件夹,在里面新建对应数据的实体类:

AQI.class

public class AQI {

    public AQICity city;

    public class AQICity{
        public String aqi;
        public String pm25;
        public String co;
        public String o3;
        public String pm10;
        public String so2;
    }
}

Basic.class

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

    @SerializedName("id")
    public String weatherId;

    public  Update update;

    public class Update{
        public String loc;

    }


}

Forecast.class

public class Forecast {

    public String date;

    @SerializedName("tmp")
    public Temperature temperature;

    @SerializedName("cond")
    public More more;

    public class More{
        @SerializedName("txt_d")
        public String info;

        @SerializedName("code_d")
        public int code;
    }

    public class Temperature{
        public String max;
        public String min;
    }

}

Hourly.class

public class Hourly {

    public Cond cond;

    public class Cond{

        public String code;

        public String txt;

    }

    public String date;

    public String tmp;
}

Now.class

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

    @SerializedName("cond")
    public More more;

    public class More{
        @SerializedName("txt")
        public String info;
    }

}

Suggestion.class

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

    @SerializedName("cw")
    public CarWash carWash;

    public Sport sport;

    @SerializedName("drsg")
    public Clothes clothes;

    @SerializedName("flu")
    public Cold cold;


    public UV uv;

    public class UV{
        @SerializedName("txt")
        public String info;

        @SerializedName("brf")
        public String sign;
    }

    public class Cold{
        @SerializedName("txt")
        public String info;

        @SerializedName("brf")
        public String sign;
    }

    public class Clothes{
        @SerializedName("txt")
        public String info;

        @SerializedName("brf")
        public String sign;
    }

    public class Comfort{
        @SerializedName("txt")
        public String info;

        @SerializedName("brf")
        public String sign;
    }

    public class CarWash{
        @SerializedName("txt")
        public String info;

        @SerializedName("brf")
        public String sign;
    }

    public class Sport{
        @SerializedName("txt")
        public String info;

        @SerializedName("brf")
        public String sign;
    }
}

最后就是返回数据的对应实体类 Weather.class

public class Weather {

    public String status;

    public Basic basic;

    public AQI aqi;

    public Now now;

    public Suggestion suggestion;

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

    @SerializedName("hourly_forecast")
    public List<Hourly> hourlyList;
}

在包名下新建目录 util , 在其中新建类:Utility.class

public class Utility {
    /**
     *
     * 处理得到的 weather 数据,转化为 weather 对象
     */
    public static Weather handleWeatherResponse(String response){
        try{
            JSONObject jsonObject = new JSONObject(response);
            JSONArray jsonArray = jsonObject.getJSONArray("HeWeather5");
            String weatherContent = jsonArray.getJSONObject(0).toString();
            return new Gson().fromJson(weatherContent, Weather.class);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

}

界面布局


相信大家都看了运行的 GIF 图,可以看到主界面的布局还是比较繁琐的,所以引入布局不失为一个好的选择,主界面布局主要分为以下几个部分:

weather_title(标题栏)
weather_now(当前天气信息)
weather_hourly(小时天气预报)
weather_forecast(未来几天的天气预报)
weather_aqi(空气质量)
weather_suggestion(生活建议)

以下是各部分的代码:

weather_title:

<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="@color/colorWhite"
    app:popupTheme="@style/ToolbarPopupTheme"
    app:subtitleTextColor="@color/colorFont">

    <TextView
        android:id="@+id/title_city"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textColor="@color/colorFont"
        android:textSize="20sp"/>


</android.support.v7.widget.Toolbar>

Toolbar 中间放置了 title_city ,用来显示当前的城市名

weather_now

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="400dp"
    android:background="@color/colorWhite">


    <RelativeLayout
        android:id="@+id/weather_now_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorWhite">

        <TextView
            android:id="@+id/degree_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:textColor="@color/colorFont"
            android:textSize="120sp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@+id/degree_text"
            android:layout_alignTop="@+id/degree_text"
            android:text="°"
            android:textColor="@color/colorFont"
            android:textSize="120sp"
            />

        <TextView
            android:id="@+id/weather_info_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_above="@+id/degree_text"
            android:layout_centerHorizontal="true"
            android:textColor="@color/colorFont"
            android:textSize="20sp"/>

        <TextView
            android:id="@+id/update_time_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/degree_text"
            android:layout_centerHorizontal="true"
            android:textColor="@color/colorFont"
            android:textSize="12sp"/>



    </RelativeLayout>

</RelativeLayout>

在视图中间,从上至下放置了三个TextView 控件:weather_info_text(天气状况),degree_text(天气温度),update_time_text(数据更新时间),另外一个“°”符号放在温度的右上角

weather_hourly:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:background="@color/colorWhite">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/weather_hourly"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"/>


</LinearLayout>

为了支持小时天气预报部分可以直接水平滑动(不过免费用户可以得到的数据量好像不需要滑动 2333333),放置了一个 RecyclerView

weather_hourly_item

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="80dp"
    android:layout_height="100dp"
    android:padding="10dp">

    <TextView
        android:id="@+id/hour_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/colorFont"
        android:layout_centerInParent="true"
        android:textSize="14sp"/>

    <TextView
        android:id="@+id/hour_degree"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/colorFont"
        android:textSize="14sp"
        android:layout_below="@+id/hour_text"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="4dp"/>

    <TextView
        android:id="@+id/hout_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/colorFont"
        android:textSize="14sp"
        android:layout_above="@+id/hour_text"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="4dp"/>


</RelativeLayout>

小时天气预报的每个子项中,从上到下放置了三个TextView控件:hour_degree(天气温度),hour_text(天气描述),hout_time(时间)

weather_forecast:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/colorWhite">


    <LinearLayout
        android:id="@+id/forecast_layout"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    </LinearLayout>


</LinearLayout>

这里采用的还是原方案,直接设置一个 LinearLayout 布局,后面直接在其中添加子布局,当然,大家也可以选用一个 ListView 来显示内容

weather_forecast_item

<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/data_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:layout_gravity="center_vertical"
        android:gravity="left"
        android:textColor="@color/colorFont"
        />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/weather_pic"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical|right"/>

        <TextView
            android:id="@+id/info_text"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:layout_marginLeft="15dp"
            android:layout_gravity="center_vertical|left"
            android:textColor="@color/colorFont"
            android:gravity="left"
            />

    </LinearLayout>

    <TextView
        android:id="@+id/max_min_text"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:gravity="right"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:textColor="@color/colorFont"
        />




</LinearLayout>

子项中水平放置了四个控件:data_text(预报日期),weather_pic(天气图片),info_text(天气信息),max_min_text(当天温度)

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

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="75dp">

        <RelativeLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:background="@color/colorWhite"
            >

            <TextView
                android:id="@+id/aqi_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:textColor="@color/colorFont"
                android:layout_centerInParent="true"
                android:textSize="18sp"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_below="@+id/aqi_text"
                android:text="AQI 指数"
                android:textSize="10sp"
                android:textColor="@color/colorFont"/>

        </RelativeLayout>


        <RelativeLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:background="@color/colorWhite">

            <TextView
                android:id="@+id/pm25_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:textColor="@color/colorFont"
                android:textSize="18sp"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_below="@+id/pm25_text"
                android:text="PM2.5 指数"
                android:textSize="10sp"
                android:textColor="@color/colorFont"/>


        </RelativeLayout>


        <RelativeLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:background="@color/colorWhite">

            <TextView
                android:id="@+id/co_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:textColor="@color/colorFont"
                android:textSize="18sp"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_below="@+id/co_text"
                android:text="CO 指数"
                android:textSize="10sp"
                android:textColor="@color/colorFont"/>


        </RelativeLayout>


    </LinearLayout>


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="75dp">

        <RelativeLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:background="@color/colorWhite">

            <TextView
                android:id="@+id/o3_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:textColor="@color/colorFont"
                android:textSize="18sp"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_below="@+id/o3_text"
                android:text="O3 指数"
                android:textSize="10sp"
                android:textColor="@color/colorFont"/>

        </RelativeLayout>


        <RelativeLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:background="@color/colorWhite">

            <TextView
                android:id="@+id/pm10_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:textColor="@color/colorFont"
                android:textSize="18sp"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_below="@+id/pm10_text"
                android:text="PM10 指数"
                android:textSize="10sp"
                android:textColor="@color/colorFont"/>


        </RelativeLayout>


        <RelativeLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:background="@color/colorWhite"
            >

            <TextView
                android:id="@+id/so2_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:textColor="@color/colorFont"
                android:textSize="18sp"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_below="@+id/so2_text"
                android:text="SO2 指数"
                android:textSize="10sp"
                android:textColor="@color/colorFont"/>

        </RelativeLayout>

    </LinearLayout>

</LinearLayout>

这里有个 2 × 3布局,上面一行为:aqi_text(AQI指数),pm25_text(PM2.5 指数),co_text(CO 指数),下面一行为:o3_text(O3 指数),pm10_text(PM10 指数),so2_text(SO2 指数)

weather_suggestion:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="150dp">

        <RelativeLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:layout_marginLeft="5dp"
            android:layout_marginRight="2.5dp"
            android:background="@color/colorWhite"

            >

            <Button
                android:id="@+id/comfort_button"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple" />

            <TextView
                android:id="@+id/comfort"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="舒适指数"
                android:textColor="@color/colorFont"
                android:textSize="12sp"/>

            <ImageView
                android:id="@+id/comfort_pic"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_centerHorizontal="true"
                android:layout_above="@+id/comfort"
                android:layout_marginBottom="10dp"/>

            <TextView
                android:id="@+id/comfort_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/colorBlack"
                android:layout_centerHorizontal="true"
                android:layout_below="@+id/comfort"
                android:textSize="18sp"/>

        </RelativeLayout>


        <RelativeLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:layout_marginLeft="2.5dp"
            android:layout_marginRight="2.5dp"
            android:background="@color/colorWhite">

            <Button
                android:id="@+id/car_wash_button"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"/>

            <TextView
                android:id="@+id/car_wash"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="洗车指数"
                android:textColor="@color/colorFont"
                android:textSize="12sp"/>

            <ImageView
                android:id="@+id/car_wash_pic"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_centerHorizontal="true"
                android:layout_above="@+id/car_wash"
                android:layout_marginBottom="10dp"/>

            <TextView
                android:id="@+id/car_wash_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_below="@+id/car_wash"
                android:textColor="@color/colorBlack"
                android:textSize="18sp"/>


        </RelativeLayout>


        <RelativeLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:layout_marginLeft="2.5dp"
            android:layout_marginRight="5dp"
            android:background="@color/colorWhite">

            <Button
                android:id="@+id/sport_button"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"/>

            <TextView
                android:id="@+id/sport"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="运动指数"
                android:textColor="@color/colorFont"
                android:textSize="12sp"/>

            <ImageView
                android:id="@+id/sport_pic"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_centerHorizontal="true"
                android:layout_above="@+id/sport"
                android:scaleType="centerCrop"/>

            <TextView
                android:id="@+id/sport_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_below="@+id/sport"
                android:textColor="@color/colorBlack"
                android:textSize="18sp"/>


        </RelativeLayout>


    </LinearLayout>


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:layout_marginTop="5dp">

        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="5dp"
            android:layout_marginRight="2.5dp"
            android:layout_weight="1"
            android:background="@color/colorWhite">


            <Button
                android:id="@+id/cold_button"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"/>

            <TextView
                android:id="@+id/cold"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="感冒指数"
                android:textColor="@color/colorFont"
                android:textSize="12sp" />

            <ImageView
                android:id="@+id/cold_pic"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_above="@+id/cold"
                android:layout_centerHorizontal="true"
                android:scaleType="centerCrop" />

            <TextView
                android:id="@+id/cold_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@+id/cold"
                android:layout_centerHorizontal="true"
                android:textColor="@color/colorBlack"
                android:textSize="18sp" />

        </RelativeLayout>


        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="2.5dp"
            android:layout_marginRight="2.5dp"
            android:layout_weight="1"
            android:background="@color/colorWhite">

            <Button
                android:id="@+id/clothes_button"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"/>

            <TextView
                android:id="@+id/clothes"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="穿衣指数"
                android:textColor="@color/colorFont"
                android:textSize="12sp" />

            <ImageView
                android:id="@+id/colthes_pic"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_above="@+id/clothes"
                android:layout_centerHorizontal="true" />

            <TextView
                android:id="@+id/clothes_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@+id/clothes"
                android:layout_centerHorizontal="true"
                android:textColor="@color/colorBlack"
                android:textSize="18sp" />

        </RelativeLayout>


        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="2.5dp"
            android:layout_marginRight="5dp"
            android:layout_weight="1"
            android:background="@color/colorWhite">

            <Button
                android:id="@+id/uv_button"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"/>

            <TextView
                android:id="@+id/uv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="紫外线指数"
                android:textColor="@color/colorFont"
                android:textSize="12sp" />

            <ImageView
                android:id="@+id/uv_pic"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_above="@+id/uv"
                android:layout_centerHorizontal="true"
                android:layout_marginBottom="10dp" />

            <TextView
                android:id="@+id/uv_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@+id/uv"
                android:layout_centerHorizontal="true"
                android:textColor="@color/colorBlack"
                android:textSize="18sp" />


        </RelativeLayout>

    </LinearLayout>

</LinearLayout>

也是2×3布局,上面一行为:comfort_text(舒适指数),car_wash_text(洗车指数),sport_text(运动指数),下面一行为:cold_text(感冒指数),clothes_text(穿衣指数),uv_text(紫外线指数)。在每个布局外面覆盖了一个 Button。除此之外,每个 text 都放置了一个 ImageView,用来放置对应的图片,不过这里没有放置,大家可以不用管


activity_main

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/coor_layout"
    android:layout_width="match_parent" android:layout_height="match_parent"
    android:background="@color/colorBackground"
    android:visibility="invisible">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:elevation="0dp"
        >

        <include
            android:id="@+id/tool_bar"
            layout="@layout/weather_title"
            app:layout_scrollFlags="scroll|enterAlways|snap"

            />


    </android.support.design.widget.AppBarLayout>



    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        >

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

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



                <include layout="@layout/weather_now"/>

                <include layout="@layout/weather_hourly"/>

                <include layout="@layout/weather_space"/>

                <include layout="@layout/weather_forecast"/>

                <include layout="@layout/weather_space"/>

                <include layout="@layout/weather_aqi"/>

                <include layout="@layout/weather_space"/>

                <include layout="@layout/weather_suggestion"/>

                <include layout="@layout/weather_author"/>


            </LinearLayout>

        </ScrollView>

    </android.support.v4.widget.SwipeRefreshLayout>


</android.support.design.widget.CoordinatorLayout>

引入对应的布局,布局 weather_space 是进行隔断处理的,这里可以看到最外层布局的可见属性为 invisible, 因为界面中存在很多不需要数据处理也可以显示的控件,不如摄氏度和描述性文字,解决方案是,默认不显示,在解析完数据之后重新设置属性为 visible


BaseActivity

public abstract class BaseActivity extends AppCompatActivity implements View.OnClickListener{

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

        initView();
        initData();
        initListener();
    }


    // 初始化控件
    public abstract void initView();

    // 初始化疏数据
    public abstract void initData();

    // 初始化监听器
    public abstract void initListener();


    // 判断是否有网络
    public NetworkInfo getNetworkInfo(){
        ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
        return networkInfo;
    }

    // Toast 长时间
    public void showShort(String msg){
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }
}

在这里,写了一个 BaseActivity ,还是推荐大家使用这样的方式,先自己创建一个基类,更加方便自己对代码进行结构化处理,这里我我定义了三个抽象函数:initView() 初始化控件, initData() 初始化数据, initListener() 初始化监听器 和两个定义好的函数:根据 getNetworkInfo()函数返回的结果判定是否存在网络, showShort()就是对Toast 简单的包装 ,这样在 Activity 中只需要重写这几个方法即可

接下来是 MainActivity,代码冗长,按照定义的基类方法讲:

@Override
    public void initView() {
        setContentView(R.layout.acticity_main);

        Toolbar toolbar = (Toolbar)findViewById(R.id.tool_bar);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayShowTitleEnabled(false);
        }

        // 初始化各种控件

        weatherLayout = (ScrollView)findViewById(R.id.weather_layout);
        titleCity = (TextView)findViewById(R.id.title_city);
        forecastLayout = (LinearLayout)findViewById(R.id.forecast_layout);
        coordinatorLayout = (CoordinatorLayout)findViewById(R.id.coor_layout);

        // weather_now
        degreeText = (TextView)findViewById(R.id.degree_text);
        weatherInfoText = (TextView)findViewById(R.id.weather_info_text);
        weaherNowLayout = (RelativeLayout)findViewById(R.id.weather_now_layout);
        updateTimeText = (TextView)findViewById(R.id.update_time_text);


        // weather_hour
        hourDegree = (TextView)findViewById(R.id.hour_degree);
        hourText = (TextView)findViewById(R.id.hour_text);
        hourTime = (TextView)findViewById(R.id.hout_time);

        recyclerView = (RecyclerView)findViewById(R.id.weather_hourly);
        hourAdapter = new HourAdapter(hourList);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        recyclerView.setLayoutManager(linearLayoutManager);
        recyclerView.setAdapter(hourAdapter);



        // weather_aqi
        aqiText = (TextView)findViewById(R.id.aqi_text);
        pm25Text = (TextView)findViewById(R.id.pm25_text);
        coText = (TextView)findViewById(R.id.co_text);
        o3Text = (TextView)findViewById(R.id.o3_text);
        pm10Text = (TextView)findViewById(R.id.pm10_text);
        so2Text = (TextView)findViewById(R.id.so2_text);

        // weather_suggestion
        comfortText = (TextView)findViewById(R.id.comfort_text);
        carWashText = (TextView)findViewById(R.id.car_wash_text);
        sportText = (TextView)findViewById(R.id.sport_text);
        uvText = (TextView)findViewById(R.id.uv_text);
        clothesText = (TextView)findViewById(R.id.clothes_text);
        coldText = (TextView)findViewById(R.id.cold_text);
        comfortBtn = (Button)findViewById(R.id.comfort_button);
        carWashBtn = (Button)findViewById(R.id.car_wash_button);
        sportBtn = (Button)findViewById(R.id.sport_button);
        uvBtn = (Button)findViewById(R.id.uv_button);
        clothesBtn = (Button)findViewById(R.id.clothes_button);
        coldBtn = (Button)findViewById(R.id.cold_button);

        // LBS
        mlocationClient = new LocationClient(getApplicationContext());
        mlocationClient.registerLocationListener(new MyLocationListener());
        List<String> permissionList  = new ArrayList<>();
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED){
            permissionList.add(Manifest.permission.ACCESS_FINE_LOCATION);
        }
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED){
            permissionList.add(Manifest.permission.READ_PHONE_STATE);
        }
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
            permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
        }
        if (!permissionList.isEmpty()){
            String[] permissions = permissionList.toArray(new String[permissionList.size()]);
            ActivityCompat.requestPermissions(MainActivity.this, permissions, 1);
        }

        swipeRefresh = (SwipeRefreshLayout)findViewById(R.id.swipe_refresh);
        swipeRefresh.setColorSchemeColors(getResources().getColor(R.color.colorAccent));

    }

在 initView 中初始化控件,为了方便起见,我们在这里进行运行时权限处理,先将 List 将没有获得授权的权限进行添加,然后集中申请

/**
     * 权限申请处理
     */
    @Override
    public void onRequestPermissionsResult(int requestCode,  String[] permissions,  int[] grantResults) {
        switch (requestCode){
            case 1:
                if (grantResults.length > 0){
                    for (int result:grantResults){
                        if (result != PackageManager.PERMISSION_GRANTED){
                            // 如果存在某个权限没有处理
                            finish();
                        }
                    }
                }else{
                    // 发生未知错误
                    showShort("权限申请出现位置错误");
                }
                break;
            default:
        }
    }

在 onRequestPermissionsResult() 方法,如果存在权限未被允许,则直接退出

    public void initListener() {
        comfortBtn.setOnClickListener(this);
        carWashBtn.setOnClickListener(this);
        sportBtn.setOnClickListener(this);
        uvBtn.setOnClickListener(this);
        clothesBtn.setOnClickListener(this);
        coldBtn.setOnClickListener(this);

        swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                if (getNetworkInfo() == null){
                    Snackbar.make(swipeRefresh, "当前无网络,无法刷新 %>_<% ",Snackbar.LENGTH_LONG).setAction("去设置网络", new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            Intent intent = new Intent(Settings.ACTION_SETTINGS);
                            startActivity(intent);
                        }
                    }).show();
                    swipeRefresh.setRefreshing(false);
                }else{
                    showAnimationAlpha(weaherNowLayout);
                }
            }
        });
    }

在 initListener()方法中,对 Suggestion 部分的按钮设置监听器,同时对刷新进行监听,如果有网络的话,则执行 showAnimationAlpha(weaherNowLayout)方法,否则,显示一个可交互通知,点击通知上面的按钮,可以跳转到设置界面

    private void showAnimationAlpha(final View view){
        Animation alpha = AnimationUtils.loadAnimation(MainActivity.this,R.anim.alpha_before);
        view.startAnimation(alpha);
        alpha.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
                String cityName = prefs.getString("cityName", null);
                requestWeather(cityName);
                Animation alpha = AnimationUtils.loadAnimation(MainActivity.this,R.anim.alpha_after);
                view.startAnimation(alpha);
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
    }

在 showAnimationAlpha()方法中,我设置了一个透明度动画,同时进行监听,如果动画结束(也就是此时完全透明的时候),执行 requestWeather()方法,对数据进行更新,之后便开启一个透明度由 0 到 1 的动画

public void initData() {
        String cityName = getIntent().getStringExtra("cityName");
        if (!TextUtils.isEmpty(cityName)){
            requestWeather(cityName);
        }else{
            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
            String weatherString = prefs.getString("weatherResponse", null);        // weather 保存API 返回的字符串
            if (weatherString != null){
                // 有缓存时直接解析天气数据
                Weather weather = Utility.handleWeatherResponse(weatherString);
                showWeatherInfo(weather);
                coordinatorLayout.setVisibility(View.VISIBLE);
            }else {
                // 无缓存时向服务器查询数据
                if (getNetworkInfo() != null && getNetworkInfo().isAvailable()){
                    // 查询完之后显示 coordinatorLayout.setVisibility(View.VISIBLE);
                    LocationClientOption option = new LocationClientOption();
                    option.setIsNeedAddress(true);
                    mlocationClient.setLocOption(option);
                    mlocationClient.start();
                }else{
                    showDialog();
                }

            }
            Intent intent = new Intent(MainActivity.this, AutoUpdateService.class);
            startService(intent);
        }

    }

在 initData()方法执行一开始流程图的逻辑,在 else 分支中查看是否存在 weatherResponse ,如果有则直接进行加载,如果没有则通过getNetworkInfo()方法的结果来判断是否存在网络,如果当前有网路可用则调用:

LocationClientOption option = new LocationClientOption();
option.setIsNeedAddress(true);              // 获得详细地址
mlocationClient.setLocOption(option);
mlocationClient.start();        

调用 mlocationClient.start() 方法后,会跳转到:

    public class MyLocationListener implements BDLocationListener{
        @Override
        public void onReceiveLocation(BDLocation bdLocation) {
            currentPosition = bdLocation.getCity();
            requestWeather(currentPosition);
            showShort(currentPosition + " 定位成功");
        }

        @Override
        public void onConnectHotSpotMessage(String s, int i) {

        }
    }

则直接通过自动定位得到的城市名称来调用 requestWeather()方法,同时显示定位成功

如果当前没有网路,则调用 showDialog()

    public void showDialog(){
        AlertDialog.Builder alertDialog  = new AlertDialog.Builder(MainActivity.this);
        alertDialog.setMessage("当前无网络,请先打开网络");
        alertDialog.setCancelable(false);
        alertDialog.setPositiveButton("确定", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Intent intent = new Intent(Settings.ACTION_SETTINGS);
                startActivity(intent);
                TaskKiller.dropAllAcitivty();
            }
        });
        alertDialog.show();
    }

显示对话框,显示当前无网路,点击确定后,直接退出所有的 Activity

接下来就是程序的主要函数部分了:

public void requestWeather(final String cityName){

        String address = "https://api.heweather.com/v5/weather?city=" + cityName + "&key=bc0418b57b2d4918819d3974ac1285d9";
        HttpUtil.sendOkHttpRequest(address, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        showShort("获取天气信息1失败");
                    }
                });
            }

            @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(MainActivity.this).edit();
                            editor.putString("weatherResponse", responseText);
                            editor.putString("cityName",cityName);
                            editor.apply();
                            showWeatherInfo(weather);
                        }else{
                            showShort("获取天气信息2失败");
                        }
                    }
                });

            }
        });
        swipeRefresh.setRefreshing(false);

    }

在 requestWeather()方法中,通过城市名来向接口请求数据,如果请求成功,在onResponse()方法中将相应的数据(weatherResponse,cityName)覆盖之前的 SharedPreferences文件,然后调用 showWeatherInfo()方法

private void showWeatherInfo(Weather weather){

        String cityName = weather.basic.cityName;
        String degree = weather.now.temperature ;
        String weatherInfo = weather.now.more.info;
        String updateTime = weather.basic.update.loc;

        titleCity.setText(cityName);
        degreeText.setText(degree);
        weatherInfoText.setText(weatherInfo);
        updateTimeText.setText("数据更新时间: " + updateTime.split(" ")[1]);


        forecastLayout.removeAllViews();
        for (Forecast forecast : weather.forecastList){
            // 将未来几天的天气添加到视图中
            View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.weather_forecast_item, forecastLayout, false);
            TextView dateText = (TextView)view.findViewById(R.id.data_text);
            TextView infoText = (TextView)view.findViewById(R.id.info_text);
            TextView maxMinText = (TextView)view.findViewById(R.id.max_min_text);
            ImageView weatherPic = (ImageView)view.findViewById(R.id.weather_pic);

            // 动态获取 资源id
            String weatherCode = "weather_"+forecast.more.code;
            int resId = getResources().getIdentifier(weatherCode, "drawable", this.getPackageName());
            if (resId != 0){
                weatherPic.setImageResource(resId);
            }


            dateText.setText(Time.parseTime(forecast.date));
            infoText.setText(forecast.more.info);
            maxMinText.setText(forecast.temperature.max + " ~ " + forecast.temperature.min);
            forecastLayout.addView(view);
        }

        hourList.clear();
        for (Hourly hourly:weather.hourlyList){
            Hour hour = new Hour();
            hour.setDegree(hourly.tmp + "°" );
            hour.setText(hourly.cond.txt);
            hour.setTime(hourly.date.split(" ")[1]);
            hourList.add(hour);
        }

        hourAdapter.notifyDataSetChanged();


        // weather_aqi 空气质量
        {
            String infoText = "无";
            if (weather.aqi.city.aqi != null){
                aqiText.setText(weather.aqi.city.aqi);
                aqiText.getPaint().setFakeBoldText(true);
            }else{
                aqiText.setText(infoText);
            }

            if (weather.aqi.city.pm25 != null){
                pm25Text.setText(weather.aqi.city.pm25);
                pm25Text.getPaint().setFakeBoldText(true);
            }else{
                pm25Text.setText(infoText);
            }

            if (weather.aqi.city.co != null){
                coText.setText(weather.aqi.city.co);
                coText.getPaint().setFakeBoldText(true);
            }else{
                coText.setText(infoText);
            }

            if (weather.aqi.city.o3 != null){
                o3Text.setText(weather.aqi.city.o3);
                o3Text.getPaint().setFakeBoldText(true);
            }else{
                o3Text.setText(infoText);
            }

            if (weather.aqi.city.pm10 != null){
                pm10Text.setText(weather.aqi.city.pm10);
                pm10Text.getPaint().setFakeBoldText(true);
            }else{
                pm10Text.setText(infoText);
            }

            if (weather.aqi.city.so2 != null){
                so2Text.setText(weather.aqi.city.so2);
                so2Text.getPaint().setFakeBoldText(true);
            }else{
                so2Text.setText(infoText);
            }
        }

        comfortSign = weather.suggestion.comfort.sign;
        carWashSign = weather.suggestion.carWash.sign;
        sportSign = weather.suggestion.sport.sign;
        uvSign = weather.suggestion.uv.sign;
        clothesSign = weather.suggestion.clothes.sign;
        coldSign = weather.suggestion.cold.sign;


        comfortText.setText(comfortSign);
        comfortText.getPaint().setFakeBoldText(true);
        carWashText.setText(carWashSign);
        carWashText.getPaint().setFakeBoldText(true);
        sportText.setText(sportSign);
        sportText.getPaint().setFakeBoldText(true);
        uvText.setText(uvSign);
        uvText.getPaint().setFakeBoldText(true);
        clothesText.setText(clothesSign);
        clothesText.getPaint().setFakeBoldText(true);
        coldText.setText(coldSign);
        coldText.getPaint().setFakeBoldText(true);

        comfortInfo = weather.suggestion.comfort.info;
        carWashInfo = weather.suggestion.carWash.info;
        sportInfo = weather.suggestion.sport.info;
        uvInfo = weather.suggestion.uv.info;
        clothesInfo = weather.suggestion.clothes.info;
        coldInfo = weather.suggestion.cold.info;

        weatherLayout.setVisibility(View.VISIBLE);
        coordinatorLayout.setVisibility(View.VISIBLE);
    }

在 showWeatherInfo() 方法中进行的操作基本就是将通过得到的 Weather 对象中获取数据,然后将数值赋值到控件上,因为在 Hourly 中,我们采用的是 RecyclerView,所以需要使用 notifyDataSetChanged() 方法让适配器更新数据。在最后的部分:

weatherLayout.setVisibility(View.VISIBLE);
coordinatorLayout.setVisibility(View.VISIBLE);

将界面设置为可见

需要注意的一部分代码是:

String weatherCode = "weather_"+forecast.more.code;
int resId = getResources().getIdentifier(weatherCode, "drawable", this.getPackageName());
if (resId != 0){
       weatherPic.setImageResource(resId);
}

因为返回的数据中包含天气码,每个天气码都有一个对应的天气图片与之对应,但是我们总不能对几十个天气图片进行 if 判定,所以我们可以通过 getIdentifier() 方法动态的匹配资源 ID, 第一个参数为资源ID 的String 格式,第二个为资源所在的文件夹,第三个一般情况下是包名

另外,我们在 dateText.setText(Time.parseTime(forecast.date));中,这里用到了一个 Time.parseTime() 的函数对解析得到的天气数据进行二次

public class Time {
    /**
     * 输入时间 XXXX-XX-XX 的字符串, 放回中文指代的时间, 比如 "今天 02/09"
     */
    public static String parseTime(String timeText){
        DateTime dateTime = new DateTime();
        String[] time = timeText.split("-");

        int currentMonth = dateTime.getMonthOfYear();
        int currentDay = dateTime.getDayOfMonth();
        int currentWeak = dateTime.getDayOfWeek();
        int currentYear = dateTime.getYear();

        int month = Integer.parseInt(time[1]);
        int day = Integer.parseInt(time[2]);
        int year = Integer.parseInt(time[0]);

        int offset = 0;  // 相差量

        if (year == currentYear){
            //如果是同一年:
            if (month == currentMonth){
                // 如果是同一个月
                offset = day - currentDay;

            }else{
                offset = day + parseMonth(currentMonth, currentYear) - currentDay;
            }

        }else{
            offset = 31 - currentDay + day;
        }



        String monthAndDay = time[1] + "/" + time[2];
        if (offset == 0) return "今天 " + monthAndDay;
        if (offset == 1) return  "明天 " + monthAndDay;
        return parseWeak(currentWeak + offset) + " " + monthAndDay;

    }

    /**
     * 输入一个数字, 输出是星期几的字符串: 2  ->  周二
     */
    public static String parseWeak(int weak){
        String[] weakday = new String[]{"周一", "周二", "周三", "周四", "周五", "周六", "周日"};
        int index = (weak - 1)%7;
        return weakday[index];
    }

    /**
     * 输入月份和年份, 输出该月份的天数
     */
    public static int parseMonth(int month, int year){
        switch (month){
            case 1:
            case 3:
            case 5:
            case 7:
            case 8:
            case 10:
            case 12:return 31;
            case 2:
                if (parseYear(year)) {
                    return 29;
                }
                return 28;
            default:
                return 30;
        }
    }


    /**
     * 输入年份, 判断是否是闰年
     */
    public static boolean parseYear(int year){
        String yearOfString = String.valueOf(year);
        int len = yearOfString.length();
        char lastOne = yearOfString.charAt(len - 1);
        char lastTwo = yearOfString.charAt(len - 2);
        if (lastOne == lastTwo && lastOne == '0'){
            if (year % 400 == 0){
                return true;
            }else{
                return false;
            }
        }

        if (year % 4 == 0){
            return true;
        }else{
            return false;
        }

    }

}

这个类的作用主要是对天气数据进行处理,以此得到更加人性化的设计:

// 今天是 2017/5/16

2017/5/16   ->   今天
2017/5/17   ->   明天
2017/5/18   ->   周四 5/18
2017/5/19   ->   周五 5/19

大概就是这样的作用,没什么好介绍的,不过这个类的作用是两天的间隔相差在一个月之内(毕竟….天气预报)

    @Override
    public void onClick(View v) {
        // 通过 SuggestionInfoActivity 中的静态方法直接传值
        switch (v.getId()){
            case R.id.comfort_button:
                SuggestionInfoActivity.actionStart(this, comfortInfo,comfortSign,"舒适度指数");
                break;
            case R.id.car_wash_button:
                SuggestionInfoActivity.actionStart(this, carWashInfo,carWashSign,"洗车指数");
                break;
            case R.id.sport_button:
                SuggestionInfoActivity.actionStart(this, sportInfo,sportSign,"运动指数");
                break;
            case R.id.cold_button:
                SuggestionInfoActivity.actionStart(this, coldInfo,coldSign,"感冒指数");
                break;
            case R.id.clothes_button:
                SuggestionInfoActivity.actionStart(this, clothesInfo,clothesSign,"穿衣指数");
                break;
            case R.id.uv_button:
                SuggestionInfoActivity.actionStart(this, uvInfo,uvSign,"紫外线指数");
                break;
            default:
                break;
        }
    }

在 Suuggestion部分的button 点击事件中,调用了SuggestionInfoActivity 的静态方法 actionStart( ),并且传入对应的四个参数

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mlocationClient.stop();
    }

在 onDestor( )方法中停止 mlocationClient,毕竟,不能总在定位

    public static void actionStart(Context context ,String cityName){
        Intent intent = new Intent(context, MainActivity.class);
        intent.putExtra("cityName", cityName);
        context.startActivity(intent);
    }

这是 MainActivity 中的跳转静态函数,用来在选择地区时跳转,传入选择的地区名称


activity_suggestion_info

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

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar_suggestion_info"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorWhite"
        app:titleTextColor="@color/colorFont"
        />

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:layout_marginTop="15dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        app:cardElevation="2dp"
        app:cardCornerRadius="0dp"
        >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:background="@color/colorWhite">

            <TextView
                android:id="@+id/suggestion_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:layout_marginTop="10dp"
                android:textSize="24sp"
                android:textColor="@color/colorRed"/>

            <TextView
                android:id="@+id/suggestion_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:layout_marginLeft="10dp"
                android:textSize="14sp"
                android:textColor="@color/colorFont"/>

        </LinearLayout>



    </android.support.v7.widget.CardView>



</LinearLayout>

布局很简单, CardView 上有两个 TextView 控件

SuggestionInfoActivity

public class SuggestionInfoActivity extends BaseActivity {

    private TextView suggestText;
    private TextView suggestTitle;


    private String info;
    private String sign;
    private String source;

    @Override
    public void initView() {
        setContentView(R.layout.activity_suggestion_info);

        Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar_suggestion_info);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
        }

        suggestText = (TextView)findViewById(R.id.suggestion_text);
        suggestTitle = (TextView)findViewById(R.id.suggestion_title);
    }

    @Override
    public void initListener() {

    }

    @Override
    public void initData() {
        info = getIntent().getStringExtra("info");
        sign = getIntent().getStringExtra("sign");
        source = getIntent().getStringExtra("source");

        getSupportActionBar().setTitle(source);
        suggestTitle.setText(sign);
        suggestTitle.getPaint().setFakeBoldText(true);
        suggestText.setText(info);
    }


    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case android.R.id.home:
                finish();
        }
        return true;
    }


    @Override
    public void onClick(View v) {

    }

    public static void actionStart(Context context, String info, String sign, String source){
        Intent intent = new Intent(context, SuggestionInfoActivity.class);
        intent.putExtra("info", info);
        intent.putExtra("sign", sign);
        intent.putExtra("source", source);
        context.startActivity(intent);
    }
}

SuggestionInfoActivity 中的静态函数 actionStart( ) 传入四个参数,其他 Activity 来调用这个静态函数可以实现更加方便的跳转,而在 SuggestionInfoActivity 也可以更加方便处理传过来的参数,将传入的参数显示在控件上


新建 menu文件夹,新建文件 menu.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/setting"
        android:title="设置"
        app:showAsAction="never"/>

    <item
        android:id="@+id/night_model"
        android:title="夜间模式"
        app:showAsAction="never"/>


</menu>

两个菜单项 “设置”,“夜间模式”

在 MainActivity 中添加如下代码:

 /**
     * 添加 actionbar 菜单项
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu, menu);
        return true;
    }

    /**
     * 菜单点击事件响应
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case R.id.setting:
                // 跳转到设置界面
                Intent intent1 = new Intent(this, SettingActivity.class);
                intent1.putExtra("weather_title","设置");
                startActivity(intent1);
                break;
            case R.id.night_model:
                SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
                SharedPreferences.Editor editor = pref.edit();
                boolean isNight = pref.getBoolean("isNight", false);
                if (isNight){
                    // 如果已经是夜间模式
                    getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_NO);
                    recreate();
                    editor.putBoolean("isNight", false);
                    editor.apply();
                }else{
                    // 如果是日间模式
                    getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
                    recreate();
                    editor.putBoolean("isNight", true);
                    editor.apply();
                }
                break;
        }
        return true;
    }

我们先忽略 night_model 的点击事件,当点击 setting 按钮,跳转到设置界面 activity_setting

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

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar_setting"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorWhite"
        app:titleTextColor="@color/colorFont"
        />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="15dp"
        android:orientation="vertical"
        android:background="@color/colorBackground"
        >

        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorWhite"
            app:cardCornerRadius="0dp"
            app:cardElevation="1dp">


            <Button
                android:id="@+id/choose_area"
                android:text="选择地区"
                android:textColor="@color/colorFont"
                android:textSize="16sp"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"
                />

        </android.support.v7.widget.CardView>

        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorWhite"
            android:layout_marginTop="10dp"
            app:cardCornerRadius="0dp"
            app:cardElevation="1dp">

            <Button
                android:id="@+id/auto_update_time"
                android:text="更新频率"
                android:textColor="@color/colorFont"
                android:textSize="16sp"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"
                />

        </android.support.v7.widget.CardView>


        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorWhite"
            android:layout_marginTop="10dp"
            app:cardCornerRadius="0dp"
            app:cardElevation="1dp">

            <Button
                android:id="@+id/about_app"
                android:text="关于天气"
                android:textColor="@color/colorFont"
                android:textSize="16sp"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"
                />

        </android.support.v7.widget.CardView>

    </LinearLayout>
</LinearLayout>

布局中存在三个按钮,选择地区,更新频率,关于天气

public class SettingActivity extends BaseActivity{

    private Button chooseArea;
    private Button aboutApplication;
    private Button autoUpdateTime;

    @Override
    public void initView() {
        setContentView(R.layout.activity_setting);

        Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar_setting);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setTitle("设置");
        }

        chooseArea = (Button)findViewById(R.id.choose_area);
        aboutApplication = (Button)findViewById(R.id.about_app);
        autoUpdateTime = (Button)findViewById(R.id.auto_update_time);
    }

    @Override
    public void initListener() {
        chooseArea.setOnClickListener(this);
        aboutApplication.setOnClickListener(this);
        autoUpdateTime.setOnClickListener(this);
    }

    @Override
    public void initData() {
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case android.R.id.home:
                finish();
                break;
            default:
                break;
        }
        return true;
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.choose_area:
                // 跳转到选择地区界面
                actionStart(ChooseAreaActivity.class);
                break;
            case R.id.about_app:
                // 跳转到关于天气界面
                actionStart(AboutApplicationActivity.class);
                break;
            case R.id.auto_update_time:
                //跳转到自动更新频率界面
                actionStart(AutoUpdateTimeAcitivity.class);
                break;
            default:

        }
    }

    public void actionStart(Class<?> c){
        Intent intent = new Intent(this, c);
        startActivity(intent);
    }
}

点击不同按钮进行界面跳转时会调用 actionStart( )函数,参数之后一个类名,作用是从当前 Activity 跳转到其他的 Activity


当点击 第一个按钮 choose_area,跳转到选择地区界面,activity_choose_area:

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

    <android.support.v7.widget.Toolbar
        android:id="@+id/tool_bar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorWhite"
        app:titleTextColor="@color/colorFont"/>


    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:layout_margin="20dp"
        android:background="@color/colorWhite"
        app:cardBackgroundColor="@color/colorWhite"
        app:cardElevation="2dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="10dp"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"
            android:background="@color/colorWhite"
            android:orientation="vertical">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/colorWhite">

                <EditText
                    android:id="@+id/search_text"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textSize="16sp"
                    android:hint="查找的城市"
                    android:textColorHint="@color/colorFont"
                    android:textColor="@color/colorFont"/>

                <Button
                    android:id="@+id/search_button"
                    android:layout_width="wrap_content"
                    android:text="搜索"
                    android:textColor="@color/colorFont"
                    android:layout_height="wrap_content"
                    style="?android:attr/borderlessButtonStyle"
                    android:background="@drawable/ripple"
                    />


            </LinearLayout>

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

        </LinearLayout>


    </android.support.v7.widget.CardView>


    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="5dp"
        app:cardBackgroundColor="@color/colorWhite">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="10dp"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"
            android:background="@color/colorWhite"
            android:orientation="vertical">

            <TextView
                android:text="查找记录"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="16sp"/>

            <ListView
                android:layout_marginTop="10dp"
                android:id="@+id/list_view_recond"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scrollbars="none"/>

        </LinearLayout>


    </android.support.v7.widget.CardView>

</LinearLayout>

存在两个卡片布局,上一个卡片布局中包含三个控件,EditText,Button,ListView。其中 EditText 用来输入搜索的城市,点击 Button 进行搜索,结果显示在 ListView 上。下一个布局包含两个控件,TextView, ListView, 其中, ListView 显示搜索记录

由于要显示搜索记录,自然炫耀保存数据,这里我选择 LitePal 来保存搜索记录,这里我对搜索记录的处理是这样:只有搜索之后得到结果的搜索才能保存,其他的搜索没有得到有效的数据,自然没有保存的必要。我们先建立数据库,之前在 build.gradle 中已经声明了依赖库,我们还需要在 AndroidManifest 中添加 name 属性,以便 LitePal 可以全局获取 Context

在包下建立一个文件夹 db ,在文件夹中新建一个文件 CityRecond 用来记录搜索记录:

public class CityRecond  extends DataSupport{
    private int id;
    private String cityName;

    public int getId() {
        return id;
    }

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

    public String getCityName() {
        return cityName;
    }

    public void setCityName(String cityName) {
        this.cityName = cityName;
    }
}

在 main 目录下新建文件夹 assets ,新建 litepal 文件:

<litepal>
    <dbname value="CoolWeather"/>
    <version value="1"/>

    <list>
        <mapping class="com.example.coolweather.db.CityRecond"></mapping>
    </list>

</litepal>

这样, 数据库基本就建立好了。 因为我们在搜索界面只需要检索城市信息,城市请求URL为:

这里写图片描述

这里写图片描述

在 gson 目录下创建对应的实体类:

City.class

public class City {

    public Basic basic;

    public String status;

    public class Basic{
        public String city;
        public String cnty;
        public String lat;
        public String lon;
        public String prov;
    }
}

在 Utility 类中添加方法

 public static City handleCityResponse(String response){
        try{
            JSONObject jsonObject = new JSONObject(response);
            JSONArray jsonArray = jsonObject.getJSONArray("HeWeather5");
            String cityContent = jsonArray.getJSONObject(0).toString();
            return new Gson().fromJson(cityContent, City.class);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

直接转化为 City 类


回到 ChooaseAreaActivity

public class ChooseAreaActivity extends BaseActivity {
    private EditText searchText;
    private Button searchButton;
    private ListView listView;
    private ListView listViewRecond;
    private List<String> cityList = new ArrayList<>();
    private List<String> recondList = new ArrayList<>();
    ArrayAdapter<String> adapter;
    ArrayAdapter<String> recondAdapter;

    @Override
    public void initView() {
        setContentView(R.layout.activity_choose_area);
        Toolbar toolbar = (Toolbar)findViewById(R.id.tool_bar);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setTitle("选择地区");
        }

        searchText = (EditText)findViewById(R.id.search_text);
        searchButton = (Button)findViewById(R.id.search_button);
        listView = (ListView)findViewById(R.id.list_view);
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, cityList);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if (getNetworkInfo() != null && getNetworkInfo().isAvailable()){
                    String cityName = cityList.get(position);
                    MainActivity.actionStart(ChooseAreaActivity.this, cityName);
                    finish();
                }else{
                    showShort("当前没有网络");
                }
            }
        });

        listViewRecond = (ListView)findViewById(R.id.list_view_recond);
        recondAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, recondList);
        listViewRecond.setAdapter(recondAdapter);
        listViewRecond.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if (getNetworkInfo() != null && getNetworkInfo().isAvailable()){
                    String cityName = recondList.get(position);
                    MainActivity.actionStart(ChooseAreaActivity.this, cityName);
                    finish();
                }else{
                    showShort("当前没有网络");
                }
            }
        });
        showRecond();

    }

    @Override
    public void initData() {
        LitePal.getDatabase();

    }

    @Override
    public void initListener() {
        searchButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.search_button:
                showSearchResult();
                break;
            default:
        }
    }

    public void showSearchResult(){
        String cityName = searchText.getText().toString();
        if (!TextUtils.isEmpty(cityName)){
            // 如果不为空,则进行查询
            if (getNetworkInfo() != null && getNetworkInfo().isAvailable()){
                String address = "https://api.heweather.com/v5/search?city=" + cityName + "&key=bc0418b57b2d4918819d3974ac1285d9";
                requestData(address);
            }else{
                showShort("当前网络无连接");
            }
        }else{
            showShort("请输入城市名称");
        }

    }

    public void requestData(String address){
        HttpUtil.sendOkHttpRequest(address, new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        showShort("请求数据失败");
                    }
                });
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String responseText = response.body().string();
                final City city = Utility.handleCityResponse(responseText);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        showCity(city);
                    }
                });
            }
        });
    }

    public void showCity(City city){
        cityList.clear();
        if ("ok".equals(city.status) && city != null){
            cityList.add(city.basic.city);
            solveSearchRecond(city.basic.city);
            adapter.notifyDataSetChanged();
            listView.setSelection(0);

        }else{
            showShort("未找到该城市");
        }
    }

    /**
     * 保存城市名称 -> 数据库
     */
    public void solveSearchRecond(String cityName){
        CityRecond cityRecond = new CityRecond();
        cityRecond.setCityName(cityName);
        cityRecond.save();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case android.R.id.home:
                finish();
                break;
        }
        return true;
    }

    /**
     * 显示数据库中保存的信息
     */
    public void showRecond(){
        recondList.clear();
        List<CityRecond> list = DataSupport.select("cityName").find(CityRecond.class);
        for (CityRecond recond:list){
            recondList.add(recond.getCityName());
        }
        recondAdapter.notifyDataSetChanged();
        listViewRecond.setSelection(0);
    }
}

在 initData( ) 方法中调用了 LitePal.getDatabase( ) 创建数据库同时创建表,当点击搜索按钮时,调用 showSearchResult( )方法,判定是否为空,如果为空,则调用showShort(“请输入城市名称”) 提醒,如果不为空,通过判定getNetworkInfo( ) 是否存在网络,如果不存在则提醒,否则执行requestData( ) 方法,如果回调成功,则调用 showCity( ) 方法,传入解析得到的 City 对象,在 showCity( ) 方法中,将数据显示在搜索结果中,同时将数据保存在数据库文件中,每次打开 ChooseAreaActivity 都会调用 showRecond( ) 方法,通过DataSupport.select( )找到所有保存的 cityName, 并且将数据显示在 ListView 中。 对两个 listView的子项都进行了监听,点击子项时,还是先判断是否存在网络,如果存在网络,则调用 MainActivity 中的静态方法,传入 cityName 参数,这就是在 MainActivity 中一开始需要判定的原因,如果是 Activity 跳转,则需要根据传入的 cityName 来进行请求,解析等一系列步骤,但是如果是从桌面打开,则可以直接通过缓存文件来解析数据

到这里,ChooseAreaActivity 的任务就结束了,开始看 activity_setting 界面的第二个按钮,更新频率


点击更新频率跳转到 activity_auto_update_time

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

    <android.support.v7.widget.Toolbar
        android:id="@+id/tool_bar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorWhite"
        app:titleTextColor="@color/colorFont"
        />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="15dp"
        android:orientation="vertical"
        android:background="@color/colorBackground"
        >

        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorWhite"
            app:cardCornerRadius="0dp"
            app:cardElevation="1dp">

            <Button
                android:id="@+id/hour1"
                android:text="1 小时"
                android:textColor="@color/colorFont"
                android:textSize="16sp"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"
                />

        </android.support.v7.widget.CardView>

        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorWhite"
            android:layout_marginTop="10dp"
            app:cardCornerRadius="0dp"
            app:cardElevation="1dp">

            <Button
                android:id="@+id/hour2"
                android:text="两小时"
                android:textColor="@color/colorFont"
                android:textSize="16sp"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"
                />

        </android.support.v7.widget.CardView>

        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorWhite"
            android:layout_marginTop="10dp"
            app:cardCornerRadius="0dp"
            app:cardElevation="1dp">

            <Button
                android:id="@+id/hour3"
                android:text="三小时"
                android:textColor="@color/colorFont"
                android:textSize="16sp"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"
                />

        </android.support.v7.widget.CardView>


        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorWhite"
            android:layout_marginTop="10dp"
            app:cardCornerRadius="0dp"
            app:cardElevation="1dp">

            <Button
                android:id="@+id/hour5"
                android:text="五小时"
                android:textColor="@color/colorFont"
                android:textSize="16sp"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"
                />

        </android.support.v7.widget.CardView>

        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorWhite"
            android:layout_marginTop="10dp"
            app:cardCornerRadius="0dp"
            app:cardElevation="1dp">

            <Button
                android:id="@+id/hour10"
                android:text="十小时"
                android:textColor="@color/colorFont"
                android:textSize="16sp"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"
                />

        </android.support.v7.widget.CardView>

        <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:background="@color/colorWhite"
            android:layout_marginTop="10dp"
            app:cardCornerRadius="0dp"
            app:cardElevation="1dp">

            <Button
                android:id="@+id/hour0"
                android:text="不进行自动更新"
                android:textColor="@color/colorFont"
                android:textSize="16sp"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                style="?android:attr/borderlessButtonStyle"
                android:background="@drawable/ripple"
                />

        </android.support.v7.widget.CardView>


    </LinearLayout>






</LinearLayout>

界面很简单,有六个按钮,来控制更新的频率和是否更新,当然这个数值还是根据 SharedPerferenced 文件来传递的

AutoUpdateTimeActivity

public class AutoUpdateTimeAcitivity extends BaseActivity {
    private Button button1;
    private Button button2;
    private Button button3;
    private Button button5;
    private Button button10;
    private Button button0;

    @Override
    public void initView() {
        setContentView(R.layout.activity_auto_update_time);

        Toolbar toolbar = (Toolbar)findViewById(R.id.tool_bar);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setTitle("更新频率");
        }
        button0 = (Button)findViewById(R.id.hour0);
        button1 =(Button)findViewById(R.id.hour1);
        button2 = (Button)findViewById(R.id.hour2);
        button3 = (Button)findViewById(R.id.hour3);
        button5 = (Button)findViewById(R.id.hour5);
        button10 = (Button)findViewById(R.id.hour10);
    }

    @Override
    public void initData() {

    }

    @Override
    public void initListener() {
        button0.setOnClickListener(this);
        button2.setOnClickListener(this);
        button1.setOnClickListener(this);
        button3.setOnClickListener(this);
        button5.setOnClickListener(this);
        button10.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
        switch (v.getId()){
            case R.id.hour0:
                editor.putBoolean("isUpdateTime", false);
                break;
            case R.id.hour1:
                editor.putBoolean("isUpdateTime",true);
                editor.putInt("autoUpdateTime", 60);
                break;
            case R.id.hour2:
                editor.putBoolean("isUpdateTime",true);
                editor.putInt("autoUpdateTime", 120);
                break;
            case R.id.hour3:
                editor.putBoolean("isUpdateTime",true);
                editor.putInt("autoUpdateTime", 180);
                break;
            case R.id.hour5:
                editor.putBoolean("isUpdateTime",true);
                editor.putInt("autoUpdateTime", 300);
                break;
            case R.id.hour10:
                editor.putBoolean("isUpdateTime",true);
                editor.putInt("autoUpdateTime", 600);
                break;
            default:

        }
        editor.apply();
        Intent intent = new Intent(this, AutoUpdateService.class);
        startService(intent);
        showShort("设置成功");
        finish();
    }


    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case android.R.id.home:
                finish();
                break;
            default:
        }
        return true;
    }
}

可以很清楚的看到代码逻辑,点击不同的按键,会修改 isUpdateTime 和 autoUpdateTime 的值,然后会启动 AutoUpdateService:

public class AutoUpdateService extends Service {
    public AutoUpdateService() {

    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        SharedPreferences pref  = PreferenceManager.getDefaultSharedPreferences(this);
        boolean isUpdateTime = pref.getBoolean("isUpdateTime", true);
        if (isUpdateTime == true){
            updateWeather();
            AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
            int autoUpdateTime = pref.getInt("autoUpdateTime", 60);
            int anHour = autoUpdateTime * 60 * 1000; // 这是 60 分钟的毫秒数
            long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
            Intent i = new Intent(this, AutoUpdateService.class);
            PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
            manager.cancel(pi);
            manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pi);
        }
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * 更新信息
     */
    private void updateWeather(){
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        String cityName = prefs.getString("cityName", null);
        if(cityName != null){
            String address = "https://api.heweather.com/v5/weather?city=" + cityName + "&key=bc0418b57b2d4918819d3974ac1285d9";
            HttpUtil.sendOkHttpRequest(address, 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("weatherResponse", responseText);
                        editor.apply();
                    }

                }
            });
        }
    }


}

在 AutoUpdateService 中,updateWeather( )是通过已经存在的缓存文件中得到 cityName 的值,重新发起请求,如果请求成功,并且数据可以成功解析的话,则将成功返回的数据保存在 weatherResponse 中,这样,就可以确保在每次打开该应用程序的时候,应用程序可以解析最新的数据。
在 onStartCommand( ) 方法中,我们先查看 isUpdateTime 的值,如果为 false(也就是点击了不进行自动更新),则不执行 updateWeather( ) 方法,如果为 true, 则继续查看 autoUpdateTime 的值,以此来决定定时执行的时间间隔,这样也就完成了定时更新天气数据的任务


至于第三个按钮,也就是关于天气 activity_about_application:

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

    <android.support.v7.widget.Toolbar
        android:id="@+id/tool_bar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorWhite"
        app:titleTextColor="@color/colorFont"/>

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="20dp"
        android:background="@color/colorWhite"
        app:cardBackgroundColor="@color/colorWhite"
        app:cardElevation="2dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="10dp"
            android:layout_marginLeft="20dp"
            android:layout_marginRight="20dp"
            android:background="@color/colorWhite"
            android:orientation="vertical">

            <ImageView
                android:layout_width="80dp"
                android:layout_height="80dp"
                android:scaleType="centerCrop"
                android:src="@mipmap/logo"
                android:layout_gravity="center_horizontal"
                android:layout_marginTop="40dp"/>

            <TextView
                android:text="版本号 1.0"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/colorFont"
                android:layout_marginTop="7dp"
                android:layout_gravity="center_horizontal"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="80dp"
                android:text="应用名称  :   彼时天气"
                android:textSize="14sp"
                android:textColor="@color/colorFont"
                android:gravity="left"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="20dp"
                android:text="应用作者  :   lentitude"
                android:textColor="@color/colorFont"
                android:textSize="14sp"
                android:gravity="left"/>


            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:background="@android:color/transparent">

                <TextView
                    android:text="Github地址 :"
                    android:textSize="14sp"
                    android:textColor="@color/colorFont"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" />

                <Button
                    android:id="@+id/github"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="https://Github.com/lentitude"
                    android:textAllCaps="false"
                    android:background="@android:color/transparent"
                    android:textColor="@color/colorFont"
                    />

            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/transparent">

                <TextView
                    android:text="CSDN 主页 :"
                    android:textSize="14sp"
                    android:textColor="@color/colorFont"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" />

                <Button
                    android:id="@+id/csdn"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="http://blog.csdn.net/yiwei12"
                    android:textAllCaps="false"
                    android:background="@android:color/transparent"
                    android:textColor="@color/colorFont"
                    />

            </LinearLayout>



        </LinearLayout>




    </android.support.v7.widget.CardView>




</LinearLayout>

最主要的,这里放置了我的 Github 和 CSDN 按钮,如果大家没事,可以点击按钮给颗星星

AboutApplicationActivity:

package com.example.coolweather;

import android.content.Intent;
import android.net.Uri;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;

public class AboutApplicationActivity extends BaseActivity {

    private Button github;
    private Button csdn;
    @Override
    public void initView() {
        setContentView(R.layout.activity_about_application);

        Toolbar toolbar = (Toolbar)findViewById(R.id.tool_bar);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setTitle("关于天气");
            actionBar.setDisplayHomeAsUpEnabled(true);
        }
        github = (Button)findViewById(R.id.github);
        csdn = (Button)findViewById(R.id.csdn);
    }

    @Override
    public void initData() {

    }

    @Override
    public void initListener() {
        github.setOnClickListener(this);
        csdn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        switch (v.getId()){
            case R.id.github:
                intent.setData(Uri.parse("http://Github.com/lentitude"));
                break;
            case R.id.csdn:
                intent.setData(Uri.parse("http://blog.csdn.net/yiwei12"));
                break;
        }
        startActivity(intent);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case android.R.id.home:
                finish();
                break;
        }
        return true;
    }
}

截止现在,功能基本都实现了,还差一个重要的功能,夜间模式,当然夜间模式有很多实现方式,我这里使用的是限定符的方式:在 res 目录下新建一个 values-night 文件夹,用来存放 夜间模式采用的自定义文件

在 values 中的 color 文件:

<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#757575</color>
    <color name="colorFont">#757575</color>
    <color name="colorBackground">#f5f5f5</color>
    <color name="colorWhite">#ffffff</color>
    <color name="colorBlack">#000000</color>
    <color name="colorRed">#E91E63</color>
</resources>

在 values-night 中的 color 文件:

<resources>
    <color name="colorPrimary">#607D8B</color>
    <color name="colorPrimaryDark">#607D8B</color>
    <color name="colorAccent">#607D8B</color>
    <color name="colorFont">#f5f5f5</color>
    <color name="colorBackground">#455A64</color>
    <color name="colorWhite">#607D8B</color>
    <color name="colorBlack">#ffffff</color>
    <color name="colorRed">#00BCD4</color>
</resources>

这样,在系统调用夜间模式时,会调用 values-night 中的 color 文件,否则会调用 values 中的 color 文件

重写 BaseApplication

public class BaseApplication extends Application {
    private static Context context;
    @Override
    public void onCreate() {
        context = getApplicationContext();
        LitePal.initialize(context);
        super.onCreate();
        showTheme();
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                TaskKiller.addActivity(activity);
                // 用来对每一个 Activity 进行监听, 包括第三方 Activity
            }

            @Override
            public void onActivityStarted(Activity activity) {
            }

            @Override
            public void onActivityResumed(Activity activity) {
                showTheme();
            }

            @Override
            public void onActivityPaused(Activity activity) {

            }

            @Override
            public void onActivityStopped(Activity activity) {

            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

            }

            @Override
            public void onActivityDestroyed(Activity activity) {
                TaskKiller.dropActivity(activity);
            }
        });
    }





    public void showTheme(){
        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
        boolean isNight = pref.getBoolean("isNight",false);
        if (isNight){
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
        }else {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
        }

    }

    public static Context getContext(){
        return context;
    }


}

因为重写了 BaseApplication ,所以需要将 AndroidManifest 中的 name属性改为 .BaseApplication,那 LitePal 不能获取到 Context 怎么办呢?我们可以在 Application 的 onCreate( ) 方法中调用 LitePal.initialize(context) 对LitePal 进行初始化,效果也是一样的。在showTheme( )方法中,根据 isNight 的值来判定应用使用的主题,所以在 onCreate( )中调用该方法作为默认主题,那改变主题之后怎么办?这里实现了registerActivityLifecycleCallbacks 接口,可以在其中监听所有 Activity 的生命周期,在每一个 Activity调用resume( )方法时,调用 showTheme( )方法来改变该 Activity( )的主题,那 MainActivity 已经开启了,怎么处理呢,我们在 MainActivity 的点击事件中:

    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case R.id.setting:
                // 跳转到设置界面
                Intent intent1 = new Intent(this, SettingActivity.class);
                intent1.putExtra("weather_title","设置");
                startActivity(intent1);
                break;
            case R.id.night_model:
                SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
                SharedPreferences.Editor editor = pref.edit();
                boolean isNight = pref.getBoolean("isNight", false);
                if (isNight){
                    // 如果已经是夜间模式
                    getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_NO);
                    recreate();
                    editor.putBoolean("isNight", false);
                    editor.apply();
                }else{
                    // 如果是日间模式
                    getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
                    recreate();
                    editor.putBoolean("isNight", true);
                    editor.apply();
                }
                break;
        }
        return true;
    }

根据 isNight 的值来判断要使用什么模式,同时对界面进行重绘,修改 isNight 的值,这样就解决了所有 Activity 的模式转化


最后


好吧,我也没有想到会有这么长,但是毕竟是写完了,如果大家喜欢,给个星吧


完整代码下载地址(Github)


github.com/lentitude/B…