Skip to content

Commit 3683cf8

Browse files
authored
Android Insets like iOS (#10099)
ChangeLog: Implemented Android support to mimic the iOS API.
1 parent 89aaadf commit 3683cf8

File tree

3 files changed

+238
-24
lines changed

3 files changed

+238
-24
lines changed

internal/backends/android-activity/androidwindowadapter.rs

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use android_activity::input::{
77
ButtonState, InputEvent, KeyAction, Keycode, MotionAction, MotionEvent,
88
};
99
use android_activity::{InputStatus, MainEvent, PollEvent};
10-
use i_slint_core::api::{LogicalPosition, PhysicalPosition, PhysicalSize, PlatformError, Window};
10+
use i_slint_core::api::{
11+
LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, PlatformError, Window,
12+
};
1113
use i_slint_core::items::ColorScheme;
1214
use i_slint_core::lengths::PhysicalInset;
1315
use i_slint_core::platform::{
@@ -168,15 +170,7 @@ impl i_slint_core::window::WindowAdapterInternal for AndroidWindowAdapter {
168170
if self.fullscreen.get() {
169171
Default::default()
170172
} else {
171-
let (offset, size) =
172-
self.java_helper.get_view_rect().unwrap_or_else(|e| print_jni_error(&self.app, e));
173-
let win_size = self.size();
174-
PhysicalInset {
175-
left: offset.x.max(0),
176-
top: offset.y.max(0),
177-
right: win_size.width.saturating_sub(size.width + (offset.x as u32)) as i32,
178-
bottom: win_size.height.saturating_sub(size.height + (offset.y as u32)) as i32,
179-
}
173+
self.java_helper.get_safe_area().unwrap_or_else(|e| print_jni_error(&self.app, e))
180174
}
181175
}
182176
}
@@ -516,6 +510,57 @@ impl AndroidWindowAdapter {
516510
) {
517511
*self.requested_graphics_api.borrow_mut() = requested_graphics_api;
518512
}
513+
514+
pub(super) fn update_window_insets(
515+
&self,
516+
window_origin: PhysicalPosition,
517+
window_size: PhysicalSize,
518+
safe_area: PhysicalInset,
519+
keyboard: PhysicalInset,
520+
) {
521+
let scale_factor = self.window.scale_factor();
522+
self.window.dispatch_event(WindowEvent::SafeAreaChanged {
523+
inset: safe_area.to_logical(scale_factor),
524+
token: i_slint_core::InternalToken,
525+
});
526+
527+
let window_origin = window_origin.to_logical(scale_factor);
528+
let window_size = window_size.to_logical(scale_factor);
529+
let keyboard = keyboard.to_logical(scale_factor);
530+
531+
// Assume that the keyboard is only on one side.
532+
let rect = if keyboard.bottom > (0 as i_slint_core::Coord) {
533+
(
534+
LogicalPosition::new(
535+
window_origin.x,
536+
window_origin.y + window_size.height - keyboard.bottom as i_slint_core::Coord,
537+
),
538+
LogicalSize::new(window_size.width, keyboard.bottom as _),
539+
)
540+
} else if keyboard.top > (0 as i_slint_core::Coord) {
541+
(
542+
LogicalPosition::new(window_origin.x, window_origin.y),
543+
LogicalSize::new(window_size.width, keyboard.top as _),
544+
)
545+
} else if keyboard.left > (0 as i_slint_core::Coord) {
546+
(
547+
LogicalPosition::new(window_origin.x, window_origin.y),
548+
LogicalSize::new(keyboard.left as _, window_size.height),
549+
)
550+
} else if keyboard.right > (0 as i_slint_core::Coord) {
551+
(
552+
LogicalPosition::new(
553+
window_origin.x + window_size.width - keyboard.right as i_slint_core::Coord,
554+
window_origin.y,
555+
),
556+
LogicalSize::new(keyboard.right as _, window_size.height),
557+
)
558+
} else {
559+
Default::default()
560+
};
561+
562+
self.window.set_virtual_keyboard(rect.0, rect.1, i_slint_core::InternalToken);
563+
}
519564
}
520565

