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