NCMusicHarmony

前语

2024鸿蒙元年,写个鸿蒙的项目练练手~
写什么项目?之前学习的时分写过Jetpack Compose的仿网易云运用NCMusic、 Compose Desktop的仿网易云桌面运用NCMusicDesktop、Flutter的仿段子乐运用joke_fun_flutter, 这次挑选了网易云,写个鸿蒙版的仿网易云NCMusicHarmony。
普通开发者不配像企业开发者有api11的开发权限,只能基于api9来开发。不写不知道,一写吓一跳,基于api9已有的组件想要来完成一些常见的交互作用并不太好搞。莎士比亚说放不放弃这是一个问题~最终还是决议强撸一把直至灰飞烟没吧。
撸出来的作用还算差强任意,but~无图言吊?上动图先

鸿蒙Harmony ArkUI实战项目:仿网易云音乐NCMusicHarmony

鸿蒙Harmony ArkUI实战项目:仿网易云音乐NCMusicHarmony

鸿蒙Harmony ArkUI实战项目:仿网易云音乐NCMusicHarmony
鸿蒙Harmony ArkUI实战项目:仿网易云音乐NCMusicHarmony

状况办理

先简略归纳一下ArkUI中的状况办理

  • @State 用该修饰符修饰的变量标明该变量具有状况
  • @Prop 父子组件之间的状况传递,单向驱动,api9只支持基础数据类型
  • @Link 父子组件之间的数状况传递,双向驱动
  • @Observed、@ObjectLink 父子组件之间的状况传递,处理具有嵌套类型变量的场景
  • @Provide、@Consume 多层级子孙组件的数据传递,双向驱动
  • @Watch 状况变量改变监听
  • LocalStorage 内存等级,不会写入硬盘,UIAbility内状况同享
  • AppStorage 内存等级,不会写入硬盘,运用内状况同享
  • PersistentStorage 状况持久化,合作AppStorage完成运用内写入磁盘

项目结构

鸿蒙Harmony ArkUI实战项目:仿网易云音乐NCMusicHarmony

鸿蒙Harmony ArkUI实战项目:仿网易云音乐NCMusicHarmony

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来处理这种场景。
先看一张图

鸿蒙Harmony ArkUI实战项目:仿网易云音乐NCMusicHarmony

  • 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…