当前位置:   article > 正文

Compose Android开发终极挑战赛: 写一个天气应用

compose @stringres


/   今日科技快讯   /

共享充电宝用不起后,共享单车也骑不起了。经AI财经社了解,美团、哈啰等共享单车的收费,和最初相比都悄然发生着变化。在上海,哈啰单车目前的价格是前15分钟收费1.5元,此后每15分钟收费1元;算下来1小时的价格是4.5元。而共享充电宝也因为涨价频频登上热搜,每小时的价格从1元涨至4元,24小时封顶价从20元涨到了40元。

/   作者简介   /

本篇文章来自Zhujiang同学投稿,讲解了如何用Compose开发天气应用的相关内容,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

Zhujiang的博客地址:

https://juejin.cn/user/3913917127985240

/   前因后果   /

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

  • 第一期挑战是做一个领养宠物的应用,全球一共有五百份礼品。第一个我参加了,做了一个很简单的应用,只有一个列表和一个详情页面。但是看了 Google 官方发出的别人写的之后,又看了看自己写的,这是个啥。。。。醉了

  • 第二期挑战是做一个倒计时的应用,全球也是一共五百份。看见之后就写了一个,也是非常简单,只有一个输入框和一个显示倒计时用的 Text 。在看了别人发的之后,又看了看自己写的,这是个啥。。。。醉了

  • 第三期挑战是 Google 官方出设计图,开发照着官方给出的图做,全球只有三份礼品。三份?全球?别说全球,就是全国、全市都不容易啊!还得看编写的速度。。。算了算了,果断没参加!自己几斤几两还是有点 b 数的。。。。

  • 第四期挑战是开发一个天气应用,全球只有五份礼品。但和第三期不同的是,这回不比速度,不比速度就好,我没那么快。。。那就搞一搞吧!

既然本篇文章要搞一搞第四期挑战,那咱们就来看看详细要求吧:

看了要求之后感觉还好,但是有几个点需要注意,要求中说的是 “单个屏幕的天气预报应用”,有点迷,那我就写一个单个屏幕的呗。。。而且可以使用 “模拟的天气数据”,这就好说多了,刚看到的时候还在考虑该用什么数据,国内的天气数据怕出不了海,国外的又怕不稳定,结果后来仔细看了看要求才发现人家说了可以使用模拟数据。。。。。接下来就准备搞一搞吧!

/   开搞   /

在开搞之前还是先来看看最终的实现效果吧,上面 Gif 图有点卡顿,来看看静态的效果:

我个人觉得写得还挺好看,哈哈哈哈????,有点自恋了。。。

数据什么的都是模拟的假数据,其它就没啥了,都是 Compose 的简单使用。下面就来和大家唠唠实现过程吧。

模拟数据

第一步咱们先把数据给模拟出来吧,没有数据页面画起来也不好画。看看上面的图,咱们来总结下需要哪些数据:

  • 地址:这必须有吧,显示在第一行的,光有个天气谁知道是哪的天气,虽然是模拟的,也得像真的是不?

  • 天气:这更得有,天气预报没有天气哪能行!

  • 当前温度:这也是必要的,天气预报应用基本都有这个功能。

  • 空气质量:人们都非常关心的东西,加上吧。

  • 24小时天气:每个小时具体的天气预报,这也得有

  • 未来一周天气:预报嘛,肯定得预报啊

  • 天气基本信息:比如降水概率啊,湿度啊,紫外线啊什么的

嗯,上面列举的差不多了,想要数据肯定得先有实体类来存放数据吧,咱们来看看实体类的写法吧:

  1. data class Weather(
  2.     val weather: Int = R.string.weather_sunny,
  3.     val address: Int = R.string.city_new_york,
  4.     val currentTemperature: Int = 0,
  5.     val quality: Int = 0,
  6.     @DrawableRes val background: Int = R.drawable.home_bg_1,
  7.     @DrawableRes val backgroundGif: Int = R.drawable.bg_topgif_2,
  8.     val twentyFourHours: List<TwentyFourHour> = arrayListOf(),
  9.     val weekWeathers: List<WeekWeather> = arrayListOf(),
  10.     val basicWeathers: List<BasicWeather> = arrayListOf()
  11. )

