Compose|Compose Android 开发终极挑战赛: 天气应用

前因后果
Compose beta 版发布也快一个月了,Google 官方发起的 Android 开发挑战赛也举办到了最后一期,四期的挑战分别是:

  1. 第一期挑战是做一个领养宠物的应用,全球一共有五百份礼品。第一个我参加了,做了一个很简单的应用,只有一个列表和一个详情页面,在之前的博客中也说过。但是看了 Google 官方发出的别人写的之后,又看了看自己写的,这是个啥。。。。醉了
  2. 第二期挑战是做一个倒计时的应用,全球也是一共五百份。看见之后就写了一个,也是非常简单,只有一个输入框和一个显示倒计时用的 Text 。在看了别人发的之后,又看了看自己写的,这是个啥。。。。醉了
  3. 第三期挑战是 Google 官方出设计图,开发照着官方给出的图做,全球只有三份礼品。三份?全球?别说全球,就是全国、全市都不容易啊!还得看编写的速度。。。算了算了,果断没参加!自己几斤几两还是有点 b 数的。。。。
  4. 第四期挑战是开发一个天气应用,全球只有五份礼品。但和第三期不同的是,这回不比速度,不比速度就好,我没那么快。。。那就搞一搞吧!
既然本篇文章要搞一搞第四期挑战,那咱们就来看看详细要求吧:
Compose|Compose Android 开发终极挑战赛: 天气应用
文章图片
参赛要求 看了要求之后感觉还好,但是有几个点需要注意,要求中说的是 “单个屏幕的天气预报应用”,有点迷,那我就写一个单个屏幕的呗。。。而且可以使用 “模拟的天气数据”,这就好说多了,刚看到的时候还在考虑该用什么数据,国内的天气数据怕出不了海,国外的又怕不稳定,结果后来仔细看了看要求才发现人家说了可以使用模拟数据。。。。。接下来就准备搞一搞吧!
开搞
在开搞之前还是先来看看最终的实现效果吧,上面 Gif 图有点卡顿,来看看静态的效果,:


| Compose|Compose Android 开发终极挑战赛: 天气应用
文章图片
天气1

| 天气2 |
| --- | --- |
我个人觉得写得还挺好看,哈哈哈哈,有点自恋了。。。
数据什么的都是模拟的假数据,其它就没啥了,都是 Compose 的简单使用。下面就来和大家唠唠实现过程吧。
模拟数据 第一步咱们先把数据给模拟出来吧,没有数据页面画起来也不好画。看看上面的图,咱们来总结下需要哪些数据:
  • 地址:这必须有吧,显示在第一行的,光有个天气谁知道是哪的天气,虽然是模拟的,也得像真的是不?
  • 天气:这更得有,天气预报没有天气哪能行!
  • 当前温度:这也是必要的,天气预报应用基本都有这个功能。
  • 空气质量:人们都非常关心的东西,加上吧。
  • 24小时天气:每个小时具体的天气预报,这也得有
  • 未来一周天气:预报嘛,肯定得预报啊
  • 天气基本信息:比如降水概率啊,湿度啊,紫外线啊什么的
嗯,上面列举的差不多了,想要数据肯定得先有实体类来存放数据吧,咱们来看看实体类的写法吧:
data class Weather( val weather: Int = R.string.weather_sunny, val address: Int = R.string.city_new_york, val currentTemperature: Int = 0, val quality: Int = 0, @DrawableRes val background: Int = R.drawable.home_bg_1, @DrawableRes val backgroundGif: Int = R.drawable.bg_topgif_2, val twentyFourHours: List = arrayListOf(), val weekWeathers: List = arrayListOf(), val basicWeathers: List = arrayListOf() ) 复制代码

是不是发现上面代码中多了几个东西,没事,别着急,这就说是啥意思。就算我不说大家肯定也都知道,不就是背景图片和背景 gif 图嘛!没错,就是!
接下来看看 TwentyFourHourWeekWeatherBasicWeather 这三个类吧:
data class TwentyFourHour( val time: String = "", @DrawableRes val icon: Int, val temperature: String )data class WeekWeather( val weekStr: String = "", @DrawableRes val icon: Int, val temperature: String = "" )data class BasicWeather( val name: Int, val value: String = "" ) 复制代码

