712 lines
28 KiB
Java
712 lines
28 KiB
Java
/*
|
||
* ElBridge.java — Android Java companion to el_android.c.
|
||
*
|
||
* All public methods are static. The C JNI layer calls these to create views,
|
||
* set properties, and manage the widget tree. Views are identified by integer
|
||
* slot indices matching the C-side handle values.
|
||
*
|
||
* Threading: every method that touches a View dispatches to the UI thread
|
||
* using Activity.runOnUiThread(Runnable) and blocks with a CountDownLatch
|
||
* until the UI thread completes the operation. This mirrors the AppKit
|
||
* dispatch_sync(main_queue, ^{}) pattern in el_appkit.m.
|
||
*
|
||
* Callbacks: Java sets listeners on views that call back into C via:
|
||
* nativeOnClick(int slot)
|
||
* nativeOnChange(int slot, String text)
|
||
* nativeOnSubmit(int slot, String text)
|
||
* These are declared native and implemented in el_android.c.
|
||
*
|
||
* Usage (in your Activity.onCreate):
|
||
* System.loadLibrary("elruntime");
|
||
* ElBridge.init(this);
|
||
*
|
||
* The native library calls __native_init() which calls nativeRegisterActivity
|
||
* via the C side; alternatively call ElBridge.init(this) directly from Java.
|
||
*
|
||
* Compile requirements:
|
||
* Android minSdkVersion 21 (Lollipop) or higher.
|
||
* No third-party dependencies — uses only android.* framework classes.
|
||
* For image loading from arbitrary file paths, BitmapFactory is used.
|
||
* To replace with Glide/Picasso, edit createImageView only.
|
||
*/
|
||
|
||
package com.neuron.el;
|
||
|
||
import android.app.Activity;
|
||
import android.content.Context;
|
||
import android.graphics.Bitmap;
|
||
import android.graphics.BitmapFactory;
|
||
import android.graphics.Color;
|
||
import android.graphics.Typeface;
|
||
import android.graphics.drawable.GradientDrawable;
|
||
import android.os.Handler;
|
||
import android.os.Looper;
|
||
import android.text.Editable;
|
||
import android.text.InputType;
|
||
import android.text.TextWatcher;
|
||
import android.view.Gravity;
|
||
import android.view.View;
|
||
import android.view.ViewGroup;
|
||
import android.widget.Button;
|
||
import android.widget.EditText;
|
||
import android.widget.FrameLayout;
|
||
import android.widget.ImageView;
|
||
import android.widget.LinearLayout;
|
||
import android.widget.ScrollView;
|
||
import android.widget.TextView;
|
||
|
||
import java.util.concurrent.CountDownLatch;
|
||
|
||
public class ElBridge {
|
||
|
||
/* ── Native callbacks (implemented in el_android.c) ─────────────────── */
|
||
|
||
public static native void nativeOnClick(int slot);
|
||
public static native void nativeOnChange(int slot, String text);
|
||
public static native void nativeOnSubmit(int slot, String text);
|
||
public static native void nativeRegisterActivity(Activity activity);
|
||
|
||
/* ── State ───────────────────────────────────────────────────────────── */
|
||
|
||
private static final int MAX_SLOTS = 4096;
|
||
|
||
private static Activity sActivity;
|
||
private static Handler sUiHandler;
|
||
private static View[] sViews = new View[MAX_SLOTS];
|
||
private static int sNextSlot = 1; /* slot 0 reserved / null */
|
||
|
||
/* ── Init ────────────────────────────────────────────────────────────── */
|
||
|
||
/**
|
||
* Must be called from the Activity before any widget operations.
|
||
* Typically called from Activity.onCreate after System.loadLibrary.
|
||
*/
|
||
public static void init(Activity activity) {
|
||
sActivity = activity;
|
||
sUiHandler = new Handler(Looper.getMainLooper());
|
||
nativeRegisterActivity(activity);
|
||
}
|
||
|
||
/* ── Slot management ─────────────────────────────────────────────────── */
|
||
|
||
private static int allocSlot(View v) {
|
||
/* Find a free slot starting from sNextSlot, wrap around. */
|
||
for (int i = 0; i < MAX_SLOTS - 1; i++) {
|
||
int idx = ((sNextSlot - 1 + i) % (MAX_SLOTS - 1)) + 1;
|
||
if (sViews[idx] == null) {
|
||
sViews[idx] = v;
|
||
sNextSlot = (idx % (MAX_SLOTS - 1)) + 1;
|
||
return idx;
|
||
}
|
||
}
|
||
android.util.Log.e("ElBridge", "allocSlot: slot table full");
|
||
return -1;
|
||
}
|
||
|
||
private static View getView(int slot) {
|
||
if (slot <= 0 || slot >= MAX_SLOTS) return null;
|
||
return sViews[slot];
|
||
}
|
||
|
||
/* ── UI-thread dispatch helper ───────────────────────────────────────── */
|
||
|
||
/*
|
||
* Dispatch r on the UI thread and block until it completes.
|
||
* Safe to call from the UI thread itself (runs inline without posting).
|
||
*/
|
||
private static void runSync(final Runnable r) {
|
||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||
r.run();
|
||
} else {
|
||
final CountDownLatch latch = new CountDownLatch(1);
|
||
sUiHandler.post(new Runnable() {
|
||
@Override public void run() {
|
||
try { r.run(); } finally { latch.countDown(); }
|
||
}
|
||
});
|
||
try { latch.await(); } catch (InterruptedException e) {
|
||
Thread.currentThread().interrupt();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ── Integer slot returning runSync helper ───────────────────────────── */
|
||
|
||
private interface IntSupplier { int get(); }
|
||
|
||
private static int runSyncInt(final IntSupplier s) {
|
||
final int[] result = { -1 };
|
||
runSync(new Runnable() {
|
||
@Override public void run() { result[0] = s.get(); }
|
||
});
|
||
return result[0];
|
||
}
|
||
|
||
/* ── Context accessor ────────────────────────────────────────────────── */
|
||
|
||
private static Context ctx() { return sActivity; }
|
||
|
||
/* ── View creation ───────────────────────────────────────────────────── */
|
||
|
||
/**
|
||
* Create a LinearLayout.
|
||
* @param orientation 1=VERTICAL, 0=HORIZONTAL
|
||
* @param spacing gap between children in dp; applied as bottom/right margin
|
||
*/
|
||
public static int createLinearLayout(final int orientation, final int spacing) {
|
||
return runSyncInt(new IntSupplier() {
|
||
@Override public int get() {
|
||
LinearLayout ll = new LinearLayout(ctx());
|
||
ll.setOrientation(orientation == 1
|
||
? LinearLayout.VERTICAL
|
||
: LinearLayout.HORIZONTAL);
|
||
ll.setLayoutParams(new LinearLayout.LayoutParams(
|
||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||
/* Spacing is stored so addChild can apply margins. */
|
||
ll.setTag(R_TAG_SPACING, spacing);
|
||
return allocSlot(ll);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Create a FrameLayout (ZStack equivalent). */
|
||
public static int createFrameLayout() {
|
||
return runSyncInt(new IntSupplier() {
|
||
@Override public int get() {
|
||
FrameLayout fl = new FrameLayout(ctx());
|
||
fl.setLayoutParams(new FrameLayout.LayoutParams(
|
||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||
return allocSlot(fl);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Create a ScrollView. */
|
||
public static int createScrollView() {
|
||
return runSyncInt(new IntSupplier() {
|
||
@Override public int get() {
|
||
ScrollView sv = new ScrollView(ctx());
|
||
sv.setLayoutParams(new ScrollView.LayoutParams(
|
||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||
sv.setFillViewport(true);
|
||
return allocSlot(sv);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Create a TextView with initial text. */
|
||
public static int createTextView(final String text) {
|
||
return runSyncInt(new IntSupplier() {
|
||
@Override public int get() {
|
||
TextView tv = new TextView(ctx());
|
||
tv.setText(text != null ? text : "");
|
||
tv.setLayoutParams(new LinearLayout.LayoutParams(
|
||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||
return allocSlot(tv);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Create a Button with a label. */
|
||
public static int createButton(final String label) {
|
||
return runSyncInt(new IntSupplier() {
|
||
@Override public int get() {
|
||
Button btn = new Button(ctx());
|
||
btn.setText(label != null ? label : "");
|
||
btn.setLayoutParams(new LinearLayout.LayoutParams(
|
||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||
return allocSlot(btn);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Create an EditText.
|
||
* @param placeholder hint text
|
||
* @param singleLine true = single-line text field; false = multi-line text area
|
||
*/
|
||
public static int createEditText(final String placeholder, final boolean singleLine) {
|
||
return runSyncInt(new IntSupplier() {
|
||
@Override public int get() {
|
||
EditText et = new EditText(ctx());
|
||
et.setHint(placeholder != null ? placeholder : "");
|
||
if (singleLine) {
|
||
et.setInputType(InputType.TYPE_CLASS_TEXT
|
||
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||
et.setMaxLines(1);
|
||
et.setSingleLine(true);
|
||
} else {
|
||
et.setInputType(InputType.TYPE_CLASS_TEXT
|
||
| InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
||
et.setMinLines(3);
|
||
et.setSingleLine(false);
|
||
et.setGravity(Gravity.TOP | Gravity.START);
|
||
}
|
||
et.setLayoutParams(new LinearLayout.LayoutParams(
|
||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||
return allocSlot(et);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Create an ImageView, loading from a file path via BitmapFactory.
|
||
* If path is null/empty the ImageView is created with no image.
|
||
*/
|
||
public static int createImageView(final String path) {
|
||
return runSyncInt(new IntSupplier() {
|
||
@Override public int get() {
|
||
ImageView iv = new ImageView(ctx());
|
||
iv.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||
iv.setAdjustViewBounds(true);
|
||
if (path != null && !path.isEmpty()) {
|
||
Bitmap bmp = BitmapFactory.decodeFile(path);
|
||
if (bmp != null) {
|
||
iv.setImageBitmap(bmp);
|
||
} else {
|
||
android.util.Log.w("ElBridge",
|
||
"createImageView: failed to decode " + path);
|
||
}
|
||
}
|
||
iv.setLayoutParams(new LinearLayout.LayoutParams(
|
||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||
return allocSlot(iv);
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ── Window operations ───────────────────────────────────────────────── */
|
||
|
||
/** Set the Activity's content view to the view at slot. */
|
||
public static void setContentView(final int slot) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v != null && sActivity != null) {
|
||
sActivity.setContentView(v);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Set the Activity title. */
|
||
public static void setTitle(final String title) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
if (sActivity != null) {
|
||
sActivity.setTitle(title != null ? title : "");
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ── Tree operations ─────────────────────────────────────────────────── */
|
||
|
||
/**
|
||
* Add child view to parent view.
|
||
* LinearLayout: child added as arranged child with spacing margin.
|
||
* ScrollView: child replaces current document view.
|
||
* FrameLayout / other ViewGroup: plain addView.
|
||
*/
|
||
public static void addChild(final int parentSlot, final int childSlot) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View parent = getView(parentSlot);
|
||
View child = getView(childSlot);
|
||
if (parent == null || child == null) return;
|
||
|
||
/* Remove child from existing parent first. */
|
||
if (child.getParent() instanceof ViewGroup) {
|
||
((ViewGroup) child.getParent()).removeView(child);
|
||
}
|
||
|
||
if (parent instanceof LinearLayout) {
|
||
LinearLayout ll = (LinearLayout) parent;
|
||
Object tag = ll.getTag(R_TAG_SPACING);
|
||
int spacing = (tag instanceof Integer) ? (Integer) tag : 0;
|
||
LinearLayout.LayoutParams lp;
|
||
Object existingLp = child.getLayoutParams();
|
||
if (existingLp instanceof LinearLayout.LayoutParams) {
|
||
lp = (LinearLayout.LayoutParams) existingLp;
|
||
} else {
|
||
lp = new LinearLayout.LayoutParams(
|
||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||
}
|
||
/* Apply spacing as margin on the leading/top edge (after first child). */
|
||
if (ll.getChildCount() > 0 && spacing > 0) {
|
||
int px = dpToPx(spacing);
|
||
if (ll.getOrientation() == LinearLayout.VERTICAL) {
|
||
lp.topMargin = px;
|
||
} else {
|
||
lp.leftMargin = px;
|
||
}
|
||
}
|
||
child.setLayoutParams(lp);
|
||
ll.addView(child);
|
||
} else if (parent instanceof ScrollView) {
|
||
ScrollView sv = (ScrollView) parent;
|
||
sv.removeAllViews();
|
||
sv.addView(child);
|
||
} else if (parent instanceof ViewGroup) {
|
||
((ViewGroup) parent).addView(child);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Remove child from its parent. */
|
||
public static void removeChild(final int parentSlot, final int childSlot) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View parent = getView(parentSlot);
|
||
View child = getView(childSlot);
|
||
if (parent instanceof ViewGroup && child != null) {
|
||
((ViewGroup) parent).removeView(child);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Remove the view from its parent and release the slot. */
|
||
public static void destroyView(final int slot) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v == null) return;
|
||
if (v.getParent() instanceof ViewGroup) {
|
||
((ViewGroup) v.getParent()).removeView(v);
|
||
}
|
||
sViews[slot] = null;
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ── Property setters ────────────────────────────────────────────────── */
|
||
|
||
public static void setText(final int slot, final String text) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
String s = text != null ? text : "";
|
||
if (v instanceof EditText) {
|
||
((EditText) v).setText(s);
|
||
} else if (v instanceof Button) {
|
||
((Button) v).setText(s);
|
||
} else if (v instanceof TextView) {
|
||
((TextView) v).setText(s);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
public static String getText(final int slot) {
|
||
final String[] result = { "" };
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v instanceof TextView) {
|
||
CharSequence cs = ((TextView) v).getText();
|
||
result[0] = cs != null ? cs.toString() : "";
|
||
}
|
||
}
|
||
});
|
||
return result[0];
|
||
}
|
||
|
||
/** Set foreground text color. Components in [0,1]. */
|
||
public static void setTextColor(final int slot, final float r, final float g,
|
||
final float b, final float a) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v instanceof TextView) {
|
||
((TextView) v).setTextColor(floatToArgb(r, g, b, a));
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Set background color using a GradientDrawable so corner radius is preserved. */
|
||
public static void setBackgroundColor(final int slot, final float r, final float g,
|
||
final float b, final float a) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v == null) return;
|
||
ensureGradientBackground(v);
|
||
GradientDrawable gd = (GradientDrawable) v.getBackground();
|
||
gd.setColor(floatToArgb(r, g, b, a));
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Set font family and size.
|
||
* family: "system" or null → system default; otherwise tries to load by name.
|
||
* bold: if true uses Typeface.BOLD.
|
||
*/
|
||
public static void setFont(final int slot, final String family,
|
||
final int sizeSp, final boolean bold) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (!(v instanceof TextView)) return;
|
||
TextView tv = (TextView) v;
|
||
Typeface tf;
|
||
if (family != null && !family.isEmpty()
|
||
&& !family.equals("system")) {
|
||
Typeface base = Typeface.create(family,
|
||
bold ? Typeface.BOLD : Typeface.NORMAL);
|
||
tf = (base != null) ? base
|
||
: Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL);
|
||
} else {
|
||
tf = Typeface.defaultFromStyle(bold ? Typeface.BOLD : Typeface.NORMAL);
|
||
}
|
||
tv.setTypeface(tf);
|
||
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, sizeSp);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Set padding in dp. */
|
||
public static void setPadding(final int slot, final int top, final int right,
|
||
final int bottom, final int left) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v != null) {
|
||
v.setPadding(dpToPx(left), dpToPx(top), dpToPx(right), dpToPx(bottom));
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Set explicit width in dp. Passes MATCH_PARENT for negative values. */
|
||
public static void setWidth(final int slot, final int widthDp) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v == null) return;
|
||
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||
if (lp == null) lp = new ViewGroup.LayoutParams(
|
||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||
lp.width = widthDp < 0
|
||
? ViewGroup.LayoutParams.MATCH_PARENT
|
||
: dpToPx(widthDp);
|
||
v.setLayoutParams(lp);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Set explicit height in dp. */
|
||
public static void setHeight(final int slot, final int heightDp) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v == null) return;
|
||
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||
if (lp == null) lp = new ViewGroup.LayoutParams(
|
||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||
lp.height = heightDp < 0
|
||
? ViewGroup.LayoutParams.MATCH_PARENT
|
||
: dpToPx(heightDp);
|
||
v.setLayoutParams(lp);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Set flex weight on a child of a LinearLayout.
|
||
* flex > 0 → weight = flex, width/height = 0dp (expand).
|
||
* flex == 0 → weight = 0, wrap_content (shrink to content).
|
||
*/
|
||
public static void setFlex(final int slot, final int flex) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v == null) return;
|
||
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||
if (lp instanceof LinearLayout.LayoutParams) {
|
||
LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams) lp;
|
||
if (flex > 0) {
|
||
llp.weight = (float) flex;
|
||
/* Determine orientation from parent to set 0dp on the right axis. */
|
||
if (v.getParent() instanceof LinearLayout) {
|
||
LinearLayout parent = (LinearLayout) v.getParent();
|
||
if (parent.getOrientation() == LinearLayout.VERTICAL) {
|
||
llp.height = 0;
|
||
} else {
|
||
llp.width = 0;
|
||
}
|
||
}
|
||
} else {
|
||
llp.weight = 0f;
|
||
}
|
||
v.setLayoutParams(llp);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/** Set corner radius in dp using a GradientDrawable background. */
|
||
public static void setCornerRadius(final int slot, final float radiusDp) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v == null) return;
|
||
ensureGradientBackground(v);
|
||
GradientDrawable gd = (GradientDrawable) v.getBackground();
|
||
gd.setCornerRadius(dpToPxF(radiusDp));
|
||
}
|
||
});
|
||
}
|
||
|
||
public static void setEnabled(final int slot, final boolean enabled) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v != null) v.setEnabled(enabled);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Show or hide a view.
|
||
* @param visible true = VISIBLE, false = GONE (matches AppKit setHidden semantics)
|
||
*/
|
||
public static void setVisibility(final int slot, final boolean visible) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v != null) v.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ── Event listener registration ─────────────────────────────────────── */
|
||
|
||
/** Register an OnClickListener that calls back into C nativeOnClick. */
|
||
public static void setOnClickListener(final int slot) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (v == null) return;
|
||
final int capturedSlot = slot;
|
||
v.setOnClickListener(new View.OnClickListener() {
|
||
@Override public void onClick(View view) {
|
||
nativeOnClick(capturedSlot);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Register a TextWatcher on an EditText that calls back nativeOnChange
|
||
* for every text change.
|
||
*/
|
||
public static void setOnChangeListener(final int slot) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (!(v instanceof EditText)) return;
|
||
final int capturedSlot = slot;
|
||
((EditText) v).addTextChangedListener(new TextWatcher() {
|
||
@Override public void beforeTextChanged(CharSequence s, int start,
|
||
int count, int after) {}
|
||
@Override public void onTextChanged(CharSequence s, int start,
|
||
int before, int count) {}
|
||
@Override public void afterTextChanged(Editable s) {
|
||
nativeOnChange(capturedSlot, s != null ? s.toString() : "");
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Register an OnEditorActionListener on a single-line EditText that calls
|
||
* nativeOnSubmit when the user presses the action/enter key.
|
||
*/
|
||
public static void setOnSubmitListener(final int slot) {
|
||
runSync(new Runnable() {
|
||
@Override public void run() {
|
||
View v = getView(slot);
|
||
if (!(v instanceof EditText)) return;
|
||
final int capturedSlot = slot;
|
||
((EditText) v).setOnEditorActionListener(
|
||
new TextView.OnEditorActionListener() {
|
||
@Override
|
||
public boolean onEditorAction(TextView tv, int actionId,
|
||
android.view.KeyEvent event) {
|
||
nativeOnSubmit(capturedSlot,
|
||
tv.getText() != null ? tv.getText().toString() : "");
|
||
return true;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ── Internal helpers ─────────────────────────────────────────────────── */
|
||
|
||
/*
|
||
* Tag key used to stash the spacing value on LinearLayouts so addChild
|
||
* can apply the correct margin between children.
|
||
* We use a stable integer resource-id-like value; because we do not have
|
||
* a resources file here we use View.generateViewId() lazily.
|
||
*/
|
||
private static int sSpacingTagKey = 0;
|
||
|
||
private static int R_TAG_SPACING;
|
||
static {
|
||
R_TAG_SPACING = View.generateViewId();
|
||
}
|
||
|
||
/** Convert dp to pixels using the Activity's display metrics. */
|
||
private static int dpToPx(float dp) {
|
||
if (sActivity == null) return (int) dp;
|
||
float density = sActivity.getResources().getDisplayMetrics().density;
|
||
return Math.round(dp * density);
|
||
}
|
||
|
||
private static float dpToPxF(float dp) {
|
||
if (sActivity == null) return dp;
|
||
float density = sActivity.getResources().getDisplayMetrics().density;
|
||
return dp * density;
|
||
}
|
||
|
||
/** Convert RGBA float components (0–1) to an Android ARGB int. */
|
||
private static int floatToArgb(float r, float g, float b, float a) {
|
||
int ai = Math.round(a * 255f);
|
||
int ri = Math.round(r * 255f);
|
||
int gi = Math.round(g * 255f);
|
||
int bi = Math.round(b * 255f);
|
||
return Color.argb(ai, ri, gi, bi);
|
||
}
|
||
|
||
/**
|
||
* Ensure the view has a GradientDrawable background so that both color
|
||
* and corner radius can be set independently. If the current background
|
||
* is already a GradientDrawable it is reused; otherwise a new transparent
|
||
* one is installed.
|
||
*/
|
||
private static void ensureGradientBackground(View v) {
|
||
if (!(v.getBackground() instanceof GradientDrawable)) {
|
||
GradientDrawable gd = new GradientDrawable();
|
||
gd.setColor(Color.TRANSPARENT);
|
||
v.setBackground(gd);
|
||
}
|
||
}
|
||
}
|