killapp/Assets/Scripts/UI/Components/AutofillBridge.cs
“虞渠成” 6fa5a6c175 feat(login): 新增谷歌密码自动填充的隐形原生输入桥
新增 AutofillBridge.java:隐形原生 EditText(低透明度伪可见)设置 autofillHints,支持填充建议与保存提示,commit 采用反射调用规避符号解析问题

新增 AutofillBridge.cs:拦截 InputField 点击改由原生框承接输入,屏幕坐标换算、文本实时同步回显;登录邮箱/密码框接入

登录点击时先 commit 触发保存密码再隐藏;失焦/键盘完成时软隐藏(视图保留树上供快照采集),hide 防重入修复 NPE;proguard-user.txt 添加 keep 规则防 R8 剥离

状态:待真机验证填充与保存提示,已加 autofill 会话诊断日志
2026-09-11 09:46:33 +08:00

185 lines
6.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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