1/* 2 * Copyright (c) 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 { KeyCode } from '@ohos.multimodalInput.keyCode'; 17import measure from '@ohos.measure'; 18import mediaquery from '@ohos.mediaquery'; 19import resourceManager from '@ohos.resourceManager'; 20import { ColorMetrics, LengthMetrics, LengthUnit } from '@ohos.arkui.node'; 21import EnvironmentCallback from '@ohos.app.ability.EnvironmentCallback'; 22import { SymbolGlyphModifier } from '@ohos.arkui.modifier'; 23import componentUtils from '@ohos.arkui.componentUtils'; 24import hilog from '@ohos.hilog'; 25 26export enum ChipSize { 27 NORMAL = "NORMAL", 28 SMALL = "SMALL" 29} 30 31enum BreakPointsType { 32 SM = "SM", 33 MD = "MD", 34 LG = "LG" 35} 36 37export interface IconCommonOptions { 38 src: ResourceStr; 39 size?: SizeOptions; 40 fillColor?: ResourceColor; 41 activatedFillColor?: ResourceColor; 42} 43 44export interface SuffixIconOptions extends IconCommonOptions { 45 action?: () => void; 46} 47 48export interface PrefixIconOptions extends IconCommonOptions {} 49 50export interface ChipSymbolGlyphOptions { 51 normal?: SymbolGlyphModifier; 52 activated?: SymbolGlyphModifier; 53} 54 55export interface LabelMarginOptions { 56 left?: Dimension; 57 right?: Dimension; 58} 59 60export interface LocalizedLabelMarginOptions { 61 start?: LengthMetrics; 62 end?: LengthMetrics; 63} 64 65export interface LabelOptions { 66 text: string; 67 fontSize?: Dimension; 68 fontColor?: ResourceColor; 69 activatedFontColor?: ResourceColor; 70 fontFamily?: string; 71 labelMargin?: LabelMarginOptions; 72 localizedLabelMargin?: LocalizedLabelMarginOptions; 73} 74 75interface IconTheme { 76 size: SizeOptions; 77 fillColor: ResourceColor; 78 activatedFillColor: ResourceColor; 79} 80 81interface PrefixIconTheme extends IconTheme {} 82 83interface SuffixIconTheme extends IconTheme { 84 defaultDeleteIcon: ResourceStr; 85 focusable: boolean; 86} 87 88interface DefaultSymbolTheme { 89 normalFontColor: Array<ResourceColor>; 90 activatedFontColor: Array<ResourceColor>; 91 fontSize: Length; 92 defaultEffect: number; 93} 94 95interface LabelTheme { 96 normalFontSize: Dimension; 97 smallFontSize: Dimension; 98 fontColor: ResourceColor; 99 activatedFontColor: ResourceColor; 100 fontFamily: string; 101 normalMargin: Margin; 102 localizedNormalMargin: LocalizedMargin; 103 smallMargin: Margin; 104 localizedSmallMargin: LocalizedMargin; 105 defaultFontSize: Dimension; 106} 107 108interface ChipNodeOpacity { 109 normal: number; 110 hover: number; 111 pressed: number; 112 disabled: number; 113} 114 115interface ChipNodeConstraintWidth { 116 breakPointMinWidth: number, 117 breakPointSmMaxWidth: number, 118 breakPointMdMaxWidth: number, 119 breakPointLgMaxWidth: number, 120} 121 122interface ChipNodeTheme { 123 suitAgeScale: number; 124 minLabelWidth: Dimension; 125 normalHeight: Dimension; 126 smallHeight: Dimension; 127 enabled: boolean; 128 activated: boolean; 129 backgroundColor: ResourceColor; 130 activatedBackgroundColor: ResourceColor; 131 focusOutlineColor: ResourceColor; 132 normalBorderRadius: Dimension; 133 smallBorderRadius: Dimension; 134 borderWidth: number; 135 localizedNormalPadding: LocalizedPadding; 136 localizedSmallPadding: LocalizedPadding; 137 hoverBlendColor: ResourceColor; 138 pressedBlendColor: ResourceColor; 139 opacity: ChipNodeOpacity; 140 breakPointConstraintWidth: ChipNodeConstraintWidth; 141} 142 143interface ChipTheme { 144 prefixIcon: PrefixIconTheme; 145 label: LabelTheme; 146 suffixIcon: SuffixIconTheme; 147 defaultSymbol: DefaultSymbolTheme; 148 chipNode: ChipNodeTheme; 149} 150 151export const defaultTheme: ChipTheme = { 152 prefixIcon: { 153 size: { width: 16, height: 16 }, 154 fillColor: $r('sys.color.ohos_id_color_secondary'), 155 activatedFillColor: $r('sys.color.ohos_id_color_text_primary_contrary'), 156 }, 157 label: { 158 normalFontSize: $r('sys.float.ohos_id_text_size_button2'), 159 smallFontSize: $r('sys.float.ohos_id_text_size_button2'), 160 fontColor: $r('sys.color.ohos_id_color_text_primary'), 161 activatedFontColor: $r('sys.color.ohos_id_color_text_primary_contrary'), 162 fontFamily: "HarmonyOS Sans", 163 normalMargin: { left: 6, right: 6, top: 0, bottom: 0 }, 164 smallMargin: { left: 4, right: 4, top: 0, bottom: 0 }, 165 defaultFontSize: 14, 166 localizedNormalMargin: { 167 start: LengthMetrics.vp(6), 168 end: LengthMetrics.vp(6), 169 top: LengthMetrics.vp(0), 170 bottom: LengthMetrics.vp(0) 171 }, 172 localizedSmallMargin: { 173 start: LengthMetrics.vp(4), 174 end: LengthMetrics.vp(4), 175 top: LengthMetrics.vp(0), 176 bottom: LengthMetrics.vp(0), 177 } 178 }, 179 suffixIcon: { 180 size: { width: 16, height: 16 }, 181 fillColor: $r('sys.color.ohos_id_color_secondary'), 182 activatedFillColor: $r('sys.color.ohos_id_color_text_primary_contrary'), 183 defaultDeleteIcon: $r('sys.media.ohos_ic_public_cancel', 16, 16), 184 focusable: false, 185 }, 186 defaultSymbol: { 187 normalFontColor: [$r('sys.color.ohos_id_color_secondary')], 188 activatedFontColor: [$r('sys.color.ohos_id_color_text_primary_contrary')], 189 fontSize: 16, 190 defaultEffect: -1, 191 }, 192 chipNode: { 193 suitAgeScale: 1.75, 194 minLabelWidth: 12, 195 normalHeight: 36, 196 smallHeight: 28, 197 enabled: true, 198 activated: false, 199 backgroundColor: $r('sys.color.ohos_id_color_button_normal'), 200 activatedBackgroundColor: $r('sys.color.ohos_id_color_emphasize'), 201 focusOutlineColor: $r('sys.color.ohos_id_color_focused_outline'), 202 normalBorderRadius: $r('sys.float.ohos_id_corner_radius_tips_instant_tip'), 203 smallBorderRadius: $r('sys.float.ohos_id_corner_radius_piece'), 204 borderWidth: 2, 205 localizedNormalPadding: { 206 start: LengthMetrics.vp(16), 207 end: LengthMetrics.vp(16), 208 top: LengthMetrics.vp(4), 209 bottom: LengthMetrics.vp(4) 210 }, 211 localizedSmallPadding: { 212 start: LengthMetrics.vp(12), 213 end: LengthMetrics.vp(12), 214 top: LengthMetrics.vp(4), 215 bottom: LengthMetrics.vp(4) 216 }, 217 hoverBlendColor: $r('sys.color.ohos_id_color_hover'), 218 pressedBlendColor: $r('sys.color.ohos_id_color_click_effect'), 219 opacity: { normal: 1, hover: 0.95, pressed: 0.9, disabled: 0.4 }, 220 breakPointConstraintWidth: { 221 breakPointMinWidth: 128, 222 breakPointSmMaxWidth: 156, 223 breakPointMdMaxWidth: 280, 224 breakPointLgMaxWidth: 400 225 } 226 } 227}; 228 229const noop = () => { 230}; 231 232interface ChipOptions { 233 prefixIcon?: PrefixIconOptions; 234 prefixSymbol?: ChipSymbolGlyphOptions; 235 label: LabelOptions; 236 suffixIcon?: SuffixIconOptions; 237 suffixSymbol?: ChipSymbolGlyphOptions; 238 allowClose?: boolean; 239 enabled?: boolean; 240 activated?: boolean; 241 backgroundColor?: ResourceColor; 242 activatedBackgroundColor?: ResourceColor; 243 borderRadius?: Dimension; 244 size?: ChipSize | SizeOptions; 245 direction?: Direction; 246 onClose?: () => void 247 onClicked?: () => void 248} 249 250@Builder 251export function Chip(options: ChipOptions) { 252 ChipComponent({ 253 chipSize: options.size, 254 prefixIcon: options.prefixIcon, 255 prefixSymbol: options.prefixSymbol, 256 label: options.label, 257 suffixIcon: options.suffixIcon, 258 suffixSymbol: options.suffixSymbol, 259 allowClose: options.allowClose, 260 chipEnabled: options.enabled, 261 chipActivated: options.activated, 262 chipNodeBackgroundColor: options.backgroundColor, 263 chipNodeActivatedBackgroundColor: options.activatedBackgroundColor, 264 chipNodeRadius: options.borderRadius, 265 chipDirection: options.direction, 266 onClose: options.onClose, 267 onClicked: options.onClicked, 268 }) 269} 270 271@Component 272export struct ChipComponent { 273 private theme: ChipTheme = defaultTheme; 274 @Prop chipSize: ChipSize | SizeOptions = ChipSize.NORMAL 275 @Prop allowClose: boolean = true 276 @Prop chipDirection: Direction = Direction.Auto 277 @Prop prefixIcon: PrefixIconOptions = { src: "" } 278 @Prop prefixSymbol: ChipSymbolGlyphOptions 279 @Prop label: LabelOptions = { text: "" } 280 @Prop suffixIcon: SuffixIconOptions = { src: "" } 281 @Prop suffixSymbol: ChipSymbolGlyphOptions 282 @Prop chipNodeBackgroundColor: ResourceColor = this.theme.chipNode.backgroundColor 283 @Prop chipNodeActivatedBackgroundColor: ResourceColor = this.theme.chipNode.activatedBackgroundColor 284 @Prop chipNodeRadius: Dimension | undefined = void (0) 285 @Prop chipEnabled: boolean = true 286 @Prop chipActivated: boolean = false 287 @State isHover: boolean = false 288 @State chipScale: ScaleOptions = { x: 1, y: 1 } 289 @State chipOpacity: number = 1 290 @State chipBlendColor: ResourceColor = Color.Transparent 291 @State deleteChip: boolean = false 292 @State chipNodeOnFocus: boolean = false 293 @State useDefaultSuffixIcon: boolean = false 294 private chipNodeSize: SizeOptions = {} 295 private onClose: () => void = noop 296 private onClicked: () => void = noop 297 @State suffixIconOnFocus: boolean = false 298 @State chipBreakPoints: BreakPointsType = BreakPointsType.SM 299 private smListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync("0vp<width<600vp") 300 private mdListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync("600vp<=width<840vp") 301 private lgListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync("840vp<=width") 302 @State private isShowPressedBackGroundColor: boolean = false 303 @State fontSizeScale: number | undefined = 0 304 @State fontWeightScale: number | undefined = 0 305 private callbacks: EnvironmentCallback = { 306 onConfigurationUpdated: (configuration) => { 307 this.fontSizeScale = configuration.fontSizeScale; 308 this.fontWeightScale = configuration.fontWeightScale; 309 }, onMemoryLevel() { 310 } 311 } 312 private callbackId: number | undefined = undefined 313 @State prefixSymbolWidth: Length | undefined = this.toVp(componentUtils.getRectangleById("PrefixSymbolGlyph")?.size?.width); 314 @State suffixSymbolWidth: Length | undefined = this.toVp(componentUtils.getRectangleById("SuffixSymbolGlyph")?.size?.width); 315 @State symbolEffect: SymbolEffect = new SymbolEffect(); 316 317 private isChipSizeEnum(): boolean { 318 return typeof (this.chipSize) === 'string' 319 } 320 321 private getLabelFontSize(): Dimension { 322 if (this.label?.fontSize !== void (0) && this.toVp(this.label.fontSize) >= 0) { 323 return this.label.fontSize 324 } else { 325 if (this.isChipSizeEnum() && this.chipSize === ChipSize.SMALL) { 326 try { 327 resourceManager.getSystemResourceManager() 328 .getNumberByName((((this.theme.label.smallFontSize as Resource).params as string[])[0]).split('.')[2]) 329 return this.theme.label.smallFontSize 330 } catch (error) { 331 return this.theme.label.defaultFontSize 332 } 333 } else { 334 try { 335 resourceManager.getSystemResourceManager() 336 .getNumberByName((((this.theme.label.normalFontSize as Resource).params as string[])[0]).split('.')[2]) 337 return this.theme.label.normalFontSize 338 } catch (error) { 339 return this.theme.label.defaultFontSize 340 } 341 } 342 } 343 } 344 345 private getLabelFontColor(): ResourceColor { 346 if (this.getChipActive()) { 347 return this.label?.activatedFontColor ?? this.theme.label.activatedFontColor 348 } 349 return this.label?.fontColor ?? this.theme.label.fontColor 350 } 351 352 private getLabelFontFamily(): string { 353 return this.label?.fontFamily ?? this.theme.label.fontFamily 354 } 355 356 private getLabelFontWeight(): FontWeight { 357 if (this.getChipActive()) { 358 return FontWeight.Medium 359 } 360 return FontWeight.Regular 361 } 362 363 private lengthMetricsToVp(lengthMetrics?: LengthMetrics): number { 364 let defaultValue: number = 0; 365 if (lengthMetrics) { 366 switch (lengthMetrics.unit) { 367 case LengthUnit.PX: 368 return px2vp(lengthMetrics.value) 369 case LengthUnit.VP: 370 return lengthMetrics.value 371 case LengthUnit.FP: 372 px2vp(fp2px(lengthMetrics.value)) 373 break 374 case LengthUnit.PERCENT: 375 return Number.NEGATIVE_INFINITY 376 case LengthUnit.LPX: 377 return px2vp(lpx2px(lengthMetrics.value)) 378 } 379 } 380 return defaultValue; 381 } 382 383 private toVp(value: Dimension | Length | undefined): number { 384 if (value === void (0)) { 385 return Number.NEGATIVE_INFINITY 386 } 387 switch (typeof (value)) { 388 case 'number': 389 return value as number 390 case 'object': 391 try { 392 if ((value as Resource).id !== -1) { 393 return px2vp(getContext(this).resourceManager.getNumber((value as Resource).id)) 394 } else { 395 return px2vp(getContext(this) 396 .resourceManager 397 .getNumberByName(((value.params as string[])[0]).split('.')[2])) 398 } 399 } catch (error) { 400 return Number.NEGATIVE_INFINITY 401 } 402 case 'string': 403 let regex: RegExp = new RegExp("(-?\\d+(?:\\.\\d+)?)_?(fp|vp|px|lpx|%)?$", "i"); 404 let matches: RegExpMatchArray | null = value.match(regex); 405 if (!matches) { 406 return Number.NEGATIVE_INFINITY 407 } 408 let length: number = Number(matches?.[1] ?? 0); 409 let unit: string = matches?.[2] ?? 'vp' 410 switch (unit.toLowerCase()) { 411 case 'px': 412 length = px2vp(length) 413 break 414 case 'fp': 415 length = px2vp(fp2px(length)) 416 break 417 case 'lpx': 418 length = px2vp(lpx2px(length)) 419 break 420 case '%': 421 length = Number.NEGATIVE_INFINITY 422 break 423 case 'vp': 424 break 425 default: 426 break 427 } 428 return length 429 default: 430 return Number.NEGATIVE_INFINITY 431 } 432 } 433 434 private getLabelMargin(): Margin { 435 let labelMargin: Margin = { left: 0, right: 0 } 436 if (this.label?.labelMargin?.left !== void (0) && this.toVp(this.label.labelMargin.left) >= 0) { 437 labelMargin.left = this.label?.labelMargin?.left 438 } else if ((this.prefixSymbol?.normal || this.prefixSymbol?.activated) || this.prefixIcon?.src) { 439 if (this.isChipSizeEnum() && this.chipSize == ChipSize.SMALL) { 440 labelMargin.left = this.theme.label.smallMargin.left 441 } else { 442 labelMargin.left = this.theme.label.normalMargin.left 443 } 444 } 445 if (this.label?.labelMargin?.right !== void (0) && this.toVp(this.label.labelMargin.right) >= 0) { 446 labelMargin.right = this.label?.labelMargin?.right 447 } else if ((this.suffixSymbol?.normal || this.suffixSymbol?.activated) || 448 this.suffixIcon?.src || this.useDefaultSuffixIcon) { 449 if (this.isChipSizeEnum() && this.chipSize == ChipSize.SMALL) { 450 labelMargin.right = this.theme.label.smallMargin.right 451 } else { 452 labelMargin.right = this.theme.label.normalMargin.right 453 } 454 } 455 return labelMargin 456 } 457 458 private getLocalizedLabelMargin(): LocalizedMargin { 459 let localizedLabelMargin: LocalizedMargin = { start: LengthMetrics.vp(0), end: LengthMetrics.vp(0) } 460 if (this.label?.localizedLabelMargin?.start?.value !== void (0) && 461 this.lengthMetricsToVp(this.label.localizedLabelMargin.start) >= 0) { 462 localizedLabelMargin.start = this.label?.localizedLabelMargin?.start 463 } else if ((this.prefixSymbol?.normal || this.prefixSymbol?.activated) || this.prefixIcon?.src) { 464 if (this.isChipSizeEnum() && this.chipSize == ChipSize.SMALL) { 465 localizedLabelMargin.start = this.theme.label.localizedSmallMargin.start 466 } else { 467 localizedLabelMargin.start = this.theme.label.localizedNormalMargin.start 468 } 469 } 470 if (this.label?.localizedLabelMargin?.end?.value !== void (0) && 471 this.lengthMetricsToVp(this.label.localizedLabelMargin.end) >= 0) { 472 localizedLabelMargin.end = this.label?.localizedLabelMargin?.end 473 } else if ((this.suffixSymbol?.normal || this.suffixSymbol?.activated) || 474 this.suffixIcon?.src || this.useDefaultSuffixIcon) { 475 if (this.isChipSizeEnum() && this.chipSize == ChipSize.SMALL) { 476 localizedLabelMargin.end = this.theme.label.localizedSmallMargin.end 477 } else { 478 localizedLabelMargin.end = this.theme.label.localizedNormalMargin.end 479 } 480 } 481 return localizedLabelMargin 482 } 483 484 private getLabelStartEndVp(): LocalizedMargin { 485 let labelMargin: LocalizedMargin = this.getLocalizedLabelMargin() 486 if (this.label && (this.label.labelMargin !== void (0)) && (this.label.localizedLabelMargin === void (0))) { 487 let margin: Margin = this.getLabelMargin() 488 return { 489 start: LengthMetrics.vp(this.toVp(margin.left)), 490 end: LengthMetrics.vp(this.toVp(margin.right)) 491 } 492 } 493 return { 494 start: LengthMetrics.vp(this.lengthMetricsToVp(labelMargin.start)), 495 end: LengthMetrics.vp(this.lengthMetricsToVp(labelMargin.end)) 496 } 497 } 498 499 private getActualLabelMargin(): Margin | LocalizedMargin { 500 let localizedLabelMargin: LocalizedMargin = this.getLocalizedLabelMargin() 501 if (this.label && this.label.localizedLabelMargin !== void (0)) { 502 return localizedLabelMargin 503 } 504 if (this.label && this.label.labelMargin !== void (0)) { 505 return this.getLabelMargin() 506 } 507 return localizedLabelMargin 508 } 509 510 private getSuffixIconSize(): SizeOptions { 511 let suffixIconSize: SizeOptions = { width: 0, height: 0 } 512 if (this.suffixIcon?.size?.width !== void (0) && this.toVp(this.suffixIcon?.size?.width) >= 0) { 513 suffixIconSize.width = this.suffixIcon?.size?.width 514 } else { 515 if (this.getSuffixIconSrc()) { 516 suffixIconSize.width = this.theme.suffixIcon.size.width 517 } else { 518 suffixIconSize.width = 0 519 } 520 } 521 if (this.suffixIcon?.size?.height !== void (0) && this.toVp(this.suffixIcon?.size?.height) >= 0) { 522 suffixIconSize.height = this.suffixIcon?.size?.height 523 } else { 524 if (this.getSuffixIconSrc()) { 525 suffixIconSize.height = this.theme.suffixIcon.size.height 526 } else { 527 suffixIconSize.height = 0 528 } 529 } 530 return suffixIconSize 531 } 532 533 private getPrefixIconSize(): SizeOptions { 534 let prefixIconSize: SizeOptions = { width: 0, height: 0 } 535 if (this.prefixIcon?.size?.width !== void (0) && this.toVp(this.prefixIcon?.size?.width) >= 0) { 536 prefixIconSize.width = this.prefixIcon?.size?.width 537 } else { 538 if (this.prefixIcon?.src) { 539 prefixIconSize.width = this.theme.prefixIcon.size.width 540 } else { 541 prefixIconSize.width = 0 542 } 543 } 544 if (this.prefixIcon?.size?.height !== void (0) && this.toVp(this.prefixIcon?.size?.height) >= 0) { 545 prefixIconSize.height = this.prefixIcon?.size?.height 546 } else { 547 if (this.prefixIcon?.src) { 548 prefixIconSize.height = this.theme.prefixIcon.size.height 549 } else { 550 prefixIconSize.height = 0 551 } 552 } 553 return prefixIconSize 554 } 555 556 private getPrefixIconFilledColor(): ResourceColor { 557 if (this.getChipActive()) { 558 return this.prefixIcon?.activatedFillColor ?? this.theme.prefixIcon.activatedFillColor 559 } 560 return this.prefixIcon?.fillColor ?? this.theme.prefixIcon.fillColor 561 } 562 563 private getSuffixIconFilledColor(): ResourceColor { 564 if (this.getChipActive()) { 565 return this.suffixIcon?.activatedFillColor ?? this.theme.suffixIcon.activatedFillColor 566 } 567 return this.suffixIcon?.fillColor ?? this.theme.suffixIcon.fillColor 568 } 569 570 private getDefaultSymbolColor(): Array<ResourceColor> { 571 if (this.getChipActive()) { 572 return this.theme.defaultSymbol.activatedFontColor 573 } 574 return this.theme.defaultSymbol.normalFontColor 575 } 576 577 private getPrefixSymbolModifier(): SymbolGlyphModifier | undefined { 578 if (this.getChipActive()) { 579 return this.prefixSymbol?.activated 580 } 581 return this.prefixSymbol?.normal 582 } 583 584 private getSuffixSymbolModifier(): SymbolGlyphModifier | undefined { 585 if (this.getChipActive()) { 586 return this.suffixSymbol?.activated 587 } 588 return this.suffixSymbol?.normal 589 } 590 591 private getSuffixIconFocusable(): boolean { 592 return (this.useDefaultSuffixIcon && (this.allowClose ?? true)) || this.suffixIcon?.action !== void (0) 593 } 594 595 private getChipNodePadding(): LocalizedPadding { 596 return (this.isChipSizeEnum() && this.chipSize === ChipSize.SMALL) ? this.theme.chipNode.localizedSmallPadding : this.theme.chipNode.localizedNormalPadding 597 } 598 599 private getChipNodeRadius(): Dimension { 600 if (this.chipNodeRadius !== void (0) && this.toVp(this.chipNodeRadius) >= 0) { 601 return this.chipNodeRadius as Dimension 602 } else { 603 return ((this.isChipSizeEnum() && this.chipSize === ChipSize.SMALL) ? 604 this.theme.chipNode.smallBorderRadius : this.theme.chipNode.normalBorderRadius) 605 } 606 } 607 608 private getChipNodeBackGroundColor(): ResourceColor { 609 let currentColor: ResourceColor; 610 611 if (this.getChipActive()) { 612 currentColor = this.chipNodeActivatedBackgroundColor ?? this.theme.chipNode.activatedBackgroundColor 613 } else { 614 currentColor = this.chipNodeBackgroundColor ?? this.theme.chipNode.backgroundColor 615 } 616 617 let sourceColor: ColorMetrics; 618 619 try { 620 sourceColor = ColorMetrics.resourceColor(currentColor); 621 } catch (err) { 622 hilog.error(0x3900, 'Ace', `Chip resourceColor, error: ${err.toString()}`); 623 sourceColor = ColorMetrics.resourceColor(Color.Transparent); 624 } 625 if (!this.isShowPressedBackGroundColor) { 626 return sourceColor.color 627 } 628 629 return sourceColor 630 .blendColor(ColorMetrics.resourceColor("#19000000")) 631 .color 632 } 633 634 private getChipNodeHeight(): Length { 635 if (this.isChipSizeEnum()) { 636 return this.chipSize === ChipSize.SMALL ? this.theme.chipNode.smallHeight : this.theme.chipNode.normalHeight 637 } else { 638 this.chipNodeSize = this.chipSize as SizeOptions 639 return (this.chipNodeSize?.height !== void (0) && this.toVp(this.chipNodeSize?.height) >= 0) ? 640 this.toVp(this.chipNodeSize?.height) : this.theme.chipNode.normalHeight 641 } 642 } 643 644 private getLabelWidth(): number { 645 return px2vp(measure.measureText({ 646 textContent: this.label?.text ?? "", 647 fontSize: this.getLabelFontSize(), 648 fontFamily: this.label?.fontFamily ?? this.theme.label.fontFamily, 649 fontWeight: this.getLabelFontWeight(), 650 maxLines: 1, 651 overflow: TextOverflow.Ellipsis, 652 textAlign: TextAlign.Center 653 })) 654 } 655 656 private getCalculateChipNodeWidth(): number { 657 let calWidth: number = 0 658 let startEndVp: LocalizedMargin = this.getLabelStartEndVp() 659 calWidth += this.getChipNodePadding().start?.value ?? 0 660 calWidth += this.toVp(this.getPrefixChipWidth()) 661 calWidth += this.toVp(startEndVp.start?.value ?? 0) 662 calWidth += this.getLabelWidth() 663 calWidth += this.toVp(startEndVp.end?.value ?? 0) 664 calWidth += this.toVp(this.getSuffixChipWidth()) 665 calWidth += this.getChipNodePadding().end?.value ?? 0 666 return calWidth 667 } 668 669 private getPrefixChipWidth(): Length | undefined { 670 if (this.prefixSymbol?.normal || this.prefixSymbol?.activated) { 671 return this.prefixSymbolWidth 672 } else if (this.prefixIcon?.src) { 673 return this.getPrefixIconSize().width 674 } else { 675 return 0 676 } 677 } 678 679 private getSuffixChipWidth(): Length | undefined { 680 if (this.suffixSymbol?.normal || this.suffixSymbol?.activated) { 681 return this.suffixSymbolWidth 682 } else if (this.suffixIcon?.src) { 683 return this.getSuffixIconSize().width 684 } else if (!this.suffixIcon?.src && (this.allowClose ?? true)) { 685 return this.theme.defaultSymbol.fontSize 686 } else { 687 return 0 688 } 689 } 690 691 private getReserveChipNodeWidth(): number { 692 return this.getCalculateChipNodeWidth() - this.getLabelWidth() + (this.theme.chipNode.minLabelWidth as number) 693 } 694 695 private getChipEnable(): boolean { 696 return this.chipEnabled || this.chipEnabled === void (0) 697 } 698 699 private getChipActive(): boolean { 700 return this.chipActivated 701 } 702 703 private getChipNodeOpacity(): number { 704 return this.getChipEnable() ? this.chipOpacity : this.theme.chipNode.opacity.disabled 705 } 706 707 private handleTouch(event: TouchEvent) { 708 if (!this.getChipEnable()) { 709 return 710 } 711 if (this.isHover) { 712 if (event.type === TouchType.Down || event.type === TouchType.Move) { 713 this.isShowPressedBackGroundColor = true 714 } else if (event.type === TouchType.Up) { 715 this.isShowPressedBackGroundColor = false 716 } else { 717 this.isShowPressedBackGroundColor = false 718 } 719 } else { 720 if (event.type === TouchType.Down || event.type === TouchType.Move) { 721 this.isShowPressedBackGroundColor = true 722 } else if (event.type === TouchType.Up) { 723 this.isShowPressedBackGroundColor = false 724 } else { 725 this.isShowPressedBackGroundColor = false 726 } 727 } 728 } 729 730 private hoverAnimate(isHover: boolean) { 731 if (!this.getChipEnable()) { 732 return 733 } 734 this.isHover = isHover 735 if (this.isHover) { 736 this.isShowPressedBackGroundColor = true 737 } else { 738 this.isShowPressedBackGroundColor = false 739 } 740 } 741 742 private deleteChipNodeAnimate() { 743 animateTo({ duration: 150, curve: Curve.Sharp }, () => { 744 this.chipOpacity = 0 745 this.chipBlendColor = Color.Transparent 746 }) 747 animateTo({ 748 duration: 150, curve: Curve.FastOutLinearIn, onFinish: () => { 749 this.deleteChip = true 750 } 751 }, 752 () => { 753 this.chipScale = { x: 0.85, y: 0.85 } 754 }) 755 } 756 757 private getSuffixIconSrc(): ResourceStr | undefined { 758 this.useDefaultSuffixIcon = !this.suffixIcon?.src && (this.allowClose ?? true) 759 return this.useDefaultSuffixIcon ? this.theme.suffixIcon.defaultDeleteIcon : (this.suffixIcon?.src ?? void (0)) 760 } 761 762 private getChipNodeWidth(): Length { 763 if (!this.isChipSizeEnum()) { 764 this.chipNodeSize = this.chipSize as SizeOptions 765 if (this.chipNodeSize?.width !== void (0) && this.toVp(this.chipNodeSize.width) >= 0) { 766 return this.toVp(this.chipNodeSize.width) 767 } 768 } 769 let constraintWidth: ConstraintSizeOptions = this.getChipConstraintWidth() 770 return Math.min(Math.max(this.getCalculateChipNodeWidth(), 771 constraintWidth.minWidth as number), constraintWidth.maxWidth as number); 772 } 773 774 private getFocusOverlaySize(): SizeOptions { 775 return { 776 width: Math.max(this.getChipNodeWidth() as number, this.getChipConstraintWidth().minWidth as number) + 8, 777 height: this.getChipNodeHeight() as number + 8 778 } 779 } 780 781 private getChipConstraintWidth(): ConstraintSizeOptions { 782 let calcMinWidth: number = this.getReserveChipNodeWidth() 783 784 let constraintWidth: number = this.getCalculateChipNodeWidth() 785 let constraintSize: ConstraintSizeOptions 786 switch (this.chipBreakPoints) { 787 case BreakPointsType.SM: 788 constraintSize = { 789 minWidth: calcMinWidth, 790 maxWidth: Math.min(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointSmMaxWidth) 791 } 792 break 793 case BreakPointsType.MD: 794 constraintSize = { 795 minWidth: Math.max(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointMinWidth), 796 maxWidth: Math.min(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointMdMaxWidth) 797 } 798 break 799 case BreakPointsType.LG: 800 constraintSize = { 801 minWidth: Math.max(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointMinWidth), 802 maxWidth: Math.min(constraintWidth, this.theme.chipNode.breakPointConstraintWidth.breakPointLgMaxWidth) 803 } 804 break 805 default: 806 constraintSize = { minWidth: calcMinWidth, maxWidth: constraintWidth } 807 break 808 } 809 constraintSize.minWidth = Math.min(Math.max(this.getCalculateChipNodeWidth(), 810 constraintSize.minWidth as number), constraintSize.maxWidth as number) 811 constraintSize.minHeight = this.getChipNodeHeight() 812 if (!this.isChipSizeEnum() && this.chipNodeSize?.height !== void (0) && this.toVp(this.chipNodeSize?.height) >= 0) { 813 constraintSize.maxHeight = this.toVp(this.chipNodeSize.height) 814 constraintSize.minHeight = this.toVp(this.chipNodeSize.height) 815 } 816 if (!this.isChipSizeEnum() && this.chipNodeSize?.width !== void (0) && this.toVp(this.chipNodeSize?.width) >= 0) { 817 constraintSize.minWidth = this.toVp(this.chipNodeSize.width) 818 constraintSize.maxWidth = this.toVp(this.chipNodeSize.width) 819 } else if (this.toVp(this.fontSizeScale) >= this.theme.chipNode.suitAgeScale) { 820 constraintSize.minWidth = void (0) 821 constraintSize.maxWidth = void (0) 822 } 823 return constraintSize 824 } 825 826 @Builder 827 focusOverlay() { 828 Stack() { 829 if (this.chipNodeOnFocus && !this.suffixIconOnFocus) { 830 Stack() 831 .direction(this.chipDirection) 832 .borderRadius(this.toVp(this.getChipNodeRadius()) + 4) 833 .size(this.getFocusOverlaySize()) 834 .borderColor(this.theme.chipNode.focusOutlineColor) 835 .borderWidth(this.theme.chipNode.borderWidth) 836 } 837 } 838 .direction(this.chipDirection) 839 .size({ width: 1, height: 1 }) 840 .align(Alignment.Center) 841 } 842 843 @Styles 844 suffixIconFocusStyles() { 845 .borderColor(this.theme.chipNode.focusOutlineColor) 846 .borderWidth(this.getSuffixIconFocusable() ? this.theme.chipNode.borderWidth : 0) 847 } 848 849 @Styles 850 suffixIconNormalStyles() { 851 .borderColor(Color.Transparent) 852 .borderWidth(0) 853 } 854 855 aboutToAppear() { 856 this.smListener.on("change", (mediaQueryResult: mediaquery.MediaQueryResult) => { 857 if (mediaQueryResult.matches) { 858 this.chipBreakPoints = BreakPointsType.SM 859 } 860 }) 861 this.mdListener.on("change", (mediaQueryResult: mediaquery.MediaQueryResult) => { 862 if (mediaQueryResult.matches) { 863 this.chipBreakPoints = BreakPointsType.MD 864 } 865 }) 866 this.lgListener.on("change", (mediaQueryResult: mediaquery.MediaQueryResult) => { 867 if (mediaQueryResult.matches) { 868 this.chipBreakPoints = BreakPointsType.LG 869 } 870 }) 871 this.callbackId = this.getUIContext() 872 .getHostContext() 873 ?.getApplicationContext() 874 ?.on('environment', this.callbacks); 875 } 876 877 private getVisibility(): Visibility { 878 if (this.toVp(this.getChipNodeHeight()) > 0) { 879 return Visibility.Visible 880 } else { 881 return Visibility.None 882 } 883 } 884 885 aboutToDisappear() { 886 this.smListener.off("change") 887 this.mdListener.off("change") 888 this.lgListener.off("change") 889 if (this.callbackId) { 890 this.getUIContext() 891 .getHostContext() 892 ?.getApplicationContext() 893 ?.off('environment', this.callbackId); 894 this.callbackId = void (0) 895 } 896 } 897 898 @Builder 899 chipBuilder() { 900 Button() { 901 Row() { 902 if (this.prefixSymbol?.normal || this.prefixSymbol?.activated) { 903 SymbolGlyph() 904 .fontSize(this.theme.defaultSymbol.fontSize) 905 .fontColor(this.getDefaultSymbolColor()) 906 .attributeModifier(this.getPrefixSymbolModifier()) 907 .effectStrategy(SymbolEffectStrategy.NONE) 908 .symbolEffect(this.symbolEffect, false) 909 .symbolEffect(this.symbolEffect, this.theme.defaultSymbol.defaultEffect) 910 .onSizeChange((oldValue, newValue) => { 911 this.prefixSymbolWidth = newValue?.width 912 }) 913 .key("PrefixSymbolGlyph") 914 } else if (this.prefixIcon?.src !== "") { 915 Image(this.prefixIcon?.src) 916 .direction(this.chipDirection) 917 .opacity(this.getChipNodeOpacity()) 918 .size(this.getPrefixIconSize()) 919 .fillColor(this.getPrefixIconFilledColor()) 920 .enabled(this.getChipEnable()) 921 .objectFit(ImageFit.Cover) 922 .focusable(false) 923 .flexShrink(0) 924 .visibility(this.getVisibility()) 925 .draggable(false) 926 } 927 928 Text(this.label?.text ?? "") 929 .direction(this.chipDirection) 930 .opacity(this.getChipNodeOpacity()) 931 .fontSize(this.getLabelFontSize()) 932 .fontColor(this.getLabelFontColor()) 933 .fontFamily(this.getLabelFontFamily()) 934 .fontWeight(this.getLabelFontWeight()) 935 .margin(this.getActualLabelMargin()) 936 .enabled(this.getChipEnable()) 937 .maxLines(1) 938 .textOverflow({ overflow: TextOverflow.Ellipsis }) 939 .flexShrink(1) 940 .focusable(true) 941 .textAlign(TextAlign.Center) 942 .visibility(this.getVisibility()) 943 .draggable(false) 944 945 if (this.suffixSymbol?.normal || this.suffixSymbol?.activated) { 946 SymbolGlyph() 947 .fontSize(this.theme.defaultSymbol.fontSize) 948 .fontColor(this.getDefaultSymbolColor()) 949 .attributeModifier(this.getSuffixSymbolModifier()) 950 .effectStrategy(SymbolEffectStrategy.NONE) 951 .symbolEffect(this.symbolEffect, false) 952 .symbolEffect(this.symbolEffect, this.theme.defaultSymbol.defaultEffect) 953 .onSizeChange((oldValue, newValue) => { 954 this.suffixSymbolWidth = newValue?.width 955 }) 956 .key("SuffixSymbolGlyph") 957 } else if (this.suffixIcon?.src !== "") { 958 Image(this.getSuffixIconSrc()) 959 .direction(this.chipDirection) 960 .opacity(this.getChipNodeOpacity()) 961 .size(this.getSuffixIconSize()) 962 .fillColor(this.getSuffixIconFilledColor()) 963 .enabled(this.getChipEnable()) 964 .focusable(this.getSuffixIconFocusable()) 965 .objectFit(ImageFit.Cover) 966 .flexShrink(0) 967 .visibility(this.getVisibility()) 968 .draggable(false) 969 .onFocus(() => { 970 this.suffixIconOnFocus = true 971 }) 972 .onBlur(() => { 973 this.suffixIconOnFocus = false 974 }) 975 .onClick(() => { 976 if (!this.getChipEnable()) { 977 return 978 } 979 if (this.suffixIcon?.action) { 980 this.suffixIcon.action() 981 return 982 } 983 if ((this.allowClose ?? true) && this.useDefaultSuffixIcon) { 984 this.onClose() 985 this.deleteChipNodeAnimate() 986 return 987 } 988 this.onClicked() 989 }) 990 } else if (this.allowClose ?? true) { 991 SymbolGlyph($r('sys.symbol.xmark')) 992 .fontSize(this.theme.defaultSymbol.fontSize) 993 .fontColor(this.getDefaultSymbolColor()) 994 .onClick(() => { 995 if (!this.getChipEnable()) { 996 return 997 } 998 this.onClose() 999 this.deleteChipNodeAnimate() 1000 }) 1001 } 1002 1003 } 1004 .direction(this.chipDirection) 1005 .alignItems(VerticalAlign.Center) 1006 .justifyContent(FlexAlign.Center) 1007 .padding(this.getChipNodePadding()) 1008 .constraintSize(this.getChipConstraintWidth()) 1009 } 1010 .constraintSize(this.getChipConstraintWidth()) 1011 .direction(this.chipDirection) 1012 .type(ButtonType.Normal) 1013 .clip(false) 1014 .backgroundColor(this.getChipNodeBackGroundColor()) 1015 .borderRadius(this.getChipNodeRadius()) 1016 .enabled(this.getChipEnable()) 1017 .scale(this.chipScale) 1018 .focusable(true) 1019 .opacity(this.getChipNodeOpacity()) 1020 .onFocus(() => { 1021 this.chipNodeOnFocus = true 1022 }) 1023 .onBlur(() => { 1024 this.chipNodeOnFocus = false 1025 }) 1026 .onTouch((event) => { 1027 this.handleTouch(event) 1028 }) 1029 .onHover((isHover: boolean) => { 1030 if (isHover) { 1031 this.isShowPressedBackGroundColor = true 1032 } else { 1033 if (!this.isShowPressedBackGroundColor && isHover) { 1034 this.isShowPressedBackGroundColor = true 1035 } else { 1036 this.isShowPressedBackGroundColor = false 1037 } 1038 } 1039 }) 1040 .onKeyEvent((event) => { 1041 if (event.type === KeyType.Down && event.keyCode === KeyCode.KEYCODE_FORWARD_DEL && !this.suffixIconOnFocus) { 1042 this.deleteChipNodeAnimate() 1043 } 1044 }) 1045 .onClick(this.onClicked === noop ? undefined : this.onClicked.bind(this)) 1046 } 1047 1048 build() { 1049 if (!this.deleteChip) { 1050 this.chipBuilder() 1051 } 1052 } 1053} 1054