【Compose|Compose Android 开发终极挑战赛: 天气应用】是不是很简单,就不细说了。
下面就来看看数据的定义吧:
我简单定义了下平时可能遇到的天气状况:
晴 多云 阴 小雨 中雨 大雨 暴雨 小雪 中雪 大雪 暴风雪 雾 结冰 阴霾 复制代码

嗯,就写这些吧,肯定还有很多种天气,这里就不写那么细了,大家如果想加就下载代码自己加吧。
下面就需要一些审美了,我找了一些现在应用商店的天气预报的应用,看了看那个好看,“下载” 点资源图去,要不自己怎么搞。。这块详细步骤就不写了,大家自行百度。
数据差不多都有了,那么该怎么一一对应呢?可能我没说明白,比如说你模拟的天气是下雨,你的 gif 图总不能是在下雪吧?背景图片也不能是冰天雪地啊,对不?
这块我使用的方法是枚举,将不同天气及资源进行一一对应:
enum class WeatherEnum( @StringRes val weather: Int, @DrawableRes val icon: Int, @DrawableRes val background: Int, @DrawableRes val backgroundGif: Int, ) { SUNNY( R.string.weather_sunny, R.drawable.n_weather_icon_sunny, R.drawable.home_bg_1, R.drawable.bg_topgif_10 ),CLOUDY( R.string.weather_cloudy, R.drawable.n_weather_icon_cloud, R.drawable.home_bg_4, R.drawable.bg_topgif_10 ),OVERCAST( R.string.weather_overcast, R.drawable.n_weather_icon_overcast, R.drawable.home_bg_6, R.drawable.bg_topgif_10 ), } 复制代码

这块由于篇幅原因就不写全了,写三个作为演示吧,后面的天气状况也是这么写。
编写ViewModel 数据实体类和资源都准备好了,就差个 ViewModel 来提供数据了,说干就干,整一个 ViewModel
class WeatherPageViewModel : ViewModel() {private val _weatherLiveData = https://www.it610.com/article/MutableLiveData() val weatherLiveData: LiveData = _weatherLiveDataprivate fun onWeatherChanged(weather: Weather) { _weatherLiveData.value = https://www.it610.com/article/weather }} 复制代码

先简单定义一个 ViewModel ,什么?没看明白?回去重新看 MVVM 去。
再来写一个方法,提供给外部获取天气的方法:
fun getWeather() { val random = Random() val city = cityArray[random.nextInt(5)] val weatherEnums = WeatherEnum.values() val weatherEnum = weatherEnums[random.nextInt(14)] val calendar = Calendar.getInstance() val hours: Int = calendar.get(Calendar.HOUR) val twentyFourHours = arrayListOf() val weekWeathers = arrayListOf() for (index in hours + 1..24) { twentyFourHours.add( TwentyFourHour( "$index:00", weatherEnum.icon, "${random.nextInt(29)}°" ) ) } val week = calendar.get(Calendar.DAY_OF_WEEK) val weekListString = DateUtils.getWeekListString(week = week) for (index in weekListString.indices) { val small = random.nextInt(10) weekWeathers.add( WeekWeather( weekListString[index], getWeatherIcon(random.nextInt(35)), "$small°/${small + 7}°" ) ) }val basicWeathers = arrayListOf() basicWeathers.add(BasicWeather(R.string.basic_rain, "${random.nextInt(100)}%")) basicWeathers.add(BasicWeather(R.string.basic_humidity, "${random.nextInt(100)}%"))val weather = Weather( weatherEnum.weather, address = city, currentTemperature = random.nextInt(30), quality = random.nextInt(100), background = weatherEnum.background, backgroundGif = weatherEnum.backgroundGif, twentyFourHours = twentyFourHours, weekWeathers = weekWeathers, basicWeathers = basicWeathers ) onWeatherChanged(weather) } 复制代码

数据很简单,大部分是直接通过 Random 来随机生成的,第一次进入或刷新的时候就可以生成了。
画页面
数据都准备好了,就差页面了,画一画吧,不管什么时候,页面都是比较简单的,相对于数据逻辑来说,可能我这话说的有点绝对,也可能因为我太年轻。。。不多说了,开始画吧!
咱们就从 Activity 开始吧:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) BarUtils.transparentStatusBar(this) setContent { MyTheme { WeatherPage() } } } } 复制代码

