上一篇:Android 天气APP(九)细节优化、必应每日一图
修复每日一图,增加下拉刷新,滑动改变标题
新版-------------------
在上一篇文章中,完成了重新定位已经必应壁纸的使用,本篇文章中将修复一下每日请求必应壁纸的bug,同时增加一个下拉刷新天气数据的功能。
一、修复每日请求必应壁纸Bug
对于之前的每日第一次打开App时的判读逻辑有一些问题,问题出在不应该用当前时间与当天12点的时间进行比较,而是应该判断缓存中的时间是否为当天,是当天则不再请求必应壁纸,不是当天则请求必应壁纸,这样可能比较好一些,修改EasyDate,新增如下方法代码:
/**
* 是否当天
* @param time 时间戳
*/
public static boolean isToday(long time) {
return isToday(stampToDate(time));
}
/**
* 是否当天
* @param day 时间格式字符串
*/
public static boolean isToday(String day) {
Calendar pre = Calendar.getInstance();
Date predate = new Date(System.currentTimeMillis());
pre.setTime(predate);
Calendar cal = Calendar.getInstance();
Date date = null;
try {
date = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA).parse(day);
} catch (ParseException e) {
e.printStackTrace();
}
cal.setTime(date);
if (cal.get(Calendar.YEAR) == (pre.get(Calendar.YEAR))) {
int diffDay = cal.get(Calendar.DAY_OF_YEAR)
- pre.get(Calendar.DAY_OF_YEAR);
return diffDay == 0;
}
return false;
}
然后在SplashActivity中使用,修改checkFirstRunToday()方法,代码如下所示:
private void checkFirstRunToday() {
long todayFirstRunTime = MVUtils.getLong(Constant.FIRST_STARTUP_TIME_TODAY);
long currentTimeMillis = System.currentTimeMillis();
//满足更新启动时间的条件,1.为0表示没有保存过时间,2. 保存时间是否为今天
if (todayFirstRunTime == 0 || !EasyDate.isToday(todayFirstRunTime)) {
MVUtils.put(Constant.FIRST_STARTUP_TIME_TODAY, currentTimeMillis);
//今天第一次启动要做的事情
viewModel.bing();
}
}
二、增加下拉刷新
增加下拉刷新之前我们需要想清楚一个问题,那就是刷新的是什么?因为当前获取城市信息的方式有定位和切换城市两种,两种方式都会请求搜索城市接口,那么我们可以保存一个变量,在定位和切换城市时赋值,最后在下拉刷新的时候通过这个变量去搜索城市,同时给一个刷新的标志位,在搜索城市返回的时候判断这个标识位,如果有刷新就停止刷新。
要实现这个功能,首先增加一个下拉刷新的依赖库,在app的build.gradle的dependencies{}闭包下添加如下代码:
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
然后Sync Now,然后去修改一下activity_main.xml中的代码,为了避免你添加错误的问题,我这里贴上完整的xml代码:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/lay_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/main_bg"
android:fitsSystemWindows="true"
tools:context=".ui.MainActivity">
<!--顶部标题-->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/materialToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="城市天气"
android:textColor="@color/white"
android:textSize="@dimen/sp_16" />
</com.google.android.material.appbar.MaterialToolbar>
<!--下拉刷新视图-->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/lay_refresh"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/materialToolbar">
<!--滚动视图-->
<androidx.core.widget.NestedScrollView
android:id="@+id/lay_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--页面主要内容视图-->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_0">
<!--天气状况-->
<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_16"
android:layout_marginTop="@dimen/dp_8"
android:text="天气状况"
android:textColor="@color/white"
android:textSize="@dimen/sp_18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--温度-->
<TextView
android:id="@+id/tv_temp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_24"
android:text="0"
android:textColor="@color/white"
android:textSize="@dimen/sp_60"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_info" />
<!--摄氏度符号-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="℃"
android:textColor="@color/white"
android:textSize="@dimen/sp_24"
app:layout_constraintStart_toEndOf="@+id/tv_temp"
app:layout_constraintTop_toTopOf="@+id/tv_temp" />
<!--城市-->
<TextView
android:id="@+id/tv_city"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_32"
android:text="城市"
android:textColor="@color/white"
android:textSize="@dimen/sp_20"
app:layout_constraintEnd_toEndOf="@+id/tv_temp"
app:layout_constraintStart_toStartOf="@+id/tv_temp"
app:layout_constraintTop_toBottomOf="@+id/tv_temp" />
<!--上一次更新时间-->
<TextView
android:id="@+id/tv_update_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_8"
android:text="上次更新时间:"
android:textColor="@color/white"
android:textSize="@dimen/sp_12"
app:layout_constraintEnd_toEndOf="@+id/tv_city"
app:layout_constraintStart_toStartOf="@+id/tv_city"
app:layout_constraintTop_toBottomOf="@+id/tv_city" />
<!--天气预报列表-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_daily"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_16"
android:paddingStart="@dimen/dp_16"
android:paddingEnd="@dimen/dp_16"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_update_time" />
<!--风向风力-->
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_16"
android:layout_marginTop="8dp"
android:text="风向风力"
android:textColor="@color/white"
android:textSize="@dimen/sp_18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rv_daily" />
<!--大风车-->
<com.llw.goodweather.ui.view.WhiteWindmills
android:id="@+id/ww_big"
android:layout_width="100dp"
android:layout_height="120dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:ignore="MissingConstraints" />
<!--小风车-->
<com.llw.goodweather.ui.view.WhiteWindmills
android:id="@+id/ww_small"
android:layout_width="50dp"
android:layout_height="60dp"
android:layout_marginStart="32dp"
app:layout_constraintBottom_toBottomOf="@+id/ww_big"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/ww_big"
tools:ignore="MissingConstraints" />
<!--纵向辅助线-->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="205dp" />
<!--风向风力文字-->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="@+id/ww_big"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/ww_big">
<!--风向-->
<TextView
android:id="@+id/tv_wind_direction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="@dimen/sp_14" />
<!--风力-->
<TextView
android:id="@+id/tv_wind_power"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_24"
android:textColor="@color/white"
android:textSize="@dimen/sp_14" />
</LinearLayout>
<!--生活建议-->
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_16"
android:layout_marginTop="16dp"
android:text="生活建议"
android:textColor="@color/white"
android:textSize="@dimen/sp_18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ww_big" />
<!--生活建议列表-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_lifestyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:paddingStart="@dimen/dp_16"
android:paddingEnd="@dimen/dp_16"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
这里实际上就是在之前的滑动控件外面嵌套了一个下拉刷新控件,很简单的。
这里我分别给了刷新控件和滑动控件一个id,下面会用到,现在你什么都不用改,你可以运行,下拉试试看,会有效果,但是你关不掉这个刷新效果。
下面回到MainActivity中,新增如下变量:
//城市名称,定位和切换城市都会重新赋值。
private String mCityName;
//是否正在刷新
private boolean isRefresh;
然后是变量赋值的地方,定位返回:
切换城市返回:
然后添加下拉刷新的监听,在initView()方法中添加如下代码:
//下拉刷新监听
binding.layRefresh.setOnRefreshListener(() -> {
if (mCityName == null) {
binding.layRefresh.setRefreshing(false);
return;
}
//设置正在刷新
isRefresh = true;
//搜索城市
viewModel.searchCity(mCityName);
});
这里的代码应该好理解,然后在搜索城市的返回中,判断当前是否在刷新,如下图所示:
下面可以运行一下了。
你可以在控制看看是否有重新请求网络接口。
三、滑动改变标题
现在我们的页面如果滑动到最底部的话,你就不知道当前处于那个城市了,那么我们可以通过计算滑动距离的方式,当超过指定距离之后将城市的标题赋值个当前页面标题,这样是可以增加用户的体验的。
这里我们就用这里图中标注的高度,当滑动距离超过城市的绘制高度时就改变这个标题,下面我们还是需要修改一下activity_main.xml,将这里天气状况、温度和城市三个控件放到一个布局中,同时给一个id,然后在滑动的时候计算布局的绘制高度就可以了,这里我还是贴一下完整的布局代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/lay_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/main_bg"
android:fitsSystemWindows="true"
tools:context=".ui.MainActivity">
<!--顶部标题-->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/materialToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:ellipsize="middle"
android:singleLine="true"
android:text="城市天气"
android:textColor="@color/white"
android:textSize="@dimen/sp_16" />
</com.google.android.material.appbar.MaterialToolbar>
<!--下拉刷新视图-->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/lay_refresh"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/materialToolbar">
<!--滚动视图-->
<androidx.core.widget.NestedScrollView
android:id="@+id/lay_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--页面主要内容视图-->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp_0">
<!--滑动距离布局-->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/lay_scroll_height"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!--天气状况-->
<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_16"
android:layout_marginTop="@dimen/dp_8"
android:text="天气状况"
android:textColor="@color/white"
android:textSize="@dimen/sp_18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--温度-->
<TextView
android:id="@+id/tv_temp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_24"
android:text="0"
android:textColor="@color/white"
android:textSize="@dimen/sp_60"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_info" />
<!--摄氏度符号-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="℃"
android:textColor="@color/white"
android:textSize="@dimen/sp_24"
app:layout_constraintStart_toEndOf="@+id/tv_temp"
app:layout_constraintTop_toTopOf="@+id/tv_temp" />
<!--城市-->
<TextView
android:id="@+id/tv_city"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_32"
android:text="城市"
android:textColor="@color/white"
android:textSize="@dimen/sp_20"
app:layout_constraintEnd_toEndOf="@+id/tv_temp"
app:layout_constraintStart_toStartOf="@+id/tv_temp"
app:layout_constraintTop_toBottomOf="@+id/tv_temp" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!--上一次更新时间-->
<TextView
android:id="@+id/tv_update_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_8"
android:text="上次更新时间:"
android:textColor="@color/white"
android:textSize="@dimen/sp_12"
app:layout_constraintEnd_toEndOf="@+id/lay_scroll_height"
app:layout_constraintStart_toStartOf="@+id/lay_scroll_height"
app:layout_constraintTop_toBottomOf="@+id/lay_scroll_height" />
<!--天气预报列表-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_daily"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_16"
android:paddingStart="@dimen/dp_16"
android:paddingEnd="@dimen/dp_16"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_update_time" />
<!--风向风力-->
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_16"
android:layout_marginTop="8dp"
android:text="风向风力"
android:textColor="@color/white"
android:textSize="@dimen/sp_18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rv_daily" />
<!--大风车-->
<com.llw.goodweather.ui.view.WhiteWindmills
android:id="@+id/ww_big"
android:layout_width="100dp"
android:layout_height="120dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:ignore="MissingConstraints" />
<!--小风车-->
<com.llw.goodweather.ui.view.WhiteWindmills
android:id="@+id/ww_small"
android:layout_width="50dp"
android:layout_height="60dp"
android:layout_marginStart="32dp"
app:layout_constraintBottom_toBottomOf="@+id/ww_big"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/ww_big"
tools:ignore="MissingConstraints" />
<!--纵向辅助线-->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="205dp" />
<!--风向风力文字-->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="@+id/ww_big"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/ww_big">
<!--风向-->
<TextView
android:id="@+id/tv_wind_direction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="@dimen/sp_14" />
<!--风力-->
<TextView
android:id="@+id/tv_wind_power"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_24"
android:textColor="@color/white"
android:textSize="@dimen/sp_14" />
</LinearLayout>
<!--生活建议-->
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_16"
android:layout_marginTop="16dp"
android:text="生活建议"
android:textColor="@color/white"
android:textSize="@dimen/sp_18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ww_big" />
<!--生活建议列表-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_lifestyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:paddingStart="@dimen/dp_16"
android:paddingEnd="@dimen/dp_16"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
然后回到MainActivity,在initView()方法中,添加如下代码:
//滑动监听
binding.layScroll.setOnScrollChangeListener((View.OnScrollChangeListener) (v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
if (scrollY > oldScrollY) {
//getMeasuredHeight() 表示控件的绘制高度
if (scrollY > binding.layScrollHeight.getMeasuredHeight()) {
binding.tvTitle.setText((mCityName == null ? "城市天气" : mCityName));
}
} else if (scrollY < oldScrollY) {
if (scrollY < binding.layScrollHeight.getMeasuredHeight()) {
//改回原来的
binding.tvTitle.setText("城市天气");
}
}
});
下面运行一下看看:
四、文章源码
欢迎 Star 和 Fork
第十篇文章源码地址:GoodWeather-New-10
旧版-------------------
下拉刷新页面天气数据
根据小伙伴的评论,我增加了页面数据的下拉刷新,首先在修改布局,
可以看到我在androidx.core.widget.NestedScrollView的外层嵌套了一个com.scwang.smartrefresh.layout.SmartRefreshLayout(PS:依赖中引入的下拉刷新框架)和com.scwang.smartrefresh.header.StoreHouseHeader(PS:刷新样式)
<!--下拉刷新布局-->
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/refresh"
app:srlPrimaryColor="#00000000"<!--背景色-->
app:srlAccentColor="#FFF"<!--文字颜色-->
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--刷新头部样式-->
<com.scwang.smartrefresh.header.StoreHouseHeader
app:shhText="GOOD WEATHER"<!--自定义的文字-->
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<!--NestedScrollView 里面只能包裹一个大的布局,
当这个布局长度超出手机展示的部分就可以滚动,其中overScrollMode="never"
的意思是隐藏掉滚动条到顶部和底部时的水波纹-->
<androidx.core.widget.NestedScrollView
android:overScrollMode="never"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.core.widget.NestedScrollView>
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
只要复制上面的刷新布局和样式布局即可
接下来在Activity中
修改的地方也比较简单,这里就不贴代码了。
上图中红线框中的布局就是下拉刷新布局。
增加定位图标
之前我想了一下,定位的话还是给一个定位图标比较好,未获取到数据之前显示定位中,获取数据之后显示定位到的城市和定位图标,这样可以增加用户的体验,虽然很多人不会注意这个小细节,但是很多APP之所以受欢迎就是因为细节做得好,体验感强。
icon_location.png
这就是这个图标。
然后修改布局文件
我也修改了上面的温度的布局,让它居中
然后在MainActivity里面
@BindView(R.id.iv_location)
ImageView ivLocation;//定位图标
private boolean flag = true;//图标显示标识,true显示,false不显示,只有定位的时候才为true,切换城市和常用城市都为false
运行效果如下:
这是第十篇文章,有好的想法我会一直更新这个APP的,当然文章也会一直更新下去,虽然只是一些小功能,但是积少成多呀。
源码地址:GoodWeather
欢迎 Star 和 Fork