前因后果
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 = arrayListOf(),
val weekWeathers: List = arrayListOf(),
val basicWeathers: List = 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 = ""
)
复制代码
是不是很简单,就不细说了。
下面就来看看数据的定义吧:
我简单定义了下平时可能遇到的天气状况:
晴
多云
阴
小雨
中雨
大雨
暴雨
小雪
中雪
大雪
暴风雪
雾
结冰
阴霾
复制代码
嗯,就写这些吧,肯定还有很多种天气,这里就不写那么细了,大家如果想加就下载代码自己加吧。
下面就需要一些审美了,我找了一些现在应用商店的天气预报的应用,看了看那个好看,“下载” 点资源图去,要不自己怎么搞。。这块详细步骤就不写了,大家自行百度。
数据差不多都有了,那么该怎么一一对应呢?可能我没说明白,比如说你模拟的天气是下雨,你的 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()
val weatherLiveData: LiveData = _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()
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 = 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
中看:github.com/zhujiang521…
最后再打个小广告,如果想学习 Compose
的话,可以去看我另一个库,里面有详细的例子和 Demo 供你参考:Github
地址:github.com/zhujiang521…,别忘了是 main
分支。
先这样了,咱们下回见!
原文链接:https://juejin.cn/post/6942386785452294181