是不是发现上面代码中多了几个东西,没事,别着急,这就说是啥意思。就算我不说大家肯定也都知道,不就是背景图片和背景 gif 图嘛!没错,就是!

接下来看看 TwentyFourHour 、WeekWeather 和 BasicWeather 这三个类吧:

  1. data class TwentyFourHour(
  2.     val time: String = "",
  3.     @DrawableRes val icon: Int,
  4.     val temperature: String
  5. )
  6. data class WeekWeather(
  7.     val weekStr: String = "",
  8.     @DrawableRes val icon: Int,
  9.     val temperature: String = ""
  10. )
  11. data class BasicWeather(
  12.     val name: Int,
  13.     val value: String = ""
  14. )

是不是很简单,就不细说了。

下面就来看看数据的定义吧。我简单定义了下平时可能遇到的天气状况:

  1. <string name="weather_sunny">晴</string>
  2. <string name="weather_cloudy">多云</string>
  3. <string name="weather_overcast">阴</string>
  4. <string name="weather_small_rain">小雨</string>
  5. <string name="weather_mid_rain">中雨</string>
  6. <string name="weather_big_rain">大雨</string>
  7. <string name="weather_rainstorm">暴雨</string>
  8. <string name="weather_small_snow">小雪</string>
  9. <string name="weather_mid_snow">中雪</string>
  10. <string name="weather_big_snow">大雪</string>
  11. <string name="weather_snowstorm">暴风雪</string>
  12. <string name="weather_foggy">雾</string>
  13. <string name="weather_ice">结冰</string>
  14. <string name="weather_haze">阴霾</string>

嗯,就写这些吧,肯定还有很多种天气,这里就不写那么细了,大家如果想加就下载代码自己加吧。

下面就需要一些审美了,我找了一些现在应用商店的天气预报的应用,看了看那个好看,“下载” 点资源图去,要不自己怎么搞。。这块详细步骤就不写了,大家自行百度。

