NCMusicHarmony
前语
2024鸿蒙元年,写个鸿蒙的项目练练手~
写什么项目?之前学习的时分写过Jetpack Compose的仿网易云运用NCMusic、
Compose Desktop的仿网易云桌面运用NCMusicDesktop、Flutter的仿段子乐运用joke_fun_flutter,
这次挑选了网易云,写个鸿蒙版的仿网易云NCMusicHarmony。
普通开发者不配像企业开发者有api11的开发权限,只能基于api9来开发。不写不知道,一写吓一跳,基于api9已有的组件想要来完成一些常见的交互作用并不太好搞。莎士比亚说放不放弃这是一个问题~最终还是决议强撸一把直至灰飞烟没吧。
撸出来的作用还算差强任意,but~无图言吊?上动图先
状况办理
先简略归纳一下ArkUI中的状况办理
- @State 用该修饰符修饰的变量标明该变量具有状况
- @Prop 父子组件之间的状况传递,单向驱动,api9只支持基础数据类型
- @Link 父子组件之间的数状况传递,双向驱动
- @Observed、@ObjectLink 父子组件之间的状况传递,处理具有嵌套类型变量的场景
- @Provide、@Consume 多层级子孙组件的数据传递,双向驱动
- @Watch 状况变量改变监听
- LocalStorage 内存等级,不会写入硬盘,UIAbility内状况同享
- AppStorage 内存等级,不会写入硬盘,运用内状况同享
- PersistentStorage 状况持久化,合作AppStorage完成运用内写入磁盘
项目结构
TabLayout、TabPager
官方尽管供给了Tabs、TabContent组件,在api9中,能够完成相似android中TabLayout+ViewPager的联动切换作用,但是实践开发中,有一些作用却欠好完成, 例如Tab切换时分Indicator的偏移动画,而且构建UI的时分,标签栏和标签对应的内容是写在一起的,关于标签栏能够随着屏幕翻滚并且吸顶的作用,欠好完成。 所以自己界说了TabLayout和TabPager,二者合作TabLayoutPagerMediator完成联动。运用伪代码如下:
@State tabediator: TabLayoutPagerMediator = new TabLayoutPagerMediator({
tabItems: [], // Tab数据源
cacheCount: 1, // 页面缓存数量
indexChangedCallback: (index: number) => {
// tab索引改变回调
}
})
TabLayout({mediator: this.tabMediator})
TabPager({ mediator: this.tabMediator,
TabPageBuilder: (index: number) => {
// 页面插槽
}})
最开端的版本TabPager并不支持懒加载,在做音乐播映界面的时分,有一个唱片左右滑动切歌的作用,是运用TabPager完成的,当歌曲列表有700多首歌时, 滑动切换起来很卡。暂时替换成官方的Swiper来完成,尽管指定了Swiper的cacheCount,还是卡卡的,模拟了10000条数据,直接卡死。 不知道Swiper的cacheCount是怎么完成的。后边把自界说的TabLayout也改形成支持懒加载, 像Swiper相同经过指定cacheCount来缓存页面,模拟了10000条数据操作起来作用还行。
CollapsibleLayout
嵌套滑动在日常开发中随处可见,但是在api9中,却没有简略的api供给给开发者快速完成
(在b站看到一个视频,在api10中,官方已经新增相关的nested api来处理这类场景,惋惜我还不配运用api10),所以自界说了CollapsibleLayout来处理这种场景。
先看一张图
- AppBar:固定在页面顶部的标题栏
- ScrollHeader:可翻滚头部
- StickyHeader:粘性头部,随着页面翻滚后吸附在AppBar下方
- Content:内容区域
CollapsibleLayout完成的大概思路在ScrollHeader、StickyHeader、Content外层套一个OuterScroller,Content内部如果有多个翻滚区域(例如Content是个TabPager), 每个可翻滚区域都供给一个InnerScroller,经过Scroller的onScrollFrameBegin办法来处理滑动逻辑。对外供给CollapsibleMediator来完成嵌套滑动。
RefreshLayout
自界说了RefreshLayout来完成下拉改写、上拉加载功用,运用伪代码如下:
refreshMediator: RefreshMediator = new RefreshMediator()
RefreshLayout({ refreshMediator: this.refreshMediator,
ContentBuilder: () => {
// 内容区域
this.ContentBuilder()
},
onRefresh: () => {
// 改写逻辑
this.refreshMediator.finishRefresh(true)
},
onLoadMore: () => {
// 加载更多逻辑
this.refreshMediator.finishLoadMore()
}
})
@Builder ContentBuilder() {
List() {
}.onReachEnd(() => {
this.refreshMediator.scrollerReachEnd()
})
.onAreaChange((_, newValue: Area) => {
this.refreshMediator.scrollerAreaChange(newValue)
})
.onScrollFrameBegin((offset: number, _) => {
this.mediator.refreshMediator.getScrollerFrameRemainOffset(offset)
})
}
运用起来还挺杂乱,还需调用refreshMediator的scrollerReachEnd()、scrollerAreaChange()、getScrollerFrameRemainOffset()这三坨办法~
其实最开端的完成是在RefreshLayout内部嵌了一个Scroll组件来和谐滑动,这样在外层调用就不用写那三坨办法了。但是有一个问题:列表嵌套在Scroller里边,
必须指定列表的高度,不指定高度的话就算列表用了LazyForEach,ArkUI也是一次性全部加载一切item的,这样数据源一多就会卡卡卡,如果指了列表的高度,
那么内嵌的Scroll组件的onReachEnd()又不会回调,无法完成上拉加载更多的功用。所以后边把内嵌的Scroll组件去掉,由外层调用来告诉refreshMediator。
别的还有一点,RefreshLayout只能在List上工作,关于网格列表Grid,瀑布流WaterFlow是不起作用的。因为在api9中,Grid和WaterFlow没有供给onReachEnd()和onScrollFrameBegin()
的api,只有List组件才有,离离原上谱~原本List、Grid、WaterFlow是同一系列的组件,供给的api却有点割裂~不过在万能b站上看到,api10上面onReachEnd()这些api在Grid、WaterFlow组件上应该是有了,
后边再看看吧。
动画
官方文档中能够看到属性动画、显式动画、页面间转场、路径动画。就不细说了。
属性动画、显式动画用起来很简略,不过却找不到暂停动画、中止动画、获取当时动画进展的api。整的我很难过。
后边发现要暂停动画、中止动画、获取当时动画进展,应该调用AnimatorResult、AnimatorOptions这类api,需要用到请自行查看相关运用办法。
别的,关于页面间转场动画,一向找不到怎么一致设置整个运用的页面转场动画,不在各个页面重写pageTransition的话,默许都是左右slide的动画~
有大佬知道麻烦奉告弟弟。
网络恳求
项目中的网络恳求,直接用了官方的http,没有引进第三方框架,仅仅做了简略的封装。
一般页面涉及到网络恳求,都会有页面态、下拉改写成果状况、上拉加载更多成果状况的切换,ArkUI中的组件又没有承继的概念,各个组件都要单独处理的话也是很难过。
最终项目中界说了ViewStateLayout、ViewStatePagingLayout、恳求时构建RequestOptions时传入关于组件的ViewState、PagingLayoutMediator来一致处理。
ViewStateLayout会主动切换页面加载态、正常态、过错态、空白态,ViewStatePagingLayout除了页面态、还会主动切换改写头部、加载更多尾部的状况
- RequestOptions的界说
export interface IRequestOptions {
// 恳求url
url: string
// 恳求参数
data?: object
// 和页面绑定的ViewState
viewState?: ViewState,
// 分页和谐工具
pagingMediator?: PagingLayoutMediator,
// 恳求成功条件,默许code==200
successCondition?: (result: object) => boolean
// 判别空条件
emptyCondition?: (result: object) => boolean
// 分页数据转换
pagingListConverter?: (result: object) => object[]
// 恳求失利时是否还回来result
interceptWhenNoSuccess?: boolean
}
- ViewStateLayout运用伪代码
@State: result: Result
ViewStateLayout({ onLoadData: async (viewState) => {
// 网络恳求
this.result = await viewModel.fetchData(viewState)
} }) {
// 正常态布局
}
class ViewModel extends BaseViewModel {
async fetchData(viewState: ViewState) : Promise<Result> {
await this.get<Result>(
new RequestOptions({
url: "",
data: "",
viewState: viewState
}))
}
}
- ViewStatePagingLayout运用伪代码
@State pagingLayoutMediator: PagingLayoutMediator = new PagingLayoutMediator({})
ViewStatePagingLayout({
mediator: $pagingLayoutMediator,
ItemBuilder: (item: object, _) => {
this.ItemBuilder(item)
},
onLoadData: async (viewState: ViewState) => {
viewModel.fetchData((viewState, this.pagingLayoutMediator)
},
})
class ViewModel extends BaseViewModel {
async fetchData(viewState: ViewState, pagingMediator: PagingLayoutMediator) : Promise<Result> {
this.get<Result>(
new RequestOptions({
url: "",
data: "",
viewState: viewState,
pagingMediator: pagingMediator,
pagingListConverter: (result: Result) => {
// 将result转换成list数据源
}
}))
}
}
音乐播映
音乐播映功用,用的是官方的AVPlayer。项目中封装了NCPlayer负责实践的播映功用,MusicPlayController来调用NCPlayer和驱动各个音乐播映相关组件的烘托。 关于怎么驱动各个播映相关组件UI烘托的问题,最开端完成是想运用AppStorage,往AppStorage中更新一些播映的要害信息来驱动UI烘托, 后边发现在音乐播映界面,AppStorage更新时,唱片旋转动画会卡顿。索性都改写成运用emitter来通信,完成播映相关组件UI烘托。
主题切换
主题切换,大概思路是模仿Compose的那套主题切换,首先界说一个基础的取色盘IThemePalette
export interface IThemePalette {
primary: ResourceColor
secondary: ResourceColor,
pure: ResourceColor,
divider: ResourceColor,
commonBackground: ResourceColor,
deepenBackground: ResourceColor,
titleBackground: ResourceColor,
navBarBackground: ResourceColor,
drawerBackground: ResourceColor,
firstText: ResourceColor,
secondText: ResourceColor,
thirdText: ResourceColor,
firstIcon: ResourceColor,
secondIcon: ResourceColor,
thirdIcon: ResourceColor,
}
然后各个主题的取色盘都完成IThemePalette,例如默许主题取色盘DefaultThemePalette
export class DefaultThemePalette implements IThemePalette {
primary: ResourceColor = "#FFF0484E"
secondary: ResourceColor = "#FFF0888C"
pure: ResourceColor = "#FFFFFFFF"
divider: ResourceColor = "#FFDDDDDD"
commonBackground: ResourceColor = "#FFFFFFFF"
deepenBackground: ResourceColor = "#FFEEEEEE"
titleBackground: ResourceColor = "#FFFAFAFA"
navBarBackground: ResourceColor = "#FFFAFAFA"
drawerBackground: ResourceColor = "#FFFAFAFA"
firstText: ResourceColor = "#FF333333"
secondText: ResourceColor = "#FF666666"
thirdText: ResourceColor = "#FF999999"
firstIcon: ResourceColor = "#FF333333"
secondIcon: ResourceColor = "#FF666666"
thirdIcon: ResourceColor = "#FF999999"
}
然后界说AppTheme共外部运用
// 默许主题
export const defaultThemePalette = new DefaultThemePalette()
// 黑色主题
export const darkThemePalette = new DarkThemePalette()
// 橙色主题
export const originThemePalette = new OriginThemePalette()
// 绿色主题
export const greenThemePalette = new GreenThemePalette()
export class AppTheme {
/**
* 获取主题取色盘
*/
static palette(themeType: ThemeType): IThemePalette {
if (themeType == ThemeType.DEFAULT) {
return defaultThemePalette
} else if (themeType == ThemeType.DARK) {
return darkThemePalette
} else if (themeType == ThemeType.ORIGIN) {
return originThemePalette
} else if (themeType == ThemeType.GREEN) {
return greenThemePalette
} else {
return defaultThemePalette
}
}
}
// 主题类型
export const THEME_TYPE = "THEME_TYPE"
至于怎么运用,需要用到AppStorage,在组件内先获取当时主题色类型,然后调用AppTheme的palette()办法,获取到对应主题的取色盘的色彩, 当AppStorage中的主题类型发送改变时,组件就会主动切换色彩。
@StorageLink(THEME_TYPE) themeType: ThemeType = ThemeType.DEFAULT
Text(item.name).fontColor(AppTheme.palette(this.themeType).firstText)
最终
累了,不想写了。新手鸿蒙开发,代码不规范请不吝指教,如若有协助请给个star 源码地址:github.com/sskEvan/NCM…