diff --git a/advanced_ui_component_static/BUILD.gn b/advanced_ui_component_static/BUILD.gn index ec7d33dca8a6e2c542f69efc61282deb454a0c97..57e945b20f6104c91ddf4127c04a2cf0dd79405c 100644 --- a/advanced_ui_component_static/BUILD.gn +++ b/advanced_ui_component_static/BUILD.gn @@ -21,6 +21,7 @@ group("advanced_ui_component_static") { deps += [ "swiperefresher:swipeRefresher", "splitlayout:splitLayout", + "tabTitleBar:tabTitleBar", ] } } diff --git a/advanced_ui_component_static/tabtitlebar/@ohos.arkui.advanced.TabTitleBar.ets b/advanced_ui_component_static/tabtitlebar/@ohos.arkui.advanced.TabTitleBar.ets new file mode 100644 index 0000000000000000000000000000000000000000..206eae3cf56be53ab20452faf5bc6e9d3892bede --- /dev/null +++ b/advanced_ui_component_static/tabtitlebar/@ohos.arkui.advanced.TabTitleBar.ets @@ -0,0 +1,1075 @@ +/* + * Copyright (c) 2025 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 { Column, Row, Component, Button, Text, CustomDialog, Array, $r, Flex, List } from "@ohos.arkui.component"; +import { HoverEvent, ImageLoadResult, Canvas, BlendMode, px2vp, Scroller, ForEach } from "@ohos.arkui.component"; +import { AttributeModifier, SymbolGlyphModifier, GestureModifier, ListItem } from "@ohos.arkui.component"; +import { ResourceStr, UIGestureEvent, LongPressGestureHandler, PopupOptions } from "@ohos.arkui.component"; +import { PopupStateChangeParam, CustomPopupOptions, CustomDialogController } from "@ohos.arkui.component"; +import { ScrollOptions, ScrollAnimationOptions, SwiperController, ResourceColor } from "@ohos.arkui.component"; +import { RenderingContextSettings, CanvasRenderingContext2D, Column, Builder } from "@ohos.arkui.component"; +import { FontWeight, FlexAlign, KeyEvent, TouchEvent, ItemAlign, Stack, Alignment } from "@ohos.arkui.component"; +import { EdgeEffect, Axis, BarState, BlendApplyType, Swiper, Curve, Color } from "@ohos.arkui.component"; +import { ButtonType, SymbolGlyph, BorderStyle, TouchType, KeyType, Placement } from "@ohos.arkui.component"; +import { GeometryInfo, Layoutable, ConstraintSizeOptions, focusControl, BorderOptions } from "@ohos.arkui.component"; +import { HitTestMode, SymbolEffect, ImageCompleteEvent, Resource, Image, Callback } from "@ohos.arkui.component"; +import { ImageFit, VerticalAlign, TextOverflow, TextAlign, ShadowStyle, BlurStyle } from "@ohos.arkui.component"; +import { Margin, Padding, CommonMethod, Area, ButtonOptions, ImageError, BuilderParam } from "@ohos.arkui.component"; +import { State, Observed, Prop, StorageLink, StorageProp, Watch } from '@ohos.arkui.stateManagement'; +import { Context, UIContext, MeasureUtils } from "@ohos.arkui.UIContext"; +import { KeyCode } from '@ohos.multimodalInput.keyCode'; +import resourceManager from '@ohos.resourceManager'; +import { MeasureOptions } from '@ohos.measure'; +import common from '@ohos.app.ability.common'; +import { BusinessError } from '@ohos.base'; +import window from '@ohos.window'; +import hilog from '@ohos.hilog'; + +export interface TabTitleBarMenuItem { + value: ResourceStr; + symbolStyle?: SymbolGlyphModifier; + isEnabled?: boolean; + action?: () => void; + label?: ResourceStr; + accessibilityText?: ResourceStr; + accessibilityLevel?: string; + accessibilityDescription?: ResourceStr; +} + +export interface TabTitleBarTabItem { + title: ResourceStr; + icon?: ResourceStr; + symbolStyle?: SymbolGlyphModifier; +} + +const PUBLIC_MORE = $r('sys.symbol.dot_grid_2x2'); +const TEXT_EDITABLE_DIALOG = '18.3fp'; +const IMAGE_SIZE = '64vp'; +const MAX_DIALOG = '256vp'; +const MIN_DIALOG = '216vp'; +const RESOURCE_TYPE_SYMBOL: number = 40000; + +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(); + } + } +} + +@Component +export struct TabTitleBar { + tabItems: Array = new Array(); + menuItems: Array = new Array(); + @BuilderParam swiperContent: () => void = () => { + }; + + @State tabWidth: number = 0; + @State currentIndex: number = 0; + @State fontSize: number = 1; + + static readonly totalHeight: number = 56; + static readonly correctionOffset: number = 1; + static readonly gradientMaskWidth: number = 24; + private static instanceCount: number = 0; + + private menuSectionWidth: number = 0; + private tabOffsets: Array = new Array(); + private imageWidths: Array = new Array(); + + private scroller: Scroller = new Scroller(); + private swiperController: SwiperController = new SwiperController(); + private settings: RenderingContextSettings = new RenderingContextSettings(true); + private leftContext2D: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); + private rightContext2D: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); + + @Builder + GradientMask(context2D: CanvasRenderingContext2D, x0: number, y0: number, x1: number, y1: number) { + Column() { + Canvas(context2D) + .width(TabTitleBar.gradientMaskWidth) + .height(TabTitleBar.totalHeight) + .onReady(() => { + let grad = context2D.createLinearGradient(x0, y0, x1, y1); + grad.addColorStop(0.0, '#ffffffff'); + grad.addColorStop(1, '#00ffffff'); + context2D.fillStyle = grad; + context2D.fillRect(0, 0, TabTitleBar.gradientMaskWidth, TabTitleBar.totalHeight); + } as () => void) + } + .blendMode(BlendMode.DST_OUT, BlendApplyType.FAST) + .width(TabTitleBar.gradientMaskWidth) + .height(TabTitleBar.totalHeight) + } + + @Builder + emptyBuilder() { + } + + aboutToAppear() { + if (!this.swiperContent) { + this.swiperContent = this.emptyBuilder; + } + this.tabItems.forEach((_elem) => { + this.imageWidths.push(0); + }) + this.loadOffsets(); + } + + loadOffsets() { + this.tabOffsets.length = 0; + + let tabOffset: number = 0; + this.tabOffsets.push(tabOffset); + tabOffset += TabContentItem.marginFirst; + + this.tabItems.forEach((tabItem: TabTitleBarTabItem, index: number) => { + if (tabItem.icon !== undefined || tabItem.symbolStyle !== undefined) { + if (Math.abs(this.imageWidths[index as int]) > TabContentItem.imageHotZoneWidth) { + tabOffset += this.imageWidths[index as int]; + } else { + tabOffset += TabContentItem.imageHotZoneWidth; + } + } else { + tabOffset += TabContentItem.paddingLeft; + let uiContent: UIContext = this.getUIContext(); + let uiContextMeasure: MeasureUtils = this.getUIContext().getMeasureUtils(); + tabOffset += px2vp(uiContextMeasure.measureText({ + textContent: tabItem.title.toString(), + fontSize: 18, + fontWeight: FontWeight.Medium, + } as MeasureOptions)); + tabOffset += TabContentItem.paddingRight; + } + this.tabOffsets.push(tabOffset); + }) + } + + build() { + Column() { + Flex({ + justifyContent: FlexAlign.SpaceBetween, + alignItems: ItemAlign.Stretch + }) { + Stack({ alignContent: Alignment.End }) { + Stack({ alignContent: Alignment.Start }) { + Column() { + List({ initialIndex: 0, scroller: this.scroller, space: 0 }) { + ForEach(this.tabItems, (tabItem: TabTitleBarTabItem, index: number) => { + ListItem() { + TabContentItem({ + item: tabItem, + index: index, + maxIndex: this.tabItems.length - 1, + currentIndex: this.currentIndex, + onCustomClick: (itemIndex: number) => { + this.currentIndex = itemIndex; + }, + onImageComplete: (width: number) => { + this.imageWidths[index as int] = width; + this.loadOffsets(); + } + }) + } + }) + } + .width('100%') + .height(TabTitleBar.totalHeight) + .constraintSize({ maxWidth: this.tabWidth }) + .edgeEffect(EdgeEffect.Spring) + .listDirection(Axis.Horizontal) + .scrollBar(BarState.Off) + } + + this.GradientMask(this.leftContext2D, 0, TabTitleBar.totalHeight / 2, + TabTitleBar.gradientMaskWidth, TabTitleBar.totalHeight / 2); + } + + this.GradientMask(this.rightContext2D, TabTitleBar.gradientMaskWidth, + TabTitleBar.totalHeight / 2, 0, TabTitleBar.totalHeight / 2); + } + .blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN) + + if (this.menuItems !== undefined && this.menuItems.length > 0) { + CollapsibleMenuSection({ + menuItems: this.menuItems, + index: 1 + TabTitleBar.instanceCount++, + height: TabTitleBar.totalHeight + }) + } + } + .backgroundColor($r('sys.color.ohos_id_color_background')) + .margin({ right: $r('sys.float.ohos_id_max_padding_end') } as Margin) + .onAreaChange((_oldValue: Area, newValue: Area) => { + this.tabWidth = Number(newValue.width as number) - this.menuSectionWidth; + }) + + Column() { + Swiper(this.swiperController as SwiperController) { + this.swiperContent(); + } + .index(this.currentIndex) + .itemSpace(0) + .indicator(false) + .width('100%') + .height('100%') + .curve(Curve.Friction) + .onChange((index: number) => { + const offset = this.tabOffsets[index as int] + TabTitleBar.correctionOffset; + this.currentIndex = index; + this.scroller.scrollTo({ + xOffset: offset > 0 ? offset : 0, + yOffset: 0, + animation: { + duration: 300, + curve: Curve.EaseInOut + } as ScrollAnimationOptions + } as ScrollOptions) + } as Callback) + .onAppear(() => { + this.scroller.scrollToIndex(this.currentIndex); + this.scroller.scrollBy(TabTitleBar.correctionOffset, 0); + }) + } + } + } +} + +@Component +struct CollapsibleMenuSection { + menuItems: Array = new Array(); + index: number = 0; + height: number = 0; + item: TabTitleBarMenuItem = { + value: PUBLIC_MORE, + // symbolStyle: new SymbolGlyphModifier(PUBLIC_MORE) as SymbolGlyphModifier, + symbolStyle: undefined, + label: $r('sys.string.ohos_toolbar_more'), + action: (): void => { + throw new Error('Function not implemented.'); + } + } as TabTitleBarMenuItem; + minFontSize: number = 1.75; + isFollowingSystemFontScale: boolean = false; + maxFontScale: number = 1; + systemFontScale?: number = 1; + + static readonly maxCountOfVisibleItems: number = 1; + private static readonly focusPadding: number = 4; + private static readonly marginsNum: number = 2; + private firstFocusableIndex: number = -1; + + @State isPopupShown: boolean = false; + + @State isMoreIconOnFocus: boolean = false; + @State isMoreIconOnHover: boolean = false; + @State isMoreIconOnClick: boolean = false; + @Prop @Watch('onFontSizeUpdated') fontSize: number = 1; + + dialogController: CustomDialogController | null = new CustomDialogController({ + builder: TabTitleBarDialog({ + cancel: () => { + } as () => void, + confirm: () => { + } as () => void, + tabTitleDialog: this.item, + tabTitleBarDialog: this.item.label ? this.item.label : '', + fontSize: this.fontSize, + }), + maskColor: Color.Transparent, + isModal: true, + customStyle: true, + }); + + @Observed + @State buttonGestureModifier: ButtonGestureModifier = new ButtonGestureModifier(this.dialogController); + + getMoreIconFgColor(): ResourceColor { + return this.isMoreIconOnClick + ? $r('sys.color.ohos_id_color_titlebar_icon_pressed') + : $r('sys.color.ohos_id_color_titlebar_icon'); + } + + getMoreIconBgColor(): ResourceColor { + if (this.isMoreIconOnClick) { + return $r('sys.color.ohos_id_color_click_effect'); + } else if (this.isMoreIconOnHover) { + return $r('sys.color.ohos_id_color_hover'); + } else { + return Color.Transparent; + } + } + + aboutToAppear() { + try { + let uiContent: UIContext = this.getUIContext(); + this.isFollowingSystemFontScale = uiContent.isFollowingSystemFontScale(); + this.maxFontScale = uiContent.getMaxFontScale(); + } catch (exception) { + let code: number = (exception as BusinessError).code; + let message: string = (exception as BusinessError).message; + hilog.error(0x3900, 'Ace', `Faild to decideFontScale,cause, code: ${code}, message: ${message}`); + } + this.menuItems.forEach((item, index) => { + if (item.isEnabled && this.firstFocusableIndex === -1 && + index > CollapsibleMenuSection.maxCountOfVisibleItems - 2) { + this.firstFocusableIndex = this.index * 1000 + index + 1 + } + }) + this.fontSize = this.decideFontScale(); + } + + decideFontScale(): number { + let uiContent: UIContext = this.getUIContext(); + this.systemFontScale = (uiContent.getHostContext() as common.UIAbilityContext)?.config?.fontSizeScale ?? 1; + if (!this.isFollowingSystemFontScale) { + return 1; + } + return Math.min(Number(this.systemFontScale), Number(this.maxFontScale)); + } + + onFontSizeUpdated(flag: String): void { + this.buttonGestureModifier.fontSize = this.fontSize; + } + + build() { + Column() { + Row() { + if (this.menuItems.length <= CollapsibleMenuSection.maxCountOfVisibleItems) { + ForEach(this.menuItems, (item: TabTitleBarMenuItem, index: number) => { + ImageMenuItem({ item: item, index: this.index * 1000 + index + 1 }); + }) + } else { + ForEach(this.menuItems.slice(0, CollapsibleMenuSection.maxCountOfVisibleItems - 1), + (item: TabTitleBarMenuItem, index: number) => { + ImageMenuItem({ item: item, index: this.index * 1000 + index + 1 }); + }) + Button({ type: ButtonType.Normal, stateEffect: true } as ButtonOptions) { + SymbolGlyph(PUBLIC_MORE) + .fontSize(TabContentItem.symbolSize) + .draggable(false) + .fontColor([$r('sys.color.icon_primary')]) + .focusable(true) + } + // .accessibilityText($r('sys.string.ohos_toolbar_more')) + .width(ImageMenuItem.imageHotZoneWidth) + .height(ImageMenuItem.imageHotZoneWidth) + .borderRadius(ImageMenuItem.buttonBorderRadius) + .foregroundColor(this.getMoreIconFgColor()) + .backgroundColor(this.getMoreIconBgColor()) + .stateStyles({ + focused: (instance: CommonMethod) => { + instance.border({ + radius: $r('sys.float.ohos_id_corner_radius_clicked'), + width: ImageMenuItem.focusBorderWidth, + color: $r('sys.color.ohos_id_color_focused_outline'), + style: BorderStyle.Solid + }) + }, + normal: (instance: CommonMethod) => { + instance.border({ + radius: $r('sys.float.ohos_id_corner_radius_clicked'), + width: 0 + }) + } + }) + .onFocus(() => { + this.isMoreIconOnFocus = true; + }) + .onBlur(() => { + this.isMoreIconOnFocus = false; + }) + .onHover((isOn: boolean, event: HoverEvent) => { + this.isMoreIconOnHover = isOn; + }) + .onKeyEvent((event: KeyEvent) => { + if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { + return; + } + if (event.type === KeyType.Down) { + this.isMoreIconOnClick = true; + } + if (event.type === KeyType.Up) { + this.isMoreIconOnClick = false; + } + }) + .onTouch((event: TouchEvent) => { + if (event.type === TouchType.Down) { + this.isMoreIconOnClick = true; + } + if (event.type === TouchType.Up || event.type === TouchType.Cancel) { + this.isMoreIconOnClick = false; + if (this.fontSize >= this.minFontSize) { + this.dialogController?.close(); + } + } + }) + .onClick(() => { + this.isPopupShown = true; + } as () => void) + .gestureModifier(this.buttonGestureModifier) + .bindPopup(this.isPopupShown as boolean, { + builder: this.popupBuilder, + placement: Placement.Bottom, + popupColor: Color.White, + enableArrow: false, + onStateChange: (e: PopupStateChangeParam) => { + this.isPopupShown = e.isVisible; + if (!e.isVisible) { + this.isMoreIconOnClick = false; + } + } + } as CustomPopupOptions) + } + } + } + .height(this.height) + .justifyContent(FlexAlign.Center) + } + + onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Layoutable[], constraint: ConstraintSizeOptions): void { + children.forEach((child) => { + child.layout({ x: 0, y: 0 }); + }) + this.fontSize = this.decideFontScale(); + } + + @Builder + popupBuilder() { + Column() { + ForEach(this.menuItems.slice(CollapsibleMenuSection.maxCountOfVisibleItems - 1, this.menuItems.length), + (item: TabTitleBarMenuItem, index: number) => { + ImageMenuItem({ + item: item as TabTitleBarMenuItem, index: this.index * 1000 + + CollapsibleMenuSection.maxCountOfVisibleItems + index + }); + }) + } + .width(ImageMenuItem.imageHotZoneWidth + CollapsibleMenuSection.focusPadding * CollapsibleMenuSection.marginsNum) + .margin({ top: CollapsibleMenuSection.focusPadding, bottom: CollapsibleMenuSection.focusPadding } as Margin) + .onAppear(() => { + focusControl.requestFocus(ImageMenuItem.focusablePrefix + this.firstFocusableIndex); + }) + } +} + +@Component +struct TabContentItem { + item: TabTitleBarTabItem = { title: '' } as TabTitleBarTabItem; + index: number = 0; + maxIndex: number = 0; + onCustomClick?: (index: number) => void; + onImageComplete?: (width: number) => void; + + @Prop currentIndex: number; + + @State isOnFocus: boolean = false; + @State isOnHover: boolean = false; + @State isOnClick: boolean = false; + @State tabWidth: number = 0; + + @State imageWidth: number = 24; + @State imageHeight: number = 24; + + static readonly imageSize: number = 24; + static readonly symbolSize: string = '24vp'; + static readonly imageHotZoneWidth: number = 48; + static readonly imageMagnificationFactor: number = 1.4; + static readonly buttonBorderRadius: number = 8; + static readonly focusBorderWidth: number = 2; + static readonly paddingLeft: number = 8; + static readonly paddingRight: number = 8; + static readonly marginFirst: number = 16; + + getBgColor(): ResourceColor { + if (this.isOnClick) { + return $r('sys.color.ohos_id_color_click_effect'); + } else if (this.isOnHover) { + return $r('sys.color.ohos_id_color_hover'); + } else { + return Color.Transparent; + } + } + + getBorderAttr(): BorderOptions { + if (this.isOnFocus) { + return { + radius: $r('sys.float.ohos_id_corner_radius_clicked'), + width: TabContentItem.focusBorderWidth, + color: $r('sys.color.ohos_id_color_focused_outline'), + style: BorderStyle.Solid + }; + } + return { width: 0 } + } + + getImageScaleFactor(): number { + return this.index === this.currentIndex ? TabContentItem.imageMagnificationFactor : 1; + } + + getImageLayoutWidth(): number { + return TabContentItem.imageSize / Math.max(this.imageHeight, 1.0) * this.imageWidth; + } + + private toStringFormat(resource: ResourceStr | undefined): string | undefined | Resource { + if (typeof resource === 'string') { + return resource; + } else if (typeof resource === 'undefined') { + return ''; + } else { + let resourceString: string = ''; + try { + resourceString = resourceManager.getSystemResourceManager().getStringSync(resource as Resource); + } catch (err) { + let code: undefined | number = (err as BusinessError)?.code; + let message: undefined | string = (err as BusinessError)?.message; + hilog.error(0x3900, 'Ace', `Faild to TabTitleBar toStringFormat,code: ${code},message:${message}`); + } + return resourceString; + } + } + + build() { + Stack() { + Row() { + Column() { + if (this.item.icon === undefined && this.item.symbolStyle === undefined) { + Text(this.item.title) + .fontSize(this.index === this.currentIndex ? 24 : 15) + .fontColor(this.index === this.currentIndex ? Color.Red : Color.Blue) + .fontWeight(FontWeight.Medium) + .focusable(true)// .animation({ duration: 300 }) + .padding({ + top: this.index === this.currentIndex ? 6 : 10, + left: TabContentItem.paddingLeft, + bottom: 2, + right: TabContentItem.paddingRight + } as Padding) + .onFocus(() => { + this.isOnFocus = true; + }) + .onBlur(() => { + this.isOnFocus = false; + }) + .onHover((isOn: boolean, event: HoverEvent) => { + this.isOnHover = isOn; + }) + .onKeyEvent((event: KeyEvent) => { + if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { + return; + } + if (event.type === KeyType.Down) { + this.isOnClick = true; + } + if (event.type === KeyType.Up) { + this.isOnClick = false; + } + }) + .onTouch((event: TouchEvent) => { + if (event.type === TouchType.Down) { + this.isOnClick = true; + } + if (event.type === TouchType.Up || event.type === TouchType.Cancel) { + this.isOnClick = false; + } + }) + .onClick(() => { + if (typeof this.onCustomClick === 'function') { + this.onCustomClick?.(this.index); + } + } as () => void) + // .accessibilitySelected(this.index === this.currentIndex) + } else { + Row() { + if (this.item.symbolStyle) { + SymbolGlyph() + .fontColor([$r('sys.color.icon_primary')]) + .attributeModifier(this.item.symbolStyle) + .fontSize(TabContentItem.symbolSize) + .width(this.getImageLayoutWidth()) + .height(TabContentItem.imageSize) + //.accessibilityText(this.toStringFormat(this.item.title) as Resource) + .scale({ + x: this.getImageScaleFactor(), + y: this.getImageScaleFactor() + }) + .animation({ duration: 300 }) + .hitTestBehavior(HitTestMode.None) + .focusable(true) + .symbolEffect(new SymbolEffect(), false) + } else { + if (Util.isSymbolResource(this.item.icon)) { + SymbolGlyph(this.item.icon as Resource) + .fontColor([$r('sys.color.icon_primary')]) + .fontSize(TabContentItem.symbolSize) + .width(this.getImageLayoutWidth()) + .height(TabContentItem.imageSize) + //.accessibilityText(this.toStringFormat(this.item.title) as Resource) + .scale({ + x: this.getImageScaleFactor(), + y: this.getImageScaleFactor() + }) + .animation({ duration: 300 }) + .hitTestBehavior(HitTestMode.None) + .focusable(true) + } else { + Image(this.item.icon as Resource) + .alt(this.item.title) + .width(this.getImageLayoutWidth()) + .height(TabContentItem.imageSize) + .objectFit(ImageFit.Fill) + //.accessibilityText(this.toStringFormat(this.item.title) as Resource) + .scale({ + x: this.getImageScaleFactor(), + y: this.getImageScaleFactor() + }) + .animation({ duration: 300 }) + .hitTestBehavior(HitTestMode.None) + .focusable(true) + .onComplete((event: ImageCompleteEvent) => { + let onImageCompleteValue: number = + px2vp(event!.componentWidth) + TabContentItem.paddingLeft + TabContentItem.paddingRight; + this.onImageComplete?.(onImageCompleteValue); + } as () => void) + .onError((event: ImageError) => { + if (!this.onImageComplete) { + return; + } + let onImageComplete: number = + px2vp(event.componentWidth) + TabContentItem.paddingLeft + TabContentItem.paddingRight + this.onImageComplete?.(onImageComplete); + } as () => void) + } + } + } + .width(this.getImageLayoutWidth() * this.getImageScaleFactor() + TabContentItem.paddingLeft + + TabContentItem.paddingRight) + .constraintSize({ + minWidth: TabContentItem.imageHotZoneWidth, + minHeight: TabContentItem.imageHotZoneWidth + }) + .animation({ duration: 300 }) + .justifyContent(FlexAlign.Center) + .onFocus(() => { + this.isOnFocus = true; + } as () => void) + .onBlur(() => { + this.isOnFocus = false; + } as () => void) + .onHover((isOn: boolean, event: HoverEvent) => { + this.isOnHover = isOn; + }) + .onKeyEvent((event: KeyEvent) => { + if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { + return; + } + if (event.type === KeyType.Down) { + this.isOnClick = true; + } + if (event.type === KeyType.Up) { + this.isOnClick = false; + } + }) + .onTouch((event: TouchEvent) => { + if (event.type === TouchType.Down) { + this.isOnClick = true; + } + if (event.type === TouchType.Up || event.type === TouchType.Cancel) { + this.isOnClick = false; + } + }) + .onClick(() => { + if (typeof this.onCustomClick === 'function') { + this.onCustomClick?.(this.index as number); + } + } as () => void) + // .accessibilitySelected(this.index === this.currentIndex) + } + } + .justifyContent(FlexAlign.Center) + } + .height(TabTitleBar.totalHeight) + .alignItems(VerticalAlign.Center) + .justifyContent(FlexAlign.Center) + .borderRadius(TabContentItem.buttonBorderRadius) + .backgroundColor(this.getBgColor()) + .onAreaChange((_oldValue: Area, newValue: Area) => { + this.tabWidth = newValue.width as number; + }) + + if (this.isOnFocus && this.tabWidth > 0) { + Row() + .width(this.tabWidth) + .height(TabTitleBar.totalHeight) + .hitTestBehavior(HitTestMode.None) + .borderRadius(TabContentItem.buttonBorderRadius) + .stateStyles({ + focused: (instance: CommonMethod) => { + instance.border(this.getBorderAttr()) + }, + normal: (instance: CommonMethod) => { + instance.border({ + radius: $r('sys.float.ohos_id_corner_radius_clicked'), + width: 0 + }) + } + }) + } + } + .margin({ + left: this.index === 0 ? TabContentItem.marginFirst : 0, + right: this.index === this.maxIndex ? 12 : 0 + } as Margin) + } +} + +@Component +struct ImageMenuItem { + item: TabTitleBarMenuItem = { + value: '', + action: (): void => { + throw new Error('Function not implemented.'); + } + } as TabTitleBarMenuItem; + index: number = 0; + + static readonly imageSize: number = 24; + static readonly imageHotZoneWidth: number = 48; + static readonly buttonBorderRadius: number = 8; + static readonly focusBorderWidth: number = 2; + static readonly disabledImageOpacity: number = 0.4; + static readonly focusablePrefix: string = "Id-TabTitleBar-ImageMenuItem-"; + + @State isOnFocus: boolean = false; + @State isOnHover: boolean = false; + @State isOnClick: boolean = false; + + getFgColor(): ResourceColor { + return this.isOnClick + ? $r('sys.color.ohos_id_color_titlebar_icon_pressed') + : $r('sys.color.ohos_id_color_titlebar_icon'); + } + + getBgColor(): ResourceColor { + if (this.isOnClick) { + return $r('sys.color.ohos_id_color_click_effect'); + } else if (this.isOnHover) { + return $r('sys.color.ohos_id_color_hover'); + } else { + return Color.Transparent; + } + } + + private toStringFormat(resource: ResourceStr | undefined | string): String | undefined | Resource { + if (typeof resource === 'string') { + return resource; + } else if (typeof resource === 'undefined') { + return ''; + } else { + let resourceString: undefined | String = ''; + try { + resourceString = resourceManager.getSystemResourceManager().getStringSync(resource as Resource); + } catch (err) { + let code: undefined | number = (err as BusinessError)?.code; + let message: undefined | string = (err as BusinessError)?.message; + hilog.error(0x3900, 'Ace', `Faild to TabTitleBar toStringFormat,code: ${code},message:${message}`); + } + return resourceString; + } + } + + private getAccessibilityReadText(): string | undefined | Resource { + if (this.item.value === PUBLIC_MORE) { + return resourceManager.getSystemResourceManager().getStringByNameSync('ohos_toolbar_more'); + } else if (this.item.accessibilityText) { + return this.toStringFormat(this.item.accessibilityText); + } else if (this.item.label) { + return this.toStringFormat(this.item.label); + } + return ' '; + } + + build() { + Button({ type: ButtonType.Normal, stateEffect: this.item.isEnabled } as ButtonOptions) { + if (this.item.symbolStyle) { + SymbolGlyph() + .fontColor([$r('sys.color.font_primary')]) + .attributeModifier(this.item.symbolStyle) + .fontSize(TabContentItem.symbolSize) + .draggable(false) + .focusable(this.item?.isEnabled) + .key(ImageMenuItem.focusablePrefix + this.index) + .symbolEffect(new SymbolEffect(), false); + } else { + if (Util.isSymbolResource(this.item.value)) { + SymbolGlyph(this.item.value as Resource) + .fontColor([$r('sys.color.font_primary')]) + .fontSize(TabContentItem.symbolSize) + .draggable(false) + .focusable(this.item?.isEnabled) + .key(ImageMenuItem.focusablePrefix + this.index); + } else { + Image(this.item.value) + .width(ImageMenuItem.imageSize) + .height(ImageMenuItem.imageSize) + .focusable(this.item.isEnabled) + .key(ImageMenuItem.focusablePrefix + this.index) + .draggable(false); + } + } + } + // .accessibilityText(this.getAccessibilityReadText() as Resource) + // .accessibilityLevel(this.item?.accessibilityLevel ?? 'auto') + // .accessibilityDescription(this.toStringFormat(this.item?.accessibilityDescription) as Resource) + .width(ImageMenuItem.imageHotZoneWidth) + .height(ImageMenuItem.imageHotZoneWidth) + .borderRadius(ImageMenuItem.buttonBorderRadius) + .foregroundColor(this.getFgColor()) + .backgroundColor(this.getBgColor()) + .enabled(this.item.isEnabled ? this.item.isEnabled : false) + .stateStyles({ + focused: (instance: CommonMethod) => { + instance.border({ + radius: $r('sys.float.ohos_id_corner_radius_clicked'), + width: ImageMenuItem.focusBorderWidth, + color: $r('sys.color.ohos_id_color_focused_outline'), + style: BorderStyle.Solid + }) + }, + normal: (instance: CommonMethod) => { + instance.border({ + radius: $r('sys.float.ohos_id_corner_radius_clicked'), + width: 0 + }) + } + }) + .onFocus(() => { + if (!this.item.isEnabled) { + return; + } + this.isOnFocus = true; + }) + .onBlur(() => { + this.isOnFocus = false; + } as () => void) + .onHover((isOn: boolean, event: HoverEvent) => { + if (this.item.isEnabled) { + this.isOnHover = isOn; + } + }) + .onKeyEvent((event: KeyEvent) => { + if (!this.item.isEnabled) { + return; + } + if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { + return; + } + if (event.type === KeyType.Down) { + this.isOnClick = true; + } + if (event.type === KeyType.Up) { + this.isOnClick = false; + } + }) + .onTouch((event: TouchEvent) => { + if (!this.item.isEnabled) { + return; + } + if (event.type === TouchType.Down) { + this.isOnClick = true; + } + if (event.type === TouchType.Up || event.type === TouchType.Cancel) { + this.isOnClick = false; + } + }) + .onClick(() => { + if (this.item.isEnabled && typeof this.item?.action === 'function') { + (this.item?.action as () => void)(); + } + } as () => void) + } +} + +/** + * TabTitleBarDialog + */ +@CustomDialog +export struct TabTitleBarDialog { + tabTitleDialog: TabTitleBarMenuItem = { value: '' } as TabTitleBarMenuItem; + callbackId: number | undefined = undefined; + tabTitleBarDialog?: ResourceStr = ''; + mainWindowStage: window.Window | undefined = undefined; + controller?: CustomDialogController; + minFontSize: number = 1.75; + maxFontSize: number = 3.2; + screenWidth: number = 640; + verticalScreenLines: number = 6; + horizontalsScreenLines: number = 1; + @State fontSize: number = 1; + @State maxLines: number = 1; + @StorageProp('windowStandardHeight') windowStandardHeight: number = 0; + cancel: () => void = () => { + }; + confirm: () => void = () => { + }; + + build() { + if (this.tabTitleBarDialog) { + Column() { + if (this.tabTitleDialog.symbolStyle) { + SymbolGlyph() + .fontColor([$r('sys.color.font_primary')]) + .attributeModifier(this.tabTitleDialog.symbolStyle) + .fontSize(IMAGE_SIZE) + .draggable(false) + .focusable(this.tabTitleDialog?.isEnabled) + .margin({ + top: $r('sys.float.padding_level24'), + bottom: $r('sys.float.padding_level8'), + } as Margin) + .symbolEffect(new SymbolEffect(), false) + } else if (this.tabTitleDialog.value) { + if (Util.isSymbolResource(this.tabTitleDialog.value)) { + SymbolGlyph(this.tabTitleDialog.value as Resource) + .fontColor([$r('sys.color.font_primary')]) + .fontSize(IMAGE_SIZE) + .draggable(false) + .focusable(this.tabTitleDialog?.isEnabled) + .margin({ + top: $r('sys.float.padding_level24'), + bottom: $r('sys.float.padding_level8'), + } as Margin) + } else { + Image(this.tabTitleDialog.value) + .width(IMAGE_SIZE) + .height(IMAGE_SIZE) + .margin({ + top: $r('sys.float.padding_level24'), + bottom: $r('sys.float.padding_level8'), + } as Margin) + .fillColor($r('sys.color.icon_primary')) + } + } + Column() { + Text(this.tabTitleBarDialog) + .fontSize(TEXT_EDITABLE_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'), + } as Padding) + } + .width(this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG) + .constraintSize({ minHeight: this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG }) + .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK, undefined, { disableSystemAdaptation: true }) + .shadow(ShadowStyle.OUTER_DEFAULT_LG) + .borderRadius($r('sys.float.corner_radius_level10')) + } else { + Column() { + if (this.tabTitleDialog.symbolStyle) { + SymbolGlyph() + .fontColor([$r('sys.color.font_primary')]) + .attributeModifier(this.tabTitleDialog.symbolStyle) + .fontSize(IMAGE_SIZE) + .draggable(false) + .focusable(this.tabTitleDialog?.isEnabled) + .symbolEffect(new SymbolEffect(), false) + } else if (this.tabTitleDialog.value) { + if (Util.isSymbolResource(this.tabTitleDialog.value)) { + SymbolGlyph(this.tabTitleDialog.value as Resource) + .fontColor([$r('sys.color.font_primary')]) + .fontSize(IMAGE_SIZE) + .draggable(false) + .focusable(this.tabTitleDialog?.isEnabled) + .margin({ + top: $r('sys.float.padding_level24'), + bottom: $r('sys.float.padding_level8'), + } as Margin) + } else { + Image(this.tabTitleDialog.value) + .width(IMAGE_SIZE) + .height(IMAGE_SIZE) + .fillColor($r('sys.color.icon_primary')) + } + } + } + .width(this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG) + .constraintSize({ minHeight: this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG }) + .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK, undefined, { disableSystemAdaptation: true }) + .shadow(ShadowStyle.OUTER_DEFAULT_LG) + .borderRadius($r('sys.float.corner_radius_level10')) + .justifyContent(FlexAlign.Center) + } + } + + aboutToAppear(): void { + let context = this.getUIContext().getHostContext() as common.UIAbilityContext; + this.mainWindowStage = context.windowStage.getMainWindowSync(); + let properties: window.WindowProperties = this.mainWindowStage?.getWindowProperties() as window.WindowProperties; + let rect = properties.windowRect; + if (px2vp(rect.height) > this.screenWidth) { + this.maxLines = this.verticalScreenLines; + } else { + this.maxLines = this.horizontalsScreenLines; + } + } +} + +class Util { + public static isSymbolResource(resourceStr: ResourceStr | undefined | null): boolean { + if (!Util.isResourceType(resourceStr)) { + return false; + } + let resource = resourceStr as Resource; + return resource.type === RESOURCE_TYPE_SYMBOL; + } + + public static isResourceType(resource: ResourceStr | Resource | undefined | null): boolean { + if (!resource) { + return false; + } + if (typeof resource === 'string' || typeof resource === 'undefined') { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/advanced_ui_component_static/tabtitlebar/BUILD.gn b/advanced_ui_component_static/tabtitlebar/BUILD.gn new file mode 100644 index 0000000000000000000000000000000000000000..bdde9cb7fac62c3c3031d1740973c8484de74aa1 --- /dev/null +++ b/advanced_ui_component_static/tabtitlebar/BUILD.gn @@ -0,0 +1,47 @@ +# Copyright (c) 2025 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("//build/config/components/ets_frontend/ets2abc_config.gni") +import("//build/ohos.gni") + +generate_static_abc("tab_title_bar_ets_abc") { + base_url = "./" + files = [ "./@ohos.arkui.advanced.TabTitleBar.ets" ] + ui_enable = "True" + is_boot_abc = "True" + device_dst_file = "/system/framework/tab_title_bar_ets_abc.abc" + dst_file = target_out_dir + "/tab_title_bar_ets_abc" + out_puts = [ target_out_dir + "/tab_title_bar_ets_abc/modules_static.abc" ] +} + +ohos_copy("copy_tab_title_bar_ets_abc") { + sources = [ + target_out_dir + "/tab_title_bar_ets_abc/modules_static.abc" + ] + outputs = [ + target_out_dir + "/tab_title_bar_ets_abc.abc" + ] + deps = [ ":tab_title_bar_ets_abc" ] +} + +ohos_prebuilt_etc("tab_title_bar_ets_abc_etc") { + source = "$target_out_dir/tab_title_bar_ets_abc.abc" + module_install_dir = "framework" + subsystem_name = "arkui" + part_name = "ace_engine" + deps = [ ":copy_tab_title_bar_ets_abc" ] +} + +group("tabTitleBar"){ + deps = [":tab_title_bar_ets_abc_etc"] +} \ No newline at end of file