From ca975a0e7c1a41a388de33567e0ba2ecbd65e4a6 Mon Sep 17 00:00:00 2001 From: Julian Winkler Date: Tue, 22 Aug 2023 14:18:33 +0200 Subject: [PATCH] add support for ViewGroups with custom onLayout() A custom GtkLayoutManager is set to these objects, which calls into the java handlers when measure or layout is requested. Androids onMeasure method is quite different from GTKs measure method, because Android already defines the final size during onMeasure. Therefore, we call onMeasure from GTKs allocate callback instead of the measure callback. --- .../generated_headers/android_view_View.h | 24 ++++ src/api-impl-jni/util.c | 6 + src/api-impl-jni/util.h | 6 + src/api-impl-jni/views/android_view_View.c | 41 +++++++ .../views/android_view_ViewGroup.c | 68 ++++++++++- src/api-impl-jni/widgets/WrapperWidget.c | 8 +- src/api-impl/android/view/View.java | 113 ++++++++++++++++-- src/api-impl/android/view/ViewGroup.java | 13 ++ 8 files changed, 268 insertions(+), 11 deletions(-) diff --git a/src/api-impl-jni/generated_headers/android_view_View.h b/src/api-impl-jni/generated_headers/android_view_View.h index 02aaf7ed..7ab56fd9 100644 --- a/src/api-impl-jni/generated_headers/android_view_View.h +++ b/src/api-impl-jni/generated_headers/android_view_View.h @@ -263,6 +263,30 @@ JNIEXPORT void JNICALL Java_android_view_View_native_1set_1size_1request JNIEXPORT void JNICALL Java_android_view_View_native_1destructor (JNIEnv *, jobject, jlong); +/* + * Class: android_view_View + * Method: native_measure + * Signature: (JII)V + */ +JNIEXPORT void JNICALL Java_android_view_View_native_1measure + (JNIEnv *, jobject, jlong, jint, jint); + +/* + * Class: android_view_View + * Method: native_layout + * Signature: (JIIII)V + */ +JNIEXPORT void JNICALL Java_android_view_View_native_1layout + (JNIEnv *, jobject, jlong, jint, jint, jint, jint); + +/* + * Class: android_view_View + * Method: native_requestLayout + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_android_view_View_native_1requestLayout + (JNIEnv *, jobject, jlong); + /* * Class: android_view_View * Method: nativeInvalidate diff --git a/src/api-impl-jni/util.c b/src/api-impl-jni/util.c index 4bad3088..c0e0ca2d 100644 --- a/src/api-impl-jni/util.c +++ b/src/api-impl-jni/util.c @@ -109,6 +109,12 @@ void set_up_handle_cache(JNIEnv *env) (*env)->ExceptionDescribe(env); handle_cache.view.onDraw = _METHOD(handle_cache.view.class, "onDraw", "(Landroid/graphics/Canvas;)V"); handle_cache.view.onMeasure = _METHOD(handle_cache.view.class, "onMeasure", "(II)V"); + handle_cache.view.onLayout = _METHOD(handle_cache.view.class, "onLayout", "(ZIIII)V"); + handle_cache.view.getMeasuredWidth = _METHOD(handle_cache.view.class, "getMeasuredWidth", "()I"); + handle_cache.view.getMeasuredHeight = _METHOD(handle_cache.view.class, "getMeasuredHeight", "()I"); + handle_cache.view.getSuggestedMinimumWidth = _METHOD(handle_cache.view.class, "getSuggestedMinimumWidth", "()I"); + handle_cache.view.getSuggestedMinimumHeight = _METHOD(handle_cache.view.class, "getSuggestedMinimumHeight", "()I"); + handle_cache.view.setMeasuredDimension = _METHOD(handle_cache.view.class, "setMeasuredDimension", "(II)V"); handle_cache.asset_manager.class = _REF((*env)->FindClass(env, "android/content/res/AssetManager")); handle_cache.asset_manager.extractFromAPK = _STATIC_METHOD(handle_cache.asset_manager.class, "extractFromAPK", "(Ljava/lang/String;Ljava/lang/String;)V"); diff --git a/src/api-impl-jni/util.h b/src/api-impl-jni/util.h index d2a39948..5cc0fc06 100644 --- a/src/api-impl-jni/util.h +++ b/src/api-impl-jni/util.h @@ -70,6 +70,12 @@ struct handle_cache { jmethodID setLayoutParams; jmethodID onDraw; jmethodID onMeasure; + jmethodID onLayout; + jmethodID getMeasuredWidth; + jmethodID getMeasuredHeight; + jmethodID getSuggestedMinimumWidth; + jmethodID getSuggestedMinimumHeight; + jmethodID setMeasuredDimension; } view; struct { jclass class; diff --git a/src/api-impl-jni/views/android_view_View.c b/src/api-impl-jni/views/android_view_View.c index 9d558430..5f42dacd 100644 --- a/src/api-impl-jni/views/android_view_View.c +++ b/src/api-impl-jni/views/android_view_View.c @@ -196,3 +196,44 @@ JNIEXPORT void JNICALL Java_android_view_View_native_1destructor(JNIEnv *env, jo { g_object_unref(gtk_widget_get_parent(_PTR(widget_ptr))); } + +#define MEASURE_SPEC_UNSPECIFIED (0 << 30) +#define MEASURE_SPEC_EXACTLY (1 << 30) +#define MEASURE_SPEC_MASK (0x3 << 30) + +JNIEXPORT void JNICALL Java_android_view_View_native_1measure(JNIEnv *env, jobject this, jlong widget_ptr, jint width_spec, jint height_spec) { + int width; + int height; + int for_size; + GtkWidget *widget = gtk_widget_get_parent(GTK_WIDGET(_PTR(widget_ptr))); + + if (((height_spec & MEASURE_SPEC_MASK) == MEASURE_SPEC_EXACTLY) && ((width_spec & MEASURE_SPEC_MASK) == MEASURE_SPEC_EXACTLY)) { + width = width_spec & ~MEASURE_SPEC_MASK; + height = height_spec & ~MEASURE_SPEC_MASK; + } else { + for_size = ((height_spec & MEASURE_SPEC_MASK) == MEASURE_SPEC_EXACTLY) ? (height_spec & ~MEASURE_SPEC_MASK) : -1; + gtk_widget_measure(widget, GTK_ORIENTATION_HORIZONTAL, for_size, NULL, &width, NULL, NULL); + + for_size = ((width_spec & MEASURE_SPEC_MASK) == MEASURE_SPEC_EXACTLY) ? (width_spec & ~MEASURE_SPEC_MASK) : -1; + gtk_widget_measure(widget, GTK_ORIENTATION_VERTICAL, for_size, NULL, &height, NULL, NULL); + } + (*env)->CallVoidMethod(env, this, handle_cache.view.setMeasuredDimension, width, height); +} + +JNIEXPORT void JNICALL Java_android_view_View_native_1layout(JNIEnv *env, jobject this, jlong widget_ptr, jint l, jint t, jint r, jint b) { + GtkWidget *widget = gtk_widget_get_parent(GTK_WIDGET(_PTR(widget_ptr))); + + GtkAllocation allocation = { + .x=l, + .y=t, + .width=r-l, + .height=b-t, + }; + gtk_widget_size_allocate(widget, &allocation, -1); +} + +JNIEXPORT void JNICALL Java_android_view_View_native_1requestLayout(JNIEnv *env, jobject this, jlong widget_ptr) { + GtkWidget *widget = GTK_WIDGET(_PTR(widget_ptr)); + + gtk_widget_queue_resize(widget); +} diff --git a/src/api-impl-jni/views/android_view_ViewGroup.c b/src/api-impl-jni/views/android_view_ViewGroup.c index 6dd7b395..fec61320 100644 --- a/src/api-impl-jni/views/android_view_ViewGroup.c +++ b/src/api-impl-jni/views/android_view_ViewGroup.c @@ -8,6 +8,62 @@ #include "../generated_headers/android_view_ViewGroup.h" #include "../generated_headers/android_view_View.h" +#define MEASURE_SPEC_EXACTLY (1 << 30) + +struct _AndroidLayout { + GtkLayoutManager parent_instance; + jobject view; +}; +G_DECLARE_FINAL_TYPE(AndroidLayout, android_layout, ATL, ANDROID_LAYOUT, GtkLayoutManager); + +static void android_layout_measure(GtkLayoutManager *layout_manager, GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline) { + AndroidLayout *layout = ATL_ANDROID_LAYOUT(layout_manager); + JNIEnv *env = get_jni_env(); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + *minimum = (*env)->CallIntMethod(env, layout->view, handle_cache.view.getSuggestedMinimumWidth); + *natural = (*env)->CallIntMethod(env, layout->view, handle_cache.view.getMeasuredWidth); + } + if (orientation == GTK_ORIENTATION_VERTICAL) { + *minimum = (*env)->CallIntMethod(env, layout->view, handle_cache.view.getSuggestedMinimumHeight); + *natural = (*env)->CallIntMethod(env, layout->view, handle_cache.view.getMeasuredHeight); + } + if (*natural < *minimum) + *natural = *minimum; + + *minimum_baseline = -1; + *natural_baseline = -1; +} + +static void android_layout_allocate(GtkLayoutManager *layout_manager, GtkWidget *widget, int width, int height, int baseline) { + AndroidLayout *layout = ATL_ANDROID_LAYOUT(layout_manager); + JNIEnv *env = get_jni_env(); + + (*env)->CallVoidMethod(env, layout->view, handle_cache.view.onMeasure, MEASURE_SPEC_EXACTLY | width, MEASURE_SPEC_EXACTLY | height); + if((*env)->ExceptionCheck(env)) + (*env)->ExceptionDescribe(env); + + (*env)->CallVoidMethod(env, layout->view, handle_cache.view.onLayout, TRUE, 0, 0, width, height); + if((*env)->ExceptionCheck(env)) + (*env)->ExceptionDescribe(env); +} + +static void android_layout_class_init(AndroidLayoutClass *klass) { + klass->parent_class.measure = android_layout_measure; + klass->parent_class.allocate = android_layout_allocate; +} + +static void android_layout_init(AndroidLayout *self) { +} + +G_DEFINE_TYPE(AndroidLayout, android_layout, GTK_TYPE_LAYOUT_MANAGER) + +static GtkLayoutManager *android_layout_new(jobject view) { + AndroidLayout *layout = g_object_new(android_layout_get_type(), NULL); + layout->view = view; + return &layout->parent_instance; +} + /** * Should be overwritten by ViewGroup subclasses. * Fall back to vertical GtkBox if subclass is not implemented yet @@ -17,7 +73,17 @@ JNIEXPORT jlong JNICALL Java_android_view_ViewGroup_native_1constructor(JNIEnv * GtkWidget *wrapper = g_object_ref(wrapper_widget_new()); GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1); // spacing of 1 wrapper_widget_set_child(WRAPPER_WIDGET(wrapper), box); - gtk_widget_set_name(GTK_WIDGET(box), "ViewGroup"); + + const char *name = _CSTRING((*env)->CallObjectMethod(env, _CLASS(this), + _METHOD((*env)->FindClass(env, "java/lang/Class"), "getName", "()Ljava/lang/String;"))); + gtk_widget_set_name(box, name); + + jmethodID measure_method = _METHOD(_CLASS(this), "onMeasure", "(II)V"); + jmethodID layout_method = _METHOD(_CLASS(this), "onLayout", "(ZIIII)V"); + if (measure_method != handle_cache.view.onMeasure || layout_method != handle_cache.view.onLayout) { + gtk_widget_set_layout_manager(box, android_layout_new(_REF(this))); + } + return _INTPTR(box); } diff --git a/src/api-impl-jni/widgets/WrapperWidget.c b/src/api-impl-jni/widgets/WrapperWidget.c index d47bedca..775937b4 100644 --- a/src/api-impl-jni/widgets/WrapperWidget.c +++ b/src/api-impl-jni/widgets/WrapperWidget.c @@ -146,7 +146,13 @@ static void on_mapped(GtkWidget* self, gpointer data) JNIEnv *env; (*wrapper->jvm)->GetEnv(wrapper->jvm, (void**)&env, JNI_VERSION_1_6); - (*env)->CallVoidMethod(env, wrapper->jobj, wrapper->measure_method, MEASURE_SPEC_EXACTLY, MEASURE_SPEC_EXACTLY); + (*env)->CallVoidMethod(env, wrapper->jobj, wrapper->measure_method, MEASURE_SPEC_EXACTLY | gtk_widget_get_width(self), MEASURE_SPEC_EXACTLY | gtk_widget_get_height(self)); + int width = (*env)->CallIntMethod(env, wrapper->jobj, handle_cache.view.getMeasuredWidth); + if (width > 0) + g_object_set(G_OBJECT(self), "width-request", width, NULL); + int height = (*env)->CallIntMethod(env, wrapper->jobj, handle_cache.view.getMeasuredHeight); + if (height > 0) + g_object_set(G_OBJECT(self), "height-request", height, NULL); } } diff --git a/src/api-impl/android/view/View.java b/src/api-impl/android/view/View.java index 1369961d..4b91f661 100644 --- a/src/api-impl/android/view/View.java +++ b/src/api-impl/android/view/View.java @@ -771,6 +771,14 @@ public class View extends Object { private Context context; private Map tags = new HashMap<>(); + int measuredWidth = 0; + int measuredHeight = 0; + + private int left; + private int top; + private int right; + private int bottom; + public long widget; // pointer public static HashMap view_by_id = new HashMap(); @@ -829,7 +837,8 @@ public class View extends Object { } protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { - native_set_size_request(MeasureSpec.getSize(measuredWidth), MeasureSpec.getSize(measuredHeight)); + this.measuredWidth = measuredWidth; + this.measuredHeight = measuredHeight; } public Resources getResources() { @@ -846,6 +855,9 @@ public class View extends Object { protected native long native_constructor(Context context, AttributeSet attrs); // will create a custom GtkWidget with a custom drawing function private native void native_set_size_request(int width, int height); protected native void native_destructor(long widget); + protected native void native_measure(long widget, int widthMeasureSpec, int heightMeasureSpec); + protected native void native_layout(long widget, int l, int t, int r, int b); + protected native void native_requestLayout(long widget); // --- stubs @@ -879,7 +891,9 @@ public class View extends Object { return system_ui_visibility; }; - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {} + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + native_measure(widget, widthMeasureSpec, heightMeasureSpec); + } public void setPressed(boolean pressed) { System.out.println("calling setPressed on " + this + " with value: " + pressed); @@ -994,14 +1008,93 @@ public class View extends Object { public void setOnHoverListener(OnHoverListener listener) {} - public final void measure (int widthMeasureSpec, int heightMeasureSpec) {} + public final void measure(int widthMeasureSpec, int heightMeasureSpec) { + onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + public final int getMeasuredState() { + return 0; + } + + public static int combineMeasuredStates(int curState, int newState) { + return curState | newState; + } + + protected int getSuggestedMinimumHeight() { + return 50; + } + protected int getSuggestedMinimumWidth() { + return 100; + } + + /** + * Utility to reconcile a desired size and state, with constraints imposed + * by a MeasureSpec. Will take the desired size, unless a different size + * is imposed by the constraints. The returned value is a compound integer, + * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and + * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting + * size is smaller than the size the view wants to be. + * + * @param size How big the view wants to be + * @param measureSpec Constraints imposed by the parent + * @return Size information bit mask as defined by + * {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}. + */ + public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { + int result = size; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + result = size; + break; + case MeasureSpec.AT_MOST: + if (specSize < size) { + result = specSize | MEASURED_STATE_TOO_SMALL; + } else { + result = size; + } + break; + case MeasureSpec.EXACTLY: + result = specSize; + break; + } + return result | (childMeasuredState&MEASURED_STATE_MASK); + } public final int getMeasuredWidth() { - return getWidth(); + return this.measuredWidth & MEASURED_SIZE_MASK; } public final int getMeasuredHeight() { - return getHeight(); + return this.measuredHeight & MEASURED_SIZE_MASK; + } + + protected void onLayout(boolean changed, int l, int t, int r, int b) {} + + public void layout(int l, int t, int r, int b) { + this.left = l; + this.top = t; + this.right = r; + this.bottom = b; + native_layout(widget, l, t, r, b); + } + + public int getLeft() { + return left; + } + public int getTop() { + return top; + } + public int getRight() { + return right; + } + public int getBottom() { + return bottom; + } + + public void offsetTopAndBottom(int offset) { + layout(left, top + offset, right, bottom + offset); } public void setBackgroundDrawable(Drawable backgroundDrawable) {} @@ -1016,7 +1109,9 @@ public class View extends Object { public boolean removeCallbacks(Runnable action) {return false;} - public void requestLayout() {}; + public void requestLayout() { + native_requestLayout(widget); + }; public void setOverScrollMode(int mode) {} @@ -1024,12 +1119,12 @@ public class View extends Object { public boolean postDelayed(Runnable action, long delayMillis) { new Handler(Looper.getMainLooper()).postDelayed(action, delayMillis); - return true; - } + return true; + } public boolean post(Runnable action) { new Handler(Looper.getMainLooper()).post(action); - return true; + return true; } public void setSaveFromParentEnabled(boolean enabled) {} diff --git a/src/api-impl/android/view/ViewGroup.java b/src/api-impl/android/view/ViewGroup.java index 40cd8e7e..758e71dd 100644 --- a/src/api-impl/android/view/ViewGroup.java +++ b/src/api-impl/android/view/ViewGroup.java @@ -183,6 +183,19 @@ public class ViewGroup extends View implements ViewParent, ViewManager { return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } + protected void measureChildWithMargins(View child, + int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + /*mPaddingLeft + mPaddingRight +*/ lp.leftMargin + lp.rightMargin + + widthUsed, lp.width); + final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, + /*mPaddingTop + mPaddingBottom +*/ lp.topMargin + lp.bottomMargin + + heightUsed, lp.height); + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + public static class LayoutParams { public static final int FILL_PARENT = -1; public static final int MATCH_PARENT = -1;