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

前因后果

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

  1. 第一期挑战是做一个领养宠物的应用,全球一共有五百份礼品。第一个我参加了,做了一个很简单的应用,只有一个列表和一个详情页面,在之前的博客中也说过。但是看了 Google 官方发出的别人写的之后,又看了看自己写的,这是个啥。。。。醉了
  2. 第二期挑战是做一个倒计时的应用,全球也是一共五百份。看见之后就写了一个,也是非常简单,只有一个输入框和一个显示倒计时用的 Text 。在看了别人发的之后,又看了看自己写的,这是个啥。。。。醉了
  3. 第三期挑战是 Google 官方出设计图,开发照着官方给出的图做,全球只有三份礼品。三份?全球?别说全球,就是全国、全市都不容易啊!还得看编写的速度。。。算了算了,果断没参加!自己几斤几两还是有点 b 数的。。。。
  4. 第四期挑战是开发一个天气应用,全球只有五份礼品。但和第三期不同的是,这回不比速度,不比速度就好,我没那么快。。。那就搞一搞吧!

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

Compose Android 开发终极挑战赛: 天气应用_第1张图片
参赛要求

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

开搞

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

|
Compose Android 开发终极挑战赛: 天气应用_第2张图片
天气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 = ""
)
复制代码

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

下面就来看看数据的定义吧:

我简单定义了下平时可能遇到的天气状况:


多云

小雨
中雨
大雨
暴雨
小雪
中雪
大雪
暴风雪

结冰
阴霾
复制代码

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

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

数据差不多都有了,那么该怎么一一对应呢?可能我没说明白,比如说你模拟的天气是下雨,你的 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 实现下拉刷新和上拉加载。

再来看上面的内容:

  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 = 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

你可能感兴趣的:(Compose Android 开发终极挑战赛: 天气应用)