using UnityEngine; using UnityEngine.UI; namespace CommanderAILab.Services { /// /// Mobile-specific input handler and responsive UI utilities. /// Handles: touch controls, pinch-to-zoom, swipe gestures, /// safe area insets, orientation changes, and responsive layout. /// public class MobileInputHandler : MonoBehaviour { public static MobileInputHandler Instance { get; private set; } [Header("Touch Settings")] [SerializeField] private float pinchZoomSpeed = 0.01f; [SerializeField] private float swipeThreshold = 50f; [SerializeField] private float longPressTime = 0.5f; [Header("Safe Area")] [SerializeField] private RectTransform safeAreaRect; // Gesture state private float _touchStartTime; private Vector2 _touchStartPos; private bool _isTouching; private float _lastPinchDistance; // Events public event System.Action OnPinchZoom; // delta public event System.Action OnSwipe; // direction public event System.Action OnLongPress; // position public event System.Action OnTap; // position private void Awake() { if (Instance != null) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); } private void Start() { ApplySafeArea(); SetTargetFrameRate(); } private void Update() { if (!IsMobilePlatform()) return; HandleTouchInput(); HandlePinchZoom(); // Handle orientation changes if (Screen.orientation != _lastOrientation) { _lastOrientation = Screen.orientation; ApplySafeArea(); } } private ScreenOrientation _lastOrientation; // ── Touch Gestures ───────────────────────────────────────────── private void HandleTouchInput() { if (Input.touchCount != 1) return; Touch touch = Input.GetTouch(0); switch (touch.phase) { case TouchPhase.Began: _touchStartTime = Time.time; _touchStartPos = touch.position; _isTouching = true; break; case TouchPhase.Ended: if (!_isTouching) break; _isTouching = false; float duration = Time.time - _touchStartTime; Vector2 delta = touch.position - _touchStartPos; if (duration >= longPressTime && delta.magnitude < swipeThreshold) { OnLongPress?.Invoke(touch.position); } else if (delta.magnitude >= swipeThreshold) { OnSwipe?.Invoke(delta.normalized); } else { OnTap?.Invoke(touch.position); } break; case TouchPhase.Canceled: _isTouching = false; break; } } private void HandlePinchZoom() { if (Input.touchCount != 2) { _lastPinchDistance = 0; return; } Touch t0 = Input.GetTouch(0); Touch t1 = Input.GetTouch(1); float dist = Vector2.Distance(t0.position, t1.position); if (_lastPinchDistance > 0) { float delta = (dist - _lastPinchDistance) * pinchZoomSpeed; OnPinchZoom?.Invoke(delta); } _lastPinchDistance = dist; } // ── Safe Area (notch / rounded corners) ───────────────────── private void ApplySafeArea() { if (safeAreaRect == null) return; Rect safe = Screen.safeArea; Vector2 anchorMin = safe.position; Vector2 anchorMax = safe.position + safe.size; anchorMin.x /= Screen.width; anchorMin.y /= Screen.height; anchorMax.x /= Screen.width; anchorMax.y /= Screen.height; safeAreaRect.anchorMin = anchorMin; safeAreaRect.anchorMax = anchorMax; } // ── Platform Config ────────────────────────────────────────── private void SetTargetFrameRate() { #if UNITY_IOS || UNITY_ANDROID Application.targetFrameRate = 60; Screen.sleepTimeout = SleepTimeout.NeverSleep; #endif } public static bool IsMobilePlatform() { #if UNITY_IOS || UNITY_ANDROID return true; #else return false; #endif } /// Returns responsive font size based on screen DPI. public static int ResponsiveFontSize(int baseSizeAt96DPI) { float dpi = Screen.dpi > 0 ? Screen.dpi : 96f; return Mathf.RoundToInt(baseSizeAt96DPI * (dpi / 96f) * 0.6f); } /// Returns true if device is in portrait orientation. public static bool IsPortrait() { return Screen.height > Screen.width; } } }