1/* 2 * Copyright (c) 2023-2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { Theme } from '@ohos.arkui.theme'; 17import { LengthMetrics, LengthUnit, ColorMetrics } from '@ohos.arkui.node'; 18import { DividerModifier, SymbolGlyphModifier } from '@ohos.arkui.modifier'; 19import hilog from '@ohos.hilog'; 20import window from '@ohos.window'; 21import common from '@ohos.app.ability.common'; 22import { BusinessError } from '@ohos.base'; 23 24export enum ItemState { 25 ENABLE = 1, 26 DISABLE = 2, 27 ACTIVATE = 3, 28} 29 30// “更多”栏图标 31const PUBLIC_MORE: Resource = $r('sys.media.ohos_ic_public_more'); 32const IMAGE_SIZE: string = '24vp'; 33const DEFAULT_TOOLBAR_HEIGHT: number = 56; 34const TOOLBAR_MAX_LENGTH: number = 5; 35const MAX_FONT_SIZE = 3.2; 36const DIALOG_IMAGE_SIZE = '64vp'; 37const MAX_DIALOG = '256vp'; 38const MIN_DIALOG = '216vp'; 39const TEXT_TOOLBAR_DIALOG = '18.3fp'; 40const FOCUS_BOX_MARGIN: number = -2; 41const FOCUS_BOX_BORDER_WIDTH: number = 2; 42 43interface MenuController { 44 value: ResourceStr; 45 action: () => void; 46 enabled?: boolean; 47} 48 49export interface ToolBarSymbolGlyphOptions { 50 normal?: SymbolGlyphModifier; 51 activated?: SymbolGlyphModifier; 52} 53 54class ButtonGestureModifier implements GestureModifier { 55 public static readonly longPressTime: number = 500; 56 public static readonly minFontSize: number = 1.75; 57 public fontSize: number = 1; 58 public controller: CustomDialogController | null = null; 59 60 constructor(controller: CustomDialogController | null) { 61 this.controller = controller; 62 } 63 64 applyGesture(event: UIGestureEvent): void { 65 if (this.fontSize >= ButtonGestureModifier.minFontSize) { 66 event.addGesture( 67 new LongPressGestureHandler({ repeat: false, duration: ButtonGestureModifier.longPressTime }) 68 .onAction(() => { 69 if (event) { 70 this.controller?.open(); 71 } 72 }) 73 .onActionEnd(() => { 74 this.controller?.close(); 75 }) 76 ) 77 } else { 78 event.clearGestures(); 79 } 80 } 81} 82 83@Observed 84export class ToolBarOption { 85 public content: ResourceStr = ''; 86 public action?: () => void = undefined; 87 public icon?: Resource = undefined; 88 public state?: ItemState = 1; 89 public iconColor?: ResourceColor = $r('sys.color.icon_primary'); 90 public activatedIconColor?: ResourceColor = $r('sys.color.icon_emphasize'); 91 public textColor?: ResourceColor = $r('sys.color.font_primary'); 92 public activatedTextColor?: ResourceColor = $r('sys.color.font_emphasize'); 93 public toolBarSymbolOptions?: ToolBarSymbolGlyphOptions = undefined; 94} 95 96@Observed 97export class ToolBarOptions extends Array<ToolBarOption> { 98} 99 100export class ToolBarModifier implements AttributeModifier<ColumnAttribute> { 101 public backgroundColorValue?: ResourceColor = $r('sys.color.ohos_id_color_toolbar_bg'); 102 public heightValue?: LengthMetrics = LengthMetrics.vp(DEFAULT_TOOLBAR_HEIGHT); 103 public stateEffectValue?: boolean = true; 104 public paddingValue?: LengthMetrics = LengthMetrics.resource($r('sys.float.padding_level12')); 105 106 applyNormalAttribute(instance: ColumnAttribute): void { 107 instance.backgroundColor(this.backgroundColorValue); 108 } 109 110 public backgroundColor(backgroundColor: ResourceColor): ToolBarModifier { 111 this.backgroundColorValue = backgroundColor; 112 return this; 113 } 114 115 public height(height: LengthMetrics): ToolBarModifier { 116 this.heightValue = height; 117 return this; 118 } 119 120 public stateEffect(stateEffect: boolean): ToolBarModifier { 121 this.stateEffectValue = stateEffect; 122 return this; 123 } 124 125 public padding(padding: LengthMetrics): ToolBarModifier { 126 this.paddingValue = padding; 127 return this; 128 } 129} 130 131@Component 132export struct ToolBar { 133 @ObjectLink toolBarList: ToolBarOptions; 134 controller: TabsController = new TabsController(); 135 @Prop activateIndex: number = -1; 136 @Prop dividerModifier: DividerModifier = new DividerModifier(); 137 @Prop toolBarModifier: ToolBarModifier = 138 new ToolBarModifier() 139 .padding(LengthMetrics.resource($r('sys.float.padding_level12'))) 140 .stateEffect(true) 141 .height(LengthMetrics.vp(DEFAULT_TOOLBAR_HEIGHT)) 142 .backgroundColor('sys.color.ohos_id_color_toolbar_bg'); 143 @Prop moreText: ResourceStr = $r('sys.string.ohos_toolbar_more'); 144 @State menuContent: MenuController[] = []; 145 @State toolBarItemBackground: ResourceColor[] = []; 146 @State iconPrimaryColor: ResourceColor = $r('sys.color.icon_primary'); 147 @State iconActivePrimaryColor: ResourceColor = $r('sys.color.icon_emphasize'); 148 @State fontPrimaryColor: ResourceColor = $r('sys.color.font_primary'); 149 @State fontActivatedPrimaryColor: ResourceColor = $r('sys.color.font_emphasize'); 150 @State symbolEffect: SymbolEffect = new SymbolEffect(); 151 @State fontSize: number = 1; 152 isFollowSystem: boolean = false; 153 maxFontSizeScale: number = 3.2; 154 moreIndex: number = 4; 155 moreItem: ToolBarOption = { 156 content: $r('sys.string.ohos_toolbar_more'), 157 icon: PUBLIC_MORE, 158 } 159 160 onWillApplyTheme(theme: Theme) { 161 this.iconPrimaryColor = theme.colors.iconPrimary; 162 this.iconActivePrimaryColor = theme.colors.iconEmphasize; 163 this.fontPrimaryColor = theme.colors.fontPrimary; 164 this.fontActivatedPrimaryColor = theme.colors.fontEmphasize; 165 } 166 167 @Builder 168 MoreTabBuilder(index: number) { 169 Button({ type: ButtonType.Normal, stateEffect: false }) { 170 Column() { 171 Image(PUBLIC_MORE) 172 .width(IMAGE_SIZE) 173 .height(IMAGE_SIZE) 174 .fillColor(this.iconPrimaryColor) 175 .margin({ bottom: $r('sys.float.padding_level1') }) 176 .objectFit(ImageFit.Contain) 177 .draggable(false) 178 Text(this.moreText) 179 .fontColor(this.fontPrimaryColor) 180 .fontSize($r('sys.float.ohos_id_text_size_caption')) 181 .fontWeight(FontWeight.Medium) 182 .maxLines(1) 183 .textOverflow({ overflow: TextOverflow.Ellipsis }) 184 .textAlign(TextAlign.Center) 185 .focusable(true) 186 .focusOnTouch(true) 187 } 188 .width('100%') 189 .height('100%') 190 .justifyContent(FlexAlign.Center) 191 .padding({ 192 start: LengthMetrics.resource($r('sys.float.padding_level2')), 193 end: LengthMetrics.resource($r('sys.float.padding_level2')), 194 }) 195 .borderRadius($r('sys.float.ohos_id_corner_radius_clicked')) 196 } 197 .focusable(true) 198 .focusOnTouch(true) 199 .focusBox({ 200 margin: LengthMetrics.vp(FOCUS_BOX_MARGIN), 201 strokeWidth: LengthMetrics.vp(FOCUS_BOX_BORDER_WIDTH), 202 strokeColor: ColorMetrics.resourceColor($r('sys.color.ohos_id_color_focused_outline')) 203 }) 204 .width('100%') 205 .height('100%') 206 .bindMenu(this.menuContent, { placement: Placement.TopRight, offset: { x: -12, y : -10 } }) 207 .borderRadius($r('sys.float.ohos_id_corner_radius_clicked')) 208 .backgroundColor(this.toolBarItemBackground[index]) 209 .onHover((isHover: boolean) => { 210 if (isHover) { 211 this.toolBarItemBackground[index] = $r('sys.color.ohos_id_color_hover'); 212 } else { 213 this.toolBarItemBackground[index] = Color.Transparent; 214 } 215 }) 216 .stateStyles({ 217 pressed: { 218 .backgroundColor((!this.toolBarModifier.stateEffectValue) ? 219 this.toolBarItemBackground[index] : $r('sys.color.ohos_id_color_click_effect')) 220 } 221 }) 222 .gestureModifier(this.getItemGestureModifier(this.moreItem, index)) 223 } 224 225 @Builder 226 TabBuilder(index: number) { 227 Button({ type: ButtonType.Normal, stateEffect: false }) { 228 Column() { 229 if (this.toolBarList[index]?.toolBarSymbolOptions?.normal || 230 this.toolBarList[index]?.toolBarSymbolOptions?.activated) { 231 SymbolGlyph() 232 .fontSize(IMAGE_SIZE) 233 .symbolEffect(this.symbolEffect, false) 234 .attributeModifier(this.getToolBarSymbolModifier(index)) 235 .margin({ bottom: $r('sys.float.padding_level1') }) 236 } else { 237 Image(this.toolBarList[index]?.icon) 238 .width(IMAGE_SIZE) 239 .height(IMAGE_SIZE) 240 .fillColor(this.getIconColor(index)) 241 .margin({ bottom: $r('sys.float.padding_level1') }) 242 .objectFit(ImageFit.Contain) 243 .draggable(false) 244 } 245 Text(this.toolBarList[index]?.content) 246 .fontColor(this.getTextColor(index)) 247 .fontSize($r('sys.float.ohos_id_text_size_caption')) 248 .maxFontSize($r('sys.float.ohos_id_text_size_caption')) 249 .minFontSize(9) 250 .fontWeight(FontWeight.Medium) 251 .maxLines(1) 252 .textOverflow({ overflow: TextOverflow.Ellipsis }) 253 .textAlign(TextAlign.Center) 254 .focusable(!(this.toolBarList[index]?.state === ItemState.DISABLE)) 255 .focusOnTouch(!(this.toolBarList[index]?.state === ItemState.DISABLE)) 256 } 257 .justifyContent(FlexAlign.Center) 258 .width('100%') 259 .height('100%') 260 .borderRadius($r('sys.float.ohos_id_corner_radius_clicked')) 261 .padding({ 262 start: LengthMetrics.resource($r('sys.float.padding_level2')), 263 end: LengthMetrics.resource($r('sys.float.padding_level2')), 264 }) 265 } 266 .enabled(this.toolBarList[index]?.state !== ItemState.DISABLE) 267 .width('100%') 268 .height('100%') 269 .borderRadius($r('sys.float.ohos_id_corner_radius_clicked')) 270 .focusable(!(this.toolBarList[index]?.state === ItemState.DISABLE)) 271 .focusOnTouch(!(this.toolBarList[index]?.state === ItemState.DISABLE)) 272 .focusBox({ 273 margin: LengthMetrics.vp(FOCUS_BOX_MARGIN), 274 strokeWidth: LengthMetrics.vp(FOCUS_BOX_BORDER_WIDTH), 275 strokeColor: ColorMetrics.resourceColor($r('sys.color.ohos_id_color_focused_outline')) 276 }) 277 .backgroundColor(this.toolBarItemBackground[index]) 278 .onHover((isHover: boolean) => { 279 if (isHover && this.toolBarList[index]?.state !== ItemState.DISABLE) { 280 this.toolBarItemBackground[index] = $r('sys.color.ohos_id_color_hover'); 281 } else { 282 this.toolBarItemBackground[index] = Color.Transparent; 283 } 284 }) 285 .stateStyles({ 286 pressed: { 287 .backgroundColor((this.toolBarList[index]?.state === ItemState.DISABLE) || 288 (!this.toolBarModifier.stateEffectValue) ? 289 this.toolBarItemBackground[index] : $r('sys.color.ohos_id_color_click_effect')) 290 } 291 }) 292 .onClick(() => { 293 this.clickEventAction(index); 294 }) 295 .gestureModifier(this.getItemGestureModifier(this.toolBarList[index], index)) 296 } 297 298 private getFontSizeScale(): number { 299 let context = this.getUIContext(); 300 let fontScaleSystem = (context.getHostContext() as common.UIAbilityContext)?.config?.fontSizeScale ?? 1; 301 if (!this.isFollowSystem) { 302 return 1; 303 } else { 304 return Math.min(fontScaleSystem, this.maxFontSizeScale); 305 } 306 } 307 308 private getToolBarSymbolModifier(index: number): SymbolGlyphModifier | undefined { 309 if ((!this.toolBarList[index]?.toolBarSymbolOptions?.activated) && 310 (!this.toolBarList[index]?.toolBarSymbolOptions?.normal)) { 311 return undefined; 312 } 313 if (this.activateIndex === index && (this.toolBarList[index]?.state === ItemState.ACTIVATE)) { 314 return this.toolBarList[index]?.toolBarSymbolOptions?.activated; 315 } 316 return this.toolBarList[index]?.toolBarSymbolOptions?.normal; 317 } 318 319 private getIconColor(index: number): ResourceColor { 320 if (this.activateIndex === index && (this.toolBarList[index]?.state === ItemState.ACTIVATE)) { 321 return this.toolBarList[index]?.activatedIconColor ?? this.iconActivePrimaryColor; 322 } 323 return this.toolBarList[index]?.iconColor ?? this.iconPrimaryColor; 324 } 325 326 private getTextColor(index: number): ResourceColor { 327 if (this.activateIndex === index && (this.toolBarList[index]?.state === ItemState.ACTIVATE)) { 328 return this.toolBarList[index]?.activatedTextColor ?? this.fontActivatedPrimaryColor; 329 } 330 return this.toolBarList[index]?.textColor ?? this.fontPrimaryColor; 331 } 332 333 private toLengthString(value: LengthMetrics | undefined): string { 334 if (value === void (0)) { 335 return ''; 336 } 337 const length: number = value.value; 338 let lengthString: string = ''; 339 switch (value.unit) { 340 case LengthUnit.PX: 341 lengthString = `${length}px`; 342 break; 343 case LengthUnit.FP: 344 lengthString = `${length}fp`; 345 break; 346 case LengthUnit.LPX: 347 lengthString = `${length}lpx`; 348 break; 349 case LengthUnit.PERCENT: 350 lengthString = `${length * 100}%`; 351 break; 352 case LengthUnit.VP: 353 lengthString = `${length}vp`; 354 break; 355 default: 356 lengthString = `${length}vp`; 357 break; 358 } 359 return lengthString; 360 } 361 362 private clickEventAction(index: number): void { 363 let toolbar = this.toolBarList[index]; 364 if (toolbar.state === ItemState.ACTIVATE) { 365 if (this.activateIndex === index) { 366 this.activateIndex = -1; 367 } else { 368 this.activateIndex = index; 369 } 370 } 371 if (!(toolbar.state === ItemState.DISABLE)) { 372 toolbar.action && toolbar.action(); 373 } 374 } 375 376 private getItemGestureModifier(item: ToolBarOption, index: number): ButtonGestureModifier { 377 let buttonGestureModifier: ButtonGestureModifier = new ButtonGestureModifier(null); 378 if (item?.icon || item?.toolBarSymbolOptions?.activated || item?.toolBarSymbolOptions?.normal) { 379 buttonGestureModifier = new ButtonGestureModifier(new CustomDialogController({ 380 builder: ToolBarDialog({ 381 itemDialog: item, 382 fontSize: this.fontSize, 383 itemSymbolModifier: this.getToolBarSymbolModifier(index), 384 }), 385 maskColor: Color.Transparent, 386 isModal: true, 387 customStyle: true, 388 })) 389 buttonGestureModifier.fontSize = this.fontSize; 390 } 391 return buttonGestureModifier; 392 } 393 394 refreshData() { 395 this.menuContent = []; 396 for (let i = 0; i < this.toolBarList.length; i++) { 397 if (i >= this.moreIndex && this.toolBarList.length > TOOLBAR_MAX_LENGTH) { 398 this.menuContent[i - this.moreIndex] = { 399 value: this.toolBarList[i].content, 400 action: this.toolBarList[i].action as () => void, 401 enabled: this.toolBarList[i].state !== ItemState.DISABLE, 402 } 403 } else { 404 this.menuContent = []; 405 } 406 this.toolBarItemBackground[i] = this.toolBarItemBackground[i] ?? Color.Transparent; 407 } 408 return true; 409 } 410 411 onMeasureSize(selfLayoutInfo: GeometryInfo, children: Measurable[], constraint: ConstraintSizeOptions): SizeResult { 412 this.fontSize = this.getFontSizeScale(); 413 let sizeResult: SizeResult = { height: 0, width: 0 }; 414 children.forEach((child) => { 415 let childMeasureResult: MeasureResult = child.measure(constraint); 416 sizeResult.width = childMeasureResult.width; 417 sizeResult.height = childMeasureResult.height; 418 }); 419 return sizeResult; 420 } 421 422 aboutToAppear() { 423 this.refreshData(); 424 try { 425 this.isFollowSystem = this.getUIContext()?.isFollowingSystemFontScale(); 426 this.maxFontSizeScale = this.getUIContext()?.getMaxFontScale(); 427 } catch (err) { 428 let code: number = (err as BusinessError)?.code; 429 let message: string = (err as BusinessError)?.message; 430 hilog.error(0x3900, 'Ace', `Faild to toolBar getMaxFontScale, code: ${code}, message: ${message}`); 431 } 432 } 433 434 build() { 435 Column() { 436 Tabs({ controller: this.controller }) { 437 } 438 .visibility(Visibility.None) 439 Divider() 440 .width('100%').height(1) 441 .attributeModifier(this.dividerModifier) 442 Row() { 443 ForEach(this.toolBarList, (item: ToolBarOption, index: number) => { 444 if (this.toolBarList.length <= TOOLBAR_MAX_LENGTH || index < this.moreIndex) { 445 Row() { 446 this.TabBuilder(index); 447 } 448 .height('100%') 449 .flexShrink(1) 450 } 451 }) 452 if (this.refreshData() && this.toolBarList.length > TOOLBAR_MAX_LENGTH) { 453 Row() { 454 this.MoreTabBuilder(this.moreIndex); 455 } 456 .height('100%') 457 .flexShrink(1) 458 } 459 } 460 .justifyContent(FlexAlign.Center) 461 .constraintSize({ 462 minHeight: this.toLengthString(this.toolBarModifier.heightValue), 463 maxHeight: this.toLengthString(this.toolBarModifier.heightValue), 464 }) 465 .width('100%') 466 .height(this.toLengthString(this.toolBarModifier.heightValue)) 467 .padding({ 468 start: this.toolBarList.length < TOOLBAR_MAX_LENGTH ? 469 this.toolBarModifier.paddingValue : LengthMetrics.resource($r('sys.float.padding_level0')), 470 end: this.toolBarList.length < TOOLBAR_MAX_LENGTH ? 471 this.toolBarModifier.paddingValue : LengthMetrics.resource($r('sys.float.padding_level0')), 472 }) 473 } 474 .attributeModifier(this.toolBarModifier) 475 } 476} 477 478/** 479 * ToolBarDialog 480 * 481 * @since 2024-07-23 482 */ 483@CustomDialog 484struct ToolBarDialog { 485 itemDialog: ToolBarOption = { 486 icon: undefined, 487 content: '', 488 }; 489 itemSymbolModifier?: SymbolGlyphModifier; 490 mainWindowStage: window.Window | undefined = undefined; 491 controller?: CustomDialogController 492 screenWidth: number = 640; 493 verticalScreenLines: number = 6; 494 horizontalsScreenLines: number = 1; 495 cancel: () => void = () => { 496 } 497 confirm: () => void = () => { 498 } 499 @StorageLink('mainWindow') mainWindow: Promise<window.Window> | undefined = undefined; 500 @Prop fontSize: number = 1; 501 @State maxLines: number = 1; 502 @StorageProp('windowStandardHeight') windowStandardHeight: number = 0; 503 @State symbolEffect: SymbolEffect = new SymbolEffect(); 504 505 build() { 506 if (this.itemDialog.content) { 507 Column() { 508 if (this.itemDialog.toolBarSymbolOptions?.normal || 509 this.itemDialog.toolBarSymbolOptions?.activated) { 510 SymbolGlyph() 511 .attributeModifier(this.itemSymbolModifier) 512 .symbolEffect(this.symbolEffect, false) 513 .fontColor([$r('sys.color.icon_primary')]) 514 .fontSize(DIALOG_IMAGE_SIZE) 515 .margin({ 516 top: $r('sys.float.padding_level24'), 517 bottom: $r('sys.float.padding_level8'), 518 }) 519 } else { 520 Image(this.itemDialog.icon) 521 .width(DIALOG_IMAGE_SIZE) 522 .height(DIALOG_IMAGE_SIZE) 523 .margin({ 524 top: $r('sys.float.padding_level24'), 525 bottom: $r('sys.float.padding_level8'), 526 }) 527 .fillColor($r('sys.color.icon_primary')) 528 } 529 Column() { 530 Text(this.itemDialog.content) 531 .fontSize(TEXT_TOOLBAR_DIALOG) 532 .textOverflow({ overflow: TextOverflow.Ellipsis }) 533 .maxLines(this.maxLines) 534 .width('100%') 535 .textAlign(TextAlign.Center) 536 .fontColor($r('sys.color.font_primary')) 537 } 538 .width('100%') 539 .padding({ 540 left: $r('sys.float.padding_level4'), 541 right: $r('sys.float.padding_level4'), 542 bottom: $r('sys.float.padding_level12'), 543 }) 544 } 545 .width(this.fontSize === MAX_FONT_SIZE ? MAX_DIALOG : MIN_DIALOG) 546 .constraintSize({ minHeight: this.fontSize === MAX_FONT_SIZE ? MAX_DIALOG : MIN_DIALOG }) 547 .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK) 548 .shadow(ShadowStyle.OUTER_DEFAULT_LG) 549 .borderRadius(($r('sys.float.corner_radius_level10'))) 550 } else { 551 Column() { 552 if (this.itemDialog.toolBarSymbolOptions?.normal || 553 this.itemDialog.toolBarSymbolOptions?.activated) { 554 SymbolGlyph() 555 .attributeModifier(this.itemSymbolModifier) 556 .symbolEffect(this.symbolEffect, false) 557 .fontColor([$r('sys.color.icon_primary')]) 558 .fontSize(DIALOG_IMAGE_SIZE) 559 } else { 560 Image(this.itemDialog.icon) 561 .width(DIALOG_IMAGE_SIZE) 562 .height(DIALOG_IMAGE_SIZE) 563 .fillColor($r('sys.color.icon_primary')) 564 } 565 } 566 .width(this.fontSize === MAX_FONT_SIZE ? MAX_DIALOG : MIN_DIALOG) 567 .constraintSize({ minHeight: this.fontSize === MAX_FONT_SIZE ? MAX_DIALOG : MIN_DIALOG }) 568 .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK) 569 .shadow(ShadowStyle.OUTER_DEFAULT_LG) 570 .borderRadius(($r('sys.float.corner_radius_level10'))) 571 .justifyContent(FlexAlign.Center) 572 } 573 } 574 575 async aboutToAppear(): Promise<void> { 576 let context = this.getUIContext().getHostContext() as common.UIAbilityContext; 577 this.mainWindowStage = context.windowStage.getMainWindowSync(); 578 let properties: window.WindowProperties = this.mainWindowStage.getWindowProperties(); 579 let rect = properties.windowRect; 580 if (px2vp(rect.height) > this.screenWidth) { 581 this.maxLines = this.verticalScreenLines; 582 } else { 583 this.maxLines = this.horizontalsScreenLines; 584 } 585 } 586}