第三阶段

手动刷新天气

修改activity_weather.xml中的代码,如下所示:

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ScrollView
android:id="@+id/weatherLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:scrollbars="none"
android:visibility="invisible">
...
</ScrollView>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

可以看到,这里在ScrollView的外面嵌套了一层SwipeRefreshLayout,这样ScrollView就自 动拥有下拉刷新功能了

然后修改WeatherActivity中的代码,加入刷新天气的处理逻辑,如下所示:

class WeatherActivity: AppCompatActivity(){
val viewMode by lazy{ViewModelProvider(this).get(WeatherViewModel::class.java)}

override fun onCreate(savedInstanceState: Bundle?){
viewModel.weatherLiveData.observe(this, Observer { result ->
val weather = result.getOrNull()
if (weather != null) {
showWeatherInfo(weather)
} else {
Toast.makeText(this, "无法成功获取天气信息", Toast.LENGTH_SHORT).show()
}
swipRefresh.isRefreshing = false

})
swipRefresh = binding.swipeRefresh
swipRefresh.setColorSchemeResources(R.color.colorPrimary)
refreshWeather()
swipRefresh.setOnRefreshListener {
refreshWeather()
}
}
}

切换城市

完成了手动刷新天气的功能,接下来我们继续实现切换城市功能。

我们将之前的fragment引入到天气界面的布局中,并将它放入滑动菜单中,按照Material Desion的建议,我们需要在头布局中加入一个切换城市按钮,修改now.xml中的代码,如下所示:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:id="@+id/nowLayout"
android:layout_width="match_parent"
android:layout_height="530dp"
android:orientation="vertical">

<FrameLayout
android:id="@+id/titleLayout"
android:layout_width="match_parent"
android:layout_height="70dp"
android:fitsSystemWindows="true">

<Button
android:id="@+id/navBtn"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="15dp"
android:layout_gravity="center_vertical"
android:background="@drawable/ic_home" />
...
</FrameLayout>
...
</RelativeLayout>

这里添加了一个Button作为切换城市的按钮,并且让它居左显示。

接着修改activity_weather.xml布局来加入滑动菜单功能,如下所示:

<androidx.drawerlayout.widget.DrawerLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

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

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:clickable="true"
android:focusable="true"
android:background="@color/colorPrimary">

<fragment
android:id="@+id/placeFragment"
android:name="com.sunnyweather.android.ui.place.PlaceFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="25dp"/>

</FrameLayout>

</androidx.drawerlayout.widget.DrawerLayout>

可以看到,我们在SwipeRefreshLayout的外面又嵌套了一层DrawerLayout。 DrawerLayout中的第一个子控件用于显示主屏幕中的内容,第二个子控件用于显示滑动菜单 中的内容,因此这里我们在第二个子控件的位置添加了用于搜索全球城市数据的Fragment。另 外,为了让Fragment中的搜索框不至于和系统状态栏重合,这里特意使用外层包裹布局的方式 让它向下偏移了一段距离。

接下来需要在WeatherActivity中加入滑动菜单的逻辑处理,修改WeatherActivity中的代码, 如下所示:

class WeatherActivity: AppCompatActivity(){
、、、
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
、、、
val navBtn = binding.navBtn
navBtn.setOnClickListener{
binding.drawerLayout.openDrawer(GravityCompat.START)
}
drawerLayout.addDrawerListener{object: DrawLayout.DrawerListener{
override fun onDrawerStateChanged(newState: Int){}

override fun onDrawerOpened(drawerView: View){}

override fun onDrawerOpened(drawerView: View){}

override fun onDrawerClosed(drawerView: View){
val manager = getSystemService(Context.INPUT_METHOD_SERVICE)
as InputMethodManager
manager.hideSoftInputFromWindow(drawerView.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS)

}
}}
}
}

这里我们做了两件事:第一,在切换城市按钮的点击事件中调用DrawerLayout的openDrawer()方法来打开滑动菜单;第二,监听DrawerLayout的状态,当滑动擦弹被隐藏的时候,同时也要隐藏输入法。

另外,我们之前在PlaceFragment中做过一个数据存储状态的判断,假如已经有选中的城市保 存在SharedPreferences文件中了,那么就直接跳转到WeatherActivity。但是现在将 PlaceFragment嵌入WeatherActivity中之后,如果还执行这段逻辑肯定是不行的,因为这会 造成无限循环跳转的情况。为此需要对PlaceFragment进行如下修改:

class PlaceFragment : Fragment() { 
...
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (activity is MainActivity && viewModel.isPlaceSaved()) {
val place = viewModel.getSavedPlace()
val intent = Intent(context, WeatherActivity::class.java).apply {
putExtra("location_lng", place.location.lng)
putExtra("location_lat", place.location.lat)
putExtra("place_name", place.name)
}
startActivity(intent)
activity?.finish()
return
}
...
}
}

不过现在还没有结束,我们还需要处理切换城市后的逻辑。这个工作就必须在PlaceAdapter中 进行了,因为之前选中了某个城市后是跳转到WeatherActivity的,而现在由于我们本来就是 在WeatherActivity中的,因此并不需要跳转,只要去请求新选择城市的天气信息就可以了。