521566
fn long_press_timeout() {

internal/backends/android-activity/java/SlintAndroidJavaHelper.java

Lines changed: 121 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
import android.view.MenuItem;
99
import android.view.MotionEvent;
1010
import android.view.View;
11+
import android.view.ViewTreeObserver;
1112
import android.view.WindowInsets;
13+
import android.view.WindowInsetsAnimation;
14+
import android.view.WindowMetrics;
1215
import android.view.inputmethod.EditorInfo;
1316
import android.view.inputmethod.InputConnection;
1417
import android.content.ClipData;
@@ -18,6 +21,7 @@
1821
import android.content.res.TypedArray;
1922
import android.graphics.BlendMode;
2023
import android.graphics.BlendModeColorFilter;
24+
import android.graphics.Insets;
2125
import android.graphics.PorterDuff;
2226
import android.graphics.Rect;
2327
import android.graphics.drawable.Drawable;
@@ -31,6 +35,7 @@
3135
import android.widget.ImageView;
3236
import android.widget.PopupWindow;
3337
import android.view.inputmethod.BaseInputConnection;
38+
import android.os.Build;
3439

3540
class InputHandle extends ImageView {
3641
private PopupWindow mPopupWindow;
@@ -413,6 +418,87 @@ public void run() {
413418
mInputView.setVisibility(View.VISIBLE);
414419
}
415420
});
421+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
422+
activity.getWindow().getDecorView().getRootView()
423+
.setWindowInsetsAnimationCallback(
424+
new WindowInsetsAnimation.Callback(
425+
WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
426+
@Override
427+
public WindowInsets onProgress(WindowInsets insets,
428+
java.util.List<WindowInsetsAnimation> runningAnimations) {
429+
mActivity.runOnUiThread(new Runnable() {
430+
@Override
431+
public void run() {
432+
Insets safeAreaInsets = insets.getInsets(WindowInsets.Type.systemBars());
433+
Insets keyboardAreaInsets = insets.getInsets(WindowInsets.Type.ime());
434+
Rect windowRect = get_view_rect();
435+
436+
SlintAndroidJavaHelper.setInsets(
437+
windowRect.top, windowRect.left,
438+
windowRect.bottom, windowRect.right,
439+
safeAreaInsets.top, safeAreaInsets.left,
440+
safeAreaInsets.bottom, safeAreaInsets.right,
441+
keyboardAreaInsets.top, keyboardAreaInsets.left,
442+
keyboardAreaInsets.bottom, keyboardAreaInsets.right);
443+
}
444+
});
445+
return insets;
446+
}
447+
});
448+
} else {
449+
activity.getWindow().getDecorView().getRootView().getViewTreeObserver()
450+
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
451+
@Override
452+
public void onGlobalLayout() {
453+
mActivity.runOnUiThread(new Runnable() {
454+
@Override
455+
public void run() {
456+
Rect windowRect = get_view_rect();
457+
Rect safeAreaRect = get_safe_area();
458+
459+
// This is only an approximation, because SDK level < 30 doesn't provide
460+
// a way to get the keyboard area directly.
461+
Rect visibleRect = new Rect();
462+
mActivity.getWindow().getDecorView().getRootView()
463+
.getWindowVisibleDisplayFrame(visibleRect);
464+
int keyboardBottom = windowRect.bottom - visibleRect.bottom;
465+
int keyboardLeft = windowRect.left - visibleRect.left;
466+
int keyboardTop = windowRect.top - visibleRect.top;
467+
int keyboardRight = windowRect.right - visibleRect.right;
468+
int max = Math.max(keyboardBottom, Math.max(keyboardLeft,
469+
Math.max(keyboardTop, keyboardRight)));
470+
471+
// only take the largest value (it's probably always going to be bottom)
472+
if (max == keyboardBottom) {
473+
keyboardTop = 0;
474+
keyboardLeft = 0;
475+
keyboardRight = 0;
476+
} else if (max == keyboardLeft) {
477+
keyboardTop = 0;
478+
keyboardRight = 0;
479+
keyboardBottom = 0;
480+
} else if (max == keyboardTop) {
481+
keyboardLeft = 0;
482+
keyboardRight = 0;
483+
keyboardBottom = 0;
484+
} else {
485+
keyboardTop = 0;
486+
keyboardLeft = 0;
487+
keyboardBottom = 0;
488+
}
489+
490+
SlintAndroidJavaHelper.setInsets(
491+
windowRect.top, windowRect.left,
492+
windowRect.bottom, windowRect.right,
493+
safeAreaRect.top, safeAreaRect.left,
494+
safeAreaRect.bottom, safeAreaRect.right,
495+
keyboardTop, keyboardLeft,
496+
keyboardBottom, keyboardRight);
497+
}
498+
});
499+
}
500+
});
501+
}
416502
}
417503

