赞
踩
/ 今日科技快讯 /
共享充电宝用不起后,共享单车也骑不起了。经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小时天气:每个小时具体的天气预报,这也得有
未来一周天气:预报嘛,肯定得预报啊
天气基本信息:比如降水概率啊,湿度啊,紫外线啊什么的
嗯,上面列举的差不多了,想要数据肯定得先有实体类来存放数据吧,咱们来看看实体类的写法吧:
- 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<TwentyFourHour> = arrayListOf(),
- val weekWeathers: List<WeekWeather> = arrayListOf(),
- val basicWeathers: List<BasicWeather> = arrayListOf()
- )
-
是不是发现上面代码中多了几个东西,没事,别着急,这就说是啥意思。就算我不说大家肯定也都知道,不就是背景图片和背景 gif 图嘛!没错,就是!
接下来看看 TwentyFourHour 、WeekWeather 和 BasicWeather 这三个类吧:
- 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 = ""
- )
是不是很简单,就不细说了。
下面就来看看数据的定义吧。我简单定义了下平时可能遇到的天气状况:
- <string name="weather_sunny">晴</string>
- <string name="weather_cloudy">多云</string>
- <string name="weather_overcast">阴</string>
- <string name="weather_small_rain">小雨</string>
- <string name="weather_mid_rain">中雨</string>
- <string name="weather_big_rain">大雨</string>
- <string name="weather_rainstorm">暴雨</string>
- <string name="weather_small_snow">小雪</string>
- <string name="weather_mid_snow">中雪</string>
- <string name="weather_big_snow">大雪</string>
- <string name="weather_snowstorm">暴风雪</string>
- <string name="weather_foggy">雾</string>
- <string name="weather_ice">结冰</string>
- <string name="weather_haze">阴霾</string>
-
嗯,就写这些吧,肯定还有很多种天气,这里就不写那么细了,大家如果想加就下载代码自己加吧。
下面就需要一些审美了,我找了一些现在应用商店的天气预报的应用,看了看那个好看,“下载” 点资源图去,要不自己怎么搞。。这块详细步骤就不写了,大家自行百度。
数据差不多都有了,那么该怎么一一对应呢?可能我没说明白,比如说你模拟的天气是下雨,你的 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 = MutableLiveData<Weather>()
- val weatherLiveData: LiveData<Weather> = _weatherLiveData
-
- private fun onWeatherChanged(weather: Weather) {
- _weatherLiveData.value = 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<TwentyFourHour>()
- val weekWeathers = arrayListOf<WeekWeather>()
- 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<BasicWeather>()
- 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 = REFRESH_START
- weatherPageViewModel.getWeather()
- loadState = true
- refreshingState.value = REFRESH_STOP
- },
- progressIndicator = {
- ProgressIndicator()
- }
- ) {
- WeatherBackground(weather)
- WeatherContent(weather)
- }
- }
- }
-
这块代码就得稍微说一说了,先从 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 吧:
- @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 = 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 中看:
https://github.com/zhujiang521/Weather
推荐阅读:
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。