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 */ 15import { UIContext } from '@ohos.arkui.UIContext'; 16import { ComponentContent } from '@ohos.arkui.node'; 17import { BusinessError } from '@ohos.base'; 18import { curves } from '@kit.ArkUI'; 19 20const DIALOG_BORDER_RADIUS: Resource = $r('sys.float.ohos_id_corner_radius_default_m'); 21const DIALOG_INNER_PADDING_SIZE: number = 16; 22const DIALOG_MAX_WIDTH: number = 480; 23const DIALOG_OFFSET_X: number = 0; 24const DIALOG_OFFSET_Y_FOR_BAR: number = -88; 25const DIALOG_OFFSET_Y_FOR_NONE: number = -44; 26 27const STANDARD_MIN_COMPONENT_HEIGHT: number = 82; 28const STANDARD_MAX_COMPONENT_HEIGHT: number = 94; 29 30const DIALOG_SHADOW_RADIUS: number = 16; 31const DIALOG_SHADOW_OFFSET_Y: number = 10; 32const DIALOG_SHADOW_COLOR: ResourceStr = '#19000000'; 33 34const TITLE_LINE_DISTANCE: number = 2; 35const TITLE_MAX_LINE: number = 2; 36const SUBTITLE_MAX_LINE: number = 1; 37const TEXT_LEFT_MARGIN_SIZE: number = 16; 38const SUBTITLE_DEFAULT_COLOR: Resource = $r('sys.color.ohos_id_color_text_secondary_contrary'); 39const TITLE_DEFAULT_COLOR: Resource = $r('sys.color.ohos_id_color_text_primary_contrary'); 40 41const OPERATE_AREA_AVOID_WIDTH: number = 28; 42 43const CLOSE_ICON_DARK_RESOURCE: Resource = $r('sys.color.ohos_id_color_tertiary'); 44const CLOSE_ICON_LIGHT_RESOURCE: Resource = $r('sys.color.ohos_id_color_primary_contrary'); 45 46const CLOSE_BUTTON_BORDER_RADIUS: number = 8; 47const CLOSE_BUTTON_ICON_SIZE: number = 16; 48const CLOSE_BUTTON_HOT_SPOT_SIZE: number = 32; 49const CLOSE_BUTTON_MARGIN: number = 8; 50const CLOSE_BUTTON_ICON_OPACITY = 0.6; 51const CLOSE_BUTTON_RESPONSE_REGION_OFFSET_X: number = -8; 52const CLOSE_BUTTON_RESPONSE_REGION_OFFSET_Y: number = -8; 53const CLOSE_BUTTON_OFFSET_X: number = 0; 54const CLOSE_BUTTON_OFFSET_Y: number = -50; 55 56const FOREGROUND_IMAGE_OFFSET_X: number = 4; 57const FOREGROUND_IMAGE_OFFSET_Y: number = 0; 58 59export enum IconStyle { 60 DARK = 0, 61 LIGHT = 1 62} 63 64export enum TitlePosition { 65 TOP = 0, 66 BOTTOM = 1 67} 68 69export enum BottomOffset { 70 OFFSET_FOR_BAR = 0, 71 OFFSET_FOR_NONE = 1 72} 73 74class DialogParams { 75 public options: DialogOptions; 76 public defaultCloseAction: Callback<void>; 77 78 constructor( 79 options: DialogOptions, 80 defaultCloseAction: Callback<void>, 81 ) { 82 this.options = options; 83 this.defaultCloseAction = defaultCloseAction; 84 } 85} 86 87@Builder 88function dialogBuilder(params: DialogParams) { 89 Row() { 90 Flex() { 91 Row() { 92 SymbolGlyph($r('sys.symbol.xmark_circle_fill')) 93 .fontColor([params.options.iconStyle === IconStyle.DARK ? 94 CLOSE_ICON_DARK_RESOURCE : CLOSE_ICON_LIGHT_RESOURCE]) 95 .borderRadius(CLOSE_BUTTON_BORDER_RADIUS) 96 .width(CLOSE_BUTTON_ICON_SIZE) 97 .height(CLOSE_BUTTON_ICON_SIZE) 98 .opacity(CLOSE_BUTTON_ICON_OPACITY) 99 .draggable(false) 100 .focusable(true) 101 .responseRegion({ 102 x: CLOSE_BUTTON_RESPONSE_REGION_OFFSET_X, 103 y: CLOSE_BUTTON_RESPONSE_REGION_OFFSET_Y, 104 width: CLOSE_BUTTON_HOT_SPOT_SIZE, 105 height: CLOSE_BUTTON_HOT_SPOT_SIZE 106 }) 107 .margin(CLOSE_BUTTON_MARGIN) 108 .alignSelf(ItemAlign.End) 109 .offset({ 110 x: CLOSE_BUTTON_OFFSET_X, 111 y: CLOSE_BUTTON_OFFSET_Y 112 }) 113 .onClick(() => { 114 if (params.options.onDialogClose !== undefined) { 115 params.options.onDialogClose() 116 } 117 params.defaultCloseAction() 118 }) 119 120 Image(params.options.foregroundImage) 121 .height(STANDARD_MAX_COMPONENT_HEIGHT) 122 .objectFit(ImageFit.Contain) 123 .fitOriginalSize(true) 124 .offset({ 125 x: FOREGROUND_IMAGE_OFFSET_X, 126 y: FOREGROUND_IMAGE_OFFSET_Y 127 }) 128 .alignSelf(ItemAlign.End) 129 } 130 .padding({ left: OPERATE_AREA_AVOID_WIDTH }) 131 .direction(Direction.Rtl) 132 .defaultFocus(true) 133 .align(Alignment.End) 134 .alignSelf(ItemAlign.End) 135 .constraintSize({ 136 maxWidth: '50%', 137 minWidth: '40%' 138 }) 139 140 Flex({ 141 direction: params.options.titlePosition === TitlePosition.BOTTOM ? 142 FlexDirection.ColumnReverse : FlexDirection.Column, 143 justifyContent: FlexAlign.Center 144 }) { 145 Text(params.options.title) 146 .alignSelf(ItemAlign.Start) 147 .maxFontSize($r('sys.float.ohos_id_text_size_sub_title1')) 148 .minFontSize(16) 149 .fontColor(params.options.titleColor !== undefined ? params.options.titleColor : TITLE_DEFAULT_COLOR) 150 .fontWeight(FontWeight.Bold) 151 .margin(params.options.titlePosition ? { top: TITLE_LINE_DISTANCE } : { bottom: TITLE_LINE_DISTANCE }) 152 .maxLines(TITLE_MAX_LINE) 153 .wordBreak(WordBreak.BREAK_WORD) 154 .textOverflow({ overflow: TextOverflow.Ellipsis }) 155 Text(params.options.subtitle) 156 .alignSelf(ItemAlign.Start) 157 .maxFontSize($r('sys.float.ohos_id_text_size_caption')) 158 .minFontSize(9) 159 .fontColor(params.options.subtitleColor !== undefined ? params.options.subtitleColor : SUBTITLE_DEFAULT_COLOR) 160 .maxLines(SUBTITLE_MAX_LINE) 161 .wordBreak(WordBreak.BREAK_WORD) 162 .textOverflow({ overflow: TextOverflow.Ellipsis }) 163 } 164 .constraintSize({ 165 maxWidth: '60%', 166 minWidth: '50%' 167 }) 168 .flexGrow(1) 169 .margin({ left: TEXT_LEFT_MARGIN_SIZE }) 170 } 171 .backgroundColor(params.options.backgroundImage === undefined ? '#EBEEF5' : 'rgba(0,0,0,0)') 172 .shadow({ 173 radius: DIALOG_SHADOW_RADIUS, 174 offsetX: 0, 175 offsetY: DIALOG_SHADOW_OFFSET_Y, 176 color: DIALOG_SHADOW_COLOR 177 }) 178 .height(STANDARD_MIN_COMPONENT_HEIGHT) 179 .width('100%') 180 .alignSelf(ItemAlign.End) 181 .direction(Direction.Rtl) 182 .zIndex(1) 183 .borderRadius({ 184 topLeft: DIALOG_BORDER_RADIUS, 185 topRight: DIALOG_BORDER_RADIUS, 186 bottomLeft: DIALOG_BORDER_RADIUS, 187 bottomRight: DIALOG_BORDER_RADIUS 188 }) 189 .onClick(() => { 190 if (params.options.onDialogClick !== undefined) { 191 params.options.onDialogClick() 192 } 193 params.defaultCloseAction() 194 }) 195 196 Image(params.options.backgroundImage) 197 .width('100%') 198 .height(STANDARD_MIN_COMPONENT_HEIGHT) 199 .offset({ x: '-100%', y: 0 }) 200 .borderRadius(DIALOG_BORDER_RADIUS) 201 .zIndex(0) 202 .alignSelf(ItemAlign.End) 203 .onClick(() => { 204 if (params.options.onDialogClose !== undefined) { 205 params.options.onDialogClose() 206 } 207 params.defaultCloseAction() 208 }) 209 } 210 .backgroundColor('rgba(0,0,0,0)') 211 .width('100%') 212 .height(STANDARD_MAX_COMPONENT_HEIGHT) 213 .padding({ 214 left: DIALOG_INNER_PADDING_SIZE, 215 right: DIALOG_INNER_PADDING_SIZE 216 }) 217 .constraintSize({ 218 minHeight: STANDARD_MIN_COMPONENT_HEIGHT, 219 maxHeight: STANDARD_MAX_COMPONENT_HEIGHT, 220 maxWidth: DIALOG_MAX_WIDTH 221 }) 222} 223 224declare interface DialogOptions { 225 uiContext: UIContext, 226 bottomOffsetType?: BottomOffset, 227 title?: ResourceStr, 228 subtitle?: ResourceStr, 229 titleColor?: ResourceStr | Color, 230 subtitleColor?: ResourceStr | Color, 231 backgroundImage?: Resource, 232 foregroundImage?: Resource, 233 iconStyle?: IconStyle, 234 titlePosition?: TitlePosition, 235 onDialogClick?: Callback<void>, 236 onDialogClose?: Callback<void> 237} 238 239export class InterstitialDialogAction { 240 private uiContext: UIContext; 241 private contentNode: ComponentContent<Object>; 242 private dialogParam: DialogParams; 243 private bottomOffsetType?: BottomOffset; 244 245 constructor(dialogOptions: DialogOptions) { 246 this.uiContext = dialogOptions.uiContext; 247 this.bottomOffsetType = dialogOptions.bottomOffsetType; 248 this.dialogParam = new DialogParams( 249 dialogOptions, 250 () => { 251 this.closeDialog() 252 } 253 ); 254 this.contentNode = new ComponentContent(this.uiContext, wrapBuilder(dialogBuilder), this.dialogParam) 255 } 256 257 openDialog() { 258 if (this.contentNode !== null) { 259 this.uiContext.getPromptAction().openCustomDialog(this.contentNode, { 260 isModal: false, 261 autoCancel: false, 262 offset: { 263 dx: DIALOG_OFFSET_X, 264 dy: this.bottomOffsetType === BottomOffset.OFFSET_FOR_BAR ? 265 DIALOG_OFFSET_Y_FOR_BAR : DIALOG_OFFSET_Y_FOR_NONE 266 }, 267 alignment: DialogAlignment.Bottom, 268 transition: TransitionEffect.asymmetric( 269 TransitionEffect.OPACITY.animation({ duration: 150, curve: Curve.Sharp }) 270 .combine(TransitionEffect.scale({ x: 0.85, y: 0.85, centerX: '50%', centerY: '85%' }) 271 .animation({ curve: curves.interpolatingSpring(0, 1, 228, 24)})) 272 , 273 TransitionEffect.OPACITY.animation({ duration: 250, curve: Curve.Sharp }) 274 .combine(TransitionEffect.scale({ x: 0.85, y: 0.85, centerX: '50%', centerY: '85%' }) 275 .animation({ duration: 250, curve: Curve.Friction })) 276 ) 277 }) 278 .catch((error: BusinessError) => { 279 let message = (error as BusinessError).message 280 let code = (error as BusinessError).code 281 console.error(`${code}: ${message}`); 282 }) 283 } 284 } 285 286 closeDialog() { 287 if (this.contentNode !== null) { 288 this.uiContext.getPromptAction().closeCustomDialog(this.contentNode) 289 .catch((error: BusinessError) => { 290 let message = (error as BusinessError).message 291 let code = (error as BusinessError).code 292 console.error(`${code}: ${message}`); 293 }) 294 } 295 } 296}