418504
public void show_keyboard() {
@@ -447,6 +533,10 @@ static public native void updateText(String text, int cursorPosition, int anchor
447533

448534
static public native void popupMenuAction(int id);
449535

536+
static public native void setInsets(int window_top, int window_left, int window_bottom, int window_right,
537+
int safe_area_top, int safe_area_left, int safe_area_bottom, int safe_area_right,
538+
int keyboard_top, int keyboard_left, int keyboard_bottom, int keyboard_right);
539+
450540
public void set_imm_data(String text, int cursor_position, int anchor_position, int preedit_start, int preedit_end,
451541
int cur_x, int cur_y, int anchor_x, int anchor_y, int cursor_height, int input_type,
452542
boolean show_cursor_handles) {
@@ -485,22 +575,39 @@ public int color_scheme() {
485575
return nightModeFlags;
486576
}
487577

488-
// Get the geometry of the view minus the system bars and the keyboard
578+
// Get the size of the window
489579
public Rect get_view_rect() {
490-
Rect rect = new Rect();
491-
mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
492-
// Note: `View.getRootWindowInsets` requires API level 23 or above
493-
WindowInsets insets = mActivity.getWindow().getDecorView().getRootView().getRootWindowInsets();
494-
if (insets != null) {
495-
int dx = rect.left - insets.getSystemWindowInsetLeft();
496-
int dy = rect.top - insets.getSystemWindowInsetTop();
497-
498-
rect.left -= dx;
499-
rect.right -= dx;
500-
rect.top -= dy;
501-
rect.bottom -= dy;
580+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
581+
// On Android 11 and above, we can get the window bounds directly
582+
WindowMetrics metrics = mActivity.getWindowManager().getCurrentWindowMetrics();
583+
return metrics.getBounds();
584+
} else {
585+
View rootView = mActivity.getWindow().getDecorView().getRootView();
586+
return new Rect(rootView.getLeft(), rootView.getTop(), rootView.getRight(), rootView.getBottom());
587+
}
588+
}
589+
590+
// On SDK level < 30, returns the inset for the safe area and the keyboard.
591+
// On SDK level >= 30, returns the inset for the safe area only.
592+
public Rect get_safe_area() {
593+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
594+
WindowMetrics metrics = mActivity.getWindowManager().getCurrentWindowMetrics();
595+
WindowInsets insets = metrics.getWindowInsets();
596+
Insets systemBars = insets.getInsets(WindowInsets.Type.systemBars());
597+
return new Rect(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
598+
} else {
599+
View decorView = mActivity.getWindow().getDecorView();
600+
// Note: `View.getRootWindowInsets` requires API level 23 or above
601+
WindowInsets insets = decorView.getRootView().getRootWindowInsets();
602+
if (insets != null) {
603+
return new Rect(
604+
insets.getStableInsetLeft(),
605+
insets.getStableInsetTop(),
606+
insets.getStableInsetRight(),
607+
insets.getStableInsetBottom());
608+
}
609+
return new Rect(0, 0, 0, 0);
502610
}
503-
return rect;
504611
}
505612

506613
public void show_action_menu() {

internal/backends/android-activity/javahelper.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use super::*;
55
use i_slint_core::api::{PhysicalPosition, PhysicalSize};
66
use i_slint_core::graphics::{euclid, Color};
77
use i_slint_core::items::{ColorScheme, InputType};
8+
use i_slint_core::lengths::PhysicalInset;
89
use i_slint_core::platform::WindowAdapter;
910
use i_slint_core::SharedString;
1011
use jni::objects::{JClass, JObject, JString, JValue};
@@ -112,6 +113,11 @@ fn load_java_helper(app: &AndroidApp) -> Result<jni::objects::GlobalRef, jni::er
112113
sig: "(I)V".into(),
113114
fn_ptr: Java_SlintAndroidJavaHelper_popupMenuAction as *mut _,
114115
},
116+
jni::NativeMethod {
117+
name: "setInsets".into(),
118+
sig: "(IIIIIIIIIIII)V".into(),
119+
fn_ptr: Java_SlintAndroidJavaHelper_setInsets as *mut _,
120+
},
115121
];
116122
env.register_native_methods(&helper_class, &methods)?;
117123

@@ -255,6 +261,19 @@ impl JavaHelper {
255261
})
256262
}
257263

264+
pub fn get_safe_area(&self) -> Result<PhysicalInset, jni::errors::Error> {
265+
self.with_jni_env(|env, helper| {
266+
let rect =
267+
env.call_method(helper, "get_safe_area", "()Landroid/graphics/Rect;", &[])?.l()?;
268+
let rect = env.auto_local(rect);
269+
let left = env.get_field(&rect, "left", "I")?.i()?;
270+
let top = env.get_field(&rect, "top", "I")?.i()?;
271+
let right = env.get_field(&rect, "right", "I")?.i()?;
272+
let bottom = env.get_field(&rect, "bottom", "I")?.i()?;
273+
Ok(PhysicalInset::new(top, bottom, left, right))
274+
})
275+
}
276+
258277
pub fn set_handle_color(&self, color: Color) -> Result<(), jni::errors::Error> {
259278
self.with_jni_env(|env, helper| {
260279
env.call_method(
@@ -512,6 +531,49 @@ extern "system" fn Java_SlintAndroidJavaHelper_popupMenuAction(
512531
.unwrap()
513532
}
514533

534+
#[unsafe(no_mangle)]
535+
extern "system" fn Java_SlintAndroidJavaHelper_setInsets(
536+
_env: JNIEnv,
537+
_class: JClass,
538+
window_top: jint,
539+
window_left: jint,
540+
window_bottom: jint,
541+
window_right: jint,
542+
safe_area_top: jint,
543+
safe_area_left: jint,
544+
safe_area_bottom: jint,
545+
safe_area_right: jint,
546+
keyboard_top: jint,
547+
keyboard_left: jint,
548+
keyboard_bottom: jint,
549+
keyboard_right: jint,
550+
) {
551+
i_slint_core::api::invoke_from_event_loop(move || {
552+
if let Some(w) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
553+
w.update_window_insets(
554+
PhysicalPosition::new(window_left as _, window_top as _),
555+
PhysicalSize::new(
556+
(window_right - window_left) as _,
557+
(window_bottom - window_top) as _,
558+
),
559+
PhysicalInset::new(
560+
safe_area_top as _,
561+
safe_area_bottom as _,
562+
safe_area_left as _,
563+
safe_area_right as _,
564+
),
565+
PhysicalInset::new(
566+
keyboard_top as _,
567+
keyboard_bottom as _,
568+
keyboard_left as _,
569+
keyboard_right as _,
570+
),
571+
);
572+
}
573+
})
574+
.unwrap()
575+
}
576+
515577
/// Workaround before <https://github.com/jni-rs/jni-rs/pull/557> is merged.
516578
fn jni_get_string<'e, 'a>(
517579
obj: &'a JObject<'a>,

0 commit comments

Comments
 (0)