using AppUI.Bluetooth; using AppUI.Localization; using AppUI.Manager.View; using AppUI.View.Home.Main; using AppUIDeviceView = AppUI.View.Home.DeviceView.DeviceView; using DG.Tweening; using InfraredManager; using SmartBowSDK; using System; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; using BLEAimhandler = AppUI.Bluetooth.AimHandler; namespace AppUI.View.Component { /// /// 侧滑面板:BG 遮罩(可选点击关闭),Right 自右向左滑入; /// 左上关闭、底部切换 Step,Content 下步骤内容留给业务扩展。 /// [DisallowMultipleComponent] [ExecuteAlways] public class ModuleSideSlip : MonoBehaviour { const string PathMaskBg = "BG"; const string PathRight = "Right"; const string PathBtnClose = "Right/BtnClose"; const string PathContent = "Right/Content"; const string PathBottomBar = "Right/BottomBar"; const string PathBtnStep = "Right/BottomBar/BtnStep"; const float Step3BatteryRefreshInterval = 1f; const float DefaultSlideDuration = 0.32f; const string KeyBtnNext = "appui-common-next"; const string KeyBtnStartConnect = "appui-sideslip-btn-start-connect"; const string KeyBtnConnecting = "appui-sideslip-btn-connecting"; const string KeyBtnDone = "appui-sideslip-btn-done"; const string KeyBtnStep = "appui-sideslip-btn-step"; const string KeyBtnStepProgress = "appui-sideslip-btn-step-progress"; const string KeyBtnScreenPositioning = "appui-sideslip-btn-screen-positioning"; const string KeyScreenPositioningDone = "appui-sideslip-screen-positioning-done"; const string KeyScreenPositioningIncomplete = "appui-sideslip-screen-positioning-incomplete"; [SerializeField] bool maskCloseOnClick = true; [Header("动画")] [SerializeField] float slideDuration = DefaultSlideDuration; [SerializeField] Ease slideInEase = Ease.OutCubic; [SerializeField] Ease slideOutEase = Ease.InCubic; [Header("侧滑坐标(Right 需右锚点 + 右 Pivot,贴边 X 一般为 0)")] [Tooltip("面板完全展开时 Right 的 AnchoredPosition.x,勿用编辑器里拖偏后的值")] [SerializeField] float shownAnchorPosX; [Tooltip("打开前将 Right.PosX 归位到贴边值,避免预制体残留错误坐标")] [SerializeField] bool normalizeRightPosBeforeSlide = true; [Header("步骤(可留空,自动收集 Content 下 Step_* 子物体)")] [SerializeField] List stepPanels = new List(); [Header("引用(可留空,自动绑定)")] [SerializeField] RectTransform maskBg; [SerializeField] Button maskButton; [SerializeField] RectTransform rightPanel; [SerializeField] Button btnClose; [SerializeField] Button btnStep; [SerializeField] TMP_Text btnStepLabel; [Header("Step_0~2 按设备类型配置(可留空,留空则不改对应 Step 内容)")] [SerializeField] SideSlipDeviceGuideEntry[] guideEntries; [Header("Step_0 引导页")] [SerializeField] Image step0Image; [SerializeField] TMP_Text step0Description; [Header("Step_1 引导页")] [SerializeField] Image step1Image; [SerializeField] TMP_Text step1Description; [Header("Step_2 连接中页")] [SerializeField] Image step2Image; [SerializeField] TMP_Text step2Description; [Header("Step_3 连接成功页")] [SerializeField] Image step3DeviceImage; [SerializeField] TMP_Text step3Title; [SerializeField] Image step3BatteryBar; [SerializeField] Text step3BatteryValue; [SerializeField] TMP_Text step3ConnectStatus; [SerializeField] TMP_Text step3ScreenStatus; AimDeviceType? _guideDeviceTypeOverride; bool _closing; int _stepIndex; float _shownAnchorX; float _hiddenAnchorX; Tween _slideTween; Coroutine _slideInRoutine; float _step3BatteryTimer; bool _hubStep3RefreshBound; static readonly Color32 StatusCompleteColor = new Color32(0, 0, 0, 255); static readonly Color32 StatusIncompleteColor = new Color32(145, 144, 144, 255); public bool MaskCloseOnClick { get => maskCloseOnClick; set { maskCloseOnClick = value; ApplyMaskCloseInteractable(); } } public int StepCount => stepPanels != null ? stepPanels.Count : 0; public int CurrentStepIndex => _stepIndex; public RectTransform ContentRoot => transform.Find(PathContent) as RectTransform; public UnityEvent onClosed = new UnityEvent(); public event Action OnStepChanged; /// 补齐 BG / Right / BtnClose / Content / BottomBar 标准层级。 public static void EnsurePrefabHierarchy(Transform root) => SideSlipUIBuilder.EnsureHierarchy(root); public static ModuleSideSlip Show(bool dontDestroyOnLoad = false, AimDeviceType? guideDeviceType = null) { ViewConfig viewConfig = ViewManager.GetConfig(UIPrefabType.ModuleSideSlip); GameObject go = Instantiate(Resources.Load(viewConfig.path)); var slip = go.GetComponent(); if (slip == null) slip = go.AddComponent(); if (guideDeviceType.HasValue) slip.SetGuideDeviceType(guideDeviceType.Value); if (dontDestroyOnLoad) DontDestroyOnLoad(go); return slip; } /// 打开侧滑前指定当前引导所针对的设备类型(如从设备列表点击传入)。 public void SetGuideDeviceType(AimDeviceType deviceType) { _guideDeviceTypeOverride = deviceType; if (Application.isPlaying && isActiveAndEnabled) RefreshGuideSteps(); } void OnDestroy() { UnbindHubStep3Refresh(); //退出面板时候,取消正在连接的连接 SmartBowDeviceHub.ins?.CancelConnecting(); if (_slideInRoutine != null) StopCoroutine(_slideInRoutine); _slideTween?.Kill(); if (rightPanel != null) rightPanel.DOKill(); } #if UNITY_EDITOR void OnValidate() { if (Application.isPlaying) return; SideSlipUIBuilder.EnsureHierarchy(transform); ResolveReferences(); ResolveGuideStepReferences(); ApplyMaskCloseInteractable(); AppUILocalization.Init(); RefreshGuideSteps(); } #endif void Awake() { AppUILocalization.Init(); transform.localScale = Vector3.one; SideSlipUIBuilder.EnsureHierarchy(transform); ResolveReferences(); ApplyMaskCloseInteractable(); } void OnEnable() { AppUILocalization.OnLanguageChanged += HandleLanguageChanged; } void OnDisable() { AppUILocalization.OnLanguageChanged -= HandleLanguageChanged; } void HandleLanguageChanged(LanguageEnum _) { if (!Application.isPlaying) return; RefreshStepButtonForIndex(_stepIndex); if (_stepIndex is >= 0 and <= 2) RefreshGuideSteps(); if (_stepIndex == 3) RefreshStep3ConnectedInfo(); } void Start() { resetAimType(); BindButtons(); CollectStepPanelsIfEmpty(); ResolveGuideStepReferences(); if (Application.isPlaying) EnsureGuideDescriptionLocalization(); RefreshGuideSteps(); ShowStep(0, notify: false); _slideInRoutine = StartCoroutine(PlaySlideInAfterLayout()); } void Update() { if (!Application.isPlaying) return; UpdateBtnForConnect(); if (_stepIndex == 3) { _step3BatteryTimer += Time.deltaTime; if (_step3BatteryTimer >= Step3BatteryRefreshInterval) { _step3BatteryTimer = 0f; RefreshStep3Battery(); } } } void ResolveReferences() { if (maskBg == null) { var bgTr = transform.Find(PathMaskBg) as RectTransform; if (bgTr != null) maskBg = bgTr; } if (maskBg != null && maskButton == null) maskButton = maskBg.GetComponent