killapp/Assets/Scripts/UI/Components/AutofillBridge.cs

185 lines
6.9 KiB
C#
Raw Normal View History

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