From cc47ddaa33a7cf65f01e6edd7dfb6a80e95db241 Mon Sep 17 00:00:00 2001 From: richerfu Date: Wed, 25 Feb 2026 09:10:30 +0800 Subject: [PATCH] feat: add get avoid area method --- crates/ability/src/app.rs | 111 +++++++++++++++++- crates/ability/src/area/avoid.rs | 52 ++++++++ crates/ability/src/area/mod.rs | 2 + crates/ability/src/event.rs | 10 +- crates/ability/src/lifecycle.rs | 61 +++++++--- .../src/main/ets/ability/RustAbility.ets | 9 ++ .../src/main/ets/ability/type.ets | 2 + .../main/ets/components/DefaultXComponent.ets | 14 +++ 8 files changed, 243 insertions(+), 18 deletions(-) create mode 100644 crates/ability/src/area/avoid.rs diff --git a/crates/ability/src/app.rs b/crates/ability/src/app.rs index d42abba..9ea09dc 100644 --- a/crates/ability/src/app.rs +++ b/crates/ability/src/app.rs @@ -1,6 +1,7 @@ use std::{ cell::Cell, cell::RefCell, + collections::HashMap, fmt::Debug, rc::Rc, sync::{ @@ -11,7 +12,7 @@ use std::{ use futures_channel::oneshot; use napi_ohos::{ - bindgen_prelude::{CallbackContext, Function, JsObjectValue, Unknown}, + bindgen_prelude::{CallbackContext, Function, JsObjectValue, Object, Unknown}, threadsafe_function::ThreadsafeFunctionCallMode, Error, Result, }; @@ -22,14 +23,48 @@ use ohos_xcomponent_binding::RawWindow; use crate::{ get_helper, get_main_thread_env, get_permission_request_tsfn, unknown_to_permission_promise, - AbilityError, Configuration, Event, OpenHarmonyWaker, PermissionRequest, PermissionRequestCode, - PermissionRequestOutput, Rect, WAKER, + AbilityError, AvoidArea, AvoidAreaType, Configuration, Event, OpenHarmonyWaker, + PermissionRequest, PermissionRequestCode, PermissionRequestOutput, Rect, WAKER, }; static ID: AtomicI64 = AtomicI64::new(0); pub(crate) static HAS_EVENT: AtomicBool = AtomicBool::new(false); +const DEFAULT_AVOID_AREA_TYPES: [AvoidAreaType; 5] = [ + AvoidAreaType::System, + AvoidAreaType::Cutout, + AvoidAreaType::SystemGesture, + AvoidAreaType::Keyboard, + AvoidAreaType::NavigationIndicator, +]; + +fn parse_rect_from_object(rect: Object<'_>) -> Option { + let top = rect.get_named_property::("top").ok()?; + let left = rect.get_named_property::("left").ok()?; + let width = rect.get_named_property::("width").ok()?; + let height = rect.get_named_property::("height").ok()?; + Some(Rect { + top, + left, + width, + height, + }) +} + +fn parse_avoid_area_options(options: Object<'_>) -> Option<(AvoidAreaType, AvoidArea)> { + let area_type = AvoidAreaType::from(options.get_named_property::("type").ok()?); + let area = options.get_named_property::("area").ok()?; + let avoid_area = AvoidArea { + visible: area.get_named_property::("visible").ok()?, + left_rect: parse_rect_from_object(area.get_named_property::("leftRect").ok()?)?, + top_rect: parse_rect_from_object(area.get_named_property::("topRect").ok()?)?, + right_rect: parse_rect_from_object(area.get_named_property::("rightRect").ok()?)?, + bottom_rect: parse_rect_from_object(area.get_named_property::("bottomRect").ok()?)?, + }; + Some((area_type, avoid_area)) +} + #[derive(Clone)] pub struct OpenHarmonyAppInner { pub(crate) raw_window: Option, @@ -40,6 +75,8 @@ pub struct OpenHarmonyAppInner { id: i64, pub(crate) configuration: Configuration, pub(crate) rect: Rect, + pub(crate) window_rect: Rect, + pub(crate) avoid_areas: HashMap, } impl PartialEq for OpenHarmonyAppInner { @@ -93,6 +130,8 @@ impl OpenHarmonyAppInner { id, configuration: Default::default(), rect: Default::default(), + window_rect: Default::default(), + avoid_areas: HashMap::new(), } } @@ -132,6 +171,18 @@ impl OpenHarmonyAppInner { self.rect } + pub fn window_rect(&self) -> Rect { + self.window_rect + } + + pub fn avoid_area(&self, area_type: AvoidAreaType) -> Option { + self.avoid_areas.get(&area_type).copied() + } + + pub fn avoid_areas(&self) -> HashMap { + self.avoid_areas.clone() + } + pub fn native_window(&self) -> Option { self.raw_window } @@ -259,6 +310,60 @@ impl OpenHarmonyApp { pub fn content_rect(&self) -> Rect { self.inner.read().unwrap().content_rect() } + + pub fn window_rect(&self) -> Rect { + self.inner.read().unwrap().window_rect() + } + + fn fetch_avoid_area_from_helper( + &self, + area_type: AvoidAreaType, + ) -> Option<(AvoidAreaType, AvoidArea)> { + let helper = unsafe { get_helper() }; + let helper_borrow = helper.borrow(); + let helper_ref = helper_borrow.as_ref()?; + let env = get_main_thread_env(); + let env_borrow = env.borrow(); + let env_ref = env_borrow.as_ref()?; + let helper_object = helper_ref.get_value(env_ref).ok()?; + let get_window_avoid_area = helper_object + .get_named_property::>>("getWindowAvoidArea") + .ok()?; + let options = get_window_avoid_area.call(i32::from(area_type)).ok()?; + parse_avoid_area_options(options) + } + + fn ensure_avoid_area_cached(&self, area_type: AvoidAreaType) { + if self.inner.read().unwrap().avoid_area(area_type).is_some() { + return; + } + if let Some((fetched_type, area)) = self.fetch_avoid_area_from_helper(area_type) { + self.inner + .write() + .unwrap() + .avoid_areas + .insert(fetched_type, area); + } + } + + fn ensure_avoid_areas_cached(&self) { + if !self.inner.read().unwrap().avoid_areas.is_empty() { + return; + } + for area_type in DEFAULT_AVOID_AREA_TYPES { + self.ensure_avoid_area_cached(area_type); + } + } + + pub fn avoid_area(&self, area_type: AvoidAreaType) -> Option { + self.ensure_avoid_area_cached(area_type); + self.inner.read().unwrap().avoid_area(area_type) + } + + pub fn avoid_areas(&self) -> HashMap { + self.ensure_avoid_areas_cached(); + self.inner.read().unwrap().avoid_areas() + } pub fn native_window(&self) -> Option { self.inner.read().unwrap().native_window() } diff --git a/crates/ability/src/area/avoid.rs b/crates/ability/src/area/avoid.rs new file mode 100644 index 0000000..6d2fa93 --- /dev/null +++ b/crates/ability/src/area/avoid.rs @@ -0,0 +1,52 @@ +use crate::Rect; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AvoidAreaType { + System, + Cutout, + SystemGesture, + Keyboard, + NavigationIndicator, + Unknown(i32), +} + +impl From for AvoidAreaType { + fn from(value: i32) -> Self { + match value { + 0 => AvoidAreaType::System, + 1 => AvoidAreaType::Cutout, + 2 => AvoidAreaType::SystemGesture, + 3 => AvoidAreaType::Keyboard, + 4 => AvoidAreaType::NavigationIndicator, + _ => AvoidAreaType::Unknown(value), + } + } +} + +impl From for i32 { + fn from(value: AvoidAreaType) -> Self { + match value { + AvoidAreaType::System => 0, + AvoidAreaType::Cutout => 1, + AvoidAreaType::SystemGesture => 2, + AvoidAreaType::Keyboard => 3, + AvoidAreaType::NavigationIndicator => 4, + AvoidAreaType::Unknown(value) => value, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct AvoidArea { + pub visible: bool, + pub left_rect: Rect, + pub top_rect: Rect, + pub right_rect: Rect, + pub bottom_rect: Rect, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct AvoidAreaInfo { + pub area_type: AvoidAreaType, + pub area: AvoidArea, +} diff --git a/crates/ability/src/area/mod.rs b/crates/ability/src/area/mod.rs index c40bfa4..b5bb06d 100644 --- a/crates/ability/src/area/mod.rs +++ b/crates/ability/src/area/mod.rs @@ -1,7 +1,9 @@ +mod avoid; mod rect; mod rect_reason; mod size; +pub use avoid::*; pub use rect::*; pub use rect_reason::*; pub use size::*; diff --git a/crates/ability/src/event.rs b/crates/ability/src/event.rs index a82e1b1..8948ff2 100644 --- a/crates/ability/src/event.rs +++ b/crates/ability/src/event.rs @@ -1,6 +1,9 @@ use std::fmt::{self, Debug, Formatter}; -use crate::{Configuration, ContentRect, InputEvent, IntervalInfo, SaveLoader, SaveSaver, Size}; +use crate::{ + AvoidAreaInfo, Configuration, ContentRect, InputEvent, IntervalInfo, SaveLoader, SaveSaver, + Size, +}; #[derive(Clone)] pub enum Event<'a> { @@ -22,6 +25,10 @@ pub enum Event<'a> { /// alias window.on("windowRectChange") /// https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-window-V5#onwindowrectchange12 ContentRectChange(ContentRect), + /// window avoid area change event + /// alias window.on("avoidAreaChange") + /// https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-window-window#onavoidareachange9 + AvoidAreaChange(AvoidAreaInfo), /// window configuration changed /// alias onWindowConfigurationChanged @@ -94,6 +101,7 @@ impl<'a> Event<'a> { Event::WindowRedraw(_) => "WindowRedraw", Event::WindowResize(_) => "WindowResize", Event::ContentRectChange(_) => "ContentRectChange", + Event::AvoidAreaChange(_) => "AvoidAreaChange", Event::ConfigChanged(_) => "ConfigChanged", Event::LowMemory => "LowMemory", Event::Start => "Start", diff --git a/crates/ability/src/lifecycle.rs b/crates/ability/src/lifecycle.rs index 0f2fc14..dde19af 100644 --- a/crates/ability/src/lifecycle.rs +++ b/crates/ability/src/lifecycle.rs @@ -7,7 +7,8 @@ use napi_ohos::{ }; use crate::{ - ContentRect, Event, OpenHarmonyApp, Rect, SaveLoader, SaveSaver, Size, StageEventType, WAKER, + AvoidArea, AvoidAreaInfo, AvoidAreaType, ContentRect, Event, OpenHarmonyApp, Rect, SaveLoader, + SaveSaver, Size, StageEventType, WAKER, }; #[napi(object)] @@ -27,6 +28,7 @@ pub struct WindowStageEventCallback<'a> { pub on_window_stage_event: Function<'a, i32, ()>, pub on_window_size_change: Function<'a, Object<'a>, ()>, pub on_window_rect_change: Function<'a, Object<'a>, ()>, + pub on_avoid_area_change: Function<'a, Object<'a>, ()>, } #[napi(object)] @@ -41,6 +43,19 @@ pub struct ApplicationLifecycle<'a> { pub keyboard_event_callback: KeyboardCallback<'a>, } +fn parse_rect(rect: Object<'_>) -> Result { + let top = rect.get_named_property::("top")?; + let left = rect.get_named_property::("left")?; + let width = rect.get_named_property::("width")?; + let height = rect.get_named_property::("height")?; + Ok(Rect { + top, + left, + width, + height, + }) +} + /// create lifecycle object and return to arkts pub fn create_lifecycle_handle<'a>( env: &'a Env, @@ -158,19 +173,8 @@ pub fn create_lifecycle_handle<'a>( env.create_function_from_closure("window_rect_change", move |ctx| { let options = ctx.first_arg::()?; let reason = options.get_named_property::("reason")?; - let rect = options.get_named_property::("rect")?; - let top = rect.get_named_property::("top")?; - let left = rect.get_named_property::("left")?; - let width = rect.get_named_property::("width")?; - let height = rect.get_named_property::("height")?; - - let rect = Rect { - top, - left, - width, - height, - }; - window_rect_app.inner.write().unwrap().rect = rect; + let rect = parse_rect(options.get_named_property::("rect")?)?; + window_rect_app.inner.write().unwrap().window_rect = rect; if let Some(ref mut h) = *window_rect_app.event_loop.borrow_mut() { h(Event::ContentRectChange(ContentRect { @@ -181,6 +185,34 @@ pub fn create_lifecycle_handle<'a>( Ok(()) })?; + let avoid_area_change_app = app.clone(); + let avoid_area_change = env.create_function_from_closure("avoid_area_change", move |ctx| { + let options = ctx.first_arg::()?; + let area_type = AvoidAreaType::from(options.get_named_property::("type")?); + let area = options.get_named_property::("area")?; + let visible = area.get_named_property::("visible")?; + let avoid_area = AvoidArea { + visible, + left_rect: parse_rect(area.get_named_property::("leftRect")?)?, + top_rect: parse_rect(area.get_named_property::("topRect")?)?, + right_rect: parse_rect(area.get_named_property::("rightRect")?)?, + bottom_rect: parse_rect(area.get_named_property::("bottomRect")?)?, + }; + + { + let mut inner = avoid_area_change_app.inner.write().unwrap(); + inner.avoid_areas.insert(area_type, avoid_area); + } + + if let Some(ref mut h) = *avoid_area_change_app.event_loop.borrow_mut() { + h(Event::AvoidAreaChange(AvoidAreaInfo { + area_type, + area: avoid_area, + })) + } + Ok(()) + })?; + let on_window_stage_create_app = app.clone(); let on_window_stage_create = env.create_function_from_closure("on_ability_create", move |_ctx| { @@ -267,6 +299,7 @@ pub fn create_lifecycle_handle<'a>( on_ability_restore_state, on_window_rect_change: window_rect_change, on_window_size_change: window_resize, + on_avoid_area_change: avoid_area_change, on_window_stage_event: window_stage_event, }, keyboard_event_callback: KeyboardCallback { diff --git a/rust_ability/ability_rust/src/main/ets/ability/RustAbility.ets b/rust_ability/ability_rust/src/main/ets/ability/RustAbility.ets index 09fae58..0d3aea7 100644 --- a/rust_ability/ability_rust/src/main/ets/ability/RustAbility.ets +++ b/rust_ability/ability_rust/src/main/ets/ability/RustAbility.ets @@ -62,6 +62,15 @@ export class RustAbility extends UIAbility { }); let win = await windowStage.getMainWindow(); + win.on("windowSizeChange", (size: window.Size) => { + this.lifecycle?.windowStageEventCallback.onWindowSizeChange(size); + }); + win.on("windowRectChange", (options: window.RectChangeOptions) => { + this.lifecycle?.windowStageEventCallback.onWindowRectChange(options); + }); + win.on("avoidAreaChange", (options: window.AvoidAreaOptions) => { + this.lifecycle?.windowStageEventCallback.onAvoidAreaChange(options); + }); win.on("keyboardHeightChange", (height) => { this.lifecycle?.keyboardEventCallback.onKeyboardHeightChange(height); }); diff --git a/rust_ability/ability_rust/src/main/ets/ability/type.ets b/rust_ability/ability_rust/src/main/ets/ability/type.ets index 25a4c6b..b3445f8 100644 --- a/rust_ability/ability_rust/src/main/ets/ability/type.ets +++ b/rust_ability/ability_rust/src/main/ets/ability/type.ets @@ -24,6 +24,7 @@ export interface WindowStageEventCallback { onWindowStageEvent: (arg: number) => void; onWindowSizeChange: (arg: object) => void; onWindowRectChange: (arg: object) => void; + onAvoidAreaChange: (arg: object) => void; } export interface WebViewComponentEventCallback { @@ -72,4 +73,5 @@ export interface ArkHelper { exit: (code: number) => void; createWebview: (data: WebViewInitData) => Object; requestPermission: (permission: string | string[]) => Promise; + getWindowAvoidArea: (type: number) => Object | undefined; } diff --git a/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets b/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets index 71ea587..d314cb8 100644 --- a/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets +++ b/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets @@ -4,6 +4,7 @@ import { exit } from "../helper"; import { Loadable } from "../helper/loadable"; import { requestPermission } from "../helper/permission"; import common from "@ohos.app.ability.common"; +import window from "@ohos.window"; import { RustWebviewNodeController, WebviewStyle, @@ -22,6 +23,19 @@ export struct DefaultXComponent { const context = this.getUIContext().getHostContext() as common.UIAbilityContext; return await requestPermission(context, permission); }, + getWindowAvoidArea: (type: number): Object | undefined => { + try { + const context = this.getUIContext().getHostContext() as common.UIAbilityContext; + const win = context.windowStage.getMainWindowSync(); + const avoidType = type as window.AvoidAreaType; + return { + type: avoidType, + area: win.getWindowAvoidArea(avoidType), + } as Object; + } catch (_) { + return undefined; + } + }, createWebview: (data: NativeWebViewInitData) => { const initScripts: ScriptItem[] = (data?.initializationScripts || []).map((i) => { return {