日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

用Compose实现轻量版网易云音乐

發(fā)布時間:2024/1/1 编程问答 54 豆豆
生活随笔 收集整理的這篇文章主要介紹了 用Compose实现轻量版网易云音乐 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

簡述

這是一個幾乎全部使用Compose實現(xiàn)UI各組成部分的純Kotlin Android App,應(yīng)用取名Compose Many是因為最初想實現(xiàn)集各種小功能的工具軟件,當(dāng)然主要還是想也借此來學(xué)習(xí)Compose的整個開發(fā)流程。不過目前只做好了音樂部分。

Compose目前正式版已經(jīng)發(fā)布到了1.0.2(Alpha版本是1.1.0),目前來看官方更新速度不算太快,第一個正式版應(yīng)該還是以穩(wěn)定性為主。希望后續(xù)大版本更新時,能像Flutter2.0一樣,功能更全面的同時也帶來更豐富的生態(tài)。

效果

已經(jīng)完成的功能主要就是歌單列表的播放和評論查看,由于接口眾多,而APP主要利用空閑時間開發(fā)的,時間有限只做了推薦歌單和個人歌單的獲取,常聽的歌曲可以先聽聽了~ 然后評論功能暫時只能查看,點贊、回復(fù)這些后面有時間再做。

??

已實現(xiàn)的功能

  • 網(wǎng)易云手機賬號+密碼登錄
  • 推薦歌單、個人歌單的顯示
  • 歌單歌曲的播放
  • 歌曲評論查看、樓中回復(fù)評論查看

主要實現(xiàn)

服務(wù)器端

使用Binaryify大佬整理的網(wǎng)易云API NeteaseCloudMusicApi,可以非常方便地通過RESTful API訪問各個數(shù)據(jù)接口,倉庫里也提供了開箱即用的部署方案,這里就選用其中的Vercal方案:

于是借助寶藏網(wǎng)站 Vercel,就免費擁有自己的域名,并且可以在上面部署自己的代碼。當(dāng)然,Vercel也不是完全免費的,它對于一段時間內(nèi)的訪問量有所限制,達(dá)到比較高的訪問量時會認(rèn)為超出了個人使用用途,域名入口可能會被關(guān)停。因此作為學(xué)習(xí)目的,最好就是自己注冊一個Vercel賬號,然后App調(diào)用自己專屬的API地址

客戶端架構(gòu)

  • 界面表現(xiàn)層

應(yīng)用的界面不多,界面表現(xiàn)層使用MVVM,音樂功能為單Activity+多Fragment,Fragment內(nèi)容使用Compose構(gòu)建。

其中PlaySongsViewModel生命周期跟隨Activity:

private val playSongsViewModel by activityViewModels<PlaySongsViewModel>() 復(fù)制代碼

這樣就能實現(xiàn)無論是首頁、歌單頁底部的播放器小控件(PlayWidget),還是歌曲播放界面,他們的音樂播放狀態(tài)都一致來源于PlaySongsViewModel,任何一處的播放操作都能在其他頁面得到正確的展示。依靠ViewModel作用域合理劃分,自然地實現(xiàn)了狀態(tài)單一來源,而不必使用類似EventBus這樣容易造成狀態(tài)混亂的通知。

項目中使用Compose構(gòu)建的界面,盡量遵循了官網(wǎng)提出的狀態(tài)提升達(dá)成“單向數(shù)據(jù)流”,具體參考官網(wǎng):developer.android.google.cn/jetpack/com…

  • 依賴注入

項目使用了Jetpack Hilt管理所有依賴,它與其他大部分Jetpack組件都能提供很好支持,無論View、ViewModel還是Repository層都能很輕松地獲取到需要的依賴項。

  • 數(shù)據(jù)倉庫層

網(wǎng)絡(luò)數(shù)據(jù)源使用Retrofit、數(shù)據(jù)庫ORM使用Jetpack ROOM、應(yīng)用持久化數(shù)據(jù)使用Jetpack DataStore(ProtoBuf實現(xiàn))

界面導(dǎo)航

界面跳轉(zhuǎn)使用Jetpack Navigation,方案選擇經(jīng)過幾次迭代:

  • 只使用navigation的compose集成(ComposeNavigator)

