Implement soft keyboard visibility on Android
- Added `SoftKeyboardContext` enum and support for calculating keyboard insets from both virtual and platform sources - Updated `AppContext` to provide `soft_keyboard_rect` for determining visible keyboard area - Adjusted UI rendering to shift content when input boxes intersect with the soft keyboard, preventing overlap - Modified `MainActivity` and Android manifest to use `windowSoftInputMode="adjustResize"` and updated window inset handling - Introduced helper functions (`include_input`, `input_rect`, `clear_input_rect`) in `notedeck_ui` for tracking focused input boxes - Fixed Android JNI keyboard height reporting to clamp negative values Together, these changes allow the app to correctly detect and respond to soft keyboard visibility on Android, ensuring input fields remain accessible when typing. Fixes: https://github.com/damus-io/notedeck/issues/946 Fixes: https://github.com/damus-io/notedeck/issues/1043
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode|fontScale|smallestScreenSize"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
>
|
||||
<intent-filter>
|
||||
@@ -37,4 +38,4 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -26,12 +26,15 @@ public class MainActivity extends GameActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Shrink view so it does not get covered by insets.
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setupInsets();
|
||||
|
||||
//setupFullscreen()
|
||||
keyboardHelper = new KeyboardHeightHelper(this);
|
||||
|
||||
//keyboardHelper = new KeyboardHeightHelper(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
}
|
||||
|
||||
private void setupFullscreen() {
|
||||
@@ -61,6 +64,18 @@ public class MainActivity extends GameActivity {
|
||||
}
|
||||
|
||||
private void setupInsets() {
|
||||
|
||||
// NOTE(jb55): This is needed for keyboard visibility. Without this the
|
||||
// window still gets the right insets, but they’re consumed before they
|
||||
// reach the NDK side.
|
||||
//WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
|
||||
// NOTE(jb55): This is needed for keyboard visibility. If the bars are
|
||||
// permanently gone, Android routes the keyboard over the GL surface and
|
||||
// doesn’t change insets.
|
||||
//WindowInsetsControllerCompat ic = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
||||
//ic.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
|
||||
View content = getContent();
|
||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
|
||||
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
@@ -72,12 +87,13 @@ public class MainActivity extends GameActivity {
|
||||
mlp.rightMargin = insets.right;
|
||||
v.setLayoutParams(mlp);
|
||||
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
return windowInsets;
|
||||
});
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), true);
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
}
|
||||
|
||||
/*
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
@@ -95,6 +111,7 @@ public class MainActivity extends GameActivity {
|
||||
super.onDestroy();
|
||||
keyboardHelper.close();
|
||||
}
|
||||
*/
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
|
||||
@@ -17,7 +17,7 @@ pub async fn android_main(app: AndroidApp) {
|
||||
//std::env::set_var("DAVE_MODEL", "hhao/qwen2.5-coder-tools:latest");
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
"egui=debug,egui-winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug",
|
||||
"egui=debug,egui-winit=debug,winit=debug,notedeck=debug,notedeck_columns=debug,notedeck_chrome=debug,enostr=debug,android_activity=debug",
|
||||
);
|
||||
|
||||
//std::env::set_var(
|
||||
@@ -57,7 +57,7 @@ pub async fn android_main(app: AndroidApp) {
|
||||
|
||||
options.android_app = Some(app.clone());
|
||||
|
||||
let app_args = get_app_args(app);
|
||||
let app_args = get_app_args(app.clone());
|
||||
|
||||
let _res = eframe::run_native(
|
||||
"Damus Notedeck",
|
||||
@@ -65,6 +65,7 @@ pub async fn android_main(app: AndroidApp) {
|
||||
Box::new(move |cc| {
|
||||
let ctx = &cc.egui_ctx;
|
||||
let mut notedeck = Notedeck::new(ctx, path, &app_args);
|
||||
notedeck.set_android_context(app.clone());
|
||||
notedeck.setup(ctx);
|
||||
let chrome = Chrome::new_with_apps(cc, &app_args, &mut notedeck)?;
|
||||
notedeck.set_app(chrome);
|
||||
|
||||
@@ -8,6 +8,7 @@ use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
use nostrdb::{ProfileRecord, Transaction};
|
||||
use notedeck::Error;
|
||||
use notedeck::SoftKeyboardContext;
|
||||
use notedeck::{
|
||||
tr, App, AppAction, AppContext, Localization, Notedeck, NotedeckOptions, NotedeckTextStyle,
|
||||
UserAccount, WalletType,
|
||||
@@ -267,9 +268,40 @@ impl Chrome {
|
||||
let amt_open = self.amount_open(ui);
|
||||
let r = self.panel(ctx, StripBuilder::new(ui), amt_open);
|
||||
|
||||
// virtual keyboard
|
||||
if self.options.contains(ChromeOptions::VirtualKeyboard) {
|
||||
virtual_keyboard_ui(ui);
|
||||
let skb_ctx = if self.options.contains(ChromeOptions::VirtualKeyboard) {
|
||||
SoftKeyboardContext::Virtual
|
||||
} else {
|
||||
SoftKeyboardContext::Platform {
|
||||
ppp: ui.ctx().pixels_per_point(),
|
||||
}
|
||||
};
|
||||
|
||||
// move screen up if virtual keyboard intersects with input_rect
|
||||
let screen_rect = ui.ctx().screen_rect();
|
||||
let mut keyboard_height = 0.0;
|
||||
if let Some(vkb_rect) = ctx.soft_keyboard_rect(screen_rect, skb_ctx.clone()) {
|
||||
if let SoftKeyboardContext::Virtual = skb_ctx {
|
||||
virtual_keyboard_ui(ui, vkb_rect);
|
||||
}
|
||||
if let Some(input_rect) = notedeck_ui::input_rect(ui) {
|
||||
if input_rect.intersects(vkb_rect) {
|
||||
tracing::debug!("screen:{screen_rect} skb:{vkb_rect}");
|
||||
keyboard_height = vkb_rect.height();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// clear last input box position state
|
||||
notedeck_ui::clear_input_rect(ui);
|
||||
}
|
||||
|
||||
let anim_height =
|
||||
ui.ctx()
|
||||
.animate_value_with_time(egui::Id::new("keyboard_anim"), keyboard_height, 0.1);
|
||||
if anim_height > 0.0 {
|
||||
ui.ctx().transform_layer_shapes(
|
||||
ui.layer_id(),
|
||||
egui::emath::TSTransform::from_translation(egui::Vec2::new(0.0, -anim_height)),
|
||||
);
|
||||
}
|
||||
|
||||
r
|
||||
@@ -912,12 +944,7 @@ fn repaint_causes_window(ui: &mut egui::Ui, causes: &HashMap<egui::RepaintCause,
|
||||
});
|
||||
}
|
||||
|
||||
fn virtual_keyboard_ui(ui: &mut egui::Ui) {
|
||||
let height = notedeck::platform::virtual_keyboard_height(true);
|
||||
let screen_rect = ui.ctx().screen_rect();
|
||||
|
||||
let min = egui::Pos2::new(0.0, screen_rect.max.y - height as f32);
|
||||
let rect = Rect::from_min_max(min, screen_rect.max);
|
||||
fn virtual_keyboard_ui(ui: &mut egui::Ui, rect: egui::Rect) {
|
||||
let painter = ui.painter_at(rect);
|
||||
|
||||
painter.rect_filled(rect, 0.0, Color32::from_black_alpha(200));
|
||||
|
||||
Reference in New Issue
Block a user