ViewTreeObserver: implement onGlobalLayout properly

This commit is contained in:
Mis012
2025-10-23 17:11:31 +02:00
parent 9ab58e4736
commit 451e23fcd2
15 changed files with 189 additions and 42 deletions

View File

@@ -131,6 +131,7 @@ libtranslationlayer_so = shared_library('translation_layer_main', [
'src/api-impl-jni/views/AndroidLayout.c', 'src/api-impl-jni/views/AndroidLayout.c',
'src/api-impl-jni/views/android_view_View.c', 'src/api-impl-jni/views/android_view_View.c',
'src/api-impl-jni/views/android_view_ViewGroup.c', 'src/api-impl-jni/views/android_view_ViewGroup.c',
'src/api-impl-jni/views/android_view_ViewTreeObserver.c',
'src/api-impl-jni/views/android_view_WindowManagerImpl.c', 'src/api-impl-jni/views/android_view_WindowManagerImpl.c',
'src/api-impl-jni/widgets/WrapperWidget.c', 'src/api-impl-jni/widgets/WrapperWidget.c',
'src/api-impl-jni/widgets/android_view_SurfaceView.c', 'src/api-impl-jni/widgets/android_view_SurfaceView.c',

View File

@@ -53,3 +53,8 @@ JNIEXPORT void JNICALL Java_android_view_Window_set_1layout(JNIEnv *env, jobject
if (width > 0 && height > 0) if (width > 0 && height > 0)
gtk_window_set_default_size(gtk_window, width, height); gtk_window_set_default_size(gtk_window, width, height);
} }
JNIEXPORT void JNICALL Java_android_view_Window_set_1jobject(JNIEnv *env, jclass this, jlong window, jobject window_jobj)
{
g_object_set_data(G_OBJECT(window), "jobject", _WEAK_REF(window_jobj));
}

View File

@@ -359,6 +359,14 @@ JNIEXPORT void JNICALL Java_android_view_View_nativeRequestFocus
JNIEXPORT void JNICALL Java_android_view_View_nativeSetFullscreen JNIEXPORT void JNICALL Java_android_view_View_nativeSetFullscreen
(JNIEnv *, jobject, jlong, jboolean); (JNIEnv *, jobject, jlong, jboolean);
/*
* Class: android_view_View
* Method: native_get_window
* Signature: (J)Landroid/view/Window;
*/
JNIEXPORT jobject JNICALL Java_android_view_View_native_1get_1window
(JNIEnv *, jobject, jlong);
/* /*
* Class: android_view_View * Class: android_view_View
* Method: nativeInvalidate * Method: nativeInvalidate

View File

@@ -0,0 +1,21 @@
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class android_view_ViewTreeObserver */
#ifndef _Included_android_view_ViewTreeObserver
#define _Included_android_view_ViewTreeObserver
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: android_view_ViewTreeObserver
* Method: native_set_have_global_layout_listeners
* Signature: (Z)V
*/
JNIEXPORT void JNICALL Java_android_view_ViewTreeObserver_native_1set_1have_1global_1layout_1listeners
(JNIEnv *, jobject, jboolean);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -43,6 +43,14 @@ JNIEXPORT void JNICALL Java_android_view_Window_take_1input_1queue
JNIEXPORT void JNICALL Java_android_view_Window_set_1layout JNIEXPORT void JNICALL Java_android_view_Window_set_1layout
(JNIEnv *, jobject, jlong, jint, jint); (JNIEnv *, jobject, jlong, jint, jint);
/*
* Class: android_view_Window
* Method: set_jobject
* Signature: (JLandroid/view/Window;)V
*/
JNIEXPORT void JNICALL Java_android_view_Window_set_1jobject
(JNIEnv *, jclass, jlong, jobject);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -153,4 +153,7 @@ void set_up_handle_cache(JNIEnv *env)
handle_cache.parcel.constructor = _METHOD(handle_cache.parcel.class, "<init>", "(JJ)V"); handle_cache.parcel.constructor = _METHOD(handle_cache.parcel.class, "<init>", "(JJ)V");
handle_cache.parcel.writeParcelable = _METHOD(handle_cache.parcel.class, "writeParcelable", "(Landroid/os/Parcelable;I)V"); handle_cache.parcel.writeParcelable = _METHOD(handle_cache.parcel.class, "writeParcelable", "(Landroid/os/Parcelable;I)V");
handle_cache.parcel.readParcelable = _METHOD(handle_cache.parcel.class, "readParcelable", "(Ljava/lang/ClassLoader;)Landroid/os/Parcelable;"); handle_cache.parcel.readParcelable = _METHOD(handle_cache.parcel.class, "readParcelable", "(Ljava/lang/ClassLoader;)Landroid/os/Parcelable;");
handle_cache.view_tree_observer.class = _REF((*env)->FindClass(env, "android/view/ViewTreeObserver"));
handle_cache.view_tree_observer.dispatchOnGlobalLayout = _METHOD(handle_cache.view_tree_observer.class, "dispatchOnGlobalLayout", "()V");
} }

