/* * Copyright (c) 2023-2024 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Theme } from '@ohos.arkui.theme'; import { LengthMetrics, LengthUnit, ColorMetrics } from '@ohos.arkui.node'; import { DividerModifier, SymbolGlyphModifier } from '@ohos.arkui.modifier'; import hilog from '@ohos.hilog'; import window from '@ohos.window'; import common from '@ohos.app.ability.common'; import { BusinessError } from '@ohos.base'; export enum ItemState { ENABLE = 1, DISABLE = 2, ACTIVATE = 3, } // “更多”栏图标 const PUBLIC_MORE: Resource = $r('sys.media.ohos_ic_public_more'); const IMAGE_SIZE: string = '24vp'; const DEFAULT_TOOLBAR_HEIGHT: number = 56; const TOOLBAR_MAX_LENGTH: number = 5; const MAX_FONT_SIZE = 3.2; const DIALOG_IMAGE_SIZE = '64vp'; const MAX_DIALOG = '256vp'; const MIN_DIALOG = '216vp'; const TEXT_TOOLBAR_DIALOG = '18.3fp'; const FOCUS_BOX_MARGIN: number = -2; const FOCUS_BOX_BORDER_WIDTH: number = 2; interface MenuController { value: ResourceStr; action: () => void; enabled?: boolean; } export interface ToolBarSymbolGlyphOptions { normal?: SymbolGlyphModifier; activated?: SymbolGlyphModifier; } class ButtonGestureModifier implements GestureModifier { public static readonly longPressTime: number = 500; public static readonly minFontSize: number = 1.75; public fontSize: number = 1; public controller: CustomDialogController | null = null; constructor(controller: CustomDialogController | null) { this.controller = controller; } applyGesture(event: UIGestureEvent): void { if (this.fontSize >= ButtonGestureModifier.minFontSize) { event.addGesture( new LongPressGestureHandler({ repeat: false, duration: ButtonGestureModifier.longPressTime }) .onAction(() => { if (event) { this.controller?.open(); } }) .onActionEnd(() => { this.controller?.close(); }) ) } else { event.clearGestures(); } } } @Observed export class ToolBarOption { public content: ResourceStr = ''; public action?: () => void = undefined; public icon?: Resource = undefined; public state?: ItemState = 1; public iconColor?: ResourceColor = $r('sys.color.icon_primary'); public activatedIconColor?: ResourceColor = $r('sys.color.icon_emphasize'); public textColor?: ResourceColor = $r('sys.color.font_primary'); public activatedTextColor?: ResourceColor = $r('sys.color.font_emphasize'); public toolBarSymbolOptions?: ToolBarSymbolGlyphOptions = undefined; } @Observed export class ToolBarOptions extends Array { } export class ToolBarModifier implements AttributeModifier { public backgroundColorValue?: ResourceColor = $r('sys.color.ohos_id_color_toolbar_bg'); public heightValue?: LengthMetrics = LengthMetrics.vp(DEFAULT_TOOLBAR_HEIGHT); public stateEffectValue?: boolean = true; public paddingValue?: LengthMetrics = LengthMetrics.resource($r('sys.float.padding_level12')); applyNormalAttribute(instance: ColumnAttribute): void { instance.backgroundColor(this.backgroundColorValue); } public backgroundColor(backgroundColor: ResourceColor): ToolBarModifier { this.backgroundColorValue = backgroundColor; return this; } public height(height: LengthMetrics): ToolBarModifier { this.heightValue = height; return this; } public stateEffect(stateEffect: boolean): ToolBarModifier { this.stateEffectValue = stateEffect; return this; } public padding(padding: LengthMetrics): ToolBarModifier { this.paddingValue = padding; return this; } } @Component export struct ToolBar { @ObjectLink toolBarList: ToolBarOptions; controller: TabsController = new TabsController(); @Prop activateIndex: number = -1; @Prop dividerModifier: DividerModifier = new DividerModifier(); @Prop toolBarModifier: ToolBarModifier = new ToolBarModifier() .padding(LengthMetrics.resource($r('sys.float.padding_level12'))) .stateEffect(true) .height(LengthMetrics.vp(DEFAULT_TOOLBAR_HEIGHT)) .backgroundColor('sys.color.ohos_id_color_toolbar_bg'); @Prop moreText: ResourceStr = $r('sys.string.ohos_toolbar_more'); @State menuContent: MenuController[] = []; @State toolBarItemBackground: ResourceColor[] = []; @State iconPrimaryColor: ResourceColor = $r('sys.color.icon_primary'); @State iconActivePrimaryColor: ResourceColor = $r('sys.color.icon_emphasize'); @State fontPrimaryColor: ResourceColor = $r('sys.color.font_primary'); @State fontActivatedPrimaryColor: ResourceColor = $r('sys.color.font_emphasize'); @State symbolEffect: SymbolEffect = new SymbolEffect(); @State fontSize: number = 1; isFollowSystem: boolean = false; maxFontSizeScale: number = 3.2; moreIndex: number = 4; moreItem: ToolBarOption = { content: $r('sys.string.ohos_toolbar_more'), icon: PUBLIC_MORE, } onWillApplyTheme(theme: Theme) { this.iconPrimaryColor = theme.colors.iconPrimary; this.iconActivePrimaryColor = theme.colors.iconEmphasize; this.fontPrimaryColor = theme.colors.fontPrimary; this.fontActivatedPrimaryColor = theme.colors.fontEmphasize; } @Builder MoreTabBuilder(index: number) { Button({ type: ButtonType.Normal, stateEffect: false }) { Column() { Image(PUBLIC_MORE) .width(IMAGE_SIZE) .height(IMAGE_SIZE) .fillColor(this.iconPrimaryColor) .margin({ bottom: $r('sys.float.padding_level1') }) .objectFit(ImageFit.Contain) .draggable(false) Text(this.moreText) .fontColor(this.fontPrimaryColor) .fontSize($r('sys.float.ohos_id_text_size_caption')) .fontWeight(FontWeight.Medium) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .textAlign(TextAlign.Center) .focusable(true) .focusOnTouch(true) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .padding({ start: LengthMetrics.resource($r('sys.float.padding_level2')), end: LengthMetrics.resource($r('sys.float.padding_level2')), }) .borderRadius($r('sys.float.ohos_id_corner_radius_clicked')) } .focusable(true) .focusOnTouch(true) .focusBox({ margin: LengthMetrics.vp(FOCUS_BOX_MARGIN), strokeWidth: LengthMetrics.vp(FOCUS_BOX_BORDER_WIDTH), strokeColor: ColorMetrics.resourceColor($r('sys.color.ohos_id_color_focused_outline')) }) .width('100%') .height('100%') .bindMenu(this.menuContent, { placement: Placement.TopRight, offset: { x: -12, y : -10 } }) .borderRadius($r('sys.float.ohos_id_corner_radius_clicked')) .backgroundColor(this.toolBarItemBackground[index]) .onHover((isHover: boolean) => { if (isHover) { this.toolBarItemBackground[index] = $r('sys.color.ohos_id_color_hover'); } else { this.toolBarItemBackground[index] = Color.Transparent; } }) .stateStyles({ pressed: { .backgroundColor((!this.toolBarModifier.stateEffectValue) ? this.toolBarItemBackground[index] : $r('sys.color.ohos_id_color_click_effect')) } }) .gestureModifier(this.getItemGestureModifier(this.moreItem, index)) } @Builder TabBuilder(index: number) { Button({ type: ButtonType.Normal, stateEffect: false }) { Column() { if (this.toolBarList[index]?.toolBarSymbolOptions?.normal || this.toolBarList[index]?.toolBarSymbolOptions?.activated) { SymbolGlyph() .fontSize(IMAGE_SIZE) .symbolEffect(this.symbolEffect, false) .attributeModifier(this.getToolBarSymbolModifier(index)) .margin({ bottom: $r('sys.float.padding_level1') }) } else { Image(this.toolBarList[index]?.icon) .width(IMAGE_SIZE) .height(IMAGE_SIZE) .fillColor(this.getIconColor(index)) .margin({ bottom: $r('sys.float.padding_level1') }) .objectFit(ImageFit.Contain) .draggable(false) } Text(this.toolBarList[index]?.content) .fontColor(this.getTextColor(index)) .fontSize($r('sys.float.ohos_id_text_size_caption')) .maxFontSize($r('sys.float.ohos_id_text_size_caption')) .minFontSize(9) .fontWeight(FontWeight.Medium) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .textAlign(TextAlign.Center) .focusable(!(this.toolBarList[index]?.state === ItemState.DISABLE)) .focusOnTouch(!(this.toolBarList[index]?.state === ItemState.DISABLE)) } .justifyContent(FlexAlign.Center) .width('100%') .height('100%') .borderRadius($r('sys.float.ohos_id_corner_radius_clicked')) .padding({ start: LengthMetrics.resource($r('sys.float.padding_level2')), end: LengthMetrics.resource($r('sys.float.padding_level2')), }) } .enabled(this.toolBarList[index]?.state !== ItemState.DISABLE) .width('100%') .height('100%') .borderRadius($r('sys.float.ohos_id_corner_radius_clicked')) .focusable(!(this.toolBarList[index]?.state === ItemState.DISABLE)) .focusOnTouch(!(this.toolBarList[index]?.state === ItemState.DISABLE)) .focusBox({ margin: LengthMetrics.vp(FOCUS_BOX_MARGIN), strokeWidth: LengthMetrics.vp(FOCUS_BOX_BORDER_WIDTH), strokeColor: ColorMetrics.resourceColor($r('sys.color.ohos_id_color_focused_outline')) }) .backgroundColor(this.toolBarItemBackground[index]) .onHover((isHover: boolean) => { if (isHover && this.toolBarList[index]?.state !== ItemState.DISABLE) { this.toolBarItemBackground[index] = $r('sys.color.ohos_id_color_hover'); } else { this.toolBarItemBackground[index] = Color.Transparent; } }) .stateStyles({ pressed: { .backgroundColor((this.toolBarList[index]?.state === ItemState.DISABLE) || (!this.toolBarModifier.stateEffectValue) ? this.toolBarItemBackground[index] : $r('sys.color.ohos_id_color_click_effect')) } }) .onClick(() => { this.clickEventAction(index); }) .gestureModifier(this.getItemGestureModifier(this.toolBarList[index], index)) } private getFontSizeScale(): number { let context = this.getUIContext(); let fontScaleSystem = (context.getHostContext() as common.UIAbilityContext)?.config?.fontSizeScale ?? 1; if (!this.isFollowSystem) { return 1; } else { return Math.min(fontScaleSystem, this.maxFontSizeScale); } } private getToolBarSymbolModifier(index: number): SymbolGlyphModifier | undefined { if ((!this.toolBarList[index]?.toolBarSymbolOptions?.activated) && (!this.toolBarList[index]?.toolBarSymbolOptions?.normal)) { return undefined; } if (this.activateIndex === index && (this.toolBarList[index]?.state === ItemState.ACTIVATE)) { return this.toolBarList[index]?.toolBarSymbolOptions?.activated; } return this.toolBarList[index]?.toolBarSymbolOptions?.normal; } private getIconColor(index: number): ResourceColor { if (this.activateIndex === index && (this.toolBarList[index]?.state === ItemState.ACTIVATE)) { return this.toolBarList[index]?.activatedIconColor ?? this.iconActivePrimaryColor; } return this.toolBarList[index]?.iconColor ?? this.iconPrimaryColor; } private getTextColor(index: number): ResourceColor { if (this.activateIndex === index && (this.toolBarList[index]?.state === ItemState.ACTIVATE)) { return this.toolBarList[index]?.activatedTextColor ?? this.fontActivatedPrimaryColor; } return this.toolBarList[index]?.textColor ?? this.fontPrimaryColor; } private toLengthString(value: LengthMetrics | undefined): string { if (value === void (0)) { return ''; } const length: number = value.value; let lengthString: string = ''; switch (value.unit) { case LengthUnit.PX: lengthString = `${length}px`; break; case LengthUnit.FP: lengthString = `${length}fp`; break; case LengthUnit.LPX: lengthString = `${length}lpx`; break; case LengthUnit.PERCENT: lengthString = `${length * 100}%`; break; case LengthUnit.VP: lengthString = `${length}vp`; break; default: lengthString = `${length}vp`; break; } return lengthString; } private clickEventAction(index: number): void { let toolbar = this.toolBarList[index]; if (toolbar.state === ItemState.ACTIVATE) { if (this.activateIndex === index) { this.activateIndex = -1; } else { this.activateIndex = index; } } if (!(toolbar.state === ItemState.DISABLE)) { toolbar.action && toolbar.action(); } } private getItemGestureModifier(item: ToolBarOption, index: number): ButtonGestureModifier { let buttonGestureModifier: ButtonGestureModifier = new ButtonGestureModifier(null); if (item?.icon || item?.toolBarSymbolOptions?.activated || item?.toolBarSymbolOptions?.normal) { buttonGestureModifier = new ButtonGestureModifier(new CustomDialogController({ builder: ToolBarDialog({ itemDialog: item, fontSize: this.fontSize, itemSymbolModifier: this.getToolBarSymbolModifier(index), }), maskColor: Color.Transparent, isModal: true, customStyle: true, })) buttonGestureModifier.fontSize = this.fontSize; } return buttonGestureModifier; } refreshData() { this.menuContent = []; for (let i = 0; i < this.toolBarList.length; i++) { if (i >= this.moreIndex && this.toolBarList.length > TOOLBAR_MAX_LENGTH) { this.menuContent[i - this.moreIndex] = { value: this.toolBarList[i].content, action: this.toolBarList[i].action as () => void, enabled: this.toolBarList[i].state !== ItemState.DISABLE, } } else { this.menuContent = []; } this.toolBarItemBackground[i] = this.toolBarItemBackground[i] ?? Color.Transparent; } return true; } onMeasureSize(selfLayoutInfo: GeometryInfo, children: Measurable[], constraint: ConstraintSizeOptions): SizeResult { this.fontSize = this.getFontSizeScale(); let sizeResult: SizeResult = { height: 0, width: 0 }; children.forEach((child) => { let childMeasureResult: MeasureResult = child.measure(constraint); sizeResult.width = childMeasureResult.width; sizeResult.height = childMeasureResult.height; }); return sizeResult; } aboutToAppear() { this.refreshData(); try { this.isFollowSystem = this.getUIContext()?.isFollowingSystemFontScale(); this.maxFontSizeScale = this.getUIContext()?.getMaxFontScale(); } catch (err) { let code: number = (err as BusinessError)?.code; let message: string = (err as BusinessError)?.message; hilog.error(0x3900, 'Ace', `Faild to toolBar getMaxFontScale, code: ${code}, message: ${message}`); } } build() { Column() { Tabs({ controller: this.controller }) { } .visibility(Visibility.None) Divider() .width('100%').height(1) .attributeModifier(this.dividerModifier) Row() { ForEach(this.toolBarList, (item: ToolBarOption, index: number) => { if (this.toolBarList.length <= TOOLBAR_MAX_LENGTH || index < this.moreIndex) { Row() { this.TabBuilder(index); } .height('100%') .flexShrink(1) } }) if (this.refreshData() && this.toolBarList.length > TOOLBAR_MAX_LENGTH) { Row() { this.MoreTabBuilder(this.moreIndex); } .height('100%') .flexShrink(1) } } .justifyContent(FlexAlign.Center) .constraintSize({ minHeight: this.toLengthString(this.toolBarModifier.heightValue), maxHeight: this.toLengthString(this.toolBarModifier.heightValue), }) .width('100%') .height(this.toLengthString(this.toolBarModifier.heightValue)) .padding({ start: this.toolBarList.length < TOOLBAR_MAX_LENGTH ? this.toolBarModifier.paddingValue : LengthMetrics.resource($r('sys.float.padding_level0')), end: this.toolBarList.length < TOOLBAR_MAX_LENGTH ? this.toolBarModifier.paddingValue : LengthMetrics.resource($r('sys.float.padding_level0')), }) } .attributeModifier(this.toolBarModifier) } } /** * ToolBarDialog * * @since 2024-07-23 */ @CustomDialog struct ToolBarDialog { itemDialog: ToolBarOption = { icon: undefined, content: '', }; itemSymbolModifier?: SymbolGlyphModifier; mainWindowStage: window.Window | undefined = undefined; controller?: CustomDialogController screenWidth: number = 640; verticalScreenLines: number = 6; horizontalsScreenLines: number = 1; cancel: () => void = () => { } confirm: () => void = () => { } @StorageLink('mainWindow') mainWindow: Promise | undefined = undefined; @Prop fontSize: number = 1; @State maxLines: number = 1; @StorageProp('windowStandardHeight') windowStandardHeight: number = 0; @State symbolEffect: SymbolEffect = new SymbolEffect(); build() { if (this.itemDialog.content) { Column() { if (this.itemDialog.toolBarSymbolOptions?.normal || this.itemDialog.toolBarSymbolOptions?.activated) { SymbolGlyph() .attributeModifier(this.itemSymbolModifier) .symbolEffect(this.symbolEffect, false) .fontColor([$r('sys.color.icon_primary')]) .fontSize(DIALOG_IMAGE_SIZE) .margin({ top: $r('sys.float.padding_level24'), bottom: $r('sys.float.padding_level8'), }) } else { Image(this.itemDialog.icon) .width(DIALOG_IMAGE_SIZE) .height(DIALOG_IMAGE_SIZE) .margin({ top: $r('sys.float.padding_level24'), bottom: $r('sys.float.padding_level8'), }) .fillColor($r('sys.color.icon_primary')) } Column() { Text(this.itemDialog.content) .fontSize(TEXT_TOOLBAR_DIALOG) .textOverflow({ overflow: TextOverflow.Ellipsis }) .maxLines(this.maxLines) .width('100%') .textAlign(TextAlign.Center) .fontColor($r('sys.color.font_primary')) } .width('100%') .padding({ left: $r('sys.float.padding_level4'), right: $r('sys.float.padding_level4'), bottom: $r('sys.float.padding_level12'), }) } .width(this.fontSize === MAX_FONT_SIZE ? MAX_DIALOG : MIN_DIALOG) .constraintSize({ minHeight: this.fontSize === MAX_FONT_SIZE ? MAX_DIALOG : MIN_DIALOG }) .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK) .shadow(ShadowStyle.OUTER_DEFAULT_LG) .borderRadius(($r('sys.float.corner_radius_level10'))) } else { Column() { if (this.itemDialog.toolBarSymbolOptions?.normal || this.itemDialog.toolBarSymbolOptions?.activated) { SymbolGlyph() .attributeModifier(this.itemSymbolModifier) .symbolEffect(this.symbolEffect, false) .fontColor([$r('sys.color.icon_primary')]) .fontSize(DIALOG_IMAGE_SIZE) } else { Image(this.itemDialog.icon) .width(DIALOG_IMAGE_SIZE) .height(DIALOG_IMAGE_SIZE) .fillColor($r('sys.color.icon_primary')) } } .width(this.fontSize === MAX_FONT_SIZE ? MAX_DIALOG : MIN_DIALOG) .constraintSize({ minHeight: this.fontSize === MAX_FONT_SIZE ? MAX_DIALOG : MIN_DIALOG }) .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK) .shadow(ShadowStyle.OUTER_DEFAULT_LG) .borderRadius(($r('sys.float.corner_radius_level10'))) .justifyContent(FlexAlign.Center) } } async aboutToAppear(): Promise { let context = this.getUIContext().getHostContext() as common.UIAbilityContext; this.mainWindowStage = context.windowStage.getMainWindowSync(); let properties: window.WindowProperties = this.mainWindowStage.getWindowProperties(); let rect = properties.windowRect; if (px2vp(rect.height) > this.screenWidth) { this.maxLines = this.verticalScreenLines; } else { this.maxLines = this.horizontalsScreenLines; } } }