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.
This commit is contained in:
Julian Winkler
2023-08-22 14:18:33 +02:00
parent 4434de4a58
commit ca975a0e7c
8 changed files with 268 additions and 11 deletions

View File

@@ -263,6 +263,30 @@ JNIEXPORT void JNICALL Java_android_view_View_native_1set_1size_1request
JNIEXPORT void JNICALL Java_android_view_View_native_1destructor JNIEXPORT void JNICALL Java_android_view_View_native_1destructor
(JNIEnv *, jobject, jlong); (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 * Class: android_view_View
* Method: nativeInvalidate * Method: nativeInvalidate

View File

@@ -109,6 +109,12 @@ void set_up_handle_cache(JNIEnv *env)
(*env)->ExceptionDescribe(env); (*env)->ExceptionDescribe(env);
handle_cache.view.onDraw = _METHOD(handle_cache.view.class, "onDraw", "(Landroid/graphics/Canvas;)V"); 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.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.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"); handle_cache.asset_manager.extractFromAPK = _STATIC_METHOD(handle_cache.asset_manager.class, "extractFromAPK", "(Ljava/lang/String;Ljava/lang/String;)V");

View File

@@ -70,6 +70,12 @@ struct handle_cache {
jmethodID setLayoutParams; jmethodID setLayoutParams;
jmethodID onDraw; jmethodID onDraw;
jmethodID onMeasure; jmethodID onMeasure;
jmethodID onLayout;
jmethodID getMeasuredWidth;
jmethodID getMeasuredHeight;
jmethodID getSuggestedMinimumWidth;
jmethodID getSuggestedMinimumHeight;
jmethodID setMeasuredDimension;
} view; } view;
struct { struct {
jclass class; jclass class;

View File

@@ -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))); 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);
}

View File

@@ -8,6 +8,62 @@
#include "../generated_headers/android_view_ViewGroup.h" #include "../generated_headers/android_view_ViewGroup.h"
#include "../generated_headers/android_view_View.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. * Should be overwritten by ViewGroup subclasses.
* Fall back to vertical GtkBox if subclass is not implemented yet * 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 *wrapper = g_object_ref(wrapper_widget_new());
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1); // spacing of 1 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1); // spacing of 1
wrapper_widget_set_child(WRAPPER_WIDGET(wrapper), box); 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); return _INTPTR(box);
} }

View File

@@ -146,7 +146,13 @@ static void on_mapped(GtkWidget* self, gpointer data)
JNIEnv *env; JNIEnv *env;
(*wrapper->jvm)->GetEnv(wrapper->jvm, (void**)&env, JNI_VERSION_1_6); (*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);
} }
} }

View File

@@ -771,6 +771,14 @@ public class View extends Object {
private Context context; private Context context;
private Map<Integer,Object> tags = new HashMap<>(); private Map<Integer,Object> 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 long widget; // pointer
public static HashMap<Integer, View> view_by_id = new HashMap<Integer, View>(); public static HashMap<Integer, View> view_by_id = new HashMap<Integer, View>();
@@ -829,7 +837,8 @@ public class View extends Object {
} }
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { 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() { 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 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); private native void native_set_size_request(int width, int height);
protected native void native_destructor(long widget); 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 // --- stubs
@@ -879,7 +891,9 @@ public class View extends Object {
return system_ui_visibility; 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) { public void setPressed(boolean pressed) {
System.out.println("calling setPressed on " + this + " with value: " + 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 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() { public final int getMeasuredWidth() {
return getWidth(); return this.measuredWidth & MEASURED_SIZE_MASK;
} }
public final int getMeasuredHeight() { 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) {} public void setBackgroundDrawable(Drawable backgroundDrawable) {}
@@ -1016,7 +1109,9 @@ public class View extends Object {
public boolean removeCallbacks(Runnable action) {return false;} public boolean removeCallbacks(Runnable action) {return false;}
public void requestLayout() {}; public void requestLayout() {
native_requestLayout(widget);
};
public void setOverScrollMode(int mode) {} public void setOverScrollMode(int mode) {}
@@ -1024,12 +1119,12 @@ public class View extends Object {
public boolean postDelayed(Runnable action, long delayMillis) { public boolean postDelayed(Runnable action, long delayMillis) {
new Handler(Looper.getMainLooper()).postDelayed(action, delayMillis); new Handler(Looper.getMainLooper()).postDelayed(action, delayMillis);
return true; return true;
} }
public boolean post(Runnable action) { public boolean post(Runnable action) {
new Handler(Looper.getMainLooper()).post(action); new Handler(Looper.getMainLooper()).post(action);
return true; return true;
} }
public void setSaveFromParentEnabled(boolean enabled) {} public void setSaveFromParentEnabled(boolean enabled) {}

View File

@@ -183,6 +183,19 @@ public class ViewGroup extends View implements ViewParent, ViewManager {
return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 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 class LayoutParams {
public static final int FILL_PARENT = -1; public static final int FILL_PARENT = -1;
public static final int MATCH_PARENT = -1; public static final int MATCH_PARENT = -1;