View File

@@ -166,6 +166,10 @@ struct handle_cache {
jmethodID writeParcelable; jmethodID writeParcelable;
jmethodID readParcelable; jmethodID readParcelable;
} parcel; } parcel;
struct {
jclass class;
jmethodID dispatchOnGlobalLayout;
} view_tree_observer;
}; };
extern struct handle_cache handle_cache; extern struct handle_cache handle_cache;

View File

@@ -307,8 +307,10 @@ void _setOnTouchListener(JNIEnv *env, jobject this, GtkWidget *widget)
gtk_widget_add_controller(widget, controller); gtk_widget_add_controller(widget, controller);
g_object_set_data(G_OBJECT(widget), "on_intercept_touch_listener", controller); g_object_set_data(G_OBJECT(widget), "on_intercept_touch_listener", controller);
} }
if (!wrapper->needs_allocation && (wrapper->layout_width || wrapper->layout_height))
gtk_widget_size_allocate(GTK_WIDGET(wrapper), &(GtkAllocation){.x = 0, .y = 0, .width = wrapper->layout_width, .height = wrapper->layout_height}, 0); if (!wrapper->needs_allocation && (wrapper->real_width || wrapper->real_height))
gtk_widget_size_allocate(GTK_WIDGET(wrapper), &(GtkAllocation){.x = 0, .y = 0, .width = wrapper->real_width, .height = wrapper->real_height}, 0);
wrapper->needs_allocation = true; wrapper->needs_allocation = true;
gtk_widget_set_overflow(GTK_WIDGET(wrapper), GTK_OVERFLOW_HIDDEN); gtk_widget_set_overflow(GTK_WIDGET(wrapper), GTK_OVERFLOW_HIDDEN);
} }
@@ -340,11 +342,6 @@ JNIEXPORT jint JNICALL Java_android_view_View_getWidth(JNIEnv *env, jobject this
{ {
GtkWidget *widget = GTK_WIDGET(_PTR(_GET_LONG_FIELD(this, "widget"))); GtkWidget *widget = GTK_WIDGET(_PTR(_GET_LONG_FIELD(this, "widget")));
/* FIXME: is this needed in Gtk4?
GtkAllocation alloc;
gtk_widget_get_allocation(widget, &alloc);
printf("widget size is currently %dx%d\n", alloc.width, alloc.height);
*/
if (ATL_IS_ANDROID_LAYOUT(gtk_widget_get_layout_manager(widget))) { if (ATL_IS_ANDROID_LAYOUT(gtk_widget_get_layout_manager(widget))) {
AndroidLayout *layout = ATL_ANDROID_LAYOUT(gtk_widget_get_layout_manager(widget)); AndroidLayout *layout = ATL_ANDROID_LAYOUT(gtk_widget_get_layout_manager(widget));
return layout->real_width; return layout->real_width;
@@ -409,8 +406,6 @@ JNIEXPORT void JNICALL Java_android_view_View_native_1setLayoutParams(JNIEnv *en
} }
if (weight > 0.f) { if (weight > 0.f) {
printf(":::-: setting weight: %f\n", weight);
hexpand = TRUE; hexpand = TRUE;
vexpand = TRUE; vexpand = TRUE;
} }
@@ -534,10 +529,8 @@ JNIEXPORT jlong JNICALL Java_android_view_View_native_1constructor(JNIEnv *env,
gtk_widget_add_controller(widget, controller); gtk_widget_add_controller(widget, controller);
} }
if (_METHOD(class, "onAttachedToWindow", "()V") != handle_cache.view.onAttachedToWindow) g_signal_connect(wrapper, "map", G_CALLBACK(java_method_cb), handle_cache.view.onAttachedToWindow);
g_signal_connect(wrapper, "map", G_CALLBACK(java_method_cb), handle_cache.view.onAttachedToWindow); g_signal_connect(wrapper, "unmap", G_CALLBACK(java_method_cb), handle_cache.view.onDetachedFromWindow);
if (_METHOD(class, "onDetachedFromWindow", "()V") != handle_cache.view.onDetachedFromWindow)
g_signal_connect(wrapper, "unmap", G_CALLBACK(java_method_cb), handle_cache.view.onDetachedFromWindow);
return _INTPTR(widget); return _INTPTR(widget);
} }
@@ -870,8 +863,15 @@ JNIEXPORT void JNICALL Java_android_view_View_native_1keep_1screen_1on(JNIEnv *e
} }
} }
JNIEXPORT jboolean JNICALL Java_android_view_View_nativeIsAttachedToWindow(JNIEnv *env, jobject this, jlong widget_ptr) JNIEXPORT jboolean JNICALL Java_android_view_View_nativeIsAttachedToWindow(JNIEnv *env, jobject this, jlong widget_ptr)
{ {
GtkWidget *widget = GTK_WIDGET(_PTR(widget_ptr)); GtkWidget *widget = GTK_WIDGET(_PTR(widget_ptr));
return gtk_widget_get_mapped(widget); return gtk_widget_get_mapped(widget);
} }
JNIEXPORT jobject JNICALL Java_android_view_View_native_1get_1window(JNIEnv *env, jobject this, jlong widget_ptr)
{
GtkWidget *widget = GTK_WIDGET(_PTR(widget_ptr));
return g_object_get_data(G_OBJECT(gtk_widget_get_root(widget)), "jobject");
}