数据差不多都有了,那么该怎么一一对应呢?可能我没说明白,比如说你模拟的天气是下雨,你的 gif 图总不能是在下雪吧?背景图片也不能是冰天雪地啊,对不?这块我使用的方法是枚举,将不同天气及资源进行一一对应:

  1. enum class WeatherEnum(
  2.     @StringRes val weatherInt,
  3.     @DrawableRes val iconInt,
  4.     @DrawableRes val backgroundInt,
  5.     @DrawableRes val backgroundGifInt,
  6. {
  7.     SUNNY(
  8.         R.string.weather_sunny,
  9.         R.drawable.n_weather_icon_sunny,
  10.         R.drawable.home_bg_1,
  11.         R.drawable.bg_topgif_10
  12.     ),
  13.     CLOUDY(
  14.         R.string.weather_cloudy,
  15.         R.drawable.n_weather_icon_cloud,
  16.         R.drawable.home_bg_4,
  17.         R.drawable.bg_topgif_10
  18.     ),
  19.     OVERCAST(
  20.         R.string.weather_overcast,
  21.         R.drawable.n_weather_icon_overcast,
  22.         R.drawable.home_bg_6,
  23.         R.drawable.bg_topgif_10
  24.     ),
  25. }

这块由于篇幅原因就不写全了,写三个作为演示吧,后面的天气状况也是这么写。

编写ViewModel

数据实体类和资源都准备好了,就差个 ViewModel 来提供数据了,说干就干,整一个 ViewModel。

  1. class WeatherPageViewModel : ViewModel() {
  2.     private val _weatherLiveData = MutableLiveData<Weather>()
  3.     val weatherLiveData: LiveData<Weather> = _weatherLiveData
  4.     private fun onWeatherChanged(weather: Weather) {
  5.         _weatherLiveData.value = weather
  6.     }
  7. }

先简单定义一个 ViewModel ,什么?没看明白?回去重新看 MVVM 去。再来写一个方法,提供给外部获取天气的方法:

  1. fun getWeather() {
  2.     val random = Random()
  3.     val city = cityArray[random.nextInt(5)]
  4.     val weatherEnums = WeatherEnum.values()
  5.     val weatherEnum = weatherEnums[random.nextInt(14)]
  6.     val calendar = Calendar.getInstance()
  7.     val hours: Int = calendar.get(Calendar.HOUR)
  8.     val twentyFourHours = arrayListOf<TwentyFourHour>()
  9.     val weekWeathers = arrayListOf<WeekWeather>()
  10.     for (index in hours + 1..24) {
  11.         twentyFourHours.add(
  12.             TwentyFourHour(
  13.                 "$index:00",
  14.                 weatherEnum.icon,
  15.                 "${random.nextInt(29)}°"
  16.             )
  17.         )
  18.     }
  19.     val week = calendar.get(Calendar.DAY_OF_WEEK)
  20.     val weekListString = DateUtils.getWeekListString(week = week)
  21.     for (index in weekListString.indices) {
  22.         val small = random.nextInt(10)
  23.         weekWeathers.add(
  24.             WeekWeather(
  25.                 weekListString[index],
  26.                 getWeatherIcon(random.nextInt(35)), "$small°/${small + 7}°"
  27.             )
  28.         )
  29.     }
  30.     val basicWeathers = arrayListOf<BasicWeather>()
  31.     basicWeathers.add(BasicWeather(R.string.basic_rain, "${random.nextInt(100)}%"))
  32.     basicWeathers.add(BasicWeather(R.string.basic_humidity, "${random.nextInt(100)}%"))
  33.     val weather = Weather(
  34.         weatherEnum.weather,
  35.         address = city,
  36.         currentTemperature = random.nextInt(30),
  37.         quality = random.nextInt(100),
  38.         background = weatherEnum.background,
  39.         backgroundGif = weatherEnum.backgroundGif,
  40.         twentyFourHours = twentyFourHours,
  41.         weekWeathers = weekWeathers,
  42.         basicWeathers = basicWeathers
  43.     )
  44.     onWeatherChanged(weather)
  45. }

数据很简单,大部分是直接通过 Random 来随机生成的,第一次进入或刷新的时候就可以生成了。

/   画页面   /

数据都准备好了,就差页面了,画一画吧,不管什么时候,页面都是比较简单的,相对于数据逻辑来说,可能我这话说的有点绝对,也可能因为我太年轻。。。不多说了,开始画吧!

咱们就从 Activity 开始吧:

  1. class MainActivity : AppCompatActivity() {
  2.     override fun onCreate(savedInstanceState: Bundle?) {
  3.         super.onCreate(savedInstanceState)
  4.         BarUtils.transparentStatusBar(this)
  5.         setContent {
  6.             MyTheme {
  7.                 WeatherPage()
  8.             }
  9.         }
  10.     }
  11. }

上面代码很简单,为什么这么写在这里就不说了,大家可以去看我之前的文章。对了,里面有一行代码,顾名思义,就是设置状态栏为透明的,代码很容易找到,就不贴了。

接着来看 WeatherPage :

  1. @Composable
  2. fun WeatherPage() {
  3.     val refreshingState = remember { mutableStateOf(REFRESH_STOP) }
  4.     val weatherPageViewModel: WeatherPageViewModel = viewModel()
  5.     val weather by weatherPageViewModel.weatherLiveData.observeAsState(Weather())
  6.     var loadState by remember { mutableStateOf(false) }
  7.     if (!loadState) {
  8.         loadState = true
  9.         weatherPageViewModel.getWeather()
  10.     }
  11.     Surface(color = MaterialTheme.colors.background) {
  12.         SwipeToRefreshLayout(
  13.             refreshingState = refreshingState.value,
  14.             onRefresh = {
  15.                 refreshingState.value = REFRESH_START
  16.                 weatherPageViewModel.getWeather()
  17.                 loadState = true
  18.                 refreshingState.value = REFRESH_STOP
  19.             },
  20.             progressIndicator = {
  21.                 ProgressIndicator()
  22.             }
  23.         ) {
  24.             WeatherBackground(weather)
  25.             WeatherContent(weather)
  26.         }
  27.     }
  28. }

这块代码就得稍微说一说了,先从 Surface 看起,里面包裹了一个 SwipeToRefreshLayout ,看过上一篇文章的应该知道,这是下拉刷新的控件,如果没看过上一篇文章的,可以先去看看:Compose 实现下拉刷新和上拉加载。

再来看上面的内容:

  • refreshingState 就是是否正在刷新的状态,在 onRefresh 开始时设置为 REFRESH_START,刷新完成之后设置为 REFRESH_STOP,默认状态也是 REFRESH_STOP。

  • weatherPageViewModel 就是上面咱们写的,不过多解释

  • weather 这个就是将 ViewModel 中的 LiveData 转为 Compose 中支持观察的 State 。

  • loadState 是记住是否加载过,避免重复加载数据。

onRefresh中的刷新内容就是直接调一下weatherPageViewModel中的getWeather()方法。

然后直接开始看大括号中的内容,看着应该就知道是啥意思了,WeatherBackground 是背景,WeatherContent 是内容,那为什么要传入 Weather 呢?当然是为了展示了。。。

画背景

接下来先来看看 WeatherBackground 吧:

  1. @Composable
  2. fun WeatherBackground(weather: Weather) {
  3.     Box {
  4.         Image(
  5.             modifier = Modifier.fillMaxSize(),
  6.             painter = painterResource(weather.background),
  7.             contentDescription = stringResource(id = weather.weather),
  8.             contentScale = ContentScale.Crop
  9.         )
  10.         val context = LocalContext.current
  11.         val glide = Glide.with(context)
  12.         CompositionLocalProvider(LocalRequestManager provides glide) {
  13.             GlideImage(
  14.                 modifier = Modifier.fillMaxSize(),
  15.                 data = weather.backgroundGif,
  16.                 contentDescription = stringResource(id = weather.weather),
  17.                 contentScale = ContentScale.Crop
  18.             )
  19.         }
  20.     }
  21. }

很简单,一张背景图,一张 gif 动态图,用来展示下雨或者下雪等特效。这块的动态图使用了 Glide 的 gif 展示功能。使用方法上面已经贴出来了,下面贴下依赖吧:

  1. implementation "dev.chrisbanes.accompanist:accompanist-glide:0.6.0"

画内容

WeatherContent 是内容,这里有好几块,咱们慢慢看:

  1. @Composable
  2. fun WeatherContent(weather: Weather) {
  3.     val scrollState = rememberScrollState()
  4.     Column(
  5.         modifier = Modifier
  6.             .fillMaxSize()
  7.             .padding(horizontal = 10.dp)
  8.             .verticalScroll(scrollState),
  9.     ) {
  10.         WeatherBasic(weather, scrollState)
  11.         WeatherDetails(weather)
  12.         WeatherWeek(weather)
  13.         WeatherOther(weather)
  14.     }
  15. }

可以看到分为好几块,分别对应上面图中的几块。上面还写的有 scrollState ,保存着滚动的状态,由于咱们想要竖着滑动,所以设置 verticalScroll(scrollState) 。

WeatherBasic

这块是天气的基本信息,也就是城市啊、天气状况啊、当前温度啊啥的,上面的代码中还将滚动状态传入了这里,下面咱们就能看到作用了:

  1. @Composable
  2. fun WeatherBasic(weather: Weather, scrollState: ScrollState) {
  3.     val offset = (scrollState.value / 2)
  4.     val fontSize = (100f / offset * 70).coerceAtLeast(30f).coerceAtMost(75f).sp
  5.     val modifier = Modifier
  6.         .fillMaxWidth()
  7.         .wrapContentWidth(Alignment.CenterHorizontally)
  8.         .graphicsLayer { translationY = offset.toFloat() }
  9.     val context = LocalContext.current
  10.     Text(
  11.         modifier = modifier.padding(top = 100.dp, bottom = 5.dp),
  12.         text = stringResource(id = weather.address), fontSize = 20.sp,
  13.         color = Color.White,
  14.     )
  15.     AnimatedVisibility(visible = fontSize == 75f.sp) {
  16.         Text(
  17.             modifier = modifier.padding(top = 5.dp, bottom = 5.dp),
  18.             text = "${weather.currentTemperature}°",
  19.             fontSize = fontSize,
  20.             color = Color.White
  21.         )
  22.     }
  23.     Text(
  24.         modifier = modifier.padding(top = 5.dp, bottom = 2.5.dp),
  25.         text = stringResource(id = weather.weather), fontSize = 25.sp,
  26.         color = Color.White
  27.     )
  28.     AnimatedVisibility(visible = fontSize == 75f.sp) {
  29.         Text(
  30.             modifier = modifier.padding(top = 2.5.dp),
  31.             text = stringResource(id = R.string.weather_air_quality) + " " + weather.quality,
  32.             fontSize = 15.sp,
  33.             color = Color.White
  34.         )
  35.     }
  36.     Text(
  37.         modifier = Modifier.padding(top = 45.dp, start = 10.dp),
  38.         text = DateUtils.getDefaultDate(context, System.currentTimeMillis()),
  39.         fontSize = 16.sp,
  40.         color = Color.White
  41.     )
  42. }

是不是很简单?使用 AnimatedVisibility 来控制是否显示当前温度和空气质量,嗯,就没了。。。还有啥?大家可以试试 Modefier 的各种功能,越用越发现这个东西的强大。。。。

WeatherDetails

这个吧,就是 24 小时天气详情,上面数据已经写好了,直接展示即可:

  1. @Composable
  2. fun WeatherDetails(weather: Weather) {
  3.     val twentyFourHours = weather.twentyFourHours
  4.     LazyRow(modifier = Modifier.fillMaxWidth()) {
  5.         items(twentyFourHours) { twentyFourHour ->
  6.             WeatherHour(twentyFourHour)
  7.         }
  8.     }
  9. }
  10. @Composable
  11. fun WeatherHour(twentyFourHour: TwentyFourHour) {
  12.     val modifier = Modifier.padding(top = 9.dp)
  13.     Column(modifier = Modifier.width(50.dp), horizontalAlignment = Alignment.CenterHorizontally) {
  14.         Text(modifier = modifier, text = twentyFourHour.time, color = Color.White, fontSize = 15.sp)
  15.         Image(
  16.             modifier = modifier.size(25.dp),
  17.             painter = painterResource(id = twentyFourHour.icon),
  18.             contentDescription = twentyFourHour.temperature
  19.         )
  20.         Text(
  21.             modifier = modifier,
  22.             text = twentyFourHour.temperature,
  23.             color = Color.White,
  24.             fontSize = 15.sp
  25.         )
  26.     }
  27. }

一个 LazyRow 就搞定了,是不是很省事,比之前的 RecyclerView 还简单。。。

WeatherWeek

这是下面的未来一周的天气,和上面 24 小时类似:

  1. @Composable
  2. fun WeatherWeek(weather: Weather) {
  3.     Column(
  4.         modifier = Modifier
  5.             .fillMaxSize()
  6.             .padding(top = 10.dp)
  7.             .padding(horizontal = 10.dp)
  8.     ) {
  9.         for (weekWeather in weather.weekWeathers) {
  10.             WeatherWeekDetails(weekWeather)
  11.         }
  12.     }
  13. }

里面具体的 WeatherWeekDetails 由于篇幅原因就不贴代码了,大家可以去下载代码看。

WeatherOther

这个是当天天气的一些值,比如降水概率啊、湿度啊啥的:

  1. @Composable
  2. fun WeatherOther(weather: Weather) {
  3.     Column(
  4.         modifier = Modifier
  5.             .fillMaxSize()
  6.             .padding(top = 10.dp)
  7.             .padding(horizontal = 10.dp)
  8.     ) {
  9.         for (weekWeather in weather.basicWeathers) {
  10.             WeatherOtherDetails(weekWeather)
  11.         }
  12.     }
  13. }

/   总结   /

到这里基本上就结束了,就这么点内容,就写出了我自认为挺好看的一个天气应用。大家如果想要代码的话直接去 Github 中看:

https://github.com/zhujiang521/Weather

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

这一篇TCP总结请收下

Jetpack新成员,Paging3从吐槽到真香

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/695033
推荐阅读
相关标签
  

闽ICP备14008679号