MiniPlayerBar.ets 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. import { DeviceUtil, StrUtil } from '@pura/harmony-utils'
  2. import { resourceManager } from '@kit.LocalizationKit'
  3. import { deviceInfo } from '@kit.BasicServicesKit'
  4. import { hdsEffect } from '@kit.UIDesignKit'
  5. import { VideoItem } from '../viewmodel/VideoItem'
  6. import { PlayStatus } from '../common/PlayStatus'
  7. import { CommonConstants } from '../common/constants/CommonConstants'
  8. import { PointLightContentButton } from './PointLight/PointLightContentButton'
  9. import { PlayingIndicator } from './PlayingIndicator'
  10. @Component
  11. export struct MiniPlayerBar {
  12. @Prop bottomBarHeight: number = 64
  13. @Prop bottomSafeHeight: number = 0
  14. @Prop curDisplayIsHiCar: boolean = false
  15. @Prop cover: string | undefined = ''
  16. @Prop currentSong: VideoItem | undefined = undefined
  17. @Prop themeColor: string = CommonConstants.DEFAULT_THEME_COLOR
  18. @Prop progressValue: number = 0
  19. @Prop controlPlayStatus: number = PlayStatus.INIT
  20. @Prop isShowPrecious: boolean = false
  21. @Prop isMiniPlayerOrbMode: boolean = false
  22. @Prop isMiniPlayerModeTransitioning: boolean = false
  23. @Prop miniPlayerScaleX: number = 1
  24. @Prop miniPlayerScaleY: number = 1
  25. @Prop miniPlayerOpacity: number = 1
  26. @Prop miniPlayerBorderRadius: number = 200
  27. @Prop miniPlayerProxySize: number = 0
  28. @Prop miniPlayerProxyOpacity: number = 0
  29. @Prop miniPlayerContentScaleY: number = 1
  30. @Prop miniPlayerContentTranslateY: number = 0
  31. @Prop miniPlayerFullContentTranslateX: number = 0
  32. @Prop miniPlayerTranslateY: number = 0
  33. @Prop miniPlayerSurfaceWidth: number = 0
  34. @Prop miniPlayerSurfaceHeight: number = 0
  35. @Prop miniPlayerFullContentOpacity: number = 1
  36. @Prop miniPlayerOrbOpacity: number = 0
  37. @Prop miniPlayerOrbScale: number = 1
  38. @Prop miniPlayerOrbTranslateX: number = 0
  39. onOpenPlayer: () => void = () => {}
  40. onCollapseToOrb: () => void = () => {}
  41. onOrbTap: () => void = () => {}
  42. onPlayPrevious: () => void = () => {}
  43. onPlayOrPause: () => void = () => {}
  44. onPlayNext: () => void = () => {}
  45. onOpenPlayList: () => void = () => {}
  46. @State private pointLightOptions: hdsEffect.PointLightOptions | undefined = undefined
  47. private resolveMiniPlayerOrbSize(): number {
  48. const barHeightVp = this.bottomBarHeight > 0 ? this.bottomBarHeight : 70
  49. return Math.max(54, Math.min(60, barHeightVp - 8))
  50. }
  51. private shouldBlockTransportAction(): boolean {
  52. return this.isMiniPlayerModeTransitioning
  53. }
  54. @Builder
  55. private buildMiniPlayerMorphProxyDot() {
  56. Stack({ alignContent: Alignment.Center }) {
  57. Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt'))
  58. .width('100%')
  59. .height('100%')
  60. .objectFit(ImageFit.Cover)
  61. .alt($r('app.media.alt'))
  62. }
  63. .width(this.miniPlayerProxySize)
  64. .height(this.miniPlayerProxySize)
  65. .borderRadius(this.miniPlayerProxySize)
  66. .clip(true)
  67. .opacity(this.miniPlayerProxyOpacity)
  68. .shadow({
  69. radius: 18,
  70. type: ShadowType.BLUR,
  71. color: 'on_primary'
  72. })
  73. }
  74. @Builder
  75. private buildMiniPlayerOrbButtonContent(orbSize: number) {
  76. Stack({ alignContent: Alignment.Center }) {
  77. Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt'))
  78. .width('100%')
  79. .height('100%')
  80. .objectFit(ImageFit.Cover)
  81. .alt($r('app.media.alt'))
  82. .borderRadius(100)
  83. Column()
  84. .width('100%')
  85. .height('100%')
  86. .backgroundColor('#66000000')
  87. Progress({
  88. value: Math.floor(this.progressValue),
  89. total: 100,
  90. type: ProgressType.Ring,
  91. })
  92. .color(Color.White)
  93. .width('100%')
  94. .height('100%')
  95. .style({ strokeWidth: 3 })
  96. if (this.controlPlayStatus === PlayStatus.PLAY) {
  97. PlayingIndicator({
  98. isActive: true,
  99. indicatorSize: Math.max(16, Math.floor(orbSize * 0.34)),
  100. indicatorColor: '#FFFFFF'
  101. })
  102. }
  103. }
  104. .width('100%')
  105. .height('100%')
  106. .clip(true)
  107. .borderRadius(100)
  108. }
  109. @Builder
  110. private miniPlayerOrbControl() {
  111. PointLightContentButton({
  112. pointColor: this.themeColor,
  113. buttonRadius: this.resolveMiniPlayerOrbSize() / 2,
  114. pointLightHeight: 96,
  115. pressScale: 0.94,
  116. useShadow: true,
  117. builder: (): void => {
  118. this.buildMiniPlayerOrbButtonContent(this.resolveMiniPlayerOrbSize())
  119. }
  120. })
  121. .width(this.resolveMiniPlayerOrbSize())
  122. .height(this.resolveMiniPlayerOrbSize())
  123. .opacity(this.miniPlayerOrbOpacity)
  124. .borderRadius(this.resolveMiniPlayerOrbSize() / 2)
  125. .scale({ x: this.miniPlayerOrbScale, y: this.miniPlayerOrbScale, centerX: '50%', centerY: '50%' })
  126. .translate({ x: this.miniPlayerOrbTranslateX })
  127. .onClick((): void => {
  128. this.onOrbTap()
  129. })
  130. }
  131. @Builder
  132. private buildMiniPlayerCoverContent() {
  133. Stack({ alignContent: Alignment.Center }) {
  134. Image(StrUtil.isNotEmpty(this.currentSong?.pixelMapPath) ?
  135. this.currentSong?.pixelMapPath : $r('app.media.alt'))
  136. .width(48)
  137. .height(48)
  138. .objectFit(ImageFit.Contain)
  139. .alt($r('app.media.alt'))
  140. .fillColor(this.themeColor)
  141. .borderRadius(8)
  142. .shadow({
  143. radius: 15,
  144. type: ShadowType.BLUR,
  145. color: 'on_primary'
  146. })
  147. }
  148. .width(48)
  149. .height(48)
  150. }
  151. @Builder
  152. private miniPlayerCoverButton() {
  153. PointLightContentButton({
  154. pointColor: this.themeColor,
  155. buttonRadius: 10,
  156. pointLightHeight: 88,
  157. pressScale: 0.92,
  158. useShadow: false,
  159. builder: (): void => {
  160. this.buildMiniPlayerCoverContent()
  161. }
  162. })
  163. .width(48)
  164. .height(48)
  165. .margin({ left: 5 })
  166. .zIndex(3)
  167. .onClick((): void => {
  168. this.onCollapseToOrb()
  169. })
  170. }
  171. @Builder
  172. private buildPlayPreviousControlContent() {
  173. Row() {
  174. SymbolGlyph($r('sys.symbol.backward_end_fill'))
  175. .fontSize(28)
  176. .fontColor([Color.White])
  177. }
  178. .width('100%')
  179. .height('100%')
  180. .justifyContent(FlexAlign.Center)
  181. .alignItems(VerticalAlign.Center)
  182. }
  183. @Builder
  184. private buildPlayToggleControlContent() {
  185. Row() {
  186. Stack() {
  187. Progress({
  188. value: Math.floor(this.progressValue),
  189. type: ProgressType.Ring,
  190. })
  191. .color(this.themeColor)
  192. .height(38)
  193. .aspectRatio(CommonConstants.ASPECT_RATIO)
  194. Image(this.controlPlayStatus === PlayStatus.PLAY ? $r('app.media.hm_pause') : $r('app.media.hm_play2'))
  195. .height(36)
  196. .width(36)
  197. .fillColor(Color.White)
  198. }
  199. }
  200. .width('100%')
  201. .height('100%')
  202. .justifyContent(FlexAlign.Center)
  203. .alignItems(VerticalAlign.Center)
  204. }
  205. @Builder
  206. private buildPlayNextControlContent() {
  207. Row() {
  208. SymbolGlyph($r('sys.symbol.forward_end_fill'))
  209. .fontSize(28)
  210. .fontColor([Color.White])
  211. }
  212. .width('100%')
  213. .height('100%')
  214. .justifyContent(FlexAlign.Center)
  215. .alignItems(VerticalAlign.Center)
  216. }
  217. @Builder
  218. private buildPlayListControlContent() {
  219. Row() {
  220. SymbolGlyph($r('sys.symbol.music_note_list'))
  221. .fontSize(28)
  222. .fontColor([Color.White])
  223. }
  224. .width('100%')
  225. .height('100%')
  226. .justifyContent(FlexAlign.Center)
  227. .alignItems(VerticalAlign.Center)
  228. }
  229. @Builder
  230. private playConLeft() {
  231. Row() {
  232. this.miniPlayerCoverButton()
  233. Column() {
  234. Text(this.currentSong?.name)
  235. .fontSize(16)
  236. .textOverflow({ overflow: TextOverflow.MARQUEE })
  237. .maxLines(1)
  238. .fontColor(Color.White)
  239. .fontWeight(FontWeight.Bolder)
  240. Row() {
  241. Text(this.currentSong?.artist)
  242. .margin({ top: 2 })
  243. .fontSize(13)
  244. .textAlign(TextAlign.Start)
  245. .maxLines(1)
  246. .fontWeight(FontWeight.Bold)
  247. .textOverflow({ overflow: TextOverflow.MARQUEE })
  248. .fontColor(Color.White)
  249. }
  250. .visibility(this.currentSong?.artist ? Visibility.Visible : Visibility.None)
  251. }
  252. .padding({ left: 8 })
  253. .alignItems(HorizontalAlign.Start)
  254. .margin({ right: 22 })
  255. .onClick((): void => {
  256. this.onOpenPlayer()
  257. })
  258. }
  259. .padding({ right: 18 })
  260. .layoutWeight(1)
  261. .alignItems(VerticalAlign.Center)
  262. .justifyContent(FlexAlign.Start)
  263. }
  264. @Builder
  265. private playConRigth() {
  266. Row() {
  267. PointLightContentButton({
  268. pointColor: this.themeColor,
  269. buttonRadius: 22,
  270. pointLightHeight: 56,
  271. pressScale: 0.88,
  272. builder: (): void => {
  273. this.buildPlayPreviousControlContent()
  274. }
  275. })
  276. .width(44)
  277. .height(44)
  278. .visibility(this.isShowPrecious ? Visibility.Visible : Visibility.None)
  279. .displayPriority(2)
  280. .onClick((): void => {
  281. if (this.shouldBlockTransportAction()) {
  282. return
  283. }
  284. this.onPlayPrevious()
  285. })
  286. PointLightContentButton({
  287. pointColor: this.themeColor,
  288. buttonRadius: 23,
  289. pointLightHeight: 62,
  290. pressScale: 0.9,
  291. builder: (): void => {
  292. this.buildPlayToggleControlContent()
  293. }
  294. })
  295. .width(46)
  296. .height(46)
  297. .displayPriority(3)
  298. .onClick((): void => {
  299. if (this.shouldBlockTransportAction()) {
  300. return
  301. }
  302. this.onPlayOrPause()
  303. })
  304. PointLightContentButton({
  305. pointColor: this.themeColor,
  306. buttonRadius: 22,
  307. pointLightHeight: 56,
  308. pressScale: 0.88,
  309. builder: (): void => {
  310. this.buildPlayNextControlContent()
  311. }
  312. })
  313. .width(44)
  314. .height(44)
  315. .displayPriority(2)
  316. .onClick((): void => {
  317. if (this.shouldBlockTransportAction()) {
  318. return
  319. }
  320. this.onPlayNext()
  321. })
  322. PointLightContentButton({
  323. pointColor: this.themeColor,
  324. buttonRadius: 21,
  325. pointLightHeight: 52,
  326. pressScale: 0.88,
  327. builder: (): void => {
  328. this.buildPlayListControlContent()
  329. }
  330. })
  331. .width(42)
  332. .height(42)
  333. .displayPriority(1)
  334. .onClick((): void => {
  335. if (this.shouldBlockTransportAction()) {
  336. return
  337. }
  338. this.onOpenPlayList()
  339. })
  340. }
  341. .margin({ left: 5 })
  342. .justifyContent(FlexAlign.End)
  343. .alignItems(VerticalAlign.Center)
  344. }
  345. @Builder
  346. private hiCarLeadingCluster() {
  347. Row() {
  348. this.miniPlayerCoverButton()
  349. this.hiCarPlayConRigth()
  350. Row()
  351. .layoutWeight(1)
  352. }
  353. .width('100%')
  354. .height('100%')
  355. .justifyContent(FlexAlign.Start)
  356. .alignItems(VerticalAlign.Center)
  357. }
  358. @Builder
  359. private hiCarCenterInfo() {
  360. Column() {
  361. Text(this.currentSong?.name)
  362. .fontSize(16)
  363. .textOverflow({ overflow: TextOverflow.MARQUEE })
  364. .maxLines(1)
  365. .textAlign(TextAlign.Center)
  366. .fontColor(Color.White)
  367. .fontWeight(FontWeight.Bolder)
  368. Row() {
  369. Text(this.currentSong?.artist)
  370. .margin({ top: 2 })
  371. .fontSize(13)
  372. .textAlign(TextAlign.Center)
  373. .maxLines(1)
  374. .fontWeight(FontWeight.Bold)
  375. .textOverflow({ overflow: TextOverflow.MARQUEE })
  376. .fontColor(Color.White)
  377. }
  378. .visibility(this.currentSong?.artist ? Visibility.Visible : Visibility.None)
  379. }
  380. .width('42%')
  381. .alignItems(HorizontalAlign.Center)
  382. .onClick((): void => {
  383. this.onOpenPlayer()
  384. })
  385. }
  386. @Builder
  387. private hiCarPlayConRigth() {
  388. Row({ space: 8 }) {
  389. PointLightContentButton({
  390. pointColor: this.themeColor,
  391. buttonRadius: 22,
  392. pointLightHeight: 56,
  393. pressScale: 0.88,
  394. builder: (): void => {
  395. this.buildPlayPreviousControlContent()
  396. }
  397. })
  398. .width(44)
  399. .height(44)
  400. .visibility(this.isShowPrecious ? Visibility.Visible : Visibility.None)
  401. .displayPriority(2)
  402. .onClick((): void => {
  403. if (this.shouldBlockTransportAction()) {
  404. return
  405. }
  406. this.onPlayPrevious()
  407. })
  408. PointLightContentButton({
  409. pointColor: this.themeColor,
  410. buttonRadius: 23,
  411. pointLightHeight: 62,
  412. pressScale: 0.9,
  413. builder: (): void => {
  414. this.buildPlayToggleControlContent()
  415. }
  416. })
  417. .width(46)
  418. .height(46)
  419. .displayPriority(3)
  420. .onClick((): void => {
  421. if (this.shouldBlockTransportAction()) {
  422. return
  423. }
  424. this.onPlayOrPause()
  425. })
  426. PointLightContentButton({
  427. pointColor: this.themeColor,
  428. buttonRadius: 22,
  429. pointLightHeight: 56,
  430. pressScale: 0.88,
  431. builder: (): void => {
  432. this.buildPlayNextControlContent()
  433. }
  434. })
  435. .width(44)
  436. .height(44)
  437. .displayPriority(2)
  438. .onClick((): void => {
  439. if (this.shouldBlockTransportAction()) {
  440. return
  441. }
  442. this.onPlayNext()
  443. })
  444. PointLightContentButton({
  445. pointColor: this.themeColor,
  446. buttonRadius: 21,
  447. pointLightHeight: 52,
  448. pressScale: 0.88,
  449. builder: (): void => {
  450. this.buildPlayListControlContent()
  451. }
  452. })
  453. .width(42)
  454. .height(42)
  455. .displayPriority(1)
  456. .onClick((): void => {
  457. if (this.shouldBlockTransportAction()) {
  458. return
  459. }
  460. this.onOpenPlayList()
  461. })
  462. }
  463. .margin({ left: 16 })
  464. .justifyContent(FlexAlign.Start)
  465. .alignItems(VerticalAlign.Center)
  466. }
  467. @Builder
  468. private playController() {
  469. Row() {
  470. this.playConLeft()
  471. this.playConRigth()
  472. }
  473. .width('100%')
  474. .height(this.bottomBarHeight)
  475. .scale({ x: 1, y: this.miniPlayerContentScaleY, centerX: '50%', centerY: '50%' })
  476. .translate({ y: this.miniPlayerContentTranslateY })
  477. .hitTestBehavior(HitTestMode.Transparent)
  478. .zIndex(2)
  479. .opacity(0.9)
  480. .onClick((): void => {
  481. this.onOpenPlayer()
  482. })
  483. .onTouch((event: TouchEvent): void => {
  484. if (event.type === TouchType.Down) {
  485. this.pointLightOptions = {
  486. color: this.themeColor,
  487. intensity: 1,
  488. height: 60
  489. }
  490. } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
  491. this.pointLightOptions = undefined
  492. }
  493. })
  494. .visualEffect(deviceInfo.sdkApiVersion >= 20
  495. ? new hdsEffect.HdsEffectBuilder()
  496. .pointLight({
  497. options: this.pointLightOptions,
  498. illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
  499. })
  500. .buildEffect()
  501. : undefined)
  502. .padding({
  503. left: 16,
  504. right: 16
  505. })
  506. }
  507. @Builder
  508. private hiCarPlayController() {
  509. Stack({ alignContent: Alignment.Center }) {
  510. this.hiCarLeadingCluster()
  511. this.hiCarCenterInfo()
  512. }
  513. .opacity(0.9)
  514. .width('100%')
  515. .height(this.bottomBarHeight)
  516. .hitTestBehavior(HitTestMode.Transparent)
  517. .zIndex(2)
  518. .onClick((): void => {
  519. this.onOpenPlayer()
  520. })
  521. .onTouch((event: TouchEvent): void => {
  522. if (event.type === TouchType.Down) {
  523. this.pointLightOptions = {
  524. color: this.themeColor,
  525. intensity: 1,
  526. height: 60
  527. }
  528. } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
  529. this.pointLightOptions = undefined
  530. }
  531. })
  532. .visualEffect(deviceInfo.sdkApiVersion >= 20
  533. ? new hdsEffect.HdsEffectBuilder()
  534. .pointLight({
  535. options: this.pointLightOptions,
  536. illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
  537. })
  538. .buildEffect()
  539. : undefined)
  540. .padding({
  541. left: 16,
  542. right: 16
  543. })
  544. }
  545. build() {
  546. Row() {
  547. Stack() {
  548. if (!this.isMiniPlayerOrbMode || this.miniPlayerFullContentOpacity > 0.02) {
  549. Stack() {
  550. if (this.curDisplayIsHiCar) {
  551. this.hiCarPlayController()
  552. } else {
  553. this.playController()
  554. }
  555. }
  556. .width('100%')
  557. .height('100%')
  558. .opacity(this.miniPlayerFullContentOpacity)
  559. .translate({ x: this.miniPlayerFullContentTranslateX })
  560. }
  561. if (this.isMiniPlayerOrbMode || this.miniPlayerOrbOpacity > 0.02) {
  562. Row() {
  563. this.miniPlayerOrbControl()
  564. }
  565. .width('100%')
  566. .height('100%')
  567. .justifyContent(FlexAlign.End)
  568. .alignItems(VerticalAlign.Center)
  569. }
  570. if (this.miniPlayerProxySize > 0) {
  571. this.buildMiniPlayerMorphProxyDot()
  572. }
  573. }
  574. .width(this.miniPlayerSurfaceWidth)
  575. .height(this.miniPlayerSurfaceHeight)
  576. .borderRadius(this.miniPlayerBorderRadius)
  577. .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
  578. .backgroundImage(StrUtil.isEmpty(this.cover) ? $r('app.media.alt') : this.cover)
  579. .backgroundImageSize({ width: '100%' })
  580. .scale({ x: this.miniPlayerScaleX, y: this.miniPlayerScaleY, centerX: '50%', centerY: '50%' })
  581. .opacity(Math.min(0.98, this.miniPlayerOpacity))
  582. .clip(true)
  583. .clickEffect({ level: ClickEffectLevel.HEAVY })
  584. }
  585. .width('90%')
  586. .height(this.bottomBarHeight)
  587. .justifyContent(FlexAlign.End)
  588. .alignItems(VerticalAlign.Center)
  589. .translate({ y: this.miniPlayerTranslateY })
  590. .margin({
  591. bottom: DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_2IN1 || this.curDisplayIsHiCar
  592. ? 30 : this.bottomSafeHeight
  593. })
  594. }
  595. }