View File

@@ -0,0 +1,44 @@
#include <gtk/gtk.h>
#include "../defines.h"
#include "../util.h"
#include "../generated_headers/android_view_ViewTreeObserver.h"
static void on_global_layout_callback(GdkFrameClock *clock, jobject view_tree_observer)
{
JNIEnv *env = get_jni_env();
(*env)->CallVoidMethod(env, view_tree_observer, handle_cache.view_tree_observer.dispatchOnGlobalLayout);
if((*env)->ExceptionCheck(env)) {
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
}
}
extern GtkWidget *window;
void _gdb_force_java_stack_trace(void);
JNIEXPORT void JNICALL Java_android_view_ViewTreeObserver_native_1set_1have_1global_1layout_1listeners(JNIEnv *env, jobject this, jboolean have_listeners)
{
GtkWidget *window =_PTR(_GET_LONG_FIELD(this, "window"));
if(!window) {
fprintf(stderr, "Java_android_view_ViewTreeObserver_native_1set_1have_1global_1layout_1listeners: no window\n");
return;
}
gulong signal_handle = _GET_LONG_FIELD(this, "onGlobalLayout_signal_handle");
GdkFrameClock *clock = gtk_widget_get_frame_clock(window);
if(have_listeners && !signal_handle) {
/* this adds our callback before the existing handler, which means we effectively execute before the paint phase (after layout) */
signal_handle = g_signal_connect(G_OBJECT(clock), "paint", G_CALLBACK(on_global_layout_callback), _REF(this)); // FIXME: cleanup callback for _UNREF
_SET_LONG_FIELD(this, "onGlobalLayout_signal_handle", signal_handle);
} else if (!have_listeners && signal_handle) {
g_signal_handler_disconnect(G_OBJECT(clock), signal_handle);
_SET_LONG_FIELD(this, "onGlobalLayout_signal_handle", 0);
} else {
fprintf(stderr, "Java_android_view_ViewTreeObserver_native_1set_1have_1global_1layout_1listeners: invalid state: have_listeners: %d, signal_handle: 0x%016w64x\n", have_listeners, signal_handle);
exit(1);
}
}

View File

@@ -66,7 +66,7 @@ public class Activity extends ContextThemeWrapper implements Window.Callback, La
Class<? extends Activity> cls = Class.forName(className).asSubclass(Activity.class); Class<? extends Activity> cls = Class.forName(className).asSubclass(Activity.class);
Constructor<? extends Activity> constructor = cls.getConstructor(); Constructor<? extends Activity> constructor = cls.getConstructor();
Activity activity = constructor.newInstance(); Activity activity = constructor.newInstance();
activity.window.native_window = native_window; activity.window.set_native_window(native_window);
activity.intent = intent; activity.intent = intent;
activity.attachBaseContext(new Context()); activity.attachBaseContext(new Context());
activity.setTheme(themeResId); activity.setTheme(themeResId);

View File

@@ -40,7 +40,7 @@ public class Dialog implements Window.Callback, DialogInterface {
window.setBackgroundDrawable(background); window.setBackgroundDrawable(background);
ta.recycle(); ta.recycle();
window.native_window = nativePtr; window.set_native_window(nativePtr);
} }
public Dialog(Context context) { public Dialog(Context context) {

View File

@@ -139,7 +139,7 @@ public class Instrumentation {
Intent intent, ActivityInfo info, CharSequence title, Activity parent, Intent intent, ActivityInfo info, CharSequence title, Activity parent,
String id, Object lastNonConfigurationInstance) throws InstantiationException, IllegalAccessException { String id, Object lastNonConfigurationInstance) throws InstantiationException, IllegalAccessException {
Activity activity = (Activity)clazz.newInstance(); Activity activity = (Activity)clazz.newInstance();
activity.getWindow().native_window = Context.this_application.native_window; activity.getWindow().set_native_window(Context.this_application.native_window);
Slog.i(TAG, "activity.getWindow().native_window >"+activity.getWindow().native_window+"<"); Slog.i(TAG, "activity.getWindow().native_window >"+activity.getWindow().native_window+"<");
return activity; return activity;
} }
@@ -168,7 +168,7 @@ public class Instrumentation {
Constructor<? extends Activity> constructor = cls.getConstructor(); Constructor<? extends Activity> constructor = cls.getConstructor();
final Activity activity = constructor.newInstance(); final Activity activity = constructor.newInstance();
activity.intent = intent; activity.intent = intent;
activity.getWindow().native_window = Context.this_application.native_window; activity.getWindow().set_native_window(Context.this_application.native_window);
runOnMainSync(new Runnable() { runOnMainSync(new Runnable() {
@Override @Override
public void run() { public void run() {

View File

@@ -867,7 +867,7 @@ public class View implements Drawable.Callback {
private Context context; private Context context;
private Map<Integer,Object> tags = new HashMap<>(); private Map<Integer,Object> tags = new HashMap<>();
private Object tag; private Object tag;
int gravity = -1; // fallback gravity for layout childs int gravity = -1; // fallback gravity for layout children
int measuredWidth = 0; int measuredWidth = 0;
int measuredHeight = 0; int measuredHeight = 0;
@@ -880,6 +880,8 @@ public class View implements Drawable.Callback {
private int scrollX = 0; private int scrollX = 0;
private int scrollY = 0; private int scrollY = 0;
ViewTreeObserver floating_observer = null;
public long widget; // pointer public long widget; // pointer
private int oldWidthMeasureSpec = -1; private int oldWidthMeasureSpec = -1;
@@ -1236,8 +1238,17 @@ public class View implements Drawable.Callback {
public void setSelected(boolean selected) {} public void setSelected(boolean selected) {}
public native Window native_get_window(long widget);
public ViewTreeObserver getViewTreeObserver() { public ViewTreeObserver getViewTreeObserver() {
return new ViewTreeObserver(); Window window = native_get_window(widget);
if (window != null) {
if (window.view_tree_observer == null)
window.view_tree_observer = new ViewTreeObserver(window);
return window.view_tree_observer;
} else {
floating_observer = new ViewTreeObserver(null);
return floating_observer;
}
} }
protected void onFinishInflate() {} protected void onFinishInflate() {}
@@ -1961,8 +1972,24 @@ public class View implements Drawable.Callback {
keepScreenOn = screenOn; keepScreenOn = screenOn;
} }
protected void onAttachedToWindow() {} protected void onAttachedToWindow() {
protected void onDetachedFromWindow() {} if (onAttachStateChangeListener != null) {
onAttachStateChangeListener.onViewAttachedToWindow(this);
}
if (keepScreenOn)
native_keep_screen_on(widget, true);
if(floating_observer != null) {
getViewTreeObserver().merge(floating_observer);
floating_observer = null;
}
}
protected void onDetachedFromWindow() {
if (onAttachStateChangeListener != null) {
onAttachStateChangeListener.onViewDetachedFromWindow(this);
}
if (keepScreenOn)
native_keep_screen_on(widget, false);
}
public void setLayerType(int layerType, Paint paint) {} public void setLayerType(int layerType, Paint paint) {}
@@ -2274,4 +2301,8 @@ public class View implements Drawable.Callback {
public Bitmap getDrawingCache() { return null; } public Bitmap getDrawingCache() { return null; }
public void announceForAccessibility(CharSequence text) {} public void announceForAccessibility(CharSequence text) {}
public WindowInsetsController getWindowInsetsController() {
return native_get_window(widget).getInsetsController();
}
} }

View File

@@ -51,6 +51,11 @@ public final class ViewTreeObserver {
private boolean mAlive = true; private boolean mAlive = true;
// accessed from native code
private long onGlobalLayout_signal_handle = 0;
private long window;
/** /**
* Interface definition for a callback to be invoked when the view hierarchy is * Interface definition for a callback to be invoked when the view hierarchy is
* attached to and detached from its window. * attached to and detached from its window.
@@ -297,10 +302,9 @@ public final class ViewTreeObserver {
public void onComputeInternalInsets(InternalInsetsInfo inoutInfo); public void onComputeInternalInsets(InternalInsetsInfo inoutInfo);
} }
/** ViewTreeObserver(Window window) {
* Creates a new ViewTreeObserver. This constructor should not be called if(window != null)
*/ this.window = window.native_window;
ViewTreeObserver() {
} }
/** /**
@@ -335,7 +339,10 @@ public final class ViewTreeObserver {
} }
} }
if (observer.mOnGlobalLayoutListeners != null) { if (observer.mOnGlobalLayoutListeners != null && observer.mOnGlobalLayoutListeners.size() > 0) {
if(mOnGlobalLayoutListeners == null || mOnGlobalLayoutListeners.size() == 0)
native_set_have_global_layout_listeners(true);
if (mOnGlobalLayoutListeners != null) { if (mOnGlobalLayoutListeners != null) {
mOnGlobalLayoutListeners.addAll(observer.mOnGlobalLayoutListeners); mOnGlobalLayoutListeners.addAll(observer.mOnGlobalLayoutListeners);
} else { } else {
@@ -495,17 +502,11 @@ public final class ViewTreeObserver {
mOnGlobalLayoutListeners = new CopyOnWriteArray<OnGlobalLayoutListener>(); mOnGlobalLayoutListeners = new CopyOnWriteArray<OnGlobalLayoutListener>();
} }
if (mOnGlobalLayoutListeners.size() == 0) {
native_set_have_global_layout_listeners(true);
}
mOnGlobalLayoutListeners.add(listener); mOnGlobalLayoutListeners.add(listener);
// hack: many Applications wait for the global layout before doing anything
// so we dispatch the event immediately
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
listener.onGlobalLayout();
}
});
} }
/** /**
@@ -539,6 +540,9 @@ public final class ViewTreeObserver {
return; return;
} }
mOnGlobalLayoutListeners.remove(victim); mOnGlobalLayoutListeners.remove(victim);
if(mOnGlobalLayoutListeners.size() == 0)
native_set_have_global_layout_listeners(false);
} }
/** /**
@@ -754,6 +758,9 @@ public final class ViewTreeObserver {
* @hide * @hide
*/ */
private void kill() { private void kill() {
/* clear any callbacks */
if (mOnGlobalLayoutListeners != null && mOnGlobalLayoutListeners.size() > 0)
native_set_have_global_layout_listeners(false);
mAlive = false; mAlive = false;
} }
@@ -1038,4 +1045,6 @@ public final class ViewTreeObserver {
getArray().clear(); getArray().clear();
} }
} }
private native void native_set_have_global_layout_listeners(boolean have_listeners);
} }

View File

@@ -10,6 +10,8 @@ public class Window {
public static final int FEATURE_OPTIONS_PANEL = 0; public static final int FEATURE_OPTIONS_PANEL = 0;
public static final int FEATURE_NO_TITLE = 1; public static final int FEATURE_NO_TITLE = 1;
public ViewTreeObserver view_tree_observer = null;
public static interface Callback { public static interface Callback {
public void onContentChanged(); public void onContentChanged();
@@ -41,6 +43,11 @@ public class Window {
decorView.setId(android.R.id.content); decorView.setId(android.R.id.content);
} }
public void set_native_window(long native_window) {
this.native_window = native_window;
set_jobject(native_window, this);
}
public void addFlags(int flags) {} public void addFlags(int flags) {}
public void setFlags(int flags, int mask) {} public void setFlags(int flags, int mask) {}
public void clearFlags(int flags) {} public void clearFlags(int flags) {}
@@ -64,12 +71,6 @@ public class Window {
return decorView; return decorView;
} }
public native void set_widget_as_root(long native_window, long widget);
private native void set_title(long native_window, String title);
public native void take_input_queue(long native_window, InputQueue.Callback callback, InputQueue queue);
public native void set_layout(long native_window, int width, int height);
public void takeInputQueue(InputQueue.Callback callback) { public void takeInputQueue(InputQueue.Callback callback) {
take_input_queue(native_window, callback, new InputQueue()); take_input_queue(native_window, callback, new InputQueue());
} }
@@ -161,4 +162,16 @@ public class Window {
public void setEnterTransition(Transition transition) {} public void setEnterTransition(Transition transition) {}
public void setGravity(int gravity) {} public void setGravity(int gravity) {}
public void setDecorFitsSystemWindows(boolean fits) {}
public WindowInsetsController getInsetsController() {
return new InsetsController();
}
public native void set_widget_as_root(long native_window, long widget);
private native void set_title(long native_window, String title);
public native void take_input_queue(long native_window, InputQueue.Callback callback, InputQueue queue);
public native void set_layout(long native_window, int width, int height);
private static native void set_jobject(long ptr, Window obj);
} }