上面代码很简单,为什么这么写在这里就不说了,大家可以去看我之前的文章。
对了,里面有一行代码,顾名思义,就是设置状态栏为透明的,代码很容易找到,就不贴了。
接着来看 WeatherPage :
@Composable fun WeatherPage() { val refreshingState = remember { mutableStateOf(REFRESH_STOP) } val weatherPageViewModel: WeatherPageViewModel = viewModel() val weather by weatherPageViewModel.weatherLiveData.observeAsState(Weather()) var loadState by remember { mutableStateOf(false) } if (!loadState) { loadState = true weatherPageViewModel.getWeather() }Surface(color = MaterialTheme.colors.background) { SwipeToRefreshLayout( refreshingState = refreshingState.value, onRefresh = { refreshingState.value = https://www.it610.com/article/REFRESH_START weatherPageViewModel.getWeather() loadState = true refreshingState.value = REFRESH_STOP }, progressIndicator = { ProgressIndicator() } ) { WeatherBackground(weather) WeatherContent(weather) } } } 复制代码

这块代码就得稍微说一说了,先从 Surface 看起,里面包裹了一个 SwipeToRefreshLayout ,看过上一篇文章的应该知道,这是下拉刷新的控件,如果没看过上一篇文章的,可以先去看看:Compose 实现下拉刷新和上拉加载。
再来看上面的内容:
  1. refreshingState 就是是否正在刷新的状态,在 onRefresh 开始时设置为 REFRESH_START,刷新完成之后设置为 REFRESH_STOP,默认状态也是 REFRESH_STOP。
  2. weatherPageViewModel 就是上面咱们写的,不过多解释
  3. weather 这个就是将 ViewModel 中的 LiveData 转为 Compose中支持观察的 State
  4. loadState 是记住是否加载过,避免重复加载数据。
onRefresh 中的刷新内容就是直接调一下 weatherPageViewModel 中的 getWeather() 方法。
然后直接开始看大括号中的内容,看着应该就知道是啥意思了,WeatherBackground 是背景,WeatherContent 是内容,那为什么要传入 Weather 呢?当然是为了展示了。。。
画背景 接下来先来看看 WeatherBackground 吧:
@Composable fun WeatherBackground(weather: Weather) { Box { Image( modifier = Modifier.fillMaxSize(), painter = painterResource(weather.background), contentDescription = stringResource(id = weather.weather), contentScale = ContentScale.Crop ) val context = LocalContext.current val glide = Glide.with(context) CompositionLocalProvider(LocalRequestManager provides glide) { GlideImage( modifier = Modifier.fillMaxSize(), data = https://www.it610.com/article/weather.backgroundGif, contentDescription = stringResource(id = weather.weather), contentScale = ContentScale.Crop ) } } } 复制代码

很简单,一张背景图,一张 gif 动态图,用来展示下雨或者下雪等特效。这块的动态图使用了 Glide 的 gif 展示功能。使用方法上面已经贴出来了,下面贴下依赖吧:
implementation "dev.chrisbanes.accompanist:accompanist-glide:0.6.0" 复制代码

画内容 WeatherContent 是内容,这里有好几块,咱们慢慢看:
@Composable fun WeatherContent(weather: Weather) { val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 10.dp) .verticalScroll(scrollState), ) { WeatherBasic(weather, scrollState) WeatherDetails(weather) WeatherWeek(weather) WeatherOther(weather) } } 复制代码

可以看到分为好几块,分别对应上面图中的几块。上面还写的有 scrollState ,保存着滚动的状态,由于咱们想要竖着滑动,所以设置 verticalScroll(scrollState)
WeatherBasic 这块是天气的基本信息,也就是城市啊、天气状况啊、当前温度啊啥的,上面的代码中还将滚动状态传入了这里,下面咱们就能看到作用了:
@Composable fun WeatherBasic(weather: Weather, scrollState: ScrollState) { val offset = (scrollState.value / 2) val fontSize = (100f / offset * 70).coerceAtLeast(30f).coerceAtMost(75f).sp val modifier = Modifier .fillMaxWidth() .wrapContentWidth(Alignment.CenterHorizontally) .graphicsLayer { translationY = offset.toFloat() } val context = LocalContext.current Text( modifier = modifier.padding(top = 100.dp, bottom = 5.dp), text = stringResource(id = weather.address), fontSize = 20.sp, color = Color.White, ) AnimatedVisibility(visible = fontSize == 75f.sp) { Text( modifier = modifier.padding(top = 5.dp, bottom = 5.dp), text = "${weather.currentTemperature}°", fontSize = fontSize, color = Color.White ) } Text( modifier = modifier.padding(top = 5.dp, bottom = 2.5.dp), text = stringResource(id = weather.weather), fontSize = 25.sp, color = Color.White ) AnimatedVisibility(visible = fontSize == 75f.sp) { Text( modifier = modifier.padding(top = 2.5.dp), text = stringResource(id = R.string.weather_air_quality) + " " + weather.quality, fontSize = 15.sp, color = Color.White ) } Text( modifier = Modifier.padding(top = 45.dp, start = 10.dp), text = DateUtils.getDefaultDate(context, System.currentTimeMillis()), fontSize = 16.sp, color = Color.White ) } 复制代码

是不是很简单?使用 AnimatedVisibility 来控制是否显示当前温度和空气质量,嗯,就没了。。。还有啥?大家可以试试 Modefier 的各种功能,越用越发现这个东西的强大。。。。
WeatherDetails 这个吧,就是 24 小时天气详情,上面数据已经写好了,直接展示即可:
@Composable fun WeatherDetails(weather: Weather) { val twentyFourHours = weather.twentyFourHours LazyRow(modifier = Modifier.fillMaxWidth()) { items(twentyFourHours) { twentyFourHour -> WeatherHour(twentyFourHour) } } }@Composable fun WeatherHour(twentyFourHour: TwentyFourHour) { val modifier = Modifier.padding(top = 9.dp) Column(modifier = Modifier.width(50.dp), horizontalAlignment = Alignment.CenterHorizontally) { Text(modifier = modifier, text = twentyFourHour.time, color = Color.White, fontSize = 15.sp) Image( modifier = modifier.size(25.dp), painter = painterResource(id = twentyFourHour.icon), contentDescription = twentyFourHour.temperature ) Text( modifier = modifier, text = twentyFourHour.temperature, color = Color.White, fontSize = 15.sp ) } } 复制代码

一个 LazyRow 就搞定了,是不是很省事,比之前的 RecyclerView 还简单。。。
WeatherWeek 这是下面的未来一周的天气,和上面 24 小时类似:
@Composable fun WeatherWeek(weather: Weather) { Column( modifier = Modifier .fillMaxSize() .padding(top = 10.dp) .padding(horizontal = 10.dp) ) { for (weekWeather in weather.weekWeathers) { WeatherWeekDetails(weekWeather) } } } 复制代码

里面具体的 WeatherWeekDetails 由于篇幅原因就不贴代码了,大家可以去下载代码看。
WeatherOther 这个是当天天气的一些值,比如降水概率啊、湿度啊啥的:
@Composable fun WeatherOther(weather: Weather) { Column( modifier = Modifier .fillMaxSize() .padding(top = 10.dp) .padding(horizontal = 10.dp) ) { for (weekWeather in weather.basicWeathers) { WeatherOtherDetails(weekWeather) } } } 复制代码

文末总结
到这里基本上就结束了,就这么点内容,就写出了我自认为挺好看的一个天气应用。大家如果想要代码的话直接去 Github 中看:github.com/zhujiang521…
最后再打个小广告,如果想学习 Compose 的话,可以去看我另一个库,里面有详细的例子和 Demo 供你参考:Github 地址:github.com/zhujiang521…,别忘了是 main 分支。
先这样了,咱们下回见!
原文链接:https://juejin.cn/post/6942386785452294181

    推荐阅读