那么很显然,这里同样需要根据PlaceFragment所处的Activity来进行不同的逻辑处理,修改 PlaceAdapter中的代码,如下所示:

class PlaceAdapter(private val fragment: PlaceFragment, private val placeList: 
List<Place>): RecyclerView.Adapter<PlaceAdapter.ViewHolder>(){
inner class ViewHolder(val binding: PlaceItemBinding): RecyclerView.ViewHolder(binding.root)

override fun getIteCount(): Int{
return placeList.size
}

override fun onCreateViewHolder(parent: ViewGroup, ViewType: Int): ViewHolder{
val binding = PlaceItemBinding.inflate(LayoutInfalter.from(paretn.contenxt),parent,false)
return ViewHodler(binding)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int){
val place = placeList[position]
holder.binding.placeName.text = place.name
holder.binding.placeAddreass.text = place.address
holder.itemView.setOnClickListener{
val activity = fragment.activity
if(activity is WeaterhAtivity){
activity.getDrawerLayout().closeDrawers()
activity.viewModel.locationlng = place.location.lng
activity.viewModel.locationLnt = place.location.lat
activity.viewModel.placeName = place.name
activity.refreshWeaterh()
}else{
val intent = Intent(fragment.requireContext(),WeatherActivity::class.java).apply{
putExtra("location_lng",place.locaiton.lng)
putExtra("location_lat",place.location.lat)
putExtra("place_name",place.name)
}
fragment.startActivity(intent)
fragment.activity?.finish()
}
fragment.viewModel.savePlace(place)
}
}

}

制作App的图标

点击导航栏中的File→New→Image Asset打开Asset Studio工具,左边是操作区域,右边是预览区域。

第一行的Icon Type保持默认就可以了,表示同时创建兼容8.0系统以及老版 本系统的应用图标。第二行的Name用于指定应用图标的名称,这里也保持ic_launcher的命 名即可,这样可以覆盖掉之前自动生成的应用程序图标。接下来的3个页签,Foreground Layer用于编辑前景层,Background Layer用于编辑背景层,Legacy用于编辑老版本系统的 图标。

来看预览区域,这个就更简单了,它的主要作用就是预览应用图标的最终效果。在预览区域 中给出了可能生成的图标形状,包括圆形、圆角矩形、方形,等等。注意,每个预览图标中都 有一个圆圈,这个圆圈叫作安全区域,必须保证图标的前景层完全处于安全区域中才行,否则 可能会出现应用图标的Logo被手机厂商的mask裁剪掉的情况。

下面我们来具体操作一下吧,在Foreground Layer中选取之前准备好的那张Logo图片,并通 过下方的Resize拖动条对图片进行缩放,以保证前景层的所有内容都是在安全区域中的。然后 在Background Layer中选择“Color”这种Asset Type模式,并使用#219FDD这个颜色值作为 背景层的颜色。最终的预览效果如图15.34所示。

在预览区域可以看到,现在我们的图标已经能够应对各种不同类型的mask了。

接下来点击“Next”会进入一个确认图标生成路径的界面,然后直接点击界面上的“Finish”按钮 就可以完成图标的制作了。所有图标相关的文件都会被生成到相应分辨率的mipmap目录下,

但是,其中有一个mipmap-anydpi-v26目录中放的并不是图片,而是xml文件,这是什么意思 呢?其实只要是Android 8.0及以上系统的手机,都会使用这个目录下的文件来作为图标。我们 可以打开ic_launcher.xml文件来查看它的代码:

<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

这就是适配Android 8.0及以上系统应用图标的标准写法。可以看到,这里在标签中定义了一个标签用于指定图标的背景层,引用的是我们之前设置 的颜色值。又定义一个标签用于指定图标的前景层,引用的就是我们之前准备 的那张Logo图片。

那么这个ic_launcher.xml文件又是在哪里被引用的呢?其实只要打开一下 AndroidManifest.xml文件,所有的秘密就被解开了,代码如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
package="com.sunnyweather.android">
...
<application
android:name=".SunnyWeatherApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
</application>
</manifest>

可以看到,标签的android:icon属性就是专门用于指定应用程序图标的, 这里将图标指定成了@mipmap/ic_launcher,那么在Android 8.0及以上系统中,就会使用 mipmap-anydpi-v26目录下的ic_launcher.xml文件来作为应用图标。7.0及以下系统就会使 用mipmap相应分辨率目录下的ic_launcher.png图片来作为应用图标。另外你可能注意到了, 标签中还有一个android:roundIcon属性,这是一个只适用于Android 7.1 系统的过渡版本,很快就被8.0系统的新图标适配方案所替代了,我们可以不必关心它。这样 SunnyWeather的图标就制作完成了,现在重新运行一下程序,并观察桌面应用。

可以看到,SunnyWeather的图标在Pixel模拟器上被裁剪成了圆形,和其他应用图标的形状是 保持一致的。而如果你在别的手机上运行,得到的可能会是不同的效果。

另外,在Pixel模拟器上,由于SunnyWeather这个名字太长了,因此应用名没能得到完整的显 示。如果你想要将它修改成短一点的名字,打开res/values/string.xml文件,并编辑如下部分 内容即可:

<resources> 
<string name="app_name">SunnyWeather</string>
</resources>