1/* 2 * Copyright (c) 2023-2025 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 */ 15import curves from '@ohos.curves'; 16import { common, EnvironmentCallback } from '@kit.AbilityKit'; 17import { BusinessError } from '@kit.BasicServicesKit'; 18import { hilog } from '@kit.PerformanceAnalysisKit'; 19import { LengthMetrics, SymbolGlyphModifier } from '@kit.ArkUI'; 20import { i18n } from '@kit.LocalizationKit'; 21 22const START_TIME = 250; 23const END_TIME = 200; 24const BORDER_RADIUS = 12; 25const ZINDEX_NUM = 9; 26const SYMBOL_SIZE: number = 24; 27const MAX_SYMBOL_FONT_SCALE: number = 2; 28const MIN_SYMBOL_FONT_SCALE: number = 1; 29const DEFAULT_SYMBOL_FONT_SCALE: number = 1; 30 31export enum MarginType { 32 DEFAULT_MARGIN = 0, 33 FIT_MARGIN = 1, 34} 35 36export interface PromptOptions { 37 icon?: ResourceStr, 38 symbolStyle?: SymbolGlyphModifier, 39 tip?: ResourceStr, 40 marginType: MarginType, 41 actionText?: ResourceStr, 42 marginTop: Dimension, 43 isShown?: boolean 44} 45 46@Component 47export struct ExceptionPrompt { 48 @Prop options: PromptOptions 49 @State fontSizeScale: number | undefined = undefined; 50 touchBackgroundColor: Resource = $r('sys.color.ohos_id_color_sub_background_transparent') 51 maxAppFontScale: number = 1; 52 isFollowingSystemFontScale: boolean = false; 53 onTipClick?: () => void = undefined; 54 onActionTextClick?: () => void = undefined; 55 private callbackId: number | undefined = undefined; 56 private callbacks: EnvironmentCallback = { 57 onConfigurationUpdated: (config) => { 58 this.fontSizeScale = Math.min(this.updateFontScale(), MAX_SYMBOL_FONT_SCALE); 59 this.fontSizeScale = Math.max(this.fontSizeScale, MIN_SYMBOL_FONT_SCALE); 60 }, onMemoryLevel() { 61 } 62 }; 63 64 @Builder 65 TextBuilder() { 66 Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { 67 Row() { 68 if (this.options?.symbolStyle !== undefined) { 69 SymbolGlyph() 70 .fontColor([$r('sys.color.ohos_id_color_warning')]) 71 .attributeModifier(this.options?.symbolStyle) 72 .effectStrategy(SymbolEffectStrategy.NONE) 73 .symbolEffect(new SymbolEffect(), false) 74 .fontSize(`${(this.fontSizeScale ?? DEFAULT_SYMBOL_FONT_SCALE) * SYMBOL_SIZE}vp`) 75 } else { 76 if (Util.isSymbolResource(this.options?.icon)) { 77 SymbolGlyph((this.options?.icon as Resource | undefined) ?? $r('sys.symbol.exclamationmark_circle')) 78 .fontColor([$r('sys.color.ohos_id_color_warning')]) 79 .fontSize(`${(this.fontSizeScale ?? DEFAULT_SYMBOL_FONT_SCALE) * SYMBOL_SIZE}vp`) 80 } else { 81 Image(this.options?.icon) 82 .width('24vp') 83 .height('24vp') 84 .fillColor($r('sys.color.ohos_id_color_warning')) 85 } 86 } 87 Text(this.options.tip) 88 .fontSize($r('sys.float.ohos_id_text_size_body1')) 89 .minFontScale(1) 90 .maxFontScale(Math.min(this.updateFontScale(), 2)) 91 .fontColor($r('sys.color.ohos_id_color_warning')) 92 .textOverflow({ overflow: TextOverflow.Ellipsis }) 93 .maxLines(2) 94 .margin({ start: LengthMetrics.resource($r('sys.float.ohos_id_dialog_margin_end')) }) 95 .flexShrink(1) 96 .direction(i18n.isRTL(i18n.System.getSystemLanguage()) ? Direction.Rtl : Direction.Ltr) 97 } 98 .padding({ right: $r('sys.float.ohos_id_default_padding_end') }) 99 .width('100%') 100 .accessibilityDescription(this.onTipClick ? '' : ' ') 101 .onClick(() => { 102 this.onTipClick && this.onTipClick(); 103 }) 104 105 if (this.options.actionText) { 106 Button({ stateEffect: false, type: ButtonType.Normal }) { 107 Row() { 108 Text(this.options.actionText) 109 .fontSize($r('sys.float.ohos_id_text_size_body2')) 110 .minFontScale(1) 111 .maxFontScale(Math.min(this.updateFontScale(), 2)) 112 .fontColor($r('sys.color.ohos_id_color_text_secondary')) 113 .maxLines(2) 114 .padding(0) 115 .margin({ end: LengthMetrics.resource($r('sys.float.ohos_id_text_paragraph_margin_s')) }) 116 .textOverflow({ overflow: TextOverflow.Ellipsis }) 117 .flexShrink(1) 118 .textAlign(TextAlign.End) 119 .direction(i18n.isRTL(i18n.System.getSystemLanguage()) ? Direction.Rtl : Direction.Ltr) 120 SymbolGlyph($r('sys.symbol.chevron_right')) 121 .fontSize(`${(this.fontSizeScale ?? DEFAULT_SYMBOL_FONT_SCALE) * SYMBOL_SIZE}vp`) 122 .fontColor([$r('sys.color.ohos_id_color_tertiary')]) 123 } 124 .width('100%') 125 .justifyContent(FlexAlign.End) 126 } 127 .backgroundColor(this.touchBackgroundColor) 128 .width(this.options.actionText ? 144 : 0) 129 .borderRadius($r('sys.float.ohos_id_corner_radius_subtab')) 130 .padding({ 131 right: $r('sys.float.padding_level2'), 132 }) 133 .accessibilityDescription(this.onActionTextClick ? '' : ' ') 134 .accessibilityRole(this.onActionTextClick ? AccessibilityRoleType.BUTTON : AccessibilityRoleType.ROLE_NONE) 135 .onClick(() => { 136 this.onActionTextClick && this.onActionTextClick(); 137 }) 138 } 139 } 140 .padding({ 141 left: $r('sys.float.ohos_id_notification_margin_start'), 142 right: $r('sys.float.ohos_id_text_paragraph_margin_s'), 143 top: $r('sys.float.ohos_id_default_padding_start'), 144 bottom: $r('sys.float.ohos_id_default_padding_end') 145 }) 146 } 147 148 build() { 149 Row() { 150 Column() { 151 Column() { 152 this.TextBuilder() 153 } 154 .width('100%') 155 .borderRadius(BORDER_RADIUS) 156 .backgroundColor($r('sys.color.comp_background_warning_secondary')) 157 .zIndex(ZINDEX_NUM) 158 } 159 .padding(this.options.marginType === MarginType.DEFAULT_MARGIN ? { 160 left: $r('sys.float.ohos_id_card_margin_start'), 161 right: $r('sys.float.ohos_id_card_margin_end') 162 } : { 163 left: $r('sys.float.ohos_id_max_padding_start'), 164 right: $r('sys.float.ohos_id_max_padding_end') 165 }) 166 .transition( 167 TransitionEffect.OPACITY.animation({ 168 curve: curves.cubicBezierCurve(0.33, 0, 0.67, 1), 169 duration: this.options.isShown ? START_TIME : END_TIME 170 }) 171 ) 172 .visibility(this.options.isShown ? Visibility.Visible : Visibility.None) 173 } 174 .width('100%') 175 .position({ y: this.options.marginTop }) 176 .zIndex(ZINDEX_NUM) 177 } 178 179 aboutToAppear() { 180 try { 181 let uiContent: UIContext = this.getUIContext(); 182 this.isFollowingSystemFontScale = uiContent.isFollowingSystemFontScale(); 183 this.maxAppFontScale = uiContent.getMaxFontScale(); 184 this.fontSizeScale = Math.min(this.updateFontScale(), MAX_SYMBOL_FONT_SCALE); 185 this.fontSizeScale = Math.max(this.fontSizeScale, MIN_SYMBOL_FONT_SCALE); 186 this.callbackId = uiContent.getHostContext()?.getApplicationContext()?.on('environment', this.callbacks); 187 } catch (err) { 188 let code: number = (err as BusinessError).code; 189 let message: string = (err as BusinessError).message; 190 hilog.error(0x3900, 'Ace', `Failed to init fontsizescale info, cause, code: ${code}, message: ${message}`); 191 } 192 } 193 194 aboutToDisappear(): void { 195 if (this.callbackId) { 196 this.getUIContext().getHostContext()?.getApplicationContext()?.off('environment', this.callbackId); 197 this.callbackId = void (0); 198 } 199 } 200 201 updateFontScale(): number { 202 let uiContent: UIContext = this.getUIContext(); 203 let systemFontScale: number = (uiContent.getHostContext() as common.UIAbilityContext)?.config?.fontSizeScale ?? 1; 204 if (!this.isFollowingSystemFontScale) { 205 return 1; 206 } 207 return Math.min(systemFontScale, this.maxAppFontScale); 208 } 209} 210 211class Util { 212 private static RESOURCE_TYPE_SYMBOL = 40000; 213 214 public static isSymbolResource(resourceStr: ResourceStr | undefined): boolean { 215 if (resourceStr === undefined) { 216 return true; 217 } 218 if (!Util.isResourceType(resourceStr)) { 219 return false; 220 } 221 let resource = resourceStr as Resource; 222 return resource.type === Util.RESOURCE_TYPE_SYMBOL; 223 } 224 225 public static isResourceType(resource: ResourceStr): boolean { 226 if (!resource) { 227 return false; 228 } 229 return typeof resource !== 'string'; 230 } 231}