1/* 2 * Copyright (c) 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 */ 15 16import { curves, display, LengthMetrics, mediaquery } from '@kit.ArkUI'; 17import { image } from '@kit.ImageKit'; 18import { BusinessError, commonEventManager } from '@kit.BasicServicesKit'; 19import { hilog } from '@kit.PerformanceAnalysisKit'; 20import { systemParameterEnhance } from '@kit.BasicServicesKit'; 21import { Animator as animator, AnimatorResult, componentUtils } from '@kit.ArkUI'; 22import { AnimatorOptions } from '@ohos.animator'; 23 24const LOG_TAG: string = 'CustomAppBar'; 25const VIEW_WIDTH: number = 80; 26const VIEW_HEIGHT: number = 36; 27const BUTTON_SIZE: number = 40; 28const IMAGE_SIZE: string = '20vp'; 29const MENU_RADIUS: string = '20vp'; 30const DIVIDER_HEIGHT: string = '16vp'; 31const BORDER_WIDTH: string = '1px'; 32const VIEW_MARGIN_RIGHT: number = 8; 33const ICON_SIZE: number = 27; 34const ICON_FILL_COLOR_DEFAULT: string = '#182431'; 35const BORDER_COLOR_DEFAULT: string = '#33000000'; 36const MENU_BACK_COLOR: string = '#99FFFFFF'; 37const MENU_BACK_BLUR: number = 5; 38const MENU_MARGIN_TOP: number = 10; 39const SM_MENU_MARGIN_END: number = 16; 40const MD_MENU_MARGIN_END: number = 24; 41const LG_MENU_MARGIN_END: number = 32; 42// 半屏参数 43const BUTTON_IMAGE_SIZE: number = 18; 44const HALF_CONTAINER_BORDER_SIZE: number = 32; 45const HALF_BUTTON_BACK_COLOR: string = '#0D000000'; 46const HALF_BUTTON_IMAGE_COLOR: string = '#0C000000'; 47const HALF_MENU_MARGIN: number = 16; 48const EYELASH_HEIGHT: number = 36; 49const CHEVRON_HEIGHT: number = 20; 50const CHEVRON_WIDTH: number = 10; 51const CHEVRON_MARGIN: number = 4; 52const TITLE_FONT_SIZE: number = 14; 53const TITLE_LINE_HEIGHT: number = 16; 54const TITLE_MARGIN_RIGHT: number = 12; 55const TITLE_MARGIN_TOP: number = 8; 56const TITLE_LABEL_MARGIN: number = 8.5; 57const TITLE_CONSTRAINT_SIZE: string = 'calc(100% - 73.5vp)'; 58const MD_WIDTH: number = 480; 59const LG_WIDTH_LIMIT: number = 0.6; 60const LG_WIDTH_HEIGHT_RATIO: number = 1.95; 61const PRIVACY_MARGIN: number = 12; 62const PRIVACY_FONT_SIZE: string = '12vp'; 63const PRIVACY_TEXT_MARGIN_START: number = 4; 64const PRIVACY_TEXT_MARGIN_END: number = 8; 65const PRIVACY_CONSTRAINT_SIZE: string = 'calc(100% - 136vp)'; 66const ARKUI_APP_BAR_COLOR_CONFIGURATION: string = 'arkui_app_bar_color_configuration'; 67const ARKUI_APP_BAR_MENU_SAFE_AREA: string = 'arkui_app_bar_menu_safe_area'; 68const ARKUI_APP_BAR_CONTENT_SAFE_AREA: string = 'arkui_app_bar_content_safe_area'; 69 70const ARKUI_APP_BAR_BAR_INFO: string = 'arkui_app_bar_info'; 71const ARKUI_APP_BAR_SCREEN: string = 'arkui_app_bar_screen'; 72const ARKUI_APP_BG_COLOR: string = 'arkui_app_bg_color'; 73const ARKUI_APP_BAR_SERVICE_PANEL: string = 'arkui_app_bar_service_panel'; 74const ARKUI_APP_BAR_CLOSE: string = 'arkui_app_bar_close'; 75const ARKUI_APP_BAR_PROVIDE_SERVICE: string = 'arkui_app_bar_provide_service'; 76const EVENT_NAME_CUSTOM_APP_BAR_MENU_CLICK = 'arkui_custom_app_bar_menu_click'; 77const EVENT_NAME_CUSTOM_APP_BAR_CLOSE_CLICK = 'arkui_custom_app_bar_close_click'; 78const EVENT_NAME_CUSTOM_APP_BAR_DID_BUILD = 'arkui_custom_app_bar_did_build'; 79const EVENT_NAME_CUSTOM_APP_BAR_CREATE_SERVICE_PANEL = 'arkui_custom_app_bar_create_service_panel'; 80const ARKUI_APP_BAR_MAXIMIZE: string = 'arkui_app_bar_maximize'; 81const ARKUI_APP_BAR_PRIVACY_AUTHORIZE: string = 'arkui_app_bar_privacy_authorize'; 82 83/** 84 * 适配不同颜色模式集合 85 */ 86class ColorGroup { 87 public light: string = '#000000'; 88 public dark: string = '#FFFFFF'; 89 90 constructor(light: string, dark: string) { 91 this.light = light; 92 this.dark = dark; 93 } 94} 95 96enum BreakPointsType { 97 NONE = 'NONE', 98 SM = 'SM', 99 MD = 'MD', 100 LG = 'LG' 101} 102 103const menuMarginEndMap: Map<BreakPointsType, number> = new Map<BreakPointsType, number>([ 104 [BreakPointsType.NONE, SM_MENU_MARGIN_END], 105 [BreakPointsType.SM, SM_MENU_MARGIN_END], 106 [BreakPointsType.MD, MD_MENU_MARGIN_END], 107 [BreakPointsType.LG, LG_MENU_MARGIN_END] 108]); 109 110const colorMap: Map<string, ColorGroup> = new Map<string, ColorGroup>([ 111 [ICON_FILL_COLOR_DEFAULT, new ColorGroup('#182431', '#e5ffffff')], 112 [BORDER_COLOR_DEFAULT, new ColorGroup('#33182431', '#4Dffffff')], 113 [MENU_BACK_COLOR, new ColorGroup('#99FFFFFF', '#33000000')], 114 [HALF_BUTTON_BACK_COLOR, new ColorGroup('#0D000000', '#19FFFFFF')], 115 [HALF_BUTTON_IMAGE_COLOR, new ColorGroup('#000000', '#FFFFFF')] 116]); 117 118@Entry 119@Component 120export struct CustomAppBar { 121 @State menuResource: Resource = { 122 bundleName: '', 123 moduleName: '', 124 params: [], 125 id: 125830217, 126 type: 20000 127 }; 128 @State closeResource: Resource = { 129 bundleName: '', 130 moduleName: '', 131 params: [], 132 id: 125831084, 133 type: 20000 134 }; 135 @State privacyResource: Resource = { 136 bundleName: '', 137 moduleName: '', 138 params: [], 139 id: 125835516, 140 type: 20000 141 }; 142 @State menuFillColor: string = this.getResourceColor(ICON_FILL_COLOR_DEFAULT); 143 @State closeFillColor: string = this.getResourceColor(ICON_FILL_COLOR_DEFAULT); 144 @State menubarBorderColor: string = this.getResourceColor(BORDER_COLOR_DEFAULT); 145 @State menubarBackColor: string = this.getResourceColor(MENU_BACK_COLOR); 146 @State dividerBackgroundColor: string = this.getResourceColor(BORDER_COLOR_DEFAULT); 147 @State halfButtonBackColor: string = this.getResourceColor(HALF_BUTTON_BACK_COLOR); 148 @State halfButtonImageColor: string = this.getResourceColor(HALF_BUTTON_IMAGE_COLOR); 149 @State privacyImageColor: string = this.getResourceColor(HALF_BUTTON_IMAGE_COLOR); 150 151 @State contentMarginTop: number = 0; 152 @State contentMarginLeft: number = 0; 153 @State contentMarginRight: number = 0; 154 @State contentMarginBottom: number = 0; 155 @State menuMarginEnd: number = SM_MENU_MARGIN_END; 156 // 半屏参数 157 @State isHalfScreen: boolean = true; 158 @State containerHeight: string | number = '0%'; 159 @State containerWidth: string | number = '100%'; 160 @State stackHeight: string = '100%'; 161 @State titleOpacity: number = 0; 162 @State buttonOpacity: number = 1; 163 @State titleHeight: number = 0; 164 @State titleOffset: number = 0; 165 @State maskOpacity: number = 0; 166 @State maskBlurScale: number = 0; 167 @State contentBgColor: ResourceColor = '#FFFFFFFF'; 168 @State statusBarHeight: number = 0; 169 @State ratio: number | undefined = undefined; 170 @State @Watch('onBreakPointChange') breakPoint: BreakPointsType = BreakPointsType.NONE; 171 @State serviceMenuRead: string = this.getStringByResourceToken(ARKUI_APP_BAR_SERVICE_PANEL); 172 @State closeRead: string = this.getStringByResourceToken(ARKUI_APP_BAR_CLOSE); 173 @State maximizeRead: string = this.getStringByResourceToken(ARKUI_APP_BAR_MAXIMIZE); 174 @State provideService: string = ''; 175 @State privacyWidth: string = '0'; 176 @State privacySymbolOpacity: number = 0; 177 @State angle: string = '-90deg'; 178 @State buttonSize: number = BUTTON_SIZE; 179 @State privacyTextOpacity: number = 0; 180 @State dividerOpacity: number = 0; 181 @State isShowPrivacyAnimation: boolean = false; 182 @State privacyAuthText: string = ''; 183 private isHalfToFullScreen: boolean = false; 184 private isDark: boolean = true; 185 private bundleName: string = ''; 186 @State labelName: string = ''; 187 private isHalfScreenCompFirstLaunch: boolean = true; 188 private icon: Resource | string | PixelMap = $r('sys.media.ohos_app_icon'); 189 private fullContentMarginTop: number = 0; 190 private deviceBorderRadius: string = '0'; 191 private privacyAnimator: AnimatorResult | undefined = undefined; 192 private smListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(0vp<width) and (width<600vp)'); 193 private mdListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(600vp<=width) and (width<840vp)'); 194 private lgListener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(840vp<=width)'); 195 // 订阅者信息, 用于保存创建成功的订阅者对象,后续使用其完成订阅及退订的动作 196 private subscriber: commonEventManager.CommonEventSubscriber | null = null; 197 // 事件列表 198 private subscribeInfo: commonEventManager.CommonEventSubscribeInfo = { 199 events: ['usual.event.PRIVACY_STATE_CHANGED'] 200 }; 201 202 aboutToDisappear(): void { 203 this.smListener.off('change'); 204 this.mdListener.off('change'); 205 this.lgListener.off('change'); 206 if (this.subscriber !== null) { 207 commonEventManager.unsubscribe(this.subscriber, (err) => { 208 if (err) { 209 hilog.error(0x3900, LOG_TAG, `unsubscribe err callback, message is ${err.message}`); 210 } else { 211 this.subscriber = null; 212 } 213 }); 214 } 215 } 216 217 /** 218 * 注册监听隐私协议状态 219 */ 220 private subscribePrivacyState(): void { 221 try { 222 // 创建订阅者 223 commonEventManager.createSubscriber(this.subscribeInfo).then((commonEventSubscriber) => { 224 this.subscriber = commonEventSubscriber; 225 // 订阅公共事件 226 try { 227 commonEventManager.subscribe(this.subscriber, (err, data) => { 228 if (err) { 229 hilog.error(0x3900, LOG_TAG, `subscribe failed, code is ${err?.code}, message is ${err?.message}`); 230 return; 231 } 232 let result = JSON.parse(data?.data ?? '{}')?.resultType as number; 233 // privacyMgmtType:1 隐私同意完整模式 234 if (result === 1) { 235 if (this.isHalfScreen) { 236 return; 237 } 238 this.isShowPrivacyAnimation = true; 239 this.startPrivacyAnimation(); 240 } 241 }); 242 } catch (error) { 243 hilog.error(0x3900, LOG_TAG, `init Subscriber failed, code is ${error?.code}, message is ${error?.message}`); 244 } 245 }).catch((error: BusinessError) => { 246 hilog.error(0x3900, LOG_TAG, `createSubscriber failed, code is ${error?.code}, message is ${error?.message}`); 247 }); 248 } catch (error) { 249 hilog.error(0x3900, LOG_TAG, 250 `subscribePrivacyState failed, code is ${error?.code}, message is ${error?.message}`); 251 } 252 } 253 254 getDeviceRadiusConfig(): void { 255 try { 256 this.deviceBorderRadius = systemParameterEnhance.getSync('const.product.device_radius'); 257 hilog.info(0x3900, LOG_TAG, `read device_radius success, device_radius: ${this.deviceBorderRadius}`); 258 } catch (error) { 259 hilog.error(0x3900, LOG_TAG, `read device_radius failed`); 260 } 261 } 262 263 initBreakPointListener(): void { 264 this.smListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => { 265 if (mediaQueryResult.matches) { 266 this.breakPoint = BreakPointsType.SM; 267 } 268 }) 269 this.mdListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => { 270 if (mediaQueryResult.matches) { 271 this.breakPoint = BreakPointsType.MD; 272 } 273 }) 274 this.lgListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => { 275 if (mediaQueryResult.matches) { 276 this.breakPoint = BreakPointsType.LG; 277 } 278 }) 279 } 280 281 /** 282 * 半屏嵌入式定制使用,当半屏嵌入式组件首次被拉起或者屏幕宽度断点发生变化时被调用 283 * 被调用时更新半屏嵌入式组件的宽高比例 284 */ 285 updateRatio(): void { 286 // 屏幕断点为LG或MD时设置成直板机的宽高尺寸比,断点为SM时设置成undefined从而使控制尺寸比的字段失效 287 const isRatioBeUndefined = this.breakPoint === BreakPointsType.LG || this.breakPoint === BreakPointsType.MD; 288 this.ratio = isRatioBeUndefined ? 1 / LG_WIDTH_HEIGHT_RATIO : undefined; 289 } 290 291 onBreakPointChange(): void { 292 if (menuMarginEndMap.has(this.breakPoint)) { 293 this.menuMarginEnd = menuMarginEndMap.get(this.breakPoint) as number; 294 } 295 if (this.isHalfScreen) { 296 if (this.breakPoint === BreakPointsType.SM) { 297 this.containerWidth = '100%'; 298 } else if (this.breakPoint === BreakPointsType.MD) { 299 this.containerWidth = MD_WIDTH; 300 } else if (this.breakPoint === BreakPointsType.LG) { 301 try { 302 let displayData = display.getDefaultDisplaySync(); 303 let windowWidth = px2vp(displayData.width); 304 let windowHeight = px2vp(displayData.height); 305 this.containerWidth = windowWidth > windowHeight ? windowHeight * LG_WIDTH_LIMIT : windowWidth * LG_WIDTH_LIMIT; 306 } catch (error) { 307 hilog.error(0x3900, LOG_TAG, `getDefaultDisplaySync failed, code is ${error?.code}, message is ${error?.message}`); 308 } 309 } 310 } 311 if (!this.isHalfScreenCompFirstLaunch) { 312 this.updateRatio(); 313 } 314 } 315 316 parseBoolean(value: string): boolean { 317 if (value === 'true') { 318 return true; 319 } 320 return false; 321 } 322 323 getResourceColor(defaultColor: string): string { 324 if (colorMap.has(defaultColor)) { 325 const colorGroup = colorMap.get(defaultColor); 326 if (colorGroup) { 327 return this.isDark ? colorGroup.dark : colorGroup.light; 328 } 329 } 330 return defaultColor; 331 } 332 333 getStringByResourceToken(resName: string, value?: string): string { 334 try { 335 if (value) { 336 return getContext(this).resourceManager.getStringByNameSync(resName, value); 337 } 338 return getContext(this).resourceManager.getStringByNameSync(resName); 339 } catch (err) { 340 hilog.error(0x3900, LOG_TAG, `getAccessibilityDescription, error: ${err.toString()}`); 341 } 342 return ''; 343 } 344 345 updateStringByResource(): void { 346 if (this.isHalfScreen) { 347 this.provideService = this.getStringByResourceToken(ARKUI_APP_BAR_PROVIDE_SERVICE, this.labelName); 348 this.maximizeRead = this.getStringByResourceToken(ARKUI_APP_BAR_MAXIMIZE); 349 } 350 this.closeRead = this.getStringByResourceToken(ARKUI_APP_BAR_CLOSE); 351 this.serviceMenuRead = this.getStringByResourceToken(ARKUI_APP_BAR_SERVICE_PANEL); 352 this.privacyAuthText = this.getStringByResourceToken(ARKUI_APP_BAR_PRIVACY_AUTHORIZE); 353 } 354 355 /** 356 * atomicservice侧的事件变化回调 357 * @param eventName 事件名称 358 * @param param 事件参数 359 */ 360 setCustomCallback(eventName: string, param: string): void { 361 if (param === null || param === '' || param === undefined) { 362 hilog.error(0x3900, LOG_TAG, 'invalid params'); 363 return; 364 } 365 if (eventName === ARKUI_APP_BAR_COLOR_CONFIGURATION) { 366 this.onColorConfigurationUpdate(this.parseBoolean(param)); 367 } else if (eventName === ARKUI_APP_BAR_MENU_SAFE_AREA) { 368 if (this.statusBarHeight === px2vp(Number(param))) { 369 return; 370 } 371 this.statusBarHeight = Number(param); 372 this.titleHeight = EYELASH_HEIGHT + 2 * TITLE_MARGIN_TOP + this.statusBarHeight; 373 } else if (eventName === ARKUI_APP_BAR_CONTENT_SAFE_AREA) { 374 //top left right bottom 375 let splitArray: string[] = param.split('|'); 376 if (splitArray.length < 4) { 377 return; 378 } 379 this.contentMarginTop = this.isHalfScreen ? 0 : Number(splitArray[0]); 380 this.fullContentMarginTop = Number(splitArray[0]); 381 this.contentMarginLeft = Number(splitArray[1]); 382 this.contentMarginRight = Number(splitArray[2]); 383 this.contentMarginBottom = Number(splitArray[3]); 384 } else if (eventName === ARKUI_APP_BAR_BAR_INFO) { 385 let splitArray: string[] = param.split('|'); 386 if (splitArray.length < 2) { 387 return; 388 } 389 this.bundleName = splitArray[0]; 390 this.labelName = splitArray[1]; 391 this.updateStringByResource(); 392 } else if (eventName === ARKUI_APP_BAR_SCREEN) { 393 this.isHalfScreen = this.parseBoolean(param); 394 this.initBreakPointListener(); 395 } else if (eventName === ARKUI_APP_BG_COLOR) { 396 if (this.isHalfScreen) { 397 this.contentBgColor = Color.Transparent; 398 } else { 399 this.contentBgColor = param; 400 } 401 } 402 } 403 404 /** 405 * 颜色变化设置 406 * @param isDark 是否是深色模式 407 */ 408 onColorConfigurationUpdate(isDark: boolean): void { 409 this.isDark = isDark; 410 this.menuFillColor = this.getResourceColor(ICON_FILL_COLOR_DEFAULT); 411 this.closeFillColor = this.getResourceColor(ICON_FILL_COLOR_DEFAULT); 412 this.menubarBorderColor = this.getResourceColor(BORDER_COLOR_DEFAULT); 413 this.dividerBackgroundColor = this.getResourceColor(BORDER_COLOR_DEFAULT); 414 this.menubarBackColor = this.getResourceColor(MENU_BACK_COLOR); 415 this.halfButtonBackColor = this.getResourceColor(HALF_BUTTON_BACK_COLOR); 416 this.halfButtonImageColor = this.getResourceColor(HALF_BUTTON_IMAGE_COLOR); 417 this.privacyImageColor = this.getResourceColor(HALF_BUTTON_IMAGE_COLOR); 418 } 419 420 /** 421 * 标题栏图标回调 422 * @param pixelMap 423 */ 424 setAppIcon(pixelMap: image.PixelMap): void { 425 this.icon = pixelMap; 426 } 427 428 /** 429 * 服务面板按钮点击回调 430 */ 431 onMenuButtonClick(): void { 432 } 433 434 /** 435 * 关闭按钮点击回调 436 */ 437 onCloseButtonClick(): void { 438 } 439 440 /** 441 * 点击title栏 442 */ 443 onEyelashTitleClick(): void { 444 } 445 446 /** 447 * 触发构建回调 448 */ 449 onDidBuild(): void { 450 } 451 452 /** 453 * 半屏拉起动效 454 */ 455 halfScreenShowAnimation(): void { 456 animateTo({ 457 duration: 250, 458 curve: Curve.Sharp, 459 }, () => { 460 this.maskOpacity = 0.3; 461 this.maskBlurScale = 1; 462 }); 463 animateTo({ 464 duration: 250, 465 curve: curves.interpolatingSpring(0, 1, 328, 36), 466 }, () => { 467 this.containerHeight = '100%'; 468 this.updateRatio(); 469 }); 470 // 标题栏渐显 471 animateTo({ 472 duration: 100, 473 curve: curves.cubicBezierCurve(0.2, 0, 0.2, 1), 474 }, () => { 475 this.titleOpacity = 1; 476 }); 477 this.isHalfScreenCompFirstLaunch = false; 478 } 479 480 /** 481 * 半屏放大至全屏动效 482 */ 483 expendContainerAnimation(): void { 484 this.isHalfToFullScreen = true; 485 animateTo({ 486 duration: 150, 487 curve: curves.interpolatingSpring(0, 1, 328, 36), 488 onFinish: () => { 489 this.contentBgColor = '#FFFFFF'; 490 } 491 }, () => { 492 this.containerWidth = '100%'; 493 this.contentMarginTop = this.fullContentMarginTop; 494 this.titleOffset = -this.titleHeight; 495 this.isHalfScreen = false; 496 }); 497 // 标题栏渐隐 498 animateTo({ 499 duration: 100, 500 curve: curves.cubicBezierCurve(0.2, 0, 0.2, 1), 501 }, () => { 502 this.titleOpacity = 0; 503 }); 504 } 505 506 /** 507 * 嵌入式关闭动效 508 */ 509 closeContainerAnimation(): void { 510 if (this.isHalfScreen) { 511 this.closeHalfContainerAnimation(); 512 return; 513 } 514 if (this.isHalfToFullScreen) { 515 // 关闭弹框 516 animateTo({ 517 duration: 250, 518 curve: curves.interpolatingSpring(0, 1, 328, 36), 519 onFinish: () => { 520 this.onCloseButtonClick(); 521 } 522 }, () => { 523 this.stackHeight = '0%'; 524 }); 525 } else { 526 this.onCloseButtonClick(); 527 } 528 this.isHalfScreenCompFirstLaunch = true; 529 } 530 531 closeHalfContainerAnimation() { 532 // 关闭弹框 533 animateTo({ 534 duration: 250, 535 curve: curves.interpolatingSpring(0, 1, 328, 36), 536 onFinish: () => { 537 this.onCloseButtonClick(); 538 } 539 }, () => { 540 this.containerHeight = '0%'; 541 this.ratio = undefined; 542 }); 543 // 蒙层渐隐 544 animateTo({ 545 duration: 250, 546 curve: Curve.Sharp, 547 }, () => { 548 this.maskOpacity = 0; 549 this.maskBlurScale = 0; 550 }); 551 // 标题栏渐隐 552 animateTo({ 553 duration: 100, 554 curve: curves.cubicBezierCurve(0.2, 0, 0.2, 1), 555 }, () => { 556 this.titleOpacity = 0; 557 }); 558 } 559 560 /** 561 * 开始隐私标识动效 562 */ 563 private startPrivacyAnimation(): void { 564 animateTo({ 565 curve: curves.interpolatingSpring(2, 1, 328, 26), 566 }, () => { 567 this.privacyWidth = ''; 568 }); 569 animateTo({ 570 duration: 250, 571 curve: Curve.Sharp, 572 delay: 100, 573 }, () => { 574 this.privacyTextOpacity = 1; 575 }); 576 animateTo({ 577 delay: 200, 578 curve: curves.interpolatingSpring(2, 1, 500, 26), 579 }, () => { 580 this.angle = '0'; 581 }); 582 animateTo({ 583 duration: 100, 584 delay: 200, 585 curve: Curve.Sharp, 586 }, () => { 587 this.privacySymbolOpacity = 1; 588 this.dividerOpacity = 1; 589 }); 590 // 延迟5s后开始退出动画 591 setTimeout(() => { 592 this.initPrivacyAnimator(); 593 animateTo({ 594 duration: 50, 595 curve: Curve.Sharp, 596 }, () => { 597 this.dividerOpacity = 0; 598 }); 599 animateTo({ 600 duration: 100, 601 curve: Curve.Sharp, 602 }, () => { 603 this.privacyTextOpacity = 0; 604 }); 605 animateTo({ 606 duration: 150, 607 curve: Curve.Sharp, 608 }, () => { 609 this.privacySymbolOpacity = 0; 610 }); 611 this.privacyAnimator?.play(); 612 }, 5000); 613 } 614 615 /** 616 * 隐私标识动效退出,menubar长度缩小帧动画初始化 617 */ 618 private initPrivacyAnimator(): void { 619 let privacyTextLength = px2vp(componentUtils.getRectangleById('AtomicServiceMenuPrivacyId').size.width); 620 let options: AnimatorOptions = { 621 duration: 500, 622 easing: 'interpolating-spring(1, 1, 328, 26)', 623 delay: 0, 624 fill: 'forwards', 625 direction: 'normal', 626 iterations: 1, 627 begin: privacyTextLength + PRIVACY_MARGIN + PRIVACY_TEXT_MARGIN_START + PRIVACY_TEXT_MARGIN_END, 628 end: 0 629 }; 630 this.privacyAnimator = animator.create(options); 631 this.privacyAnimator.onFrame = (value: number) => { 632 // 当动画帧值小于0.01时,不做动画。 633 if (value <= 0 && Math.abs(value) < 0.01) { 634 this.buttonSize = BUTTON_SIZE; 635 this.privacyWidth = '0'; 636 return; 637 } 638 // 当动画帧值小于0时,对menu按钮做动画;大于0时,对隐私动效宽度做动画。 639 if (value < 0) { 640 this.buttonSize = BUTTON_SIZE + value; 641 } else { 642 this.privacyWidth = JSON.stringify(value); 643 } 644 } 645 this.privacyAnimator.onFinish = () => { 646 this.isShowPrivacyAnimation = false; 647 }; 648 } 649 650 @Builder 651 privacySecurityLabel() { 652 Row() { 653 SymbolGlyph(this.privacyResource) 654 .fontSize(IMAGE_SIZE) 655 .fontColor([this.privacyImageColor, Color.Blue]) 656 .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR) 657 .margin({ start: LengthMetrics.vp(PRIVACY_MARGIN) }) 658 .opacity(this.privacySymbolOpacity) 659 .rotate({ 660 x: 0, 661 y: 1, 662 z: 0, 663 centerX: '50%', 664 centerY: '50%', 665 angle: this.angle 666 }) 667 Text(this.privacyAuthText) 668 .fontSize(PRIVACY_FONT_SIZE) 669 .fontWeight(FontWeight.Regular) 670 .textAlign(TextAlign.Center) 671 .padding({ 672 start: LengthMetrics.vp(PRIVACY_TEXT_MARGIN_START), 673 end: LengthMetrics.vp(PRIVACY_TEXT_MARGIN_END) 674 }) 675 .textAlign(TextAlign.Start) 676 .textOverflow({ overflow: TextOverflow.Ellipsis }) 677 .wordBreak(WordBreak.BREAK_WORD) 678 .opacity(this.privacyTextOpacity) 679 .ellipsisMode(EllipsisMode.END) 680 .constraintSize({ maxWidth: PRIVACY_CONSTRAINT_SIZE }) 681 .fontColor($r('sys.color.ohos_id_color_text_primary')) 682 .renderFit(RenderFit.RESIZE_FILL) 683 .maxLines(1) 684 .id('AtomicServiceMenuPrivacyId') 685 } 686 .width(this.privacyWidth) 687 688 Divider() 689 .id('AtomicServiceDividerId') 690 .vertical(true) 691 .color(this.dividerBackgroundColor) 692 .lineCap(LineCapStyle.Round) 693 .strokeWidth(BORDER_WIDTH) 694 .height(DIVIDER_HEIGHT) 695 .opacity(this.dividerOpacity) 696 } 697 698 @Builder 699 fullScreenMenubar() { 700 Row() { 701 Row() { 702 if (this.isShowPrivacyAnimation) { 703 this.privacySecurityLabel() 704 } 705 706 Button() { 707 Image(this.menuResource) 708 .id('AtomicServiceMenuIconId') 709 .width(IMAGE_SIZE) 710 .height(IMAGE_SIZE) 711 .fillColor(this.menuFillColor) 712 .draggable(false) 713 .interpolation(ImageInterpolation.High) 714 } 715 .id('AtomicServiceMenuId') 716 .type(ButtonType.Normal) 717 .borderRadius({ topLeft: MENU_RADIUS, bottomLeft: MENU_RADIUS }) 718 .backgroundColor(Color.Transparent) 719 .width(this.buttonSize) 720 .height(VIEW_HEIGHT) 721 .accessibilityText(this.serviceMenuRead) 722 .gesture(TapGesture().onAction(() => { 723 this.onMenuButtonClick(); 724 })) 725 726 Divider() 727 .id('AtomicServiceDividerId') 728 .vertical(true) 729 .color(this.dividerBackgroundColor) 730 .lineCap(LineCapStyle.Round) 731 .strokeWidth(BORDER_WIDTH) 732 .height(DIVIDER_HEIGHT) 733 734 Button() { 735 Image(this.closeResource) 736 .id('AtomicServiceCloseIconId') 737 .width(IMAGE_SIZE) 738 .height(IMAGE_SIZE) 739 .fillColor(this.closeFillColor) 740 .draggable(false) 741 .interpolation(ImageInterpolation.High) 742 } 743 .id('AtomicServiceCloseId') 744 .type(ButtonType.Normal) 745 .backgroundColor(Color.Transparent) 746 .borderRadius({ topRight: MENU_RADIUS, bottomRight: MENU_RADIUS }) 747 .width(BUTTON_SIZE) 748 .height(VIEW_HEIGHT) 749 .accessibilityText(this.closeRead) 750 .gesture(TapGesture().onAction(() => { 751 this.closeContainerAnimation(); 752 })) 753 } 754 .borderRadius(MENU_RADIUS) 755 .borderWidth(BORDER_WIDTH) 756 .borderColor(this.menubarBorderColor) 757 .backgroundColor(this.menubarBackColor) 758 .backgroundEffect({ 759 radius: MENU_BACK_BLUR, 760 color: this.menubarBackColor 761 }) 762 .height(VIEW_HEIGHT) 763 .align(Alignment.Top) 764 .draggable(false) 765 .geometryTransition('menubar') 766 .id('AtomicServiceMenubarId') 767 } 768 .id('AtomicServiceMenubarRowId') 769 .margin({ 770 top: LengthMetrics.vp(this.statusBarHeight + MENU_MARGIN_TOP), 771 end: LengthMetrics.vp(this.menuMarginEnd) 772 }) 773 .justifyContent(FlexAlign.End) 774 .height(VIEW_HEIGHT) 775 .hitTestBehavior(HitTestMode.Transparent) 776 .width('100%') 777 } 778 779 @Builder 780 eyelashTitle() { 781 Column() { 782 Row() { 783 Row() { 784 Image(this.icon).height(ICON_SIZE).width(ICON_SIZE) 785 .margin({ 786 start: LengthMetrics.vp(CHEVRON_MARGIN) 787 }) 788 Text(this.provideService) 789 .fontSize(TITLE_FONT_SIZE) 790 .lineHeight(TITLE_LINE_HEIGHT) 791 .fontWeight(FontWeight.Medium) 792 .fontColor('#FFFFFF') 793 .margin({ start: LengthMetrics.vp(TITLE_LABEL_MARGIN) }) 794 .maxLines(1) 795 .textOverflow({ overflow: TextOverflow.Ellipsis }) 796 .ellipsisMode(EllipsisMode.END) 797 .constraintSize({ maxWidth: TITLE_CONSTRAINT_SIZE }) 798 SymbolGlyph($r('sys.symbol.chevron_right')) 799 .height(CHEVRON_HEIGHT) 800 .width(CHEVRON_WIDTH) 801 .margin({ start: LengthMetrics.vp(CHEVRON_MARGIN), end: LengthMetrics.vp(CHEVRON_MARGIN) }) 802 .fontColor([Color.White]) 803 } 804 .height(EYELASH_HEIGHT) 805 .stateStyles({ 806 focused: { 807 .backgroundColor('#0D000000') 808 }, 809 pressed: { 810 .backgroundColor('#1A000000') 811 }, 812 normal: { 813 .backgroundColor(Color.Transparent) 814 } 815 }) 816 .borderRadius(EYELASH_HEIGHT / 2) 817 .onClick(() => { 818 this.onEyelashTitleClick(); 819 }) 820 .margin({ start: LengthMetrics.vp(TITLE_MARGIN_RIGHT) }) 821 } 822 .margin({ 823 top: LengthMetrics.vp(this.statusBarHeight + TITLE_MARGIN_TOP), 824 bottom: LengthMetrics.vp(TITLE_MARGIN_TOP) 825 }) 826 .opacity(this.titleOpacity) 827 .justifyContent(FlexAlign.Start) 828 .width('100%') 829 .hitTestBehavior(HitTestMode.Transparent) 830 } 831 .justifyContent(FlexAlign.Start) 832 .height(this.titleHeight) 833 .offset({ y: this.titleOffset }) 834 .hitTestBehavior(HitTestMode.Transparent) 835 } 836 837 @Builder 838 halfScreenMenuBar() { 839 Column() { 840 Row() { 841 Row() { 842 Button({ type: ButtonType.Circle }) { 843 SymbolGlyph($r('sys.symbol.arrow_up_left_and_arrow_down_right')) 844 .fontSize(BUTTON_IMAGE_SIZE) 845 .fontWeight(FontWeight.Medium) 846 .fontColor([this.halfButtonImageColor]) 847 }.width(BUTTON_SIZE).height(BUTTON_SIZE).backgroundColor(this.halfButtonBackColor) 848 .onClick(() => { 849 this.expendContainerAnimation(); 850 }) 851 .accessibilityText(this.maximizeRead) 852 Button({ type: ButtonType.Circle }) { 853 SymbolGlyph($r('sys.symbol.xmark')) 854 .fontSize(BUTTON_IMAGE_SIZE) 855 .fontWeight(FontWeight.Medium) 856 .fontColor([this.halfButtonImageColor]) 857 } 858 .width(BUTTON_SIZE) 859 .height(BUTTON_SIZE) 860 .margin({ 861 start: LengthMetrics.vp(VIEW_MARGIN_RIGHT), 862 }) 863 .backgroundColor(this.halfButtonBackColor) 864 .onClick(() => { 865 this.closeContainerAnimation(); 866 }) 867 .accessibilityText(this.closeRead) 868 } 869 .geometryTransition('menubar') 870 .justifyContent(FlexAlign.End) 871 .transition(TransitionEffect.OPACITY) 872 .borderRadius(MENU_RADIUS) 873 .height(BUTTON_SIZE) 874 .margin({ 875 top: LengthMetrics.vp(this.titleHeight + HALF_MENU_MARGIN), 876 end: LengthMetrics.vp(HALF_MENU_MARGIN) 877 }) 878 } 879 .width(this.containerWidth) 880 .height(this.containerHeight) 881 .aspectRatio(this.ratio) 882 .alignItems(VerticalAlign.Top) 883 .justifyContent(FlexAlign.End) 884 .opacity(this.buttonOpacity) 885 }.height('100%') 886 .width('100%') 887 .justifyContent(FlexAlign.End) 888 .hitTestBehavior(HitTestMode.Transparent) 889 } 890 891 build() { 892 Column() { 893 Stack({ alignContent: Alignment.TopEnd }) { 894 if (this.isHalfScreen) { 895 // 透明模糊背板 896 Column() 897 .width('100%') 898 .height('100%') 899 .backgroundColor('#262626') 900 .opacity(this.maskOpacity) 901 .foregroundBlurStyle(BlurStyle.BACKGROUND_REGULAR, { colorMode: ThemeColorMode.LIGHT, scale: this.maskBlurScale }) 902 .onClick(() => { 903 this.closeContainerAnimation(); 904 }) 905 } 906 Column() { 907 Column() { 908 if (this.isHalfScreen) { 909 this.eyelashTitle() 910 } 911 Row() { 912 } 913 .padding({ 914 top: this.contentMarginTop, 915 left: this.contentMarginLeft, 916 right: this.contentMarginRight, 917 bottom: this.contentMarginBottom 918 }) 919 .layoutWeight(1) 920 .backgroundColor(Color.Transparent) 921 .backgroundBlurStyle(this.isHalfScreen ? BlurStyle.COMPONENT_ULTRA_THICK : undefined) 922 .borderRadius({ 923 topLeft: this.isHalfScreen ? HALF_CONTAINER_BORDER_SIZE : this.deviceBorderRadius, 924 topRight: this.isHalfScreen ? HALF_CONTAINER_BORDER_SIZE : this.deviceBorderRadius, 925 }) 926 .clip(true) 927 .alignItems(VerticalAlign.Bottom) 928 .width('100%') 929 .onClick(() => { 930 // 拦截到背板的点击事件 931 }) 932 .id('AtomicServiceStageId') 933 } 934 .height(this.containerHeight) 935 .width(this.containerWidth) 936 .aspectRatio(this.ratio) 937 .justifyContent(FlexAlign.End) 938 .hitTestBehavior(HitTestMode.Transparent) 939 .id('AtomicServiceStageColumnId') 940 }.height('100%') 941 .width('100%') 942 .justifyContent(FlexAlign.End) 943 .hitTestBehavior(HitTestMode.Transparent) 944 .id('AtomicServiceStageLayoutId') 945 946 if (this.isHalfScreen) { 947 this.halfScreenMenuBar() 948 } else { 949 this.fullScreenMenubar() 950 } 951 } 952 .height(this.stackHeight) 953 .width('100%') 954 .backgroundColor(this.contentBgColor) 955 .hitTestBehavior(HitTestMode.Transparent) 956 .id('AtomicServiceContainerId') 957 .onAppear(() => { 958 if (this.isHalfScreen) { 959 this.contentBgColor = Color.Transparent; 960 this.titleHeight = EYELASH_HEIGHT + 2 * TITLE_MARGIN_TOP + this.statusBarHeight; 961 this.halfScreenShowAnimation(); 962 } else { 963 this.containerHeight = '100%'; 964 this.containerWidth = '100%'; 965 } 966 this.updateStringByResource(); 967 this.getDeviceRadiusConfig(); 968 this.subscribePrivacyState(); 969 }) 970 } 971 .height('100%') 972 .width('100%') 973 .justifyContent(FlexAlign.End) 974 .backgroundColor(Color.Transparent) 975 .hitTestBehavior(HitTestMode.Transparent) 976 } 977}