最開始打算單純使用navigation的compose集成,可以像以下代碼這樣非常方便地實現(xiàn)兩個composable界面的跳轉(zhuǎn):

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 = "隱私政策")}... ... } 復(fù)制代碼

完全使用ComposeNavigator的好處是可以做到Compose界面跳轉(zhuǎn)的過渡,并且后續(xù)版本還能實現(xiàn)界面間共享元素。

  • FragmentNavigator與ComposeNavigator混用

但實際使用中發(fā)現(xiàn)上述方式目的地之間無法傳遞自定義類型的參數(shù),于是想把Fragment/Activity的Navigator和ComposeNavigator混用,但發(fā)現(xiàn)跳轉(zhuǎn)Fragment返回時(Navigation對于Fragment導(dǎo)航跳轉(zhuǎn)默認(rèn)使用replace,因此返回時重新調(diào)用onCreateView)重新創(chuàng)建的ComposeView中的總是空白。通過查看源碼和調(diào)試,發(fā)現(xiàn)NavHost中:

// NavHost.kt // // lifecycle#currentState狀態(tài)大于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 ->...} } 復(fù)制代碼

而從回退棧返回是重組NavHost時狀態(tài)是CREATED,無法獲得backStackEntry,因此也無法顯示。解決的方法想到的是在lifecycle進入到STARTED時改變NavHost的狀態(tài)主動觸發(fā)重組,比如透明度從0f -> 1f:

var navAlpha by remember { mutableStateOf(0f) } LaunchedEffect(key1 = true, block = {lifecycle.whenStarted { navAlpha = 1.0f } }) 復(fù)制代碼
  • 最終方案,只使用FragmentNavigator

這樣一來返回時界面就能正常顯示了,不過為了統(tǒng)一導(dǎo)航最終還是選擇了全部使用單純的FragmentNavigator做界面導(dǎo)航,暫時放棄ComposeNavigator,當(dāng)前的navigation版本上(2.4.0-alpha06)對于compose的支持似乎還沒有完全穩(wěn)定。

可折疊標(biāo)題欄

Compose暫時沒有類似View系統(tǒng)中的CollapsingToolbarLayout和CoordinatorLayout,或者Flutter中CustomScrollView+SliverAppBar那樣方便實現(xiàn)定制滑動可折疊標(biāo)題的控件,因此最后找到一些其他的實現(xiàn)方式:

  • 官方文檔中對于Modifier的nestedScroll有一個實現(xiàn)折疊標(biāo)題欄的例子 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}") } ) 復(fù)制代碼

    主要就是嵌套滑動算出的偏移關(guān)聯(lián)到TopAppBar中,問題主要是上拉時只要開始上拉就把折疊展開,而不是上拉到列表頂部后才展開。也許可以通過其他的計算方式達(dá)到效果,但整體比較復(fù)雜。

  • LazyColumn可以得到滑動過程的狀態(tài),然后將標(biāo)題欄作為單獨的item{}放在第一項,可以比較靈活地實現(xiàn)自己的可折疊狀態(tài)欄
  • @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) {... ...}} } 復(fù)制代碼

    使用graphicsLayer實現(xiàn)關(guān)聯(lián)偏移、折疊、透明度等等可以避免頻繁重組。

  • 一位韓國開發(fā)者維護的第三方庫,可以比較方便地實現(xiàn)折疊標(biāo)題欄:
  • 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... } 復(fù)制代碼

    唱片動畫

    Compose中實現(xiàn)控件過渡動畫會發(fā)現(xiàn)比View系統(tǒng)的簡單很多,并且表現(xiàn)力也更強。比如圖片的無限旋轉(zhuǎn)動畫使用下面的代碼就可以很容易實現(xiàn):

    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} ) 復(fù)制代碼

    但是唱片動畫有個特點,就是歌曲可以暫停,動畫也需要可暫停并且原地續(xù)播。以Flutter為例,它可以通過AnimateController的stop、forward方法暫停、繼續(xù)動畫,但Compose的動畫系統(tǒng)用起來更方便了卻也缺少了這種可以直接控制動畫流程的API,為了實現(xiàn)這樣的需求,用了更底層的Animatable。因為無限動畫的起點和終點必須相差360度才能有無限循環(huán)效果,并且起始角度是當(dāng)前角度值,所以通過角度的取余讓它在0~720度范圍內(nèi),達(dá)到視覺上無縫的無限旋轉(zhuǎn)動畫:

    /*** 無限循環(huán)的旋轉(zhuǎn)動畫*/ @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) {//從上次的暫停角度 -> 執(zhí)行動畫 -> 到目標(biāo)角度(+360°)rotation.animateTo((rotation.value % 360f) + 360f, animationSpec = infiniteRepeatable(animation = tween(duration, easing = LinearEasing)))} else {rotation.stop()//初始角度取余是為了防止每次暫停后目標(biāo)角度無限增大rotation = Animatable(rotation.value % 360f)}})return rotation } 復(fù)制代碼

    圖片圓角、模糊

    圖片的圓角、圓形裁切和模糊都能通過Coil很容易地實現(xiàn):

    Image(painter = rememberImagePainter(song?.picUrl?.limitSize(200), builder = {transformations(CircleCropTransformation(),BlurTransformation(LocalContext.current, 16f),)}) ) 復(fù)制代碼

    這里之前在實現(xiàn)模糊背景時存在一個缺陷,就是白色的圖標(biāo)(按鈕)在淺色背景下會與背景融在一起而無法看清。觀察了網(wǎng)易云音樂的原版App,發(fā)現(xiàn)即使白色背景它也會被調(diào)暗,以適應(yīng)淺色的前景按鈕和圖標(biāo)。因此順著這個思路,我這兒采用的方法是在模糊背景上遮蓋一層半透明的灰黑色蒙層:

    //模糊虛化的封面作為背景 Image(...modifier = Modifier.drawWithContent {drawContent()//背景遮上半透明顏色,改善明亮色調(diào)的背景下,白色操作按鈕的顯示效果drawRect(Color.Gray, alpha = 0.7f)},... ) 復(fù)制代碼

    這樣即使模糊背景整體偏亮色,上面的淺色按鈕也能比較容易看清。

    除此之外,應(yīng)該也能通過Image的colorFilter混合減暗顏色來達(dá)到更好的效果(未測試過):colorFilter = ColorFilter.tint(Color.Gray, BlendMode.Darken)

    底部彈窗

    底部彈窗在Compose中實現(xiàn)起來也是非常簡單

    ModalBottomSheetLayout(//彈窗內(nèi)容sheetContent = { ReplySheet(floorComment) },sheetState = sheetState,sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) ) {//主內(nèi)容CommentMain(song, commentCount, commentList, sortType) {viewModel.loadFloorReply(it.commentId)scope.launch { sheetState.show() }} } //需要返回時收起彈窗,這里處理返回鍵行為 BackHandler(sheetState.currentValue != ModalBottomSheetValue.Hidden) {scope.launch { sheetState.hide() } } 復(fù)制代碼

    最后

    第一次掘金上發(fā)文,文章排版比較散亂。這個項目作為自己的第一個Compose應(yīng)用,并且也是主要練習(xí)為目的,APP中很多功能都不完善,并且對于Compose的了解還不是非常深入,有些部分實現(xiàn)可能不是最佳實踐。整體使用開發(fā)下來的直觀感受還是與傳統(tǒng)View很大不同,尤其通過各種修飾符就能將內(nèi)置控件定制為自己想要的樣式,然后進行組合排布,重復(fù)的樣板代碼少了很多。

    Compose開發(fā)的界面在某些部分還是能看出不完善的地方,比如LayzColumn列表滑動的流暢性上是和RecyclerView還是有差距,還有很多API都還有試驗性注解(即API還不穩(wěn)定,后續(xù)可能變動)。性能方面也是官方著重在后續(xù)版本優(yōu)化的點。

    可以預(yù)見的是,現(xiàn)代的聲明式UI未來應(yīng)該會成為一個高效的UI開發(fā)模式,但能不能在Android中成為主流就看官方的開發(fā)力度和開發(fā)者們的接受度了~

    最后還有本APP的源碼地址,有空還會補充更多功能:

    Mr-lin930819/ComposeMany: 使用jetpack compose構(gòu)建的app (github.com)

    ?

    總結(jié)

    以上是生活随笔為你收集整理的用Compose实现轻量版网易云音乐的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。