android: capture current keyboard height

expose a new virtual_keyboard_height function under notedeck::platform::android

which gets the current height of the virtual keyboard. We can use this
to tranlate the view out of the way

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-03-11 10:09:39 -07:00
parent 09ad354d24
commit bd85233120
10 changed files with 348 additions and 20 deletions

1
Cargo.lock generated
View File

@@ -2783,6 +2783,7 @@ dependencies = [
"enostr",
"hex",
"image",
"jni",
"mime_guess",
"nostrdb",
"poll-promise",

View File

@@ -61,6 +61,7 @@ sha2 = "0.10.8"
bincode = "1.3.3"
mime_guess = "2.0.5"
pretty_assertions = "1.4.1"
jni = "0.21.1"
[profile.small]
inherits = 'release'

View File

@@ -6,6 +6,7 @@ description = "The APIs and data structures used by notedeck apps"
[dependencies]
nostrdb = { workspace = true }
jni = { workspace = true }
url = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
@@ -35,5 +36,8 @@ tempfile = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
security-framework = { workspace = true }
[target.'cfg(target_os = "android")'.dependencies]
jni = { workspace = true }
[features]
profiling = ["puffin", "puffin_egui"]

View File

@@ -11,6 +11,7 @@ mod muted;
pub mod note;
mod notecache;
mod persist;
pub mod platform;
pub mod relay_debug;
pub mod relayspec;
mod result;

View File

@@ -0,0 +1,26 @@
use std::sync::atomic::{AtomicI32, Ordering};
use tracing::debug;
// Thread-safe static global
static KEYBOARD_HEIGHT: AtomicI32 = AtomicI32::new(0);
/// This function is called by our main notedeck android activity when the
/// keyboard height changes. You can use [`virtual_keyboard_height`] to access
/// this
#[no_mangle]
pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHeightChanged(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
height: jni::sys::jint,
) {
debug!("updating virtual keyboard height {}", height);
// Convert and store atomically
KEYBOARD_HEIGHT.store(height as i32, Ordering::SeqCst);
}
/// Gets the current Android virtual keyboard height. Useful for transforming
/// the view
pub fn virtual_keyboard_height() -> i32 {
KEYBOARD_HEIGHT.load(Ordering::SeqCst)
}

View File

@@ -0,0 +1,2 @@
#[cfg(target_os = "android")]
pub mod android;

View File

@@ -0,0 +1,48 @@
package com.damus.notedeck;
import android.app.Activity;
import android.content.res.Configuration;
import android.util.Log;
import android.view.View;
public class KeyboardHeightHelper {
private static final String TAG = "KeyboardHeightHelper";
private KeyboardHeightProvider keyboardHeightProvider;
private Activity activity;
// Static JNI method not tied to any specific activity
private static native void nativeKeyboardHeightChanged(int height);
public KeyboardHeightHelper(Activity activity) {
this.activity = activity;
keyboardHeightProvider = new KeyboardHeightProvider(activity);
// Create observer implementation
KeyboardHeightObserver observer = (height, orientation) -> {
Log.d(TAG, "Keyboard height: " + height + "px, orientation: " +
(orientation == Configuration.ORIENTATION_PORTRAIT ? "portrait" : "landscape"));
// Call the generic native method
nativeKeyboardHeightChanged(height);
};
// Set up the provider
keyboardHeightProvider.setKeyboardHeightObserver(observer);
}
public void start() {
// Start the keyboard height provider after the view is ready
final View contentView = activity.findViewById(android.R.id.content);
contentView.post(() -> {
keyboardHeightProvider.start();
});
}
public void stop() {
keyboardHeightProvider.setKeyboardHeightObserver(null);
}
public void close() {
keyboardHeightProvider.close();
}
}

View File

@@ -0,0 +1,35 @@
/*
* This file is part of Siebe Projects samples.
*
* Siebe Projects samples is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Siebe Projects samples is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Lesser GNU General Public License for more details.
*
* You should have received a copy of the Lesser GNU General Public License
* along with Siebe Projects samples. If not, see <http://www.gnu.org/licenses/>.
*/
package com.damus.notedeck;
/**
* The observer that will be notified when the height of
* the keyboard has changed
*/
public interface KeyboardHeightObserver {
/**
* Called when the keyboard height has changed, 0 means keyboard is closed,
* >= 1 means keyboard is opened.
*
* @param height The height of the keyboard in pixels
* @param orientation The orientation either: Configuration.ORIENTATION_PORTRAIT or
* Configuration.ORIENTATION_LANDSCAPE
*/
void onKeyboardHeightChanged(int height, int orientation);
}

View File

@@ -0,0 +1,174 @@
/*
* This file is part of Siebe Projects samples.
*
* Siebe Projects samples is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Siebe Projects samples is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Lesser GNU General Public License for more details.
*
* You should have received a copy of the Lesser GNU General Public License
* along with Siebe Projects samples. If not, see <http://www.gnu.org/licenses/>.
*/
package com.damus.notedeck;
import android.app.Activity;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.Log;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.WindowManager.LayoutParams;
import android.widget.PopupWindow;
/**
* The keyboard height provider, this class uses a PopupWindow
* to calculate the window height when the floating keyboard is opened and closed.
*/
public class KeyboardHeightProvider extends PopupWindow {
/** The tag for logging purposes */
private final static String TAG = "sample_KeyboardHeightProvider";
/** The keyboard height observer */
private KeyboardHeightObserver observer;
/** The cached landscape height of the keyboard */
private int keyboardLandscapeHeight;
/** The cached portrait height of the keyboard */
private int keyboardPortraitHeight;
/** The view that is used to calculate the keyboard height */
private View popupView;
/** The parent view */
private View parentView;
/** The root activity that uses this KeyboardHeightProvider */
private Activity activity;
/**
* Construct a new KeyboardHeightProvider
*
* @param activity The parent activity
*/
public KeyboardHeightProvider(Activity activity) {
super(activity);
this.activity = activity;
//LayoutInflater inflator = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
//this.popupView = inflator.inflate(android.R.layout.popupwindow, null, false);
this.popupView = new View(activity);
setContentView(popupView);
setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_RESIZE | LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
parentView = activity.findViewById(android.R.id.content);
setWidth(0);
setHeight(LayoutParams.MATCH_PARENT);
popupView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (popupView != null) {
handleOnGlobalLayout();
}
}
});
}
/**
* Start the KeyboardHeightProvider, this must be called after the onResume of the Activity.
* PopupWindows are not allowed to be registered before the onResume has finished
* of the Activity.
*/
public void start() {
if (!isShowing() && parentView.getWindowToken() != null) {
setBackgroundDrawable(new ColorDrawable(0));
showAtLocation(parentView, Gravity.NO_GRAVITY, 0, 0);
}
}
/**
* Close the keyboard height provider,
* this provider will not be used anymore.
*/
public void close() {
this.observer = null;
dismiss();
}
/**
* Set the keyboard height observer to this provider. The
* observer will be notified when the keyboard height has changed.
* For example when the keyboard is opened or closed.
*
* @param observer The observer to be added to this provider.
*/
public void setKeyboardHeightObserver(KeyboardHeightObserver observer) {
this.observer = observer;
}
/**
* Popup window itself is as big as the window of the Activity.
* The keyboard can then be calculated by extracting the popup view bottom
* from the activity window height.
*/
private void handleOnGlobalLayout() {
Point screenSize = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(screenSize);
Rect rect = new Rect();
popupView.getWindowVisibleDisplayFrame(rect);
// REMIND, you may like to change this using the fullscreen size of the phone
// and also using the status bar and navigation bar heights of the phone to calculate
// the keyboard height. But this worked fine on a Nexus.
int orientation = getScreenOrientation();
int keyboardHeight = screenSize.y - rect.bottom;
if (keyboardHeight == 0) {
notifyKeyboardHeightChanged(0, orientation);
}
else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
this.keyboardPortraitHeight = keyboardHeight;
notifyKeyboardHeightChanged(keyboardPortraitHeight, orientation);
}
else {
this.keyboardLandscapeHeight = keyboardHeight;
notifyKeyboardHeightChanged(keyboardLandscapeHeight, orientation);
}
}
private int getScreenOrientation() {
return activity.getResources().getConfiguration().orientation;
}
private void notifyKeyboardHeightChanged(int height, int orientation) {
if (observer != null) {
observer.onKeyboardHeightChanged(height, orientation);
}
}
}

View File

@@ -20,11 +20,48 @@ public class MainActivity extends GameActivity {
System.loadLibrary("notedeck_chrome");
}
private native void nativeOnKeyboardHeightChanged(int height);
private KeyboardHeightHelper keyboardHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
// Shrink view so it does not get covered by insets.
View content = getWindow().getDecorView().findViewById(android.R.id.content);
setupInsets();
//setupFullscreen()
keyboardHelper = new KeyboardHeightHelper(this);
super.onCreate(savedInstanceState);
}
private void setupFullscreen() {
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
WindowInsetsControllerCompat controller =
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
if (controller != null) {
controller.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
);
controller.hide(WindowInsetsCompat.Type.systemBars());
}
//focus(getContent())
}
// not sure if this does anything
private void focus(View content) {
content.setFocusable(true);
content.setFocusableInTouchMode(true);
content.requestFocus();
}
private View getContent() {
return getWindow().getDecorView().findViewById(android.R.id.content);
}
private void setupInsets() {
View content = getContent();
ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
@@ -39,25 +76,24 @@ public class MainActivity extends GameActivity {
});
WindowCompat.setDecorFitsSystemWindows(getWindow(), true);
//WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
/*
WindowInsetsControllerCompat controller =
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
if (controller != null) {
controller.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
);
controller.hide(WindowInsetsCompat.Type.systemBars());
}
*/
content.setFocusable(true);
content.setFocusableInTouchMode(true);
content.requestFocus();
@Override
public void onResume() {
super.onResume();
keyboardHelper.start();
}
super.onCreate(savedInstanceState);
@Override
public void onPause() {
super.onPause();
keyboardHelper.stop();
}
@Override
public void onDestroy() {
super.onDestroy();
keyboardHelper.close();
}
@Override