用Compose实现轻量版网易云音乐
简述
这是一个几乎全部使用Compose实现UI各组成部分的纯Kotlin Android App,应用取名Compose Many是因为最初想实现集各种小功能的工具软件,当然主要还是想也借此来学习Compose的整个开发流程。不过目前只做好了音乐部分。
Compose目前正式版已经发布到了1.0.2(Alpha版本是1.1.0),目前来看官方更新速度不算太快,第一个正式版应该还是以稳定性为主。希望后续大版本更新时,能像Flutter2.0一样,功能更全面的同时也带来更丰富的生态。
效果
已经完成的功能主要就是歌单列表的播放和评论查看,由于接口众多,而APP主要利用空闲时间开发的,时间有限只做了推荐歌单和个人歌单的获取,常听的歌曲可以先听听了~ 然后评论功能暂时只能查看,点赞、回复这些后面有时间再做。
已实现的功能
- 网易云手机账号+密码登录
- 推荐歌单、个人歌单的显示
- 歌单歌曲的播放
- 歌曲评论查看、楼中回复评论查看
主要实现
服务器端
使用Binaryify大佬整理的网易云API NeteaseCloudMusicApi,可以非常方便地通过RESTful API访问各个数据接口,仓库里也提供了开箱即用的部署方案,这里就选用其中的Vercal方案:
于是借助宝藏网站 Vercel,就免费拥有自己的域名,并且可以在上面部署自己的代码。当然,Vercel也不是完全免费的,它对于一段时间内的访问量有所限制,达到比较高的访问量时会认为超出了个人使用用途,域名入口可能会被关停。因此作为学习目的,最好就是自己注册一个Vercel账号,然后App调用自己专属的API地址
客户端架构
- 界面表现层
应用的界面不多,界面表现层使用MVVM,音乐功能为单Activity
+多Fragment
,Fragment
内容使用Compose构建。
其中PlaySongsViewModel
生命周期跟随Activity
:
private val playSongsViewModel by activityViewModels<PlaySongsViewModel>()
复制代码
这样就能实现无论是首页、歌单页底部的播放器小控件(PlayWidget
),还是歌曲播放界面,他们的音乐播放状态都一致来源于PlaySongsViewModel
,任何一处的播放操作都能在其他页面得到正确的展示。依靠ViewModel作用域合理划分,自然地实现了状态单一来源,而不必使用类似EventBus这样容易造成状态混乱的通知。
项目中使用Compose构建的界面,尽量遵循了官网提出的
状态提升
达成“单向数据流”,具体参考官网:developer.android.google.cn/jetpack/com…
- 依赖注入
项目使用了Jetpack Hilt
管理所有依赖,它与其他大部分Jetpack组件都能提供很好支持,无论View、ViewModel还是Repository层都能很轻松地获取到需要的依赖项。
- 数据仓库层
网络数据源使用Retrofit
、数据库ORM使用Jetpack ROOM
、应用持久化数据使用Jetpack DataStore
(ProtoBuf实现)
界面导航
界面跳转使用Jetpack Navigation
,方案选择经过几次迭代:
- 只使用navigation的compose集成(ComposeNavigator)
最开始打算单纯使用navigation的compose集成,可以像以下代码这样非常方便地实现两个composable界面的跳转:
val navController = rememberNavController()
NavHost(navController = navController, startDestination = ScreenRoutes.AboutUs.path) {composable(ScreenRoutes.AboutUs.path) {AboutUsPage(onBackClick = { finish() }) {navController.navigate(ScreenRoutes.Privacy.path)}}composable(ScreenRoutes.Privacy.path) {HtmlDocumentViewer(title = "隐私政策")}... ...
}
复制代码
完全使用ComposeNavigator
的好处是可以做到Compose界面跳转的过渡,并且后续版本还能实现界面间共享元素。
- FragmentNavigator与ComposeNavigator混用
但实际使用中发现上述方式目的地之间无法传递自定义类型的参数,于是想把Fragment/Activity的Navigator和ComposeNavigator
混用,但发现跳转Fragment返回时(Navigation对于Fragment导航跳转默认使用replace,因此返回时重新调用onCreateView)重新创建的ComposeView中的总是空白。通过查看源码和调试,发现NavHost
中:
// NavHost.kt
//
// lifecycle#currentState状态大于STARTED时才做渲染
val backStackEntry = transitionsInProgress.lastOrNull { entry ->entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
} ?: backStack.lastOrNull { entry ->entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
}
... ...
if (backStackEntry != null) {Crossfade(backStackEntry, modifier) { currentEntry ->...}
}
复制代码
而从回退栈返回是重组NavHost时状态是CREATED
,无法获得backStackEntry
,因此也无法显示。解决的方法想到的是在lifecycle进入到STARTED
时改变NavHost
的状态主动触发重组,比如透明度从0f -> 1f:
var navAlpha by remember { mutableStateOf(0f) }
LaunchedEffect(key1 = true, block = {lifecycle.whenStarted { navAlpha = 1.0f }
})
复制代码
- 最终方案,只使用FragmentNavigator
这样一来返回时界面就能正常显示了,不过为了统一导航最终还是选择了全部使用单纯的FragmentNavigator
做界面导航,暂时放弃ComposeNavigator
,当前的navigation版本上(2.4.0-alpha06)对于compose的支持似乎还没有完全稳定。
可折叠标题栏
Compose暂时没有类似View系统中的CollapsingToolbarLayout
和CoordinatorLayout
,或者Flutter中CustomScrollView
+SliverAppBar
那样方便实现定制滑动可折叠标题的控件,因此最后找到一些其他的实现方式:
- 官方文档中对于
Modifier
的nestedScroll
有一个实现折叠标题栏的例子 androidx.compose.ui.input.nestedscroll | Android Developers (google.cn)
val nestedScrollConnection = remember {object : NestedScrollConnection {override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {val delta = available.yval newOffset = toolbarOffsetHeightPx.value + deltatoolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)return Offset.Zero}}
}
... ...
TopAppBar(modifier = Modifier.height(toolbarHeight).offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) },title = { Text("toolbar offset is ${toolbarOffsetHeightPx.value}") }
)
复制代码
主要就是嵌套滑动算出的偏移关联到TopAppBar中,问题主要是上拉时只要开始上拉就把折叠展开,而不是上拉到列表顶部后才展开。也许可以通过其他的计算方式达到效果,但整体比较复杂。
LazyColumn
可以得到滑动过程的状态,然后将标题栏作为单独的item{}
放在第一项,可以比较灵活地实现自己的可折叠状态栏
@Composable
fun CollapsingEffectScreen() {val items = (1..100).map { "Item $it" }val lazyListState = rememberLazyListState()var scrolledY = 0fvar previousOffset = 0LazyColumn(Modifier.fillMaxSize(),lazyListState,) {item {Image(modifier = Modifier.graphicsLayer {scrolledY += lazyListState.firstVisibleItemScrollOffset - previousOffsettranslationY = scrolledY * 0.5fpreviousOffset = lazyListState.firstVisibleItemScrollOffset})}items(items) {... ...}}
}
复制代码
使用graphicsLayer
实现关联偏移、折叠、透明度等等可以避免频繁重组。
- 一位韩国开发者维护的第三方库,可以比较方便地实现折叠标题栏:
onebone/compose-collapsing-toolbar: A simple implementation of collapsing toolbar for Jetpack Compose (github.com)
CollapsingToolbarScaffold(state = rememberCollapsingToolbarScaffoldState(), // provide the state of the scaffoldtoolbar = {// contents of toolbar go here...}
) {// main contents go here...
}
复制代码
唱片动画
Compose中实现控件过渡动画会发现比View系统的简单很多,并且表现力也更强。比如图片的无限旋转动画使用下面的代码就可以很容易实现:
val infiniteTransition = rememberInfiniteTransition()
val rotation by infiniteTransition.animateFloat(initialValue = 0f, targetValue = 360f,animationSpec = infiniteRepeatable(animation = tween(15 * 1000, easing = LinearEasing))
)
Image(modifier = Modifier.graphicsLayer {rotationZ = rotation}
)
复制代码
但是唱片动画有个特点,就是歌曲可以暂停,动画也需要可暂停并且原地续播。以Flutter为例,它可以通过AnimateController
的stop、forward方法暂停、继续动画,但Compose的动画系统用起来更方便了却也缺少了这种可以直接控制动画流程的API,为了实现这样的需求,用了更底层的Animatable。因为无限动画的起点和终点必须相差360度才能有无限循环效果,并且起始角度是当前角度值,所以通过角度的取余让它在0~720度范围内,达到视觉上无缝的无限旋转动画:
/*** 无限循环的旋转动画*/
@Composable
private fun infiniteRotation(startRotate: Boolean,duration: Int = 15 * 1000
): Animatable<Float, AnimationVector1D> {var rotation by remember { mutableStateOf(Animatable(0f)) }LaunchedEffect(key1 = startRotate, block = {if (startRotate) {//从上次的暂停角度 -> 执行动画 -> 到目标角度(+360°)rotation.animateTo((rotation.value % 360f) + 360f, animationSpec = infiniteRepeatable(animation = tween(duration, easing = LinearEasing)))} else {rotation.stop()//初始角度取余是为了防止每次暂停后目标角度无限增大rotation = Animatable(rotation.value % 360f)}})return rotation
}
复制代码
图片圆角、模糊
图片的圆角、圆形裁切和模糊都能通过Coil很容易地实现:
Image(painter = rememberImagePainter(song?.picUrl?.limitSize(200), builder = {transformations(CircleCropTransformation(),BlurTransformation(LocalContext.current, 16f),)})
)
复制代码
这里之前在实现模糊背景时存在一个缺陷,就是白色的图标(按钮)在浅色背景下会与背景融在一起而无法看清。观察了网易云音乐的原版App,发现即使白色背景它也会被调暗,以适应浅色的前景按钮和图标。因此顺着这个思路,我这儿采用的方法是在模糊背景上遮盖一层半透明的灰黑色蒙层:
//模糊虚化的封面作为背景
Image(...modifier = Modifier.drawWithContent {drawContent()//背景遮上半透明颜色,改善明亮色调的背景下,白色操作按钮的显示效果drawRect(Color.Gray, alpha = 0.7f)},...
)
复制代码
这样即使模糊背景整体偏亮色,上面的浅色按钮也能比较容易看清。
除此之外,应该也能通过Image的colorFilter混合减暗颜色来达到更好的效果(未测试过):colorFilter = ColorFilter.tint(Color.Gray, BlendMode.Darken)
底部弹窗
底部弹窗在Compose中实现起来也是非常简单
ModalBottomSheetLayout(//弹窗内容sheetContent = { ReplySheet(floorComment) },sheetState = sheetState,sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {//主内容CommentMain(song, commentCount, commentList, sortType) {viewModel.loadFloorReply(it.commentId)scope.launch { sheetState.show() }}
}
//需要返回时收起弹窗,这里处理返回键行为
BackHandler(sheetState.currentValue != ModalBottomSheetValue.Hidden) {scope.launch { sheetState.hide() }
}
复制代码
最后
第一次掘金上发文,文章排版比较散乱。这个项目作为自己的第一个Compose应用,并且也是主要练习为目的,APP中很多功能都不完善,并且对于Compose的了解还不是非常深入,有些部分实现可能不是最佳实践。整体使用开发下来的直观感受还是与传统View很大不同,尤其通过各种修饰符就能将内置控件定制为自己想要的样式,然后进行组合排布,重复的样板代码少了很多。
Compose开发的界面在某些部分还是能看出不完善的地方,比如LayzColumn
列表滑动的流畅性上是和RecyclerView还是有差距,还有很多API都还有试验性注解(即API还不稳定,后续可能变动)。性能方面也是官方着重在后续版本优化的点。
可以预见的是,现代的声明式UI未来应该会成为一个高效的UI开发模式,但能不能在Android中成为主流就看官方的开发力度和开发者们的接受度了~
最后还有本APP的源码地址,有空还会补充更多功能:
Mr-lin930819/ComposeMany: 使用jetpack compose构建的app (github.com)
用Compose实现轻量版网易云音乐相关推荐
- Android版网易云音乐唱片机唱片磁盘旋转及唱片机机械臂动画关键代码实现思路...
Android版网易云音乐唱片机唱片磁盘旋转及唱片机机械臂动画关键代码实现思路 先看一看我的代码运行结果. 代码运行起来初始化状态: 点击开始按钮,唱片机的机械臂匀速接近唱片磁盘,同时唱片磁盘也 ...
- 树莓派云音乐c语言,基于树莓派的红外遥控版网易云音乐播放器
基于树莓派的红外遥控版网易云音乐播放器.下面是遥控键盘示意图: CH- CH CH+ << >> || - + EQ 0 100+ 200+ 1 2 3 4 5 6 7 8 9 ...
- linux树莓派网易云音乐,基于树莓派的红外遥控版网易云音乐播放器
基于树莓派的红外遥控版网易云音乐播放器.下面是遥控键盘示意图: CH- CH CH+ << >> || - + EQ 0 100+ 200+ 1 2 3 4 5 6 7 8 9 ...
- HTML+CSS+JAVASCRIPT 高仿低配网页版网易云音乐播放器 1
HTML+CSS+JAVASCRIPT 高仿低配网页版网易云音乐播放器 前言 没有使用任何框架,只是想用最简单纯js的代码实现下 前台: Javascript+jQuery 后台: php/nodej ...
- 关于 Linux 版网易云音乐音高畸变的问题解决
问题描述: Linux 版本的网易云音乐最后更新于 2019 年,对于今天来说,其中的许多库文件已经有些过时了.我在使用 Linux 版网易云音乐时,在暂停之后继续播放音乐,经常能感受到歌曲整体音调出 ...
- 网易云音乐刷听歌量_网易云音乐极速版悄然上线!听歌体验同之前没有差别
了解更多热门资讯.玩机技巧.数码评测.科普深扒,点击右上角关注我们 ---------------------------------- 7月2日消息,"网易云音乐极速版"App在 ...
- python刷网易云_牛逼了!用Python开发的命令行版网易云音乐,Github获8300颗星!...
大家好,我是程序员G哥 最近在逛Github发现了一个非常有趣的库musicbox,是用纯Python打造的,收获了8300颗星.Python语言简单易学,好玩有趣,身边越来越多的小伙伴都开始学习Py ...
- 由 UWP 版网易云音乐闪退引发的博文
今天,不知怎么的.网易云音乐出现了一打开就闪退的情况.百度了好些时候未果,就直接 Windows + i 打开 Windows 设置 > 应用 在应用和功能列表中找到网易云音乐,在展开的 高级选 ...
- 基于Django3.0的Python版网易云音乐API
文章目录 项目地址 文档 测试链接 关于 新增 支持直接用js引用api 支持简易的日志记录 安装 运行 接口调用须知 目录 项目地址 https://github.com/Kevin0z0/Pyth ...
最新文章
- 【Java】剑指 Offer 52. 两个链表的第一个公共节点
- 筛指定区间的素数[区间偏移二次筛法]
- 领导应该怎么当?盯目标、抓计划、管时间、做农夫、当仆人……
- oracle 数据立方_大数据之数据仓库分层
- boost 递归锁_c++/boost互斥量与锁
- 如何使用API的方式消费SAP Commerce Cloud的订单服务
- 如何在iPhone和iPad上允许“不受信任的快捷方式”
- 深入了解java虚拟机(JVM) 第四章 对象的创建
- 常用类中的方法 —— java.util.Map
- 新东方php面试题,新东方学校各教师面试题和笔试题及答案(9套)
- mysql的粒度_MySQL中权限的粒度和时效性
- java 接口和抽象类的区别_Java中的接口与抽象类:有什么区别?
- 从应用层修改系统日期和时间
- 华为路由器配置命令——【简单实用的华为路由器配置命令】
- Oracle去重sql语句
- 音乐在计算机中的应用,计算机音乐技术在音乐教育中的应用
- rust怎么弄区域网_Rust10个实用小技巧,教你轻松省时省空间!
- 模乘与Montgomery 模乘
- JavaScript连缀
- IIS MIME设置