新增 AutofillBridge.java:隐形原生 EditText(低透明度伪可见)设置 autofillHints,支持填充建议与保存提示,commit 采用反射调用规避符号解析问题 新增 AutofillBridge.cs:拦截 InputField 点击改由原生框承接输入,屏幕坐标换算、文本实时同步回显;登录邮箱/密码框接入 登录点击时先 commit 触发保存密码再隐藏;失焦/键盘完成时软隐藏(视图保留树上供快照采集),hide 防重入修复 NPE;proguard-user.txt 添加 keep 规则防 R8 剥离 状态:待真机验证填充与保存提示,已加 autofill 会话诊断日志
185 lines
6.9 KiB
C#
185 lines
6.9 KiB
C#
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
namespace Kill.UI.Components
|
||
{
|
||
/// <summary>
|
||
/// autofill 语义类型(对应 Android autofillHints)
|
||
/// </summary>
|
||
public enum AutofillHint
|
||
{
|
||
EmailAddress, // emailAddress
|
||
Username, // username
|
||
Password, // password
|
||
}
|
||
|
||
/// <summary>
|
||
/// 隐形原生输入桥(Android 谷歌密码自动填充)
|
||
///
|
||
/// 用法:AutofillBridge.Attach(inputField, AutofillHint.Password)
|
||
/// Attach 后会在 InputField 上叠一层透明按钮拦截点击:点击不再激活 Unity 自带键盘,
|
||
/// 而是在 InputField 的屏幕位置弹出带 autofillHints 的隐形原生 EditText,
|
||
/// 由谷歌密码管理器提供填充建议;输入内容实时同步回 Unity InputField 显示。
|
||
/// 登录成功时调用 Commit() 触发"保存密码"提示。
|
||
/// </summary>
|
||
public static class AutofillBridge
|
||
{
|
||
private const string JavaClass = "com.photonmatrix.autofill.AutofillBridge";
|
||
private const string ReceiverGoName = "AutofillBridgeReceiver";
|
||
|
||
private static AndroidJavaClass sJava;
|
||
private static GameObject sReceiverGo;
|
||
private static InputField sActiveField;
|
||
|
||
/// <summary>密码页配套的账号(保存凭据时账号密码成对)</summary>
|
||
public static string CompanionUsername { get; set; } = "";
|
||
|
||
public static bool Supported =>
|
||
!Application.isEditor && Application.platform == RuntimePlatform.Android;
|
||
|
||
/// <summary>
|
||
/// 绑定 InputField:叠加透明按钮拦截点击,改由原生输入桥承接
|
||
/// </summary>
|
||
public static void Attach(InputField field, AutofillHint hint)
|
||
{
|
||
if (field == null || !Supported) return;
|
||
EnsureReceiver();
|
||
|
||
var blockerGo = new GameObject(field.name + "_AutofillBlocker", typeof(RectTransform), typeof(Image), typeof(Button));
|
||
var blockerRt = blockerGo.GetComponent<RectTransform>();
|
||
var fieldRt = field.transform as RectTransform;
|
||
blockerRt.SetParent(fieldRt.parent, false);
|
||
// 复制 InputField 的布局,完全重合
|
||
blockerRt.anchorMin = fieldRt.anchorMin;
|
||
blockerRt.anchorMax = fieldRt.anchorMax;
|
||
blockerRt.pivot = fieldRt.pivot;
|
||
blockerRt.anchoredPosition = fieldRt.anchoredPosition;
|
||
blockerRt.sizeDelta = fieldRt.sizeDelta;
|
||
blockerRt.localScale = Vector3.one;
|
||
blockerRt.SetAsLastSibling(); // 保持在 InputField 之上,优先接收点击
|
||
|
||
var img = blockerGo.GetComponent<Image>();
|
||
img.color = new Color(0, 0, 0, 0); // 全透明,仅用于接收射线
|
||
|
||
var btn = blockerGo.GetComponent<Button>();
|
||
btn.transition = Selectable.Transition.None;
|
||
btn.onClick.AddListener(() => Show(field, hint));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示隐形原生输入框(一般由 Attach 的拦截按钮触发)
|
||
/// </summary>
|
||
public static void Show(InputField field, AutofillHint hint)
|
||
{
|
||
if (field == null || !Supported) return;
|
||
EnsureReceiver();
|
||
|
||
GetScreenRect(field, out int x, out int y, out int w, out int h);
|
||
sActiveField = field;
|
||
|
||
GetJava().CallStatic("show",
|
||
ToHintString(hint),
|
||
field.text ?? "",
|
||
x, y, w, h,
|
||
field.characterLimit,
|
||
CompanionUsername ?? "");
|
||
}
|
||
|
||
/// <summary>隐藏输入桥(切换页面时调用)</summary>
|
||
public static void Hide()
|
||
{
|
||
if (!Supported) return;
|
||
sActiveField = null;
|
||
GetJava().CallStatic("hide", false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 触发 autofill 保存流程(弹出"保存密码"提示)
|
||
/// 登录点击时调用:先 Commit 再 Hide
|
||
/// </summary>
|
||
public static void Commit()
|
||
{
|
||
if (!Supported) return;
|
||
GetJava().CallStatic("commit");
|
||
}
|
||
|
||
/// <summary>提交并隐藏(登录按钮常用组合)</summary>
|
||
public static void CommitAndHide()
|
||
{
|
||
if (!Supported) return;
|
||
sActiveField = null;
|
||
GetJava().CallStatic("hide", true);
|
||
}
|
||
|
||
/// <summary>原生文本变化回调(Java 端 UnitySendMessage 进入)</summary>
|
||
internal static void DispatchNativeText(string text)
|
||
{
|
||
if (sActiveField != null)
|
||
sActiveField.text = text;
|
||
}
|
||
|
||
private static AndroidJavaClass GetJava()
|
||
{
|
||
if (sJava == null)
|
||
sJava = new AndroidJavaClass(JavaClass);
|
||
return sJava;
|
||
}
|
||
|
||
private static void EnsureReceiver()
|
||
{
|
||
if (sReceiverGo != null) return;
|
||
var existing = GameObject.Find(ReceiverGoName);
|
||
sReceiverGo = existing != null ? existing : new GameObject(ReceiverGoName);
|
||
if (existing == null)
|
||
sReceiverGo.AddComponent<AutofillBridgeReceiver>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// InputField 的屏幕像素矩形,并转换为 Android 坐标系(原点左上、y 向下)
|
||
/// </summary>
|
||
private static void GetScreenRect(InputField field, out int x, out int y, out int w, out int h)
|
||
{
|
||
var rt = field.transform as RectTransform;
|
||
var canvas = field.GetComponentInParent<Canvas>();
|
||
|
||
Camera cam = null;
|
||
if (canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay)
|
||
cam = canvas.worldCamera;
|
||
|
||
var corners = new Vector3[4];
|
||
rt.GetWorldCorners(corners); // [0]左下 [2]右上
|
||
Vector2 p0 = RectTransformUtility.WorldToScreenPoint(cam, corners[0]);
|
||
Vector2 p2 = RectTransformUtility.WorldToScreenPoint(cam, corners[2]);
|
||
|
||
int minX = Mathf.RoundToInt(Mathf.Min(p0.x, p2.x));
|
||
int maxX = Mathf.RoundToInt(Mathf.Max(p0.x, p2.x));
|
||
int minY = Mathf.RoundToInt(Mathf.Min(p0.y, p2.y));
|
||
int maxY = Mathf.RoundToInt(Mathf.Max(p0.y, p2.y));
|
||
|
||
x = minX;
|
||
w = maxX - minX;
|
||
h = maxY - minY;
|
||
// Unity y 向上 → Android y 向下
|
||
y = Screen.height - maxY;
|
||
}
|
||
|
||
private static string ToHintString(AutofillHint hint)
|
||
{
|
||
switch (hint)
|
||
{
|
||
case AutofillHint.Password: return "password";
|
||
case AutofillHint.Username: return "username";
|
||
default: return "emailAddress";
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Java 端 UnitySendMessage 的接收者(GameObject 名固定为 AutofillBridgeReceiver)
|
||
/// </summary>
|
||
public class AutofillBridgeReceiver : MonoBehaviour
|
||
{
|
||
public void OnNativeTextChanged(string text) => AutofillBridge.DispatchNativeText(text);
|
||
}
|
||
}
|