From 0fb485f71b4957c542838b02ba28b436eb6d4586 Mon Sep 17 00:00:00 2001 From: haohaoai0 <116936512+haohaoai0@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:55:47 +0800 Subject: [PATCH] feat: adapt UI for HarmonyOS 7 --- .gitignore | 2 - build-profile.json5 | 40 +++++++++++++ entry/build-profile.json5 | 27 +++++++++ entry/oh-package-lock.json5 | 2 +- entry/src/main/ets/AppTheme.ets | 24 +++++++- .../main/ets/common/layout/AppBreakpoints.ets | 8 ++- .../src/main/ets/common/local/LocalValues.ets | 35 +++++++----- .../ets/common/local/SettingsConstants.ets | 7 ++- .../common/platform/HarmonyOSCapabilities.ets | 20 +++++++ .../main/ets/common/style/MaterialLevel.ets | 21 ------- entry/src/main/ets/common/style/NavStyles.ets | 7 ++- .../main/ets/common/theme/ThemeManager.ets | 5 ++ .../main/ets/entryability/EntryAbility.ets | 56 +++++++++++++------ entry/src/main/ets/pages/Index.ets | 8 +-- .../pages/common/LottieOnboardingSheet.ets | 4 +- .../onboarding/OnboardingSetupPages.ets | 16 +++--- .../ets/pages/components/AdaptedHdsTabs.ets | 21 ++++--- .../components/HdsTitleBarMenuButton.ets | 37 +++++++----- .../pages/components/HdsTitleBarSegment.ets | 43 +++++--------- .../pages/components/SettingsComponents.ets | 18 ++++++ .../ets/pages/components/TopSafeContainer.ets | 15 ++++- entry/src/main/ets/pages/routes/ai/AIPage.ets | 6 +- .../ets/pages/routes/capture/CapturePage.ets | 4 +- .../ets/pages/routes/main/MainHomePage.ets | 6 +- .../pages/routes/main/search/SearchPage.ets | 54 ++++++++---------- .../user/settings/AppearanceSettings.ets | 28 +++++----- .../settings/components/ColorCustomizer.ets | 12 ++-- .../settings/components/ThemeSelectSheet.ets | 4 +- entry/src/main/ets/utils/ColorGenerator.ets | 45 ++++++++------- .../main/resources/base/element/color.json | 6 ++ .../main/resources/base/element/string.json | 30 +++++++++- .../main/resources/dark/element/color.json | 11 +++- hvigor/hvigor-config.json5 | 2 +- oh-package.json5 | 4 +- 34 files changed, 411 insertions(+), 217 deletions(-) create mode 100644 build-profile.json5 create mode 100644 entry/build-profile.json5 create mode 100644 entry/src/main/ets/common/platform/HarmonyOSCapabilities.ets diff --git a/.gitignore b/.gitignore index 5d10082..f8fbbd0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,5 @@ /.clang-tidy **/.test /.appanalyzer -build-profile.json5 agconnect-services.json -HdsMiniBarButton.ets .deadliner-core/ diff --git a/build-profile.json5 b/build-profile.json5 new file mode 100644 index 0000000..bf9fb44 --- /dev/null +++ b/build-profile.json5 @@ -0,0 +1,40 @@ +{ + "app": { + "products": [ + { + "name": "default", + "targetSdkVersion": "26.0.0", + "compatibleSdkVersion": "6.0.0(20)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug" + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} diff --git a/entry/build-profile.json5 b/entry/build-profile.json5 new file mode 100644 index 0000000..d470607 --- /dev/null +++ b/entry/build-profile.json5 @@ -0,0 +1,27 @@ +{ + "apiType": "stageMode", + "buildOption": {}, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + } + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest" + } + ] +} diff --git a/entry/oh-package-lock.json5 b/entry/oh-package-lock.json5 index ed4d8f9..cade19b 100644 --- a/entry/oh-package-lock.json5 +++ b/entry/oh-package-lock.json5 @@ -24,4 +24,4 @@ "registryType": "local" } } -} \ No newline at end of file +} diff --git a/entry/src/main/ets/AppTheme.ets b/entry/src/main/ets/AppTheme.ets index b5dbe26..82846c4 100644 --- a/entry/src/main/ets/AppTheme.ets +++ b/entry/src/main/ets/AppTheme.ets @@ -6,6 +6,9 @@ export class DeadlinerColors implements CustomColors { // 品牌色 brand: ResourceColor + primary: ResourceColor + onPrimary: ResourceColor = $r('app.color.font_on_primary') + container: ResourceColor = $r('app.color.background_secondary') // 告警/确认 warning: ResourceColor = $r('app.color.warning') @@ -15,19 +18,32 @@ export class DeadlinerColors implements CustomColors { // 文本 fontPrimary: ResourceColor = $r('app.color.font_primary') fontSecondary: ResourceColor = $r('app.color.font_secondary') + fontTertiary: ResourceColor = $r('app.color.font_tertiary') + fontFourth: ResourceColor = $r('app.color.font_fourth') fontEmphasize: ResourceColor = $r('app.color.font_emphasize') + fontOnPrimary: ResourceColor = $r('app.color.font_on_primary') // 图标 iconPrimary: ResourceColor = $r('app.color.icon_primary') iconSecondary: ResourceColor = $r('app.color.icon_secondary') + iconTertiary: ResourceColor = $r('app.color.icon_tertiary') + iconFourth: ResourceColor = $r('app.color.icon_fourth') iconEmphasize: ResourceColor = $r('app.color.icon_emphasize') + iconOnPrimary: ResourceColor = $r('app.color.icon_on_primary') // 背景 backgroundPrimary: ResourceColor backgroundSecondary: ResourceColor = $r('app.color.background_secondary') backgroundTertiary: ResourceColor = $r('app.color.background_tertiary') + backgroundFourth: ResourceColor = $r('app.color.background_emphasize') backgroundEmphasize: ResourceColor = $r('app.color.background_emphasize') + // 组件表面 + compBackgroundPrimary: ResourceColor = $r('app.color.background_secondary') + compBackgroundSecondary: ResourceColor = $r('app.color.background_tertiary') + compBackgroundTertiary: ResourceColor = $r('app.color.background_primary_50') + compBackgroundEmphasize: ResourceColor + // 分割线 compDivider: ResourceColor = $r('app.color.comp_divider') @@ -35,12 +51,18 @@ export class DeadlinerColors implements CustomColors { interactiveActive: ResourceColor = $r('app.color.interactive_active') interactiveSelect: ResourceColor = $r('app.color.interactive_select') interactivePressed: ResourceColor = $r('app.color.interactive_pressed') + interactiveHover: ResourceColor = $r('app.color.interactive_select') + interactiveFocus: ResourceColor + interactiveClick: ResourceColor = $r('app.color.interactive_pressed') // 构造函数:接收当前的 ITheme 配置 constructor(theme: ITheme) { this.brand = theme.brand + this.primary = theme.brand this.alert = theme.brand // 复用 brand this.backgroundPrimary = theme.bgPrimary + this.compBackgroundEmphasize = theme.brand + this.interactiveFocus = theme.brand } } @@ -50,4 +72,4 @@ export class DeadlinerTheme implements CustomTheme { constructor(theme: ITheme) { this.colors = new DeadlinerColors(theme) } -} \ No newline at end of file +} diff --git a/entry/src/main/ets/common/layout/AppBreakpoints.ets b/entry/src/main/ets/common/layout/AppBreakpoints.ets index 3a5d9a9..54827cd 100644 --- a/entry/src/main/ets/common/layout/AppBreakpoints.ets +++ b/entry/src/main/ets/common/layout/AppBreakpoints.ets @@ -6,7 +6,8 @@ export enum AppBreakpoint { } export const APP_BREAKPOINT_STORAGE_KEY: string = 'appBreakpoint'; -export const APP_BREAKPOINT_DEFAULT: AppBreakpoint = AppBreakpoint.MD; +// Compact is the safest first-frame fallback before the window callback arrives. +export const APP_BREAKPOINT_DEFAULT: AppBreakpoint = AppBreakpoint.SM; export const APP_BREAKPOINT_ORDER: AppBreakpoint[] = [ AppBreakpoint.SM, @@ -19,6 +20,7 @@ export class AppBreakpointUtils { static readonly MD_MIN_WIDTH: number = 600; static readonly LG_MIN_WIDTH: number = 840; static readonly XL_MIN_WIDTH: number = 1320; + static readonly COMPACT_HEIGHT_MAX: number = 480; static mediaCondition(breakpoint: AppBreakpoint): string { switch (breakpoint) { @@ -51,4 +53,8 @@ export class AppBreakpointUtils { static isWide(breakpoint: string): boolean { return breakpoint === AppBreakpoint.LG || breakpoint === AppBreakpoint.XL; } + + static isCompactHeight(heightVp: number): boolean { + return heightVp > 0 && heightVp <= AppBreakpointUtils.COMPACT_HEIGHT_MAX; + } } diff --git a/entry/src/main/ets/common/local/LocalValues.ets b/entry/src/main/ets/common/local/LocalValues.ets index 5ed454f..9a6e581 100644 --- a/entry/src/main/ets/common/local/LocalValues.ets +++ b/entry/src/main/ets/common/local/LocalValues.ets @@ -2,7 +2,7 @@ import { preferences } from '@kit.ArkData'; import { SettingsConstants as C } from './SettingsConstants'; import { SecureStorage } from './SecureStorage'; import { MainButtonType } from './LocalModels'; -import { deviceInfo } from '@kit.BasicServicesKit'; +import { HarmonyOSCapabilities } from '../platform/HarmonyOSCapabilities'; interface WebDavAuthInterface { user: string; @@ -153,13 +153,14 @@ export class LocalValues { const showPaperTalkShare = await this.pref.get(C.KEY_SHOW_PAPERTALK_SHARE, true); AppStorage.setOrCreate(C.KEY_SHOW_PAPERTALK_SHARE, showPaperTalkShare); - const enableApi23 = await this.pref.get(C.KEY_ENABLE_API23, true); - AppStorage.setOrCreate(C.KEY_ENABLE_API23, enableApi23); + const immersiveMaterialPreference = await this.pref.get(C.KEY_ENABLE_IMMERSIVE_MATERIAL, true) as boolean; + const immersiveMaterial = HarmonyOSCapabilities.supportsImmersiveMaterial() && immersiveMaterialPreference; + AppStorage.setOrCreate(C.KEY_ENABLE_IMMERSIVE_MATERIAL, immersiveMaterial); - const materialLevel = await this.pref.get(C.KEY_API23_MATERIAL_LEVEL, 'EXQUISITE'); - AppStorage.setOrCreate(C.KEY_API23_MATERIAL_LEVEL, materialLevel) + const materialLevel = await this.pref.get(C.KEY_MATERIAL_LEVEL, 'ADAPTIVE'); + AppStorage.setOrCreate(C.KEY_MATERIAL_LEVEL, materialLevel) - const minibar = await this.pref.get(C.KEY_SHOW_MINIBAR, (deviceInfo.distributionOSApiVersion >= 60100)); + const minibar = await this.pref.get(C.KEY_SHOW_MINIBAR, HarmonyOSCapabilities.supportsImmersiveMaterial()); AppStorage.setOrCreate(C.KEY_SHOW_MINIBAR, minibar); const experimentalComponents = await this.pref.get(C.KEY_EXPERIMENTAL_COMPONENTS, true); @@ -679,12 +680,20 @@ export class LocalValues { await this.setValue(C.KEY_TOMBSTONE_RETENTION_DAYS, days); } + isImmersiveMaterialEnabled(): boolean { + return AppStorage.get(C.KEY_ENABLE_IMMERSIVE_MATERIAL) ?? true; + } + + async setImmersiveMaterialEnabled(enable: boolean): Promise { + await this.setValue(C.KEY_ENABLE_IMMERSIVE_MATERIAL, enable); + } + getApi23Enable(): boolean { - return AppStorage.get(C.KEY_ENABLE_API23) ?? true; + return this.isImmersiveMaterialEnabled(); } - async setApi23Enable(enable: boolean) { - await this.setValue(C.KEY_ENABLE_API23, enable); + async setApi23Enable(enable: boolean): Promise { + await this.setImmersiveMaterialEnabled(enable); } getExperimentalComponents(): boolean { @@ -704,21 +713,21 @@ export class LocalValues { } getMaterialLevel(): string { - return AppStorage.get(C.KEY_API23_MATERIAL_LEVEL) ?? 'EXQUISITE'; + return AppStorage.get(C.KEY_MATERIAL_LEVEL) ?? 'ADAPTIVE'; } async setMaterialLevel(level: string) { const levels = ['ADAPTIVE', 'SMOOTH', 'GENTLE', 'EXQUISITE']; const actual = (levels.includes(level)) ? level : 'ADAPTIVE'; - await this.setValue(C.KEY_API23_MATERIAL_LEVEL, actual); + await this.setValue(C.KEY_MATERIAL_LEVEL, actual); } getMiniBar(): boolean { - return AppStorage.get(C.KEY_SHOW_MINIBAR) ?? (deviceInfo.distributionOSApiVersion >= 60100); + return AppStorage.get(C.KEY_SHOW_MINIBAR) ?? HarmonyOSCapabilities.supportsImmersiveMaterial(); } async setMiniBar(enable: boolean) { - if (deviceInfo.distributionOSApiVersion >= 60100) { + if (HarmonyOSCapabilities.supportsImmersiveMaterial()) { await this.setValue(C.KEY_SHOW_MINIBAR, enable); return; } diff --git a/entry/src/main/ets/common/local/SettingsConstants.ets b/entry/src/main/ets/common/local/SettingsConstants.ets index 2702527..0d84d7a 100644 --- a/entry/src/main/ets/common/local/SettingsConstants.ets +++ b/entry/src/main/ets/common/local/SettingsConstants.ets @@ -53,8 +53,11 @@ export class SettingsConstants { static readonly KEY_AUTO_ARCHIVE_DAYS = 'auto_archive_days'; static readonly KEY_TOMBSTONE_RETENTION_DAYS = 'tombstone_retention_days'; - static readonly KEY_ENABLE_API23 = 'enable_api23'; - static readonly KEY_API23_MATERIAL_LEVEL = 'material_level'; + // Keep the persisted keys stable while exposing version-neutral names in code. + static readonly KEY_ENABLE_IMMERSIVE_MATERIAL = 'enable_api23'; + static readonly KEY_MATERIAL_LEVEL = 'material_level'; + static readonly KEY_ENABLE_API23 = SettingsConstants.KEY_ENABLE_IMMERSIVE_MATERIAL; + static readonly KEY_API23_MATERIAL_LEVEL = SettingsConstants.KEY_MATERIAL_LEVEL; static readonly KEY_EXPERIMENTAL_COMPONENTS = 'experimental_components'; static readonly KEY_OVERVIEW_CARD_ORDER = 'overview_card_order'; diff --git a/entry/src/main/ets/common/platform/HarmonyOSCapabilities.ets b/entry/src/main/ets/common/platform/HarmonyOSCapabilities.ets new file mode 100644 index 0000000..a56d6cf --- /dev/null +++ b/entry/src/main/ets/common/platform/HarmonyOSCapabilities.ets @@ -0,0 +1,20 @@ +import { deviceInfo } from '@kit.BasicServicesKit'; + +/** + * HarmonyOS UI capability gates. + * + * Use the public SDK API level instead of parsing distribution version strings. + * This keeps preview, emulator, phone, tablet and 2-in-1 behavior consistent. + */ +export class HarmonyOSCapabilities { + static readonly IMMERSIVE_MATERIAL_API: number = 23; + static readonly HARMONYOS_7_API: number = 26; + + static supportsImmersiveMaterial(): boolean { + return deviceInfo.sdkApiVersion >= HarmonyOSCapabilities.IMMERSIVE_MATERIAL_API; + } + + static isHarmonyOS7OrLater(): boolean { + return deviceInfo.sdkApiVersion >= HarmonyOSCapabilities.HARMONYOS_7_API; + } +} diff --git a/entry/src/main/ets/common/style/MaterialLevel.ets b/entry/src/main/ets/common/style/MaterialLevel.ets index 39d3148..85e24fe 100644 --- a/entry/src/main/ets/common/style/MaterialLevel.ets +++ b/entry/src/main/ets/common/style/MaterialLevel.ets @@ -23,24 +23,3 @@ export function getHdsMaterialLevel(level?: string): hdsMaterial.MaterialLevel { return hdsMaterial.MaterialLevel.ADAPTIVE } } - -export function getHdsMaterialLevelRaw(level?: string): number { - if (!level) { - level = LocalValues.getInstance().getMaterialLevel() - } - - console.log(`Material Level ${level}`) - - switch (level) { - case 'ADAPTIVE': - return 10 - case 'SMOOTH': - return 2 - case 'GENTLE': - return 1 - case 'EXQUISITE': - return 0 - default: - return 10 - } -} \ No newline at end of file diff --git a/entry/src/main/ets/common/style/NavStyles.ets b/entry/src/main/ets/common/style/NavStyles.ets index 500bbaf..6c0ac7d 100644 --- a/entry/src/main/ets/common/style/NavStyles.ets +++ b/entry/src/main/ets/common/style/NavStyles.ets @@ -1,9 +1,9 @@ import { LengthMetrics } from '@kit.ArkUI'; import { hdsMaterial, HdsNavigationTitleBarStyle, ScrollEffectType, TitleBarStyleOptions } from '@kit.UIDesignKit'; import { ThemeManager } from '../theme/ThemeManager'; -import { deviceInfo } from '@kit.BasicServicesKit'; import { LocalValues } from '../local/LocalValues'; import { getHdsMaterialLevel } from './MaterialLevel'; +import { HarmonyOSCapabilities } from '../platform/HarmonyOSCapabilities'; /** * 获取统一的沉浸式标题栏样式配置 @@ -16,7 +16,8 @@ export function getImmersiveTitleBarStyle( disableMaterial: boolean = false, level?: string, ) : TitleBarStyleOptions { - if (deviceInfo.distributionOSApiVersion >= 60100 && LocalValues.getInstance().getApi23Enable()) { + if (HarmonyOSCapabilities.supportsImmersiveMaterial() && + LocalValues.getInstance().isImmersiveMaterialEnabled()) { return { scrollEffectOpts: { // enableScrollEffect: !isWideScreen, @@ -81,4 +82,4 @@ export function getImmersiveTitleBarStyle( }, }; } -} \ No newline at end of file +} diff --git a/entry/src/main/ets/common/theme/ThemeManager.ets b/entry/src/main/ets/common/theme/ThemeManager.ets index 8daf0b7..a0086a9 100644 --- a/entry/src/main/ets/common/theme/ThemeManager.ets +++ b/entry/src/main/ets/common/theme/ThemeManager.ets @@ -280,6 +280,11 @@ export class ThemeManager { } } + /** Reapply after ArkUI content has been attached to the window. */ + static reapplySystemTheme(): void { + ThemeManager.applySystemTheme(ThemeManager.getCurrentTheme()); + } + // ================================================================= // 公共 API (切换逻辑) // ================================================================= diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index f580e20..c94bab0 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -1,6 +1,6 @@ import { AbilityConstant, Configuration, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; -import { display, ThemeControl, window } from '@kit.ArkUI'; +import { display, window } from '@kit.ArkUI'; import { LocalValues } from '../common/local/LocalValues'; import { DatabaseHelper } from '../common/data/DatabaseHelper'; import { TaskRepository } from '../common/repository/TaskRepository'; @@ -17,6 +17,7 @@ import { ThemeManager } from '../common/theme/ThemeManager'; import { DynamicIconUtils } from '../utils/DynamicIconUtils'; import { AIService } from '../ai/AIServcice'; import { AppBreakpointUtils, APP_BREAKPOINT_STORAGE_KEY } from '../common/layout/AppBreakpoints'; +import { HarmonyOSCapabilities } from '../common/platform/HarmonyOSCapabilities'; const DOMAIN = 0x0000; @@ -76,8 +77,7 @@ export default class EntryAbility extends UIAbility { this.mainWindowClass = windowStage.getMainWindowSync(); - let systemProperties = this.mainWindowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); - AppStorage.set("statusBarHeight", systemProperties.topRect.height); + this.handleResponsiveImmersive(windowStage); windowStage.loadContent('pages/Index', (err) => { if (err.code) { @@ -87,7 +87,8 @@ export default class EntryAbility extends UIAbility { hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); - this.handleResponsiveImmersive(windowStage); + // ThemeControl takes effect only after ArkUI has attached page content. + ThemeManager.reapplySystemTheme(); this.context.eventHub.on('refresh_window_mode', () => { let windowClass = windowStage.getMainWindowSync(); @@ -169,16 +170,18 @@ export default class EntryAbility extends UIAbility { this.updateWindowMode(windowClass); // 3. 监听窗口尺寸变化 (用户旋转屏幕或折叠屏展开/折叠时触发) - windowClass.on('windowSizeChange', (size) => { + windowClass.on('windowSizeChange', (_size: window.Size) => { this.updateWindowMode(windowClass); }); // 4. 保持避让区域监听 (用于全屏模式下的精确避让) - windowClass.on('avoidAreaChange', (data) => { + windowClass.on('avoidAreaChange', (data: window.AvoidAreaOptions) => { if (data.type === window.AvoidAreaType.TYPE_SYSTEM) { - AppStorage.setOrCreate('topRectHeight', data.area.topRect.height); + const topHeightVp = this.pxToVp(data.area.topRect.height); + AppStorage.setOrCreate('statusBarHeight', topHeightVp); + AppStorage.setOrCreate('topRectHeight', topHeightVp); } else if (data.type == window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) { - AppStorage.setOrCreate('bottomRectHeight', data.area.bottomRect.height); + AppStorage.setOrCreate('bottomRectHeight', this.pxToVp(data.area.bottomRect.height)); } }); } @@ -195,14 +198,23 @@ export default class EntryAbility extends UIAbility { let breakpoint = AppBreakpointUtils.fromWidthVp(widthVp); let isWideScreen = AppBreakpointUtils.isWide(breakpoint); + let heightVp = windowProperties.windowRect.height / displayClass.densityPixels; + let isHarmonyOS7 = HarmonyOSCapabilities.isHarmonyOS7OrLater(); + let useImmersiveWindow = isHarmonyOS7 || (isWideScreen && enableTabletUi); let isDark = this.context.config.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK; AppStorage.setOrCreate(APP_BREAKPOINT_STORAGE_KEY, breakpoint); AppStorage.setOrCreate('isWideScreen', isWideScreen); + AppStorage.setOrCreate('windowWidthVp', widthVp); + AppStorage.setOrCreate('windowHeightVp', heightVp); + AppStorage.setOrCreate('isLandscape', widthVp > heightVp); + AppStorage.setOrCreate('isCompactHeight', AppBreakpointUtils.isCompactHeight(heightVp)); + AppStorage.setOrCreate('isHarmonyOS7', isHarmonyOS7); - if (isWideScreen && enableTabletUi) { - windowClass.setWindowLayoutFullScreen(true); - windowClass.setWindowSystemBarProperties({ + windowClass.setImmersiveModeEnabledState(useImmersiveWindow); + + if (useImmersiveWindow) { + void windowClass.setWindowSystemBarProperties({ navigationBarColor: '#00000000', statusBarColor: '#00000000', statusBarContentColor: isDark ? '#FFFFFF' : '#000000', @@ -211,8 +223,7 @@ export default class EntryAbility extends UIAbility { hilog.info(DOMAIN, 'testTag', `Width: ${widthVp}vp breakpoint=${breakpoint}. Mode: Immersive`); } else { - windowClass.setWindowLayoutFullScreen(false); - windowClass.setWindowSystemBarProperties({ + void windowClass.setWindowSystemBarProperties({ navigationBarColor: isDark ? '#000000' : '#FFFFFF', statusBarColor: '#00000000', statusBarContentColor: isDark ? '#FFFFFF' : '#000000', @@ -223,14 +234,25 @@ export default class EntryAbility extends UIAbility { } this.updateAvoidRects(windowClass); - }; + } + + private pxToVp(valuePx: number): number { + const densityPixels = display.getDefaultDisplaySync().densityPixels; + if (densityPixels <= 0) { + return valuePx; + } + return valuePx / densityPixels; + } - private updateAvoidRects(windowClass: window.Window) { + private updateAvoidRects(windowClass: window.Window): void { let topAvoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM); let bottomAvoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR); + let topHeightVp = this.pxToVp(topAvoidArea.topRect.height); + let bottomHeightVp = this.pxToVp(bottomAvoidArea.bottomRect.height); - AppStorage.setOrCreate('topRectHeight', topAvoidArea.topRect.height); - AppStorage.setOrCreate('bottomRectHeight', bottomAvoidArea.bottomRect.height); + AppStorage.setOrCreate('statusBarHeight', topHeightVp); + AppStorage.setOrCreate('topRectHeight', topHeightVp); + AppStorage.setOrCreate('bottomRectHeight', bottomHeightVp); } private async requestNotificationPermissionIfNeeded(): Promise { diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index c59ed6d..c020884 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -12,7 +12,6 @@ import { ITheme } from '../common/theme/ThemeModel'; import { TaskRepository } from '../common/repository/TaskRepository'; import { hdsMaterial, HdsNavigation, HdsTabs, HdsTabsController } from '@kit.UIDesignKit'; import { AdaptedHdsTabs } from './components/AdaptedHdsTabs'; -import { deviceInfo } from '@kit.BasicServicesKit'; import { CapturePage } from './routes/capture/CapturePage'; import { LottieOnboardingSheet } from './common/LottieOnboardingSheet'; import { AISheetController } from './components/AISheetController'; @@ -24,6 +23,7 @@ import { APP_BREAKPOINT_STORAGE_KEY, AppBreakpointUtils } from '../common/layout/AppBreakpoints'; +import { HarmonyOSCapabilities } from '../common/platform/HarmonyOSCapabilities'; interface TabData { title: string, @@ -66,18 +66,18 @@ struct Index { @StorageProp('statusBarHeight') @Watch('syncTopAvoidExtraHeight') statusBarHeight: number = 38; @StorageProp('topAvoidExtraHeight') topAvoidExtraHeight: number = 0; @StorageLink(C.KEY_ENABLE_TABLET_UI) tabletUi: boolean = true; - @StorageProp(C.KEY_ENABLE_API23) enableApi23: boolean = false; + @StorageProp(C.KEY_ENABLE_IMMERSIVE_MATERIAL) immersiveMaterialEnabled: boolean = false; @StorageLink('NotificationPermissionRequestInProgress') @Watch('onNotificationPermissionFlowChange') isNotificationPermissionRequestInProgress: boolean = false; getFloatingTabs(): boolean { - return (deviceInfo.distributionOSApiVersion >= 60100) && this.enableApi23 + return HarmonyOSCapabilities.supportsImmersiveMaterial() && this.immersiveMaterialEnabled; } private breakpointListeners: mediaquery.MediaQueryListener[] = []; private syncTopAvoidExtraHeight(): void { - const nextTopAvoidExtraHeight = AppBreakpointUtils.isWide(this.currentBreakpoint) ? this.statusBarHeight + 8 : 0; + const nextTopAvoidExtraHeight = AppBreakpointUtils.isWide(this.currentBreakpoint) ? 8 : 0; if (this.topAvoidExtraHeight !== nextTopAvoidExtraHeight) { AppStorage.setOrCreate('topAvoidExtraHeight', nextTopAvoidExtraHeight); } diff --git a/entry/src/main/ets/pages/common/LottieOnboardingSheet.ets b/entry/src/main/ets/pages/common/LottieOnboardingSheet.ets index bf46dd6..9c9d275 100644 --- a/entry/src/main/ets/pages/common/LottieOnboardingSheet.ets +++ b/entry/src/main/ets/pages/common/LottieOnboardingSheet.ets @@ -1,6 +1,5 @@ import { DividerShowType, HdsNavigation, HdsNavigationMenuItemOptions, HdsNavigationTitleMode } from '@kit.UIDesignKit'; import { promptAction } from '@kit.ArkUI'; -import { deviceInfo } from '@kit.BasicServicesKit'; import { ITheme } from '../../common/theme/ThemeModel'; import { getImmersiveTitleBarStyle } from '../../common/style/NavStyles'; import { ManualDataManager } from '../../common/manual/ManualModel'; @@ -10,6 +9,7 @@ import { IntroHeroPage, LottieScenePage, PrivacyAgreementPage } from './onboardi import { AiPage, HuaweiLoginPage, ImmersiveEffectPage, NotificationPage, PersonalizationPage, SyncPage } from './onboarding/OnboardingSetupPages'; import { GuidePageId, LottieSceneMeta, PANEL_BG, RawFileResourceManager } from './onboarding/OnboardingTypes'; import { HelpDetailSheet } from '../routes/user/manual/HelpDetailSheet'; +import { HarmonyOSCapabilities } from '../../common/platform/HarmonyOSCapabilities'; @Component export struct LottieOnboardingSheet { @@ -51,7 +51,7 @@ export struct LottieOnboardingSheet { } private pageIds(): GuidePageId[] { - return buildGuidePageIds(deviceInfo.distributionOSApiVersion >= 60100, this.lottieScenes); + return buildGuidePageIds(HarmonyOSCapabilities.supportsImmersiveMaterial(), this.lottieScenes); } private currentPageId(): GuidePageId { diff --git a/entry/src/main/ets/pages/common/onboarding/OnboardingSetupPages.ets b/entry/src/main/ets/pages/common/onboarding/OnboardingSetupPages.ets index 3a389fd..432608e 100644 --- a/entry/src/main/ets/pages/common/onboarding/OnboardingSetupPages.ets +++ b/entry/src/main/ets/pages/common/onboarding/OnboardingSetupPages.ets @@ -1,7 +1,7 @@ import auth from '@hw-agconnect/auth'; import { LoginWithHuaweiIDButton, loginComponentManager } from '@kit.AccountKit'; import { promptAction } from '@kit.ArkUI'; -import { BusinessError, deviceInfo } from '@kit.BasicServicesKit'; +import { BusinessError } from '@kit.BasicServicesKit'; import { notificationManager } from '@kit.NotificationKit'; import { ITheme } from '../../../common/theme/ThemeModel'; import { LocalValues } from '../../../common/local/LocalValues'; @@ -22,6 +22,7 @@ import { } from '../../components/SettingsComponents'; import { MATERIAL_OPTIONS } from './OnboardingConfig'; import { MaterialOption, SyncOption } from './OnboardingTypes'; +import { HarmonyOSCapabilities } from '../../../common/platform/HarmonyOSCapabilities'; import { GuideContainer } from './OnboardingCorePages'; @Component @@ -364,7 +365,7 @@ export struct NotificationPage { export struct ImmersiveEffectPage { @Prop @Watch('onActiveChanged') isActive: boolean = false; @StorageProp('currentTheme') theme: ITheme = {} as ITheme; - @StorageLink(C.KEY_ENABLE_API23) api23: boolean = true; + @StorageLink(C.KEY_ENABLE_IMMERSIVE_MATERIAL) immersiveMaterial: boolean = true; @State materialLevel: string = 'EXQUISITE'; @State menuOptions: SelectOption[] = [ { value: '自适应' }, @@ -419,7 +420,7 @@ export struct ImmersiveEffectPage { .translate({ y: this.visible ? 0 : 20 }) .animation({ duration: 450, curve: Curve.EaseOut, delay: 50 }) - Text('如果你的设备是 60100+,这里可以先打开沉浸光感,并快速挑一个你喜欢的材质等级。') + Text($r('app.string.onboarding_immersive_material_description')) .fontSize(15) .lineHeight(24) .fontColor($r('app.color.font_secondary')) @@ -435,12 +436,13 @@ export struct ImmersiveEffectPage { SubSettingsSwitch({ title: '沉浸光感视效', subtitle: '打开后可获得更完整的沉浸材质与光感效果。', - isOn: this.api23, - onChange: (isOn: boolean): Promise => LocalValues.getInstance().setApi23Enable(isOn) + isOn: this.immersiveMaterial, + onChange: (isOn: boolean): Promise => + LocalValues.getInstance().setImmersiveMaterialEnabled(isOn) }) .enabled(this.currentPageEnabled()) - if (this.api23 && this.currentPageEnabled()) { + if (this.immersiveMaterial && this.currentPageEnabled()) { SettingsDivider() SettingsSelect({ @@ -469,7 +471,7 @@ export struct ImmersiveEffectPage { } private currentPageEnabled(): boolean { - return deviceInfo.distributionOSApiVersion >= 60100; + return HarmonyOSCapabilities.supportsImmersiveMaterial(); } } diff --git a/entry/src/main/ets/pages/components/AdaptedHdsTabs.ets b/entry/src/main/ets/pages/components/AdaptedHdsTabs.ets index 950591f..68004b7 100644 --- a/entry/src/main/ets/pages/components/AdaptedHdsTabs.ets +++ b/entry/src/main/ets/pages/components/AdaptedHdsTabs.ets @@ -1,13 +1,8 @@ -import { DividerMode, HdsBarStyle, hdsMaterial, HdsTabs, - HdsTabsBarChangeMode, - HdsTabsController } from '@kit.UIDesignKit'; -import { deviceInfo } from '@kit.BasicServicesKit'; +import { DividerMode, hdsMaterial, HdsTabs, HdsTabsController } from '@kit.UIDesignKit'; import { ITheme } from '../../common/theme/ThemeModel'; import { LocalValues } from '../../common/local/LocalValues'; import { getHdsMaterialLevel } from '../../common/style/MaterialLevel'; -import { ThemeManager } from '../../common/theme/ThemeManager'; -import { Symbol } from './infra/Symbol'; -import { SettingsConstants as C } from '../../common/local/SettingsConstants' +import { HarmonyOSCapabilities } from '../../common/platform/HarmonyOSCapabilities'; @Component export struct AdaptedHdsTabs { @@ -18,6 +13,7 @@ export struct AdaptedHdsTabs { @Prop isWideScreen: boolean; @Prop tabletUi: boolean; @Prop theme: ITheme; + @StorageProp('isCompactHeight') isCompactHeight: boolean = false; // 控制器 controller: HdsTabsController = new HdsTabsController(); @@ -31,7 +27,8 @@ export struct AdaptedHdsTabs { onMiniBarClick?: () => void; build() { - if (deviceInfo.distributionOSApiVersion >= 60100 && LocalValues.getInstance().getApi23Enable()) { + if (HarmonyOSCapabilities.supportsImmersiveMaterial() && + LocalValues.getInstance().isImmersiveMaterialEnabled()) { HdsTabs({ barPosition: BarPosition.End, @@ -56,19 +53,21 @@ export struct AdaptedHdsTabs { } }) .clip(false) + .divider({ mode: DividerMode.NONE }) .barFloatingStyle({ - barSideMargin: 24, - barBottomMargin: this.isWideScreen ? 24 : 0, + barSideMargin: this.isCompactHeight ? 12 : 24, + barBottomMargin: this.isCompactHeight ? 8 : (this.isWideScreen ? 24 : 12), systemMaterialEffect: { materialType: hdsMaterial.MaterialType.IMMERSIVE, materialLevel: getHdsMaterialLevel() }, gradientMask: { maskColor: $r('app.color.background_primary_50'), - maskHeight: 42 + maskHeight: this.isCompactHeight ? 30 : 42 }, lightColor: this.theme.useWhiteGlow ? Color.White : this.theme.brand50, adaptToHandedness: this.isWideScreen, + thermoCtrl: true }) } else { if (this.isWideScreen && this.tabletUi) { diff --git a/entry/src/main/ets/pages/components/HdsTitleBarMenuButton.ets b/entry/src/main/ets/pages/components/HdsTitleBarMenuButton.ets index fdcbdea..fff56c4 100644 --- a/entry/src/main/ets/pages/components/HdsTitleBarMenuButton.ets +++ b/entry/src/main/ets/pages/components/HdsTitleBarMenuButton.ets @@ -1,8 +1,8 @@ import { SettingsConstants as C } from '../../common/local/SettingsConstants'; -import { getHdsMaterialLevelRaw } from '../../common/style/MaterialLevel'; import { BlurBackgroundModifier } from '../../utils/animation/BlurBackgroundModifier'; -import { HdsMiniBarButton } from './infra/HdsMiniBarButton'; import { Symbol } from './infra/Symbol'; +import { uiMaterial } from '@kit.ArkUI'; +import { HarmonyOSCapabilities } from '../../common/platform/HarmonyOSCapabilities'; @Component export struct HdsTitleBarMenuButton { @@ -17,11 +17,9 @@ export struct HdsTitleBarMenuButton { @Require onButtonClick: () => void; @StorageProp('customBackgroundImage') customBg: string = ''; - @StorageProp(C.KEY_ENABLE_API23) api23: boolean = false; + @StorageProp(C.KEY_ENABLE_IMMERSIVE_MATERIAL) immersiveMaterial: boolean = false; @StorageLink(C.KEY_EXPERIMENTAL_COMPONENTS) experimentalComponents: boolean = true; - @StorageProp(C.KEY_API23_MATERIAL_LEVEL) materialLevel: string = 'EXQUISITE'; - private handleClick() { if (!this.isEnabled) { return; @@ -40,24 +38,33 @@ export struct HdsTitleBarMenuButton { } build() { - if (this.experimentalComponents && this.api23) { - HdsMiniBarButton({ - barWidth: this.buttonSize, - barHeight: this.barHeight, - onButtonClick: () => { - this.handleClick(); - }, - materialLevel: getHdsMaterialLevelRaw(this.materialLevel) - }) { + if (this.experimentalComponents && this.immersiveMaterial && + HarmonyOSCapabilities.isHarmonyOS7OrLater()) { + Button({ type: ButtonType.Circle, stateEffect: true }) { this.buttonIcon() } + .width(this.buttonSize) + .height(this.buttonSize) + .backgroundColor(Color.Transparent) + .systemMaterial(new uiMaterial.ImmersiveMaterial({ + style: uiMaterial.ImmersiveStyle.THIN, + colorInvert: true, + applyShadow: true, + interactive: true, + lightEffect: {} + })) + .onClick(() => { + this.handleClick(); + }) + .enabled(this.isEnabled) } else { Button({ type: ButtonType.Circle, stateEffect: true }) { this.buttonIcon() } .width(this.buttonSize) .height(this.buttonSize) - .attributeModifier(new BlurBackgroundModifier((!!this.customBg && this.api23), $r('sys.color.comp_background_tertiary'))) + .attributeModifier(new BlurBackgroundModifier((!!this.customBg && this.immersiveMaterial), + $r('sys.color.comp_background_tertiary'))) .onClick(() => { this.handleClick(); }) diff --git a/entry/src/main/ets/pages/components/HdsTitleBarSegment.ets b/entry/src/main/ets/pages/components/HdsTitleBarSegment.ets index 33a3c3b..3ec7b69 100644 --- a/entry/src/main/ets/pages/components/HdsTitleBarSegment.ets +++ b/entry/src/main/ets/pages/components/HdsTitleBarSegment.ets @@ -1,7 +1,6 @@ -import { ItemRestriction, SegmentButton, SegmentButtonOptions, SegmentButtonTextItem } from '@kit.ArkUI'; +import { ItemRestriction, SegmentButton, SegmentButtonOptions, SegmentButtonTextItem, uiMaterial } from '@kit.ArkUI'; import { SettingsConstants as C } from '../../common/local/SettingsConstants'; -import { getHdsMaterialLevelRaw } from '../../common/style/MaterialLevel'; -import { HdsMiniBarButton } from './infra/HdsMiniBarButton'; +import { HarmonyOSCapabilities } from '../../common/platform/HarmonyOSCapabilities'; @Component export struct HdsTitleBarSegment { @@ -14,18 +13,14 @@ export struct HdsTitleBarSegment { @Prop experimentalWidth: Length = '100%'; @Prop experimentalHeight: number = 52; - @State measuredWidth: number = 0; - @StorageLink(C.KEY_EXPERIMENTAL_COMPONENTS) experimentalComponents: boolean = true; - @StorageProp(C.KEY_ENABLE_API23) api23: boolean = false; + @StorageProp(C.KEY_ENABLE_IMMERSIVE_MATERIAL) immersiveMaterial: boolean = false; @StorageProp('customBackgroundImage') customBg: string = ''; - @StorageProp(C.KEY_API23_MATERIAL_LEVEL) materialLevel: string = 'EXQUISITE'; - private buildDefaultOptions(): SegmentButtonOptions { return SegmentButtonOptions.tab({ buttons: this.buttons, - backgroundBlurStyle: (!!this.customBg && this.api23) ? BlurStyle.Thin : BlurStyle.NONE, + backgroundBlurStyle: (!!this.customBg && this.immersiveMaterial) ? BlurStyle.Thin : BlurStyle.NONE, selectedBackgroundColor: $r('sys.color.segment_button_v2_tab_selected_item_background'), backgroundColor: $r('sys.color.comp_background_tertiary'), buttonPadding: 10 @@ -38,17 +33,17 @@ export struct HdsTitleBarSegment { backgroundBlurStyle: BlurStyle.NONE, selectedBackgroundColor: $r('sys.color.segment_button_v2_tab_selected_item_background'), backgroundColor: Color.Transparent, + backgroundSystemMaterial: new uiMaterial.ImmersiveMaterial({ + style: uiMaterial.ImmersiveStyle.THIN, + colorInvert: true, + applyShadow: true, + interactive: false, + lightEffect: {} + }), buttonPadding: 10 }); } - private resolveExperimentalBarWidth(): number { - if (this.measuredWidth > 0) { - return this.measuredWidth; - } - return 10 + this.buttons.length * 84; - } - @Builder private segmentContent(options: SegmentButtonOptions) { SegmentButton({ @@ -61,23 +56,13 @@ export struct HdsTitleBarSegment { } build() { - if (this.experimentalComponents && this.api23) { + if (this.experimentalComponents && this.immersiveMaterial && + HarmonyOSCapabilities.isHarmonyOS7OrLater()) { Stack({ alignContent: Alignment.Center }) { - HdsMiniBarButton({ - barWidth: this.resolveExperimentalBarWidth(), - barHeight: this.experimentalHeight, - materialLevel: getHdsMaterialLevelRaw(this.materialLevel) - }) { - this.segmentContent(this.buildExperimentalOptions()) - } + this.segmentContent(this.buildExperimentalOptions()) } .width(this.experimentalWidth) .height(this.experimentalHeight) - .onAreaChange((_, area) => { - if (typeof area.width === 'number' && area.width > 0) { - this.measuredWidth = area.width; - } - }) .margin({ top: -8 }) } else { Stack({ alignContent: Alignment.Center }) { diff --git a/entry/src/main/ets/pages/components/SettingsComponents.ets b/entry/src/main/ets/pages/components/SettingsComponents.ets index e3575f9..29b5b51 100644 --- a/entry/src/main/ets/pages/components/SettingsComponents.ets +++ b/entry/src/main/ets/pages/components/SettingsComponents.ets @@ -107,6 +107,9 @@ export struct SettingsItem { .margin({ top: 2, bottom: 2, start: LengthMetrics.vp(2), end: LengthMetrics.vp(2) }) // 按钮离容器边缘的间隙 .backgroundColor(this.isPressed ? $r('sys.color.ohos_id_color_click_effect') : Color.Transparent) // 点击态 .animation({ duration: 200 }) // 简单的颜色过渡动画 + .focusable(true) + .focusOnTouch(true) + .hoverEffect(HoverEffect.Auto) .onTouch((event) => { if (event.type === TouchType.Down) { this.isPressed = true; @@ -159,6 +162,9 @@ export struct SettingsGridItem { .borderRadius(20) .backgroundColor(this.isPressed ? $r('sys.color.ohos_id_color_click_effect') : Color.Transparent) .animation({ duration: 200 }) + .focusable(true) + .focusOnTouch(true) + .hoverEffect(HoverEffect.Auto) .onTouch((event) => { if (event.type === TouchType.Down) { this.isPressed = true; @@ -241,6 +247,9 @@ export struct SettingsSwitch { .margin({ top: 2, bottom: 2, start: LengthMetrics.vp(2), end: LengthMetrics.vp(2) }) .backgroundColor(this.isPressed ? $r('sys.color.ohos_id_color_click_effect') : Color.Transparent) .animation({ duration: 200 }) + .focusable(true) + .focusOnTouch(true) + .hoverEffect(HoverEffect.Auto) // 按压效果 .onTouch((event) => { if (event.type === TouchType.Down) { @@ -457,6 +466,9 @@ export struct SettingsUserProfile { .width('100%') .padding({ top: 12, bottom: 12, left: 12, right: 12 }) .backgroundColor(Color.Transparent) + .focusable(true) + .focusOnTouch(true) + .hoverEffect(HoverEffect.Auto) .onClick(() => { this.handleCardClick(); }) @@ -527,6 +539,9 @@ export struct SubSettingsSwitch { .backgroundColor(this.isPressed ? $r('sys.color.ohos_id_color_click_effect') : Color.Transparent) .borderRadius(20) // 保持圆角一致 .animation({ duration: 200 }) + .focusable(true) + .focusOnTouch(true) + .hoverEffect(HoverEffect.Auto) .onTouch((event) => { if (event.type === TouchType.Down) { this.isPressed = true; @@ -612,6 +627,9 @@ export struct SettingsCheckItem { .borderRadius(20) .backgroundColor(this.isPressed ? $r('sys.color.ohos_id_color_click_effect') : Color.Transparent) .animation({ duration: 200 }) + .focusable(true) + .focusOnTouch(true) + .hoverEffect(HoverEffect.Auto) .onTouch((event) => { if (event.type === TouchType.Down) { this.isPressed = true; diff --git a/entry/src/main/ets/pages/components/TopSafeContainer.ets b/entry/src/main/ets/pages/components/TopSafeContainer.ets index e286d0b..9ddf083 100644 --- a/entry/src/main/ets/pages/components/TopSafeContainer.ets +++ b/entry/src/main/ets/pages/components/TopSafeContainer.ets @@ -12,6 +12,8 @@ import { ITheme } from '../../common/theme/ThemeModel'; export struct TopSafeContainer { @StorageProp('currentTheme') theme: ITheme = {} as ITheme; @StorageProp('customBackgroundImage') customBg: string = ''; + @StorageProp('bottomRectHeight') bottomAvoidHeight: number = 0; + @StorageProp('isCompactHeight') isCompactHeight: boolean = false; @BuilderParam content: () => void; @@ -26,6 +28,11 @@ export struct TopSafeContainer { // 外层滚动控制器 scroller: Scroller = new Scroller(); + private getBottomContentInset(): number { + const navigationInset = this.isCompactHeight ? 56 : 80; + return navigationInset + this.bottomAvoidHeight; + } + build() { if (this.scrollable) { Scroll(this.scroller) { @@ -40,6 +47,12 @@ export struct TopSafeContainer { if (this.content) { this.content() } + + // Keep the final control reachable above the floating navigation bar + // and the gesture navigation indicator on every window size. + Blank() + .width('100%') + .height(this.getBottomContentInset()) } .width('100%') @@ -75,4 +88,4 @@ export struct TopSafeContainer { .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM, SafeAreaEdge.TOP]) } } -} \ No newline at end of file +} diff --git a/entry/src/main/ets/pages/routes/ai/AIPage.ets b/entry/src/main/ets/pages/routes/ai/AIPage.ets index 07c9228..0266607 100644 --- a/entry/src/main/ets/pages/routes/ai/AIPage.ets +++ b/entry/src/main/ets/pages/routes/ai/AIPage.ets @@ -294,8 +294,8 @@ export struct AIPage { Circle() .width(this.usesExtraWideLayout() ? 42 : 38) .height(this.usesExtraWideLayout() ? 42 : 38) - .fill(active ? this.theme.brand : $r('app.color.background_tertiary')) - .fillOpacity(active ? 0.16 : 1) + .backgroundColor(active ? this.theme.brand : $r('app.color.background_tertiary')) + .opacity(active ? 0.16 : 1) if (active) { Image($r('app.media.lifi')) @@ -412,7 +412,7 @@ export struct AIPage { Circle() .width(34) .height(34) - .fill($r('app.color.background_tertiary')) + .backgroundColor($r('app.color.background_tertiary')) SymbolGlyph($r('sys.symbol.archivebox')) .fontSize(15) diff --git a/entry/src/main/ets/pages/routes/capture/CapturePage.ets b/entry/src/main/ets/pages/routes/capture/CapturePage.ets index ef7a0cb..844f7a1 100644 --- a/entry/src/main/ets/pages/routes/capture/CapturePage.ets +++ b/entry/src/main/ets/pages/routes/capture/CapturePage.ets @@ -465,7 +465,9 @@ export struct CapturePage { } } else { HdsListItem({ - customItemBuilder: this.inspirationRowBuilder.bind(this, item), + customItemBuilder: (): void => { + this.inspirationRowBuilder(item); + }, swipeActionOptions: { icons: [ { diff --git a/entry/src/main/ets/pages/routes/main/MainHomePage.ets b/entry/src/main/ets/pages/routes/main/MainHomePage.ets index 88eeaa8..9a07e87 100644 --- a/entry/src/main/ets/pages/routes/main/MainHomePage.ets +++ b/entry/src/main/ets/pages/routes/main/MainHomePage.ets @@ -774,7 +774,7 @@ export struct MainHomePage { RelativeContainer() { TaskListSection({ scroller: this.scroller, - topAvoidHeight: this.getMiniTopAvoidHeight() + this.statusBarHeight, + topAvoidHeight: this.getMiniTopAvoidHeight(), isSelectionMode: $isTaskSelectionMode, controller: this.taskController, confetti: this.confettiController @@ -808,7 +808,7 @@ export struct MainHomePage { RelativeContainer() { HabitListSection({ scroller: this.dummyScroller, - topAvoidHeight: this.getMiniTopAvoidHeight() + this.statusBarHeight, + topAvoidHeight: this.getMiniTopAvoidHeight(), isSelectionMode: $isHabitSelectionMode, controller: this.habitController, confetti: this.confettiController @@ -843,7 +843,7 @@ export struct MainHomePage { SideDashboard() .width(108) .height('100%') - .padding({ top: this.getMiniTopAvoidHeight() + this.statusBarHeight }) + .padding({ top: this.getMiniTopAvoidHeight() }) } } .width('100%') diff --git a/entry/src/main/ets/pages/routes/main/search/SearchPage.ets b/entry/src/main/ets/pages/routes/main/search/SearchPage.ets index ea87939..3d688c0 100644 --- a/entry/src/main/ets/pages/routes/main/search/SearchPage.ets +++ b/entry/src/main/ets/pages/routes/main/search/SearchPage.ets @@ -1,7 +1,7 @@ import { HdsNavDestination } from '@kit.UIDesignKit'; -import { promptAction } from '@kit.ArkUI'; +import { promptAction, uiMaterial } from '@kit.ArkUI'; import { TopSafeContainer } from '../../../components/TopSafeContainer'; import { TaskRepository } from '../../../../common/repository/TaskRepository'; import { HabitRepository } from '../../../../common/repository/HabitRepository'; @@ -22,8 +22,7 @@ import { AddTaskSheet } from '../modify/task/AddTaskSheet'; import { AddHabitSheet } from '../modify/habit/AddHabitSheet'; import { getImmersiveTitleBarStyle } from '../../../../common/style/NavStyles'; import { SettingsConstants as C } from '../../../../common/local/SettingsConstants'; -import { getHdsMaterialLevelRaw } from '../../../../common/style/MaterialLevel'; -import { HdsMiniBarButton } from '../../../components/infra/HdsMiniBarButton'; +import { HarmonyOSCapabilities } from '../../../../common/platform/HarmonyOSCapabilities'; @Builder export function SearchPageBuilder() { @@ -68,12 +67,17 @@ export struct SearchPage { @State directTaskName: string = ''; @State directTaskNote: string = ''; @State directHabitName: string = ''; - @State searchBarMeasuredWidth: number = 0; - - readonly SEARCH_INPUT_ID = 'search_input'; @StorageLink(C.KEY_EXPERIMENTAL_COMPONENTS) experimentalComponents: boolean = true; - @StorageProp(C.KEY_ENABLE_API23) api23: boolean = false; - @StorageProp(C.KEY_API23_MATERIAL_LEVEL) materialLevel: string = 'EXQUISITE'; + @StorageProp(C.KEY_ENABLE_IMMERSIVE_MATERIAL) immersiveMaterial: boolean = false; + + private usesNativeMaterialSearch(): boolean { + return this.experimentalComponents && this.immersiveMaterial && + HarmonyOSCapabilities.isHarmonyOS7OrLater(); + } + + private getSearchInputId(): string { + return this.usesNativeMaterialSearch() ? 'search_input_material' : 'search_input'; + } async aboutToAppear() { await this.logic.preloadData(); @@ -85,7 +89,7 @@ export struct SearchPage { }); }); setTimeout(() => { - focusControl.requestFocus(this.SEARCH_INPUT_ID); + focusControl.requestFocus(this.getSearchInputId()); }, 500); setTimeout(() => { this.isFirstLoad = false; }, 750); } @@ -295,20 +299,23 @@ export struct SearchPage { this.isAddHabitSheetShow = true; } - private resolveSearchBarWidth(): number { - return this.searchBarMeasuredWidth > 0 ? this.searchBarMeasuredWidth : 320; - } - @Builder private searchInputContent(isTransparentBg: boolean = false) { if (isTransparentBg) { Search({ value: this.searchText, placeholder: '搜索任务、习惯或灵感...' }) .searchButton('搜索', { fontColor: this.theme.brand }) - .id('search_input') + .id('search_input_material') .defaultFocus(true) .width('100%') .height(40) .backgroundColor(Color.Transparent) + .systemMaterial(new uiMaterial.ImmersiveMaterial({ + style: uiMaterial.ImmersiveStyle.REGULAR, + colorInvert: true, + applyShadow: true, + interactive: false, + lightEffect: {} + })) .placeholderColor($r('sys.color.ohos_id_color_text_secondary')) .onChange((value: string) => this.handleSearch(value)) .margin({ top: 0 }) @@ -325,24 +332,9 @@ export struct SearchPage { } @Builder searchBuilder() { - if (this.experimentalComponents && this.api23) { + if (this.usesNativeMaterialSearch()) { Row() { - Stack({ alignContent: Alignment.Center }) { - HdsMiniBarButton({ - barWidth: this.resolveSearchBarWidth(), - barHeight: 52, - materialLevel: getHdsMaterialLevelRaw(this.materialLevel) - }) { - this.searchInputContent(true) - } - } - .width('100%') - .height(52) - .onAreaChange((_, area) => { - if (typeof area.width === 'number' && area.width > 0) { - this.searchBarMeasuredWidth = area.width; - } - }) + this.searchInputContent(true) } .padding({ left: 64, right: 16 }) } else { diff --git a/entry/src/main/ets/pages/routes/user/settings/AppearanceSettings.ets b/entry/src/main/ets/pages/routes/user/settings/AppearanceSettings.ets index 747bbed..3f03b35 100644 --- a/entry/src/main/ets/pages/routes/user/settings/AppearanceSettings.ets +++ b/entry/src/main/ets/pages/routes/user/settings/AppearanceSettings.ets @@ -11,12 +11,12 @@ import { common, Want } from '@kit.AbilityKit'; import { getImmersiveTitleBarStyle } from '../../../../common/style/NavStyles'; import { promptAction, window } from '@kit.ArkUI'; import { ThemeSelectSheet } from './components/ThemeSelectSheet'; -import { deviceInfo } from '@kit.BasicServicesKit'; import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { ThemeManager } from '../../../../common/theme/ThemeManager'; import { ITheme } from '../../../../common/theme/ThemeModel'; import { ImageCropSheet } from './components/ImageCropSheet'; import { IconSelectSheet } from './components/IconSelectSheet'; +import { HarmonyOSCapabilities } from '../../../../common/platform/HarmonyOSCapabilities'; @Builder export function AppearanceSettingsBuilder() { @@ -51,10 +51,10 @@ struct AppearanceSettings { @StorageLink(C.KEY_CONFETTI_AFTER_FINISHED) confetti: boolean = true; @StorageLink(C.KEY_ENGLISH_SOMEWHERE) english: boolean = true; @StorageLink(C.KEY_ADD_SYMBOL_WEIGHT) weight: number = 600; - @StorageLink(C.KEY_ENABLE_API23) api23: boolean = true; + @StorageLink(C.KEY_ENABLE_IMMERSIVE_MATERIAL) immersiveMaterial: boolean = true; @StorageLink(C.KEY_EXPERIMENTAL_COMPONENTS) experimentalComponents: boolean = true; - @StorageLink(C.KEY_SHOW_MINIBAR) showMiniBar: boolean = (deviceInfo.distributionOSApiVersion >= 60100); + @StorageLink(C.KEY_SHOW_MINIBAR) showMiniBar: boolean = HarmonyOSCapabilities.supportsImmersiveMaterial(); @StorageLink(C.KEY_ENABLE_TABLET_UI) tabletUi: boolean = true; @StorageLink(C.KEY_TABLET_SIDE_DASHBOARD) tabletSd: boolean = true; @@ -225,19 +225,19 @@ struct AppearanceSettings { SettingsSection({ title: '界面显示' }) { SubSettingsSwitch({ - title: "沉浸光感视效", - subtitle: "API23以上可以打开该选项获得最佳的视觉体验 (需重启生效)。", - isOn: this.api23, - onChange: (isOn): Promise => LocalValues.getInstance().setApi23Enable(isOn) + title: $r('app.string.immersive_material_title'), + subtitle: $r('app.string.immersive_material_description'), + isOn: this.immersiveMaterial, + onChange: (isOn): Promise => LocalValues.getInstance().setImmersiveMaterialEnabled(isOn) }) - .enabled(deviceInfo.distributionOSApiVersion >= 60100) + .enabled(HarmonyOSCapabilities.supportsImmersiveMaterial()) - if (this.api23 && deviceInfo.distributionOSApiVersion >= 60100) { + if (this.immersiveMaterial && HarmonyOSCapabilities.supportsImmersiveMaterial()) { SettingsDivider() SettingsSelect({ - title: '材质等级', - subtitle: '选择沉浸光感材质等级', + title: $r('app.string.material_level_title'), + subtitle: $r('app.string.material_level_description'), options: this.menuOptions, selectedIndex: $currentSelectIndex, onChange: (index, value) => { @@ -250,15 +250,15 @@ struct AppearanceSettings { SubSettingsDivider() SubSettingsSwitch({ - title: '实验性组件', - subtitle: '开启后,主页顶部按钮将优先使用实验性的沉浸光感组件。', + title: $r('app.string.immersive_components_title'), + subtitle: $r('app.string.immersive_components_description'), isOn: this.experimentalComponents, onChange: (isOn): Promise => LocalValues.getInstance().setExperimentalComponents(isOn) }) } } - // if (this.api23 && deviceInfo.distributionOSApiVersion >= 60100) { + // The mini bar remains hidden until its product interaction is finalized. // SettingsSection() { // SubSettingsSwitch({ // title: "底栏 Lifi AI 入口", diff --git a/entry/src/main/ets/pages/routes/user/settings/components/ColorCustomizer.ets b/entry/src/main/ets/pages/routes/user/settings/components/ColorCustomizer.ets index c6c5c32..3b79618 100644 --- a/entry/src/main/ets/pages/routes/user/settings/components/ColorCustomizer.ets +++ b/entry/src/main/ets/pages/routes/user/settings/components/ColorCustomizer.ets @@ -13,6 +13,8 @@ interface ChartOption { @Component export struct ColorCustomizer { + private colorGenerator: ColorGenerator = new ColorGenerator(); + // === 状态管理 === @State hue: number = 210; // 初始蓝色 @State styleStrength: number = 50; // 0=莫兰迪, 100=生动 @@ -59,16 +61,14 @@ export struct ColorCustomizer { // 核心刷新逻辑 private updateColors() { - if (!ColorGenerator) return; - // 1. 生成 Brand - const brandSwatch = ColorGenerator.generateBrand(this.hue, this.styleStrength); + const brandSwatch = this.colorGenerator.generateBrand(this.hue, this.styleStrength); this.currentBrand = brandSwatch.color; // 2. 生成推荐的 Accents // 计算当前近似的饱和度传给生成器,保证 Accent 风格一致 const estimatedSat = 15 + (this.styleStrength / 100) * (90 - 15); - this.accentStrategies = ColorGenerator.generateAccentStrategies(this.hue, estimatedSat); + this.accentStrategies = this.colorGenerator.generateAccentStrategies(this.hue, estimatedSat); // 3. 更新当前选中的 Accent const activeStrategy = this.accentStrategies.find(s => s.key === this.selectedAccentKey); @@ -313,6 +313,8 @@ export struct ColorCustomizer { // ========================================= @Component struct HueSlider { + private colorGenerator: ColorGenerator = new ColorGenerator(); + @Link hue: number; onHueChange?: (val: number) => void; @@ -409,7 +411,7 @@ struct HueSlider { } getHueHex(h: number): string { - return ColorGenerator.hsvToHex(h, 100, 100); + return this.colorGenerator.hsvToHex(h, 100, 100); } } diff --git a/entry/src/main/ets/pages/routes/user/settings/components/ThemeSelectSheet.ets b/entry/src/main/ets/pages/routes/user/settings/components/ThemeSelectSheet.ets index 71f8448..dc69aaf 100644 --- a/entry/src/main/ets/pages/routes/user/settings/components/ThemeSelectSheet.ets +++ b/entry/src/main/ets/pages/routes/user/settings/components/ThemeSelectSheet.ets @@ -6,8 +6,8 @@ import { TopSafeContainer } from '../../../../components/TopSafeContainer'; import { FloatUpModifier } from '../../../../../utils/animation/FloatUpModifier'; import { SettingsItem, SettingsSection, SettingsDivider, SubSettingsSwitch } from '../../../../components/SettingsComponents'; import { IndicatorThemeSheet } from './IndicatorThemeSelect'; -import { deviceInfo } from '@kit.BasicServicesKit'; import { WidgetUpdater } from '../../../../../utils/WidgetUpdater'; +import { HarmonyOSCapabilities } from '../../../../../common/platform/HarmonyOSCapabilities'; interface ThemeOption { key: string; @@ -132,7 +132,7 @@ export struct ThemeSelectSheet { Column().height(12) - if (deviceInfo.distributionOSApiVersion >= 60100) { + if (HarmonyOSCapabilities.supportsImmersiveMaterial()) { SettingsSection({ bgColor: $r('sys.color.search_container_focus_color') }) { SubSettingsSwitch({ title: '白光辉映', diff --git a/entry/src/main/ets/utils/ColorGenerator.ets b/entry/src/main/ets/utils/ColorGenerator.ets index c2d1791..cad705d 100644 --- a/entry/src/main/ets/utils/ColorGenerator.ets +++ b/entry/src/main/ets/utils/ColorGenerator.ets @@ -18,7 +18,7 @@ export class ColorGenerator { * @param hue 色相 0-360 * @param styleStrength 风格强度 0-100 (0=极度莫兰迪/灰,100=极度生动) */ - static generateBrand(hue: number, styleStrength: number): ColorSwatch { + generateBrand(hue: number, styleStrength: number): ColorSwatch { // 算法微调: // 莫兰迪(Low Style): 低饱和(S:15-30),中高明度(V:85-95) -> 柔和 // 生动(High Style): 高饱和(S:70-90),中等明度(V:80-90) -> 清晰 @@ -31,7 +31,7 @@ export class ColorGenerator { const minV = 72; const maxV = 82; const v = minV + (styleStrength / 100) * (maxV - minV); - const hex = ColorGenerator.hsvToHex(hue, s, v); + const hex = this.hsvToHex(hue, s, v); // 简单判断亮度:S低V高通常是浅色 const isLight = (v > 80 && s < 40); @@ -42,7 +42,7 @@ export class ColorGenerator { /** * 生成强调色方案策略 (严格模式:禁止浅色) */ - static generateAccentStrategies(brandHue: number, brandSat: number): AccentOption[] { + generateAccentStrategies(brandHue: number, brandSat: number): AccentOption[] { const strategies: AccentOption[] = []; // 1. 基础饱和度 (S):Accent 通常比 Brand 更鲜艳一点 @@ -53,46 +53,37 @@ export class ColorGenerator { // 之前的 90 太亮了,改为 80 (或者跟 Brand 保持一致的逻辑) const baseV = 80; - // 辅助函数:针对特定色相压暗亮度 (解决黄色/青色太亮的问题) - const getSafeV = (h: number): number => { - // 黄色(40-70) 和 青色(160-200) 视觉亮度极高,必须压暗 - if ((h > 40 && h < 70) || (h > 160 && h < 190)) { - return 65; - } - return baseV; - }; - // 1. 同色系 (Monochromatic) strategies.push({ key: 'mono', name: '同频', - color: ColorGenerator.hsvToHex(brandHue, baseS, getSafeV(brandHue)) + color: this.hsvToHex(brandHue, baseS, this.getSafeValue(brandHue, baseV)) }); // 2. 邻近色 (Analogous) - 偏移 30度 const analogHue = (brandHue + 30) % 360; strategies.push({ key: 'analog', name: '邻近', - color: ColorGenerator.hsvToHex(analogHue, baseS, getSafeV(analogHue)) + color: this.hsvToHex(analogHue, baseS, this.getSafeValue(analogHue, baseV)) }); // 3. 互补色 (Complementary) - 偏移 180度 (强对比) const compHue = (brandHue + 180) % 360; strategies.push({ key: 'comp', name: '撞色', - color: ColorGenerator.hsvToHex(compHue, baseS, getSafeV(compHue)) + color: this.hsvToHex(compHue, baseS, this.getSafeValue(compHue, baseV)) }); // 4. 分裂互补 (Split) - 偏移 150度 (活跃) const splitHue = (brandHue + 150) % 360; strategies.push({ key: 'split', name: '活跃', - color: ColorGenerator.hsvToHex(splitHue, baseS, getSafeV(splitHue)) + color: this.hsvToHex(splitHue, baseS, this.getSafeValue(splitHue, baseV)) }); return strategies; } - static hsvToHex(h: number, s: number, v: number): string { + hsvToHex(h: number, s: number, v: number): string { s /= 100; v /= 100; let c = v * s; let x = c * (1 - Math.abs(((h / 60) % 2) - 1)); @@ -106,10 +97,18 @@ export class ColorGenerator { else if (240 <= h && h < 300) { r = x; g = 0; b = c; } else if (300 <= h && h < 360) { r = c; g = 0; b = x; } - const toHex = (n: number) => { - const hex = Math.round((n + m) * 255).toString(16); - return hex.length === 1 ? '0' + hex : hex; - }; - return `#FF${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase(); + return `#FF${this.channelToHex(r, m)}${this.channelToHex(g, m)}${this.channelToHex(b, m)}`.toUpperCase(); + } + + private getSafeValue(hue: number, baseValue: number): number { + if ((hue > 40 && hue < 70) || (hue > 160 && hue < 190)) { + return 65; + } + return baseValue; } -} \ No newline at end of file + + private channelToHex(channel: number, offset: number): string { + const hex = Math.round((channel + offset) * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; + } +} diff --git a/entry/src/main/resources/base/element/color.json b/entry/src/main/resources/base/element/color.json index b6815ff..067ebe5 100644 --- a/entry/src/main/resources/base/element/color.json +++ b/entry/src/main/resources/base/element/color.json @@ -78,9 +78,15 @@ { "name": "func_manual", "value": "#4DB6AC" }, { "name": "font_primary", "value": "#1A1A1A" }, { "name": "font_secondary", "value": "#666666" }, + { "name": "font_tertiary", "value": "#99000000" }, + { "name": "font_fourth", "value": "#66000000" }, + { "name": "font_on_primary", "value": "#FFFFFFFF" }, { "name": "font_emphasize", "value": "#000000" }, { "name": "icon_primary", "value": "#000000" }, { "name": "icon_secondary", "value": "#666666" }, + { "name": "icon_tertiary", "value": "#99000000" }, + { "name": "icon_fourth", "value": "#66000000" }, + { "name": "icon_on_primary", "value": "#FFFFFFFF" }, { "name": "icon_emphasize", "value": "#000000" }, { "name": "background_primary", "value": "#F2F3F5" }, diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index 59b0e31..df0e8c4 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -155,6 +155,34 @@ { "name": "hud_clock_desc", "value": "任务信息时钟透明组件" + }, + { + "name": "immersive_material_title", + "value": "沉浸光感视效" + }, + { + "name": "immersive_material_description", + "value": "HarmonyOS 6.1 及以上支持,HarmonyOS 7 可获得完整空间光感(重启后生效)" + }, + { + "name": "material_level_title", + "value": "材质等级" + }, + { + "name": "material_level_description", + "value": "自适应会根据设备性能与温度动态调整光感效果" + }, + { + "name": "immersive_components_title", + "value": "沉浸光感组件" + }, + { + "name": "immersive_components_description", + "value": "主页顶部按钮优先使用系统沉浸光感组件" + }, + { + "name": "onboarding_immersive_material_description", + "value": "在支持的设备上开启沉浸光感,并选择适合当前设备的材质等级。" } ] -} \ No newline at end of file +} diff --git a/entry/src/main/resources/dark/element/color.json b/entry/src/main/resources/dark/element/color.json index 9cc43bd..0f1874d 100644 --- a/entry/src/main/resources/dark/element/color.json +++ b/entry/src/main/resources/dark/element/color.json @@ -7,6 +7,7 @@ { "name": "brand", "value": "#FF7CA6D6"}, { "name": "brand_50", "value": "#807CA6D6"}, + { "name": "accent", "value": "#FFF08A7E" }, { "name": "brand_vibrant", "value": "#FF5C9DFF" }, { "name": "brand_vibrant_50", "value": "#805C9DFF" }, @@ -77,9 +78,15 @@ { "name": "func_manual", "value": "#80CBC4" }, { "name": "font_primary", "value": "#FFFFFF"}, { "name": "font_secondary", "value": "#B3B3B3"}, + { "name": "font_tertiary", "value": "#99FFFFFF" }, + { "name": "font_fourth", "value": "#66FFFFFF" }, + { "name": "font_on_primary", "value": "#FF000000" }, { "name": "font_emphasize", "value": "#FFFFFF"}, { "name": "icon_primary", "value": "#FFFFFF"}, { "name": "icon_secondary", "value": "#B3B3B3"}, + { "name": "icon_tertiary", "value": "#99FFFFFF" }, + { "name": "icon_fourth", "value": "#66FFFFFF" }, + { "name": "icon_on_primary", "value": "#FF000000" }, { "name": "icon_emphasize", "value": "#FFFFFF"}, { "name": "background_primary", "value": "#000000"}, { "name": "background_secondary", "value": "#191919"}, @@ -280,6 +287,8 @@ { "name": "widget_bg_grad_start", "value": "#FF1A1D21" }, { "name": "widget_bg_grad_end", "value": "#FF141619" }, - { "name": "widget_item_bg", "value": "#FF282C31" } + { "name": "widget_item_bg", "value": "#FF282C31" }, + + { "name": "item_title_font", "value": "#E6FFFFFF" } ] } diff --git a/hvigor/hvigor-config.json5 b/hvigor/hvigor-config.json5 index 3729371..fac94d0 100644 --- a/hvigor/hvigor-config.json5 +++ b/hvigor/hvigor-config.json5 @@ -1,5 +1,5 @@ { - "modelVersion": "6.0.1", + "modelVersion": "6.0.2", "dependencies": { }, "execution": { diff --git a/oh-package.json5 b/oh-package.json5 index 5944f1b..39cee73 100644 --- a/oh-package.json5 +++ b/oh-package.json5 @@ -1,5 +1,5 @@ { - "modelVersion": "6.0.1", + "modelVersion": "6.0.2", "description": "Please describe the basic information.", "dependencies": { "@mcui/mccharts": "^2.8.9", @@ -14,4 +14,4 @@ "@ohos/hamock": "1.0.0" }, "dynamicDependencies": {} -} \ No newline at end of file +}