import { DeviceUtil, StrUtil } from '@pura/harmony-utils' import { resourceManager } from '@kit.LocalizationKit' import { deviceInfo } from '@kit.BasicServicesKit' import { hdsEffect } from '@kit.UIDesignKit' import { VideoItem } from '../viewmodel/VideoItem' import { PlayStatus } from '../common/PlayStatus' import { CommonConstants } from '../common/constants/CommonConstants' import { PointLightContentButton } from './PointLight/PointLightContentButton' import { PlayingIndicator } from './PlayingIndicator' @Component export struct MiniPlayerBar { @Prop bottomBarHeight: number = 64 @Prop bottomSafeHeight: number = 0 @Prop curDisplayIsHiCar: boolean = false @Prop cover: string | undefined = '' @Prop currentSong: VideoItem | undefined = undefined @Prop themeColor: string = CommonConstants.DEFAULT_THEME_COLOR @Prop progressValue: number = 0 @Prop controlPlayStatus: number = PlayStatus.INIT @Prop isShowPrecious: boolean = false @Prop isMiniPlayerOrbMode: boolean = false @Prop isMiniPlayerModeTransitioning: boolean = false @Prop miniPlayerScaleX: number = 1 @Prop miniPlayerScaleY: number = 1 @Prop miniPlayerOpacity: number = 1 @Prop miniPlayerBorderRadius: number = 200 @Prop miniPlayerProxySize: number = 0 @Prop miniPlayerProxyOpacity: number = 0 @Prop miniPlayerContentScaleY: number = 1 @Prop miniPlayerContentTranslateY: number = 0 @Prop miniPlayerFullContentTranslateX: number = 0 @Prop miniPlayerTranslateY: number = 0 @Prop miniPlayerSurfaceWidth: number = 0 @Prop miniPlayerSurfaceHeight: number = 0 @Prop miniPlayerFullContentOpacity: number = 1 @Prop miniPlayerOrbOpacity: number = 0 @Prop miniPlayerOrbScale: number = 1 @Prop miniPlayerOrbTranslateX: number = 0 onOpenPlayer: () => void = () => {} onCollapseToOrb: () => void = () => {} onOrbTap: () => void = () => {} onPlayPrevious: () => void = () => {} onPlayOrPause: () => void = () => {} onPlayNext: () => void = () => {} onOpenPlayList: () => void = () => {} @State private pointLightOptions: hdsEffect.PointLightOptions | undefined = undefined private resolveMiniPlayerOrbSize(): number { const barHeightVp = this.bottomBarHeight > 0 ? this.bottomBarHeight : 70 return Math.max(54, Math.min(60, barHeightVp - 8)) } private shouldBlockTransportAction(): boolean { return this.isMiniPlayerModeTransitioning } @Builder private buildMiniPlayerMorphProxyDot() { Stack({ alignContent: Alignment.Center }) { Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt')) .width('100%') .height('100%') .objectFit(ImageFit.Cover) .alt($r('app.media.alt')) } .width(this.miniPlayerProxySize) .height(this.miniPlayerProxySize) .borderRadius(this.miniPlayerProxySize) .clip(true) .opacity(this.miniPlayerProxyOpacity) .shadow({ radius: 18, type: ShadowType.BLUR, color: 'on_primary' }) } @Builder private buildMiniPlayerOrbButtonContent(orbSize: number) { Stack({ alignContent: Alignment.Center }) { Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt')) .width('100%') .height('100%') .objectFit(ImageFit.Cover) .alt($r('app.media.alt')) .borderRadius(100) Column() .width('100%') .height('100%') .backgroundColor('#66000000') Progress({ value: Math.floor(this.progressValue), total: 100, type: ProgressType.Ring, }) .color(Color.White) .width('100%') .height('100%') .style({ strokeWidth: 3 }) if (this.controlPlayStatus === PlayStatus.PLAY) { PlayingIndicator({ isActive: true, indicatorSize: Math.max(16, Math.floor(orbSize * 0.34)), indicatorColor: '#FFFFFF' }) } } .width('100%') .height('100%') .clip(true) .borderRadius(100) } @Builder private miniPlayerOrbControl() { PointLightContentButton({ pointColor: this.themeColor, buttonRadius: this.resolveMiniPlayerOrbSize() / 2, pointLightHeight: 96, pressScale: 0.94, useShadow: true, builder: (): void => { this.buildMiniPlayerOrbButtonContent(this.resolveMiniPlayerOrbSize()) } }) .width(this.resolveMiniPlayerOrbSize()) .height(this.resolveMiniPlayerOrbSize()) .opacity(this.miniPlayerOrbOpacity) .borderRadius(this.resolveMiniPlayerOrbSize() / 2) .scale({ x: this.miniPlayerOrbScale, y: this.miniPlayerOrbScale, centerX: '50%', centerY: '50%' }) .translate({ x: this.miniPlayerOrbTranslateX }) .onClick((): void => { this.onOrbTap() }) } @Builder private buildMiniPlayerCoverContent() { Stack({ alignContent: Alignment.Center }) { Image(StrUtil.isNotEmpty(this.currentSong?.pixelMapPath) ? this.currentSong?.pixelMapPath : $r('app.media.alt')) .width(48) .height(48) .objectFit(ImageFit.Contain) .alt($r('app.media.alt')) .fillColor(this.themeColor) .borderRadius(8) .shadow({ radius: 15, type: ShadowType.BLUR, color: 'on_primary' }) } .width(48) .height(48) } @Builder private miniPlayerCoverButton() { PointLightContentButton({ pointColor: this.themeColor, buttonRadius: 10, pointLightHeight: 88, pressScale: 0.92, useShadow: false, builder: (): void => { this.buildMiniPlayerCoverContent() } }) .width(48) .height(48) .margin({ left: 5 }) .zIndex(3) .onClick((): void => { this.onCollapseToOrb() }) } @Builder private buildPlayPreviousControlContent() { Row() { SymbolGlyph($r('sys.symbol.backward_end_fill')) .fontSize(28) .fontColor([Color.White]) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) } @Builder private buildPlayToggleControlContent() { Row() { Stack() { Progress({ value: Math.floor(this.progressValue), type: ProgressType.Ring, }) .color(this.themeColor) .height(38) .aspectRatio(CommonConstants.ASPECT_RATIO) Image(this.controlPlayStatus === PlayStatus.PLAY ? $r('app.media.hm_pause') : $r('app.media.hm_play2')) .height(36) .width(36) .fillColor(Color.White) } } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) } @Builder private buildPlayNextControlContent() { Row() { SymbolGlyph($r('sys.symbol.forward_end_fill')) .fontSize(28) .fontColor([Color.White]) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) } @Builder private buildPlayListControlContent() { Row() { SymbolGlyph($r('sys.symbol.music_note_list')) .fontSize(28) .fontColor([Color.White]) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) } @Builder private playConLeft() { Row() { this.miniPlayerCoverButton() Column() { Text(this.currentSong?.name) .fontSize(16) .textOverflow({ overflow: TextOverflow.MARQUEE }) .maxLines(1) .fontColor(Color.White) .fontWeight(FontWeight.Bolder) Row() { Text(this.currentSong?.artist) .margin({ top: 2 }) .fontSize(13) .textAlign(TextAlign.Start) .maxLines(1) .fontWeight(FontWeight.Bold) .textOverflow({ overflow: TextOverflow.MARQUEE }) .fontColor(Color.White) } .visibility(this.currentSong?.artist ? Visibility.Visible : Visibility.None) } .padding({ left: 8 }) .alignItems(HorizontalAlign.Start) .margin({ right: 22 }) .onClick((): void => { this.onOpenPlayer() }) } .padding({ right: 18 }) .layoutWeight(1) .alignItems(VerticalAlign.Center) .justifyContent(FlexAlign.Start) } @Builder private playConRigth() { Row() { PointLightContentButton({ pointColor: this.themeColor, buttonRadius: 22, pointLightHeight: 56, pressScale: 0.88, builder: (): void => { this.buildPlayPreviousControlContent() } }) .width(44) .height(44) .visibility(this.isShowPrecious ? Visibility.Visible : Visibility.None) .displayPriority(2) .onClick((): void => { if (this.shouldBlockTransportAction()) { return } this.onPlayPrevious() }) PointLightContentButton({ pointColor: this.themeColor, buttonRadius: 23, pointLightHeight: 62, pressScale: 0.9, builder: (): void => { this.buildPlayToggleControlContent() } }) .width(46) .height(46) .displayPriority(3) .onClick((): void => { if (this.shouldBlockTransportAction()) { return } this.onPlayOrPause() }) PointLightContentButton({ pointColor: this.themeColor, buttonRadius: 22, pointLightHeight: 56, pressScale: 0.88, builder: (): void => { this.buildPlayNextControlContent() } }) .width(44) .height(44) .displayPriority(2) .onClick((): void => { if (this.shouldBlockTransportAction()) { return } this.onPlayNext() }) PointLightContentButton({ pointColor: this.themeColor, buttonRadius: 21, pointLightHeight: 52, pressScale: 0.88, builder: (): void => { this.buildPlayListControlContent() } }) .width(42) .height(42) .displayPriority(1) .onClick((): void => { if (this.shouldBlockTransportAction()) { return } this.onOpenPlayList() }) } .margin({ left: 5 }) .justifyContent(FlexAlign.End) .alignItems(VerticalAlign.Center) } @Builder private hiCarLeadingCluster() { Row() { this.miniPlayerCoverButton() this.hiCarPlayConRigth() Row() .layoutWeight(1) } .width('100%') .height('100%') .justifyContent(FlexAlign.Start) .alignItems(VerticalAlign.Center) } @Builder private hiCarCenterInfo() { Column() { Text(this.currentSong?.name) .fontSize(16) .textOverflow({ overflow: TextOverflow.MARQUEE }) .maxLines(1) .textAlign(TextAlign.Center) .fontColor(Color.White) .fontWeight(FontWeight.Bolder) Row() { Text(this.currentSong?.artist) .margin({ top: 2 }) .fontSize(13) .textAlign(TextAlign.Center) .maxLines(1) .fontWeight(FontWeight.Bold) .textOverflow({ overflow: TextOverflow.MARQUEE }) .fontColor(Color.White) } .visibility(this.currentSong?.artist ? Visibility.Visible : Visibility.None) } .width('42%') .alignItems(HorizontalAlign.Center) .onClick((): void => { this.onOpenPlayer() }) } @Builder private hiCarPlayConRigth() { Row({ space: 8 }) { PointLightContentButton({ pointColor: this.themeColor, buttonRadius: 22, pointLightHeight: 56, pressScale: 0.88, builder: (): void => { this.buildPlayPreviousControlContent() } }) .width(44) .height(44) .visibility(this.isShowPrecious ? Visibility.Visible : Visibility.None) .displayPriority(2) .onClick((): void => { if (this.shouldBlockTransportAction()) { return } this.onPlayPrevious() }) PointLightContentButton({ pointColor: this.themeColor, buttonRadius: 23, pointLightHeight: 62, pressScale: 0.9, builder: (): void => { this.buildPlayToggleControlContent() } }) .width(46) .height(46) .displayPriority(3) .onClick((): void => { if (this.shouldBlockTransportAction()) { return } this.onPlayOrPause() }) PointLightContentButton({ pointColor: this.themeColor, buttonRadius: 22, pointLightHeight: 56, pressScale: 0.88, builder: (): void => { this.buildPlayNextControlContent() } }) .width(44) .height(44) .displayPriority(2) .onClick((): void => { if (this.shouldBlockTransportAction()) { return } this.onPlayNext() }) PointLightContentButton({ pointColor: this.themeColor, buttonRadius: 21, pointLightHeight: 52, pressScale: 0.88, builder: (): void => { this.buildPlayListControlContent() } }) .width(42) .height(42) .displayPriority(1) .onClick((): void => { if (this.shouldBlockTransportAction()) { return } this.onOpenPlayList() }) } .margin({ left: 16 }) .justifyContent(FlexAlign.Start) .alignItems(VerticalAlign.Center) } @Builder private playController() { Row() { this.playConLeft() this.playConRigth() } .width('100%') .height(this.bottomBarHeight) .scale({ x: 1, y: this.miniPlayerContentScaleY, centerX: '50%', centerY: '50%' }) .translate({ y: this.miniPlayerContentTranslateY }) .hitTestBehavior(HitTestMode.Transparent) .zIndex(2) .opacity(0.9) .onClick((): void => { this.onOpenPlayer() }) .onTouch((event: TouchEvent): void => { if (event.type === TouchType.Down) { this.pointLightOptions = { color: this.themeColor, intensity: 1, height: 60 } } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { this.pointLightOptions = undefined } }) .visualEffect(deviceInfo.sdkApiVersion >= 20 ? new hdsEffect.HdsEffectBuilder() .pointLight({ options: this.pointLightOptions, illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT }) .buildEffect() : undefined) .padding({ left: 16, right: 16 }) } @Builder private hiCarPlayController() { Stack({ alignContent: Alignment.Center }) { this.hiCarLeadingCluster() this.hiCarCenterInfo() } .opacity(0.9) .width('100%') .height(this.bottomBarHeight) .hitTestBehavior(HitTestMode.Transparent) .zIndex(2) .onClick((): void => { this.onOpenPlayer() }) .onTouch((event: TouchEvent): void => { if (event.type === TouchType.Down) { this.pointLightOptions = { color: this.themeColor, intensity: 1, height: 60 } } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { this.pointLightOptions = undefined } }) .visualEffect(deviceInfo.sdkApiVersion >= 20 ? new hdsEffect.HdsEffectBuilder() .pointLight({ options: this.pointLightOptions, illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT }) .buildEffect() : undefined) .padding({ left: 16, right: 16 }) } build() { Row() { Stack() { if (!this.isMiniPlayerOrbMode || this.miniPlayerFullContentOpacity > 0.02) { Stack() { if (this.curDisplayIsHiCar) { this.hiCarPlayController() } else { this.playController() } } .width('100%') .height('100%') .opacity(this.miniPlayerFullContentOpacity) .translate({ x: this.miniPlayerFullContentTranslateX }) } if (this.isMiniPlayerOrbMode || this.miniPlayerOrbOpacity > 0.02) { Row() { this.miniPlayerOrbControl() } .width('100%') .height('100%') .justifyContent(FlexAlign.End) .alignItems(VerticalAlign.Center) } if (this.miniPlayerProxySize > 0) { this.buildMiniPlayerMorphProxyDot() } } .width(this.miniPlayerSurfaceWidth) .height(this.miniPlayerSurfaceHeight) .borderRadius(this.miniPlayerBorderRadius) .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK) .backgroundImage(StrUtil.isEmpty(this.cover) ? $r('app.media.alt') : this.cover) .backgroundImageSize({ width: '100%' }) .scale({ x: this.miniPlayerScaleX, y: this.miniPlayerScaleY, centerX: '50%', centerY: '50%' }) .opacity(Math.min(0.98, this.miniPlayerOpacity)) .clip(true) .clickEffect({ level: ClickEffectLevel.HEAVY }) } .width('90%') .height(this.bottomBarHeight) .justifyContent(FlexAlign.End) .alignItems(VerticalAlign.Center) .translate({ y: this.miniPlayerTranslateY }) .margin({ bottom: DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_2IN1 || this.curDisplayIsHiCar ? 30 : this.bottomSafeHeight }) } }