ModuleSideSlip.cs 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255
  1. using AppUI.Bluetooth;
  2. using AppUI.Localization;
  3. using AppUI.Manager.View;
  4. using AppUI.View.Home.Main;
  5. using AppUIDeviceView = AppUI.View.Home.DeviceView.DeviceView;
  6. using DG.Tweening;
  7. using InfraredManager;
  8. using SmartBowSDK;
  9. using System;
  10. using System.Collections;
  11. using System.Collections.Generic;
  12. using TMPro;
  13. using UnityEngine;
  14. using UnityEngine.Events;
  15. using UnityEngine.UI;
  16. using BLEAimhandler = AppUI.Bluetooth.AimHandler;
  17. namespace AppUI.View.Component
  18. {
  19. /// <summary>
  20. /// 侧滑面板:<c>BG</c> 遮罩(可选点击关闭),<c>Right</c> 自右向左滑入;
  21. /// 左上关闭、底部切换 Step,<c>Content</c> 下步骤内容留给业务扩展。
  22. /// </summary>
  23. [DisallowMultipleComponent]
  24. [ExecuteAlways]
  25. public class ModuleSideSlip : MonoBehaviour
  26. {
  27. const string PathMaskBg = "BG";
  28. const string PathRight = "Right";
  29. const string PathBtnClose = "Right/BtnClose";
  30. const string PathContent = "Right/Content";
  31. const string PathBottomBar = "Right/BottomBar";
  32. const string PathBtnStep = "Right/BottomBar/BtnStep";
  33. const float Step3BatteryRefreshInterval = 1f;
  34. const float DefaultSlideDuration = 0.32f;
  35. const string KeyBtnNext = "appui-common-next";
  36. const string KeyBtnStartConnect = "appui-sideslip-btn-start-connect";
  37. const string KeyBtnConnecting = "appui-sideslip-btn-connecting";
  38. const string KeyBtnDone = "appui-sideslip-btn-done";
  39. const string KeyBtnStep = "appui-sideslip-btn-step";
  40. const string KeyBtnStepProgress = "appui-sideslip-btn-step-progress";
  41. const string KeyBtnScreenPositioning = "appui-sideslip-btn-screen-positioning";
  42. const string KeyScreenPositioningDone = "appui-sideslip-screen-positioning-done";
  43. const string KeyScreenPositioningIncomplete = "appui-sideslip-screen-positioning-incomplete";
  44. [SerializeField]
  45. bool maskCloseOnClick = true;
  46. [Header("动画")]
  47. [SerializeField]
  48. float slideDuration = DefaultSlideDuration;
  49. [SerializeField]
  50. Ease slideInEase = Ease.OutCubic;
  51. [SerializeField]
  52. Ease slideOutEase = Ease.InCubic;
  53. [Header("侧滑坐标(Right 需右锚点 + 右 Pivot,贴边 X 一般为 0)")]
  54. [Tooltip("面板完全展开时 Right 的 AnchoredPosition.x,勿用编辑器里拖偏后的值")]
  55. [SerializeField]
  56. float shownAnchorPosX;
  57. [Tooltip("打开前将 Right.PosX 归位到贴边值,避免预制体残留错误坐标")]
  58. [SerializeField]
  59. bool normalizeRightPosBeforeSlide = true;
  60. [Header("步骤(可留空,自动收集 Content 下 Step_* 子物体)")]
  61. [SerializeField]
  62. List<GameObject> stepPanels = new List<GameObject>();
  63. [Header("引用(可留空,自动绑定)")]
  64. [SerializeField]
  65. RectTransform maskBg;
  66. [SerializeField]
  67. Button maskButton;
  68. [SerializeField]
  69. RectTransform rightPanel;
  70. [SerializeField]
  71. Button btnClose;
  72. [SerializeField]
  73. Button btnStep;
  74. [SerializeField]
  75. TMP_Text btnStepLabel;
  76. [Header("Step_0~2 按设备类型配置(可留空,留空则不改对应 Step 内容)")]
  77. [SerializeField]
  78. SideSlipDeviceGuideEntry[] guideEntries;
  79. [Header("Step_0 引导页")]
  80. [SerializeField] Image step0Image;
  81. [SerializeField] TMP_Text step0Description;
  82. [Header("Step_1 引导页")]
  83. [SerializeField] Image step1Image;
  84. [SerializeField] TMP_Text step1Description;
  85. [Header("Step_2 连接中页")]
  86. [SerializeField] Image step2Image;
  87. [SerializeField] TMP_Text step2Description;
  88. [Header("Step_3 连接成功页")]
  89. [SerializeField] Image step3DeviceImage;
  90. [SerializeField] TMP_Text step3Title;
  91. [SerializeField] Image step3BatteryBar;
  92. [SerializeField] Text step3BatteryValue;
  93. [SerializeField] TMP_Text step3ConnectStatus;
  94. [SerializeField] TMP_Text step3ScreenStatus;
  95. AimDeviceType? _guideDeviceTypeOverride;
  96. bool _closing;
  97. int _stepIndex;
  98. float _shownAnchorX;
  99. float _hiddenAnchorX;
  100. Tween _slideTween;
  101. Coroutine _slideInRoutine;
  102. float _step3BatteryTimer;
  103. bool _hubStep3RefreshBound;
  104. static readonly Color32 StatusCompleteColor = new Color32(0, 0, 0, 255);
  105. static readonly Color32 StatusIncompleteColor = new Color32(145, 144, 144, 255);
  106. public bool MaskCloseOnClick
  107. {
  108. get => maskCloseOnClick;
  109. set
  110. {
  111. maskCloseOnClick = value;
  112. ApplyMaskCloseInteractable();
  113. }
  114. }
  115. public int StepCount => stepPanels != null ? stepPanels.Count : 0;
  116. public int CurrentStepIndex => _stepIndex;
  117. public RectTransform ContentRoot => transform.Find(PathContent) as RectTransform;
  118. public UnityEvent onClosed = new UnityEvent();
  119. public event Action<int> OnStepChanged;
  120. /// <summary>补齐 BG / Right / BtnClose / Content / BottomBar 标准层级。</summary>
  121. public static void EnsurePrefabHierarchy(Transform root) => SideSlipUIBuilder.EnsureHierarchy(root);
  122. public static ModuleSideSlip Show(bool dontDestroyOnLoad = false, AimDeviceType? guideDeviceType = null)
  123. {
  124. ViewConfig viewConfig = ViewManager.GetConfig(UIPrefabType.ModuleSideSlip);
  125. GameObject go = Instantiate(Resources.Load<GameObject>(viewConfig.path));
  126. var slip = go.GetComponent<ModuleSideSlip>();
  127. if (slip == null)
  128. slip = go.AddComponent<ModuleSideSlip>();
  129. if (guideDeviceType.HasValue)
  130. slip.SetGuideDeviceType(guideDeviceType.Value);
  131. if (dontDestroyOnLoad)
  132. DontDestroyOnLoad(go);
  133. return slip;
  134. }
  135. /// <summary>打开侧滑前指定当前引导所针对的设备类型(如从设备列表点击传入)。</summary>
  136. public void SetGuideDeviceType(AimDeviceType deviceType)
  137. {
  138. _guideDeviceTypeOverride = deviceType;
  139. if (Application.isPlaying && isActiveAndEnabled)
  140. RefreshGuideSteps();
  141. }
  142. void OnDestroy()
  143. {
  144. UnbindHubStep3Refresh();
  145. //退出面板时候,取消正在连接的连接
  146. SmartBowDeviceHub.ins?.CancelConnecting();
  147. if (_slideInRoutine != null)
  148. StopCoroutine(_slideInRoutine);
  149. _slideTween?.Kill();
  150. if (rightPanel != null)
  151. rightPanel.DOKill();
  152. }
  153. #if UNITY_EDITOR
  154. void OnValidate()
  155. {
  156. if (Application.isPlaying)
  157. return;
  158. SideSlipUIBuilder.EnsureHierarchy(transform);
  159. ResolveReferences();
  160. ResolveGuideStepReferences();
  161. ApplyMaskCloseInteractable();
  162. AppUILocalization.Init();
  163. RefreshGuideSteps();
  164. }
  165. #endif
  166. void Awake()
  167. {
  168. AppUILocalization.Init();
  169. transform.localScale = Vector3.one;
  170. SideSlipUIBuilder.EnsureHierarchy(transform);
  171. ResolveReferences();
  172. ApplyMaskCloseInteractable();
  173. }
  174. void OnEnable()
  175. {
  176. AppUILocalization.OnLanguageChanged += HandleLanguageChanged;
  177. }
  178. void OnDisable()
  179. {
  180. AppUILocalization.OnLanguageChanged -= HandleLanguageChanged;
  181. }
  182. void HandleLanguageChanged(LanguageEnum _)
  183. {
  184. if (!Application.isPlaying)
  185. return;
  186. RefreshStepButtonForIndex(_stepIndex);
  187. if (_stepIndex is >= 0 and <= 2)
  188. RefreshGuideSteps();
  189. if (_stepIndex == 3)
  190. RefreshStep3ConnectedInfo();
  191. }
  192. void Start()
  193. {
  194. resetAimType();
  195. BindButtons();
  196. CollectStepPanelsIfEmpty();
  197. ResolveGuideStepReferences();
  198. if (Application.isPlaying)
  199. EnsureGuideDescriptionLocalization();
  200. RefreshGuideSteps();
  201. ShowStep(0, notify: false);
  202. _slideInRoutine = StartCoroutine(PlaySlideInAfterLayout());
  203. }
  204. void Update()
  205. {
  206. if (!Application.isPlaying)
  207. return;
  208. UpdateBtnForConnect();
  209. if (_stepIndex == 3)
  210. {
  211. _step3BatteryTimer += Time.deltaTime;
  212. if (_step3BatteryTimer >= Step3BatteryRefreshInterval)
  213. {
  214. _step3BatteryTimer = 0f;
  215. RefreshStep3Battery();
  216. }
  217. }
  218. }
  219. void ResolveReferences()
  220. {
  221. if (maskBg == null)
  222. {
  223. var bgTr = transform.Find(PathMaskBg) as RectTransform;
  224. if (bgTr != null)
  225. maskBg = bgTr;
  226. }
  227. if (maskBg != null && maskButton == null)
  228. maskButton = maskBg.GetComponent<Button>();
  229. if (rightPanel == null)
  230. rightPanel = transform.Find(PathRight) as RectTransform;
  231. if (btnClose == null)
  232. {
  233. var tr = transform.Find(PathBtnClose);
  234. if (tr != null)
  235. btnClose = tr.GetComponent<Button>();
  236. }
  237. if (btnStep == null)
  238. {
  239. var tr = transform.Find(PathBtnStep);
  240. if (tr != null)
  241. btnStep = tr.GetComponent<Button>();
  242. }
  243. if (btnStepLabel == null && btnStep != null)
  244. btnStepLabel = btnStep.GetComponentInChildren<TMP_Text>(true);
  245. }
  246. void BindButtons()
  247. {
  248. if (maskButton != null)
  249. {
  250. maskButton.onClick.RemoveListener(OnMaskClicked);
  251. maskButton.onClick.AddListener(OnMaskClicked);
  252. }
  253. if (btnClose != null)
  254. {
  255. btnClose.onClick.RemoveListener(Close);
  256. btnClose.onClick.AddListener(Close);
  257. }
  258. if (btnStep != null)
  259. {
  260. btnStep.onClick.RemoveListener(OnStepButtonClicked);
  261. btnStep.onClick.AddListener(OnStepButtonClicked);
  262. }
  263. }
  264. void ApplyMaskCloseInteractable()
  265. {
  266. if (maskButton == null && maskBg != null)
  267. maskButton = maskBg.GetComponent<Button>();
  268. if (maskButton != null)
  269. maskButton.interactable = maskCloseOnClick;
  270. }
  271. IEnumerator PlaySlideInAfterLayout()
  272. {
  273. // 等 Canvas / Layout 算出正确宽度,避免 Start 当帧 rect.width 为 0
  274. yield return null;
  275. Canvas.ForceUpdateCanvases();
  276. ForceRebuildLayout();
  277. if (normalizeRightPosBeforeSlide && rightPanel != null)
  278. {
  279. var pos = rightPanel.anchoredPosition;
  280. rightPanel.anchoredPosition = new Vector2(shownAnchorPosX, pos.y);
  281. }
  282. CacheSlideAnchors();
  283. PlaySlideIn();
  284. _slideInRoutine = null;
  285. }
  286. void ForceRebuildLayout()
  287. {
  288. var canvas = GetComponentInParent<Canvas>();
  289. if (canvas != null)
  290. {
  291. var canvasRt = canvas.transform as RectTransform;
  292. if (canvasRt != null)
  293. LayoutRebuilder.ForceRebuildLayoutImmediate(canvasRt);
  294. }
  295. if (rightPanel != null)
  296. LayoutRebuilder.ForceRebuildLayoutImmediate(rightPanel);
  297. }
  298. void CacheSlideAnchors()
  299. {
  300. if (rightPanel == null)
  301. return;
  302. ForceRebuildLayout();
  303. _shownAnchorX = shownAnchorPosX;
  304. var width = GetPanelSlideWidth();
  305. // 右锚 + 右 Pivot:X 增大表示向右(屏外),滑入终点为 shown,起点为 shown + 宽度
  306. _hiddenAnchorX = _shownAnchorX + width;
  307. }
  308. float GetPanelSlideWidth()
  309. {
  310. var w = Mathf.Abs(rightPanel.rect.width);
  311. if (w > 1f)
  312. return w;
  313. w = Mathf.Abs(rightPanel.sizeDelta.x);
  314. if (w > 1f)
  315. return w;
  316. var parent = rightPanel.parent as RectTransform;
  317. if (parent != null)
  318. {
  319. w = Mathf.Abs(parent.rect.width);
  320. if (w > 1f)
  321. return w;
  322. }
  323. return 900f;
  324. }
  325. void CollectStepPanelsIfEmpty()
  326. {
  327. if (stepPanels != null && stepPanels.Count > 0)
  328. return;
  329. stepPanels = new List<GameObject>();
  330. var content = transform.Find(PathContent);
  331. if (content == null)
  332. return;
  333. for (var i = 0; i < content.childCount; i++)
  334. {
  335. var child = content.GetChild(i);
  336. if (child.name.StartsWith("Step_", StringComparison.Ordinal))
  337. stepPanels.Add(child.gameObject);
  338. }
  339. stepPanels.Sort((a, b) => string.CompareOrdinal(a.name, b.name));
  340. }
  341. void PlaySlideIn()
  342. {
  343. if (rightPanel == null)
  344. return;
  345. _slideTween?.Kill();
  346. rightPanel.DOKill();
  347. var pos = rightPanel.anchoredPosition;
  348. rightPanel.anchoredPosition = new Vector2(_hiddenAnchorX, pos.y);
  349. _slideTween = rightPanel
  350. .DOAnchorPosX(_shownAnchorX, slideDuration)
  351. .SetEase(slideInEase)
  352. .SetUpdate(true);
  353. }
  354. void PlaySlideOut(Action onComplete)
  355. {
  356. if (rightPanel == null)
  357. {
  358. onComplete?.Invoke();
  359. return;
  360. }
  361. CacheSlideAnchors();
  362. _slideTween?.Kill();
  363. rightPanel.DOKill();
  364. var pos = rightPanel.anchoredPosition;
  365. _slideTween = rightPanel
  366. .DOAnchorPosX(_hiddenAnchorX, slideDuration)
  367. .SetEase(slideOutEase)
  368. .SetUpdate(true)
  369. .OnComplete(() => onComplete?.Invoke());
  370. }
  371. void OnMaskClicked()
  372. {
  373. if (!maskCloseOnClick)
  374. return;
  375. Close();
  376. }
  377. void OnStepButtonClicked()
  378. {
  379. if (_stepIndex == 3)
  380. {
  381. NavigateAfterConnect();
  382. return;
  383. }
  384. if (StepCount <= 1)
  385. return;
  386. int next = _stepIndex + 1;
  387. if (next >= StepCount)
  388. next = 0;
  389. ShowStep(next);
  390. }
  391. /// <summary>切换并显示指定步骤(0 起)。</summary>
  392. public void ShowStep(int index, bool notify = true)
  393. {
  394. CollectStepPanelsIfEmpty();
  395. if (StepCount == 0)
  396. return;
  397. _stepIndex = Mathf.Clamp(index, 0, StepCount - 1);
  398. for (var i = 0; i < stepPanels.Count; i++)
  399. {
  400. if (stepPanels[i] != null)
  401. stepPanels[i].SetActive(i == _stepIndex);
  402. }
  403. RefreshStepButtonForIndex(_stepIndex);
  404. if (_stepIndex is >= 0 and <= 2)
  405. RefreshGuideSteps();
  406. if (_stepIndex == 3)
  407. {
  408. ResolveStep3References();
  409. _step3BatteryTimer = 0f;
  410. BindHubStep3Refresh();
  411. RefreshStep3ConnectedInfo();
  412. RefreshDeviceViewConnectContent();
  413. }
  414. else
  415. {
  416. UnbindHubStep3Refresh();
  417. }
  418. if (notify)
  419. OnStepChanged?.Invoke(_stepIndex);
  420. }
  421. void ResolveGuideStepReferences()
  422. {
  423. ResolveGuideStepReference(ref step0Image, ref step0Description, 0);
  424. ResolveGuideStepReference(ref step1Image, ref step1Description, 1);
  425. ResolveGuideStepReference(ref step2Image, ref step2Description, 2);
  426. ResolveStep3References();
  427. }
  428. void ResolveStep3References()
  429. {
  430. if (StepCount <= 3 || stepPanels[3] == null)
  431. return;
  432. Transform stepRoot = stepPanels[3].transform;
  433. if (step3DeviceImage == null)
  434. {
  435. Transform imageTr = stepRoot.Find("Image");
  436. if (imageTr != null)
  437. step3DeviceImage = imageTr.GetComponent<Image>();
  438. step3DeviceImage.SetNativeSize();
  439. }
  440. if (step3Title == null)
  441. {
  442. Transform titleTr = stepRoot.Find("title");
  443. if (titleTr == null)
  444. titleTr = stepRoot.Find("Title");
  445. if (titleTr != null)
  446. step3Title = titleTr.GetComponent<TMP_Text>();
  447. }
  448. if (step3BatteryBar == null)
  449. {
  450. Transform batteryTr = stepRoot.Find("battery");
  451. if (batteryTr != null)
  452. step3BatteryBar = batteryTr.GetComponent<Image>();
  453. }
  454. if (step3BatteryValue == null && step3BatteryBar != null)
  455. {
  456. Transform valueTr = step3BatteryBar.transform.Find("Text");
  457. if (valueTr != null)
  458. step3BatteryValue = valueTr.GetComponent<Text>();
  459. }
  460. if (step3ConnectStatus == null)
  461. {
  462. Transform statusTr = stepRoot.Find("connectStatus");
  463. if (statusTr == null)
  464. statusTr = stepRoot.Find("ConnectStatus");
  465. if (statusTr != null)
  466. step3ConnectStatus = statusTr.GetComponent<TMP_Text>();
  467. }
  468. if (step3ScreenStatus == null)
  469. {
  470. Transform screenTr = stepRoot.Find("screenStatus");
  471. if (screenTr == null)
  472. screenTr = stepRoot.Find("ScreenStatus");
  473. if (screenTr != null)
  474. step3ScreenStatus = screenTr.GetComponent<TMP_Text>();
  475. }
  476. }
  477. void ResolveGuideStepReference(ref Image image, ref TMP_Text description, int stepIndex)
  478. {
  479. if (stepIndex >= StepCount || stepPanels[stepIndex] == null)
  480. return;
  481. Transform stepRoot = stepPanels[stepIndex].transform;
  482. if (image == null)
  483. {
  484. Transform imageTr = stepRoot.Find("Image");
  485. if (imageTr == null)
  486. imageTr = stepRoot.Find("Content");
  487. if (imageTr != null)
  488. image = imageTr.GetComponent<Image>();
  489. }
  490. if (description == null)
  491. {
  492. Transform textTr = stepRoot.Find("Text (TMP)");
  493. if (textTr != null)
  494. description = textTr.GetComponent<TMP_Text>();
  495. }
  496. }
  497. static void EnsureGuideDescriptionLocalization(TMP_Text step0, TMP_Text step1, TMP_Text step2)
  498. {
  499. EnsureLocalizedComponent(step0);
  500. EnsureLocalizedComponent(step1);
  501. EnsureLocalizedComponent(step2);
  502. }
  503. void EnsureGuideDescriptionLocalization()
  504. {
  505. EnsureGuideDescriptionLocalization(step0Description, step1Description, step2Description);
  506. }
  507. static void EnsureLocalizedComponent(TMP_Text text)
  508. {
  509. if (text == null || text.GetComponent<AppUITextAutoLanguage>() != null)
  510. return;
  511. text.gameObject.AddComponent<AppUITextAutoLanguage>();
  512. }
  513. AimDeviceType ResolveGuideDeviceType()
  514. {
  515. if (_guideDeviceTypeOverride.HasValue && _guideDeviceTypeOverride.Value != AimDeviceType.NONE)
  516. return _guideDeviceTypeOverride.Value;
  517. SmartBowDeviceHub hub = SmartBowDeviceHub.ins;
  518. BLEAimhandler aim = hub?.Aim;
  519. if (aim == null)
  520. return AimDeviceType.NONE;
  521. BluetoothPlayer player = hub.GetActivePlayer();
  522. int deviceType = aim.GetDeviceType(player);
  523. if (deviceType == (int)AimDeviceType.NONE)
  524. {
  525. int tempType = aim.GetTempDeviceType();
  526. if (tempType != -1)
  527. deviceType = tempType;
  528. }
  529. return (AimDeviceType)deviceType;
  530. }
  531. void RefreshGuideSteps()
  532. {
  533. AimDeviceType deviceType = ResolveGuideDeviceType();
  534. if (deviceType == AimDeviceType.NONE)
  535. return;
  536. TryGetGuideEntry(deviceType, out SideSlipDeviceGuideEntry entry);
  537. ApplyStepDeviceVisualRoots(1, GetActiveDeviceVisualRoot(entry?.step1));
  538. ApplyStepDeviceVisualRoots(2, GetActiveDeviceVisualRoot(entry?.step2));
  539. if (entry == null)
  540. return;
  541. ApplyGuideStepContent(step0Image, step0Description, entry.step0);
  542. ApplyGuideStepContent(step1Image, step1Description, entry.step1);
  543. ApplyGuideStepContent(step2Image, step2Description, entry.step2);
  544. }
  545. void ApplyStepDeviceVisualRoots(int stepIndex, GameObject activeRoot)
  546. {
  547. if (guideEntries == null)
  548. return;
  549. for (int i = 0; i < guideEntries.Length; i++)
  550. {
  551. SideSlipDeviceGuideEntry guideEntry = guideEntries[i];
  552. if (guideEntry == null)
  553. continue;
  554. SideSlipGuideStepContent stepContent = stepIndex == 1 ? guideEntry.step1 : guideEntry.step2;
  555. if (!IsDeviceVisualRootEnabled(stepContent))
  556. continue;
  557. GameObject root = stepContent.deviceVisualRoot;
  558. if (root == null)
  559. continue;
  560. SetActiveSafe(root, root == activeRoot);
  561. }
  562. }
  563. static GameObject GetActiveDeviceVisualRoot(SideSlipGuideStepContent content)
  564. {
  565. if (!IsDeviceVisualRootEnabled(content))
  566. return null;
  567. return content.deviceVisualRoot;
  568. }
  569. static bool IsDeviceVisualRootEnabled(SideSlipGuideStepContent content) =>
  570. content != null && content.useDeviceVisualRoot;
  571. static void SetActiveSafe(GameObject go, bool active)
  572. {
  573. if (go == null || go.activeSelf == active)
  574. return;
  575. go.SetActive(active);
  576. }
  577. bool TryGetGuideEntry(AimDeviceType deviceType, out SideSlipDeviceGuideEntry entry)
  578. {
  579. entry = null;
  580. if (guideEntries == null)
  581. return false;
  582. for (int i = 0; i < guideEntries.Length; i++)
  583. {
  584. SideSlipDeviceGuideEntry candidate = guideEntries[i];
  585. if (candidate == null || candidate.deviceType != deviceType)
  586. continue;
  587. entry = candidate;
  588. return true;
  589. }
  590. return false;
  591. }
  592. static void ApplyGuideStepContent(Image image, TMP_Text description, SideSlipGuideStepContent content)
  593. {
  594. if (content == null)
  595. return;
  596. if (content.useImage && image != null && content.image != null)
  597. {
  598. image.sprite = content.image;
  599. image.SetNativeSize();
  600. image.enabled = true;
  601. }
  602. if (description == null)
  603. return;
  604. if (content.useDescriptionKey && !string.IsNullOrEmpty(content.descriptionKey))
  605. {
  606. AppUITextAutoLanguage localized = description.GetComponent<AppUITextAutoLanguage>();
  607. if (localized != null)
  608. {
  609. localized.SetTextKey(content.descriptionKey);
  610. return;
  611. }
  612. description.text = AppUILocalization.GetTextByKey(content.descriptionKey);
  613. return;
  614. }
  615. if (content.useDescription && !string.IsNullOrEmpty(content.description))
  616. description.text = content.description;
  617. }
  618. void RefreshStep3ConnectedInfo()
  619. {
  620. SmartBowDeviceHub hub = SmartBowDeviceHub.ins;
  621. BLEAimhandler aim = hub?.Aim;
  622. if (hub == null || aim == null)
  623. return;
  624. BluetoothPlayer player = hub.GetActivePlayer();
  625. int deviceType = aim.GetDeviceType(player);
  626. if (deviceType != (int)AimDeviceType.NONE)
  627. {
  628. var deviceTypeEnum = (AimDeviceType)deviceType;
  629. if (step3Title != null)
  630. step3Title.text = hub.GetDeviceDisplayName(deviceTypeEnum);
  631. Sprite sprite = hub.GetDeviceDisplaySprite(deviceTypeEnum);
  632. if (step3DeviceImage != null && sprite != null)
  633. {
  634. step3DeviceImage.sprite = sprite;
  635. step3DeviceImage.SetNativeSize();
  636. }
  637. }
  638. RefreshStep3Battery();
  639. RefreshStep3StatusTexts(hub, aim, player);
  640. }
  641. void BindHubStep3Refresh()
  642. {
  643. SmartBowDeviceHub hub = SmartBowDeviceHub.ins;
  644. if (hub == null || _hubStep3RefreshBound)
  645. return;
  646. hub.OnStateChanged += OnHubStateChangedForStep3;
  647. _hubStep3RefreshBound = true;
  648. }
  649. void UnbindHubStep3Refresh()
  650. {
  651. if (!_hubStep3RefreshBound)
  652. return;
  653. SmartBowDeviceHub hub = SmartBowDeviceHub.ins;
  654. if (hub != null)
  655. hub.OnStateChanged -= OnHubStateChangedForStep3;
  656. _hubStep3RefreshBound = false;
  657. }
  658. void OnHubStateChangedForStep3(SmartBowDeviceState state)
  659. {
  660. if (_stepIndex != 3)
  661. return;
  662. RefreshStep3Battery();
  663. SmartBowDeviceHub hub = SmartBowDeviceHub.ins;
  664. BLEAimhandler aim = hub?.Aim;
  665. if (hub == null || aim == null)
  666. return;
  667. RefreshStep3StatusTexts(hub, aim, hub.GetActivePlayer());
  668. }
  669. void RefreshStep3Battery()
  670. {
  671. SmartBowDeviceHub hub = SmartBowDeviceHub.ins;
  672. if (hub == null)
  673. return;
  674. BluetoothPlayer player = hub.GetActivePlayer();
  675. SmartBowHelper helper = hub.GetHelper(player);
  676. int battery = helper != null ? helper.GetBattery() : hub.GetBattery(player);
  677. battery = Mathf.Clamp(battery, 0, 100);
  678. if (step3BatteryBar != null)
  679. step3BatteryBar.fillAmount = battery / 100f;
  680. if (step3BatteryValue != null)
  681. step3BatteryValue.text = battery + "%";
  682. }
  683. void RefreshStep3StatusTexts(SmartBowDeviceHub hub, BLEAimhandler aim, BluetoothPlayer player)
  684. {
  685. bool connected = hub.IsConnected(player);
  686. if (step3ConnectStatus != null)
  687. {
  688. string statusKey = hub.GetStatusTextKey(player);
  689. step3ConnectStatus.text = AppUILocalization.GetTextByKey(statusKey, step3ConnectStatus);
  690. step3ConnectStatus.color = connected ? StatusCompleteColor : StatusIncompleteColor;
  691. }
  692. if (step3ScreenStatus == null)
  693. return;
  694. if (!BLEAimhandler.IsInfraredDeviceType(aim.GetDeviceType(player)))
  695. {
  696. step3ScreenStatus.gameObject.SetActive(false);
  697. return;
  698. }
  699. step3ScreenStatus.gameObject.SetActive(true);
  700. bool positioned = IsScreenPositioned();
  701. step3ScreenStatus.text = AppUILocalization.GetTextByKey(
  702. positioned ? KeyScreenPositioningDone : KeyScreenPositioningIncomplete, step3ScreenStatus);
  703. step3ScreenStatus.color = positioned ? StatusCompleteColor : StatusIncompleteColor;
  704. }
  705. static bool IsScreenPositioned()
  706. {
  707. InfraredCameraHelper helper = InfraredCameraHelper.GetInstance();
  708. return helper != null && helper.IsScreenLoateOK();
  709. }
  710. void RefreshStepButtonForIndex(int index)
  711. {
  712. if (btnStepLabel == null)
  713. return;
  714. switch (index)
  715. {
  716. case 0:
  717. btnStepLabel.text = AppUILocalization.GetTextByKey(KeyBtnNext);
  718. if (btnStep != null)
  719. btnStep.interactable = true;
  720. break;
  721. case 1:
  722. btnStepLabel.text = AppUILocalization.GetTextByKey(KeyBtnStartConnect);
  723. if (btnStep != null)
  724. btnStep.interactable = true;
  725. break;
  726. case 2:
  727. btnStepLabel.text = AppUILocalization.GetTextByKey(KeyBtnConnecting);
  728. OnClick_ConnectBLE();
  729. if (btnStep != null)
  730. btnStep.interactable = false;
  731. break;
  732. case 3:
  733. ApplyPostConnectStepLabel();
  734. if (btnStep != null)
  735. btnStep.interactable = true;
  736. break;
  737. }
  738. }
  739. void ApplyPostConnectStepLabel()
  740. {
  741. BLEAimhandler aim = SmartBowDeviceHub.ins?.Aim;
  742. if (aim != null && aim.IsActivePlayerInfraredDevice())
  743. {
  744. btnStepLabel.text = AppUILocalization.GetTextByKey(KeyBtnScreenPositioning);
  745. return;
  746. }
  747. if (aim != null && aim.IsActivePlayerNineAxisDevice())
  748. {
  749. btnStepLabel.text = AppUILocalization.GetTextByKey(KeyBtnNext);
  750. return;
  751. }
  752. btnStepLabel.text = AppUILocalization.GetTextByKey(KeyBtnDone);
  753. }
  754. void NavigateAfterConnect()
  755. {
  756. AudioMgr.ins.PlayBtn();
  757. SmartBowDeviceHub hub = SmartBowDeviceHub.ins;
  758. BLEAimhandler aim = hub?.Aim;
  759. Close();
  760. if (aim == null)
  761. return;
  762. if (aim.IsActivePlayerInfraredDevice())
  763. {
  764. ViewManager.ShowView(UIViewType.InfraredView);
  765. }
  766. else if (aim.IsActivePlayerNineAxisDevice())
  767. {
  768. hub?.EnsureNineAxisSensorsStarted();
  769. ViewManager.ShowView(UIViewType.GyrGuidanceView);
  770. }
  771. }
  772. /// <summary>九轴设备需等 moduleInited 后再进入步骤 3,并预开传感(对齐 DemoStarter)。</summary>
  773. static bool TryAdvanceToPostConnectStep(SmartBowDeviceHub hub)
  774. {
  775. BLEAimhandler aim = hub?.Aim;
  776. if (aim == null)
  777. return true;
  778. if (!aim.IsActivePlayerNineAxisDevice())
  779. return true;
  780. BluetoothPlayer player = hub.GetActivePlayer();
  781. if (!hub.IsModuleInited(player))
  782. return false;
  783. hub.EnsureNineAxisSensorsStarted(player);
  784. return true;
  785. }
  786. void RefreshStepButtonLabel()
  787. {
  788. if (btnStepLabel == null)
  789. return;
  790. btnStepLabel.text = StepCount <= 1
  791. ? AppUILocalization.GetTextByKey(KeyBtnStep)
  792. : string.Format(
  793. AppUILocalization.GetTextByKey(KeyBtnStepProgress),
  794. _stepIndex + 1,
  795. StepCount);
  796. }
  797. /// <summary>关闭:右侧面板滑出后销毁。</summary>
  798. public void Close()
  799. {
  800. if (_closing)
  801. return;
  802. _closing = true;
  803. if (maskButton != null)
  804. maskButton.interactable = false;
  805. if (btnClose != null)
  806. btnClose.interactable = false;
  807. if (btnStep != null)
  808. btnStep.interactable = false;
  809. PlaySlideOut(() =>
  810. {
  811. UnbindHubStep3Refresh();
  812. SmartBowDeviceHub hub = SmartBowDeviceHub.ins;
  813. if (hub != null && hub.IsConnected(hub.GetActivePlayer()))
  814. {
  815. // 未打开时 HideView 仅清理缓存,不会抛错
  816. ViewManager.HideView(UIViewType.DeviceArcheryView);
  817. ViewManager.HideView(UIViewType.DeviceGunView);
  818. }
  819. RefreshDeviceViewConnectContent();
  820. onClosed?.Invoke();
  821. Destroy(gameObject);
  822. });
  823. }
  824. /// <summary>侧滑关闭或连接成功时,关闭设备选型子页并刷新 DeviceView。</summary>
  825. static void RefreshDeviceViewConnectContent()
  826. {
  827. AppUIDeviceView active = AppUIDeviceView.Active;
  828. if (active == null) return;
  829. active.EndDeviceSelectionFlow();
  830. active.RefreshConnectContent(force: true);
  831. }
  832. #region 操作蓝牙连接的按钮事件,放在这里是因为这个侧滑面板目前只用在连接蓝牙设备的引导上,如果后续有其他用途再考虑重构
  833. //[SerializeField] GameObject btnConnectBow;
  834. //[SerializeField] Text btnOmitText;
  835. private int dotCount = 0;
  836. private float interval = 0.5f; // 标点更新间隔
  837. private float timer = 0f;
  838. bool isNext = false;
  839. SmartBowSDK.BluetoothStatusEnum bowStatus1P;
  840. SmartBowSDK.BluetoothStatusEnum bowStatus2P;
  841. public void OnClick_ConnectBLE()
  842. {
  843. //此页面连接当作重新初始化
  844. resetAimDevice();
  845. SmartBowDeviceHub hub = SmartBowDeviceHub.ins;
  846. if (hub == null) return;
  847. Debug.Log("SmartBowDeviceHub ActivePlayer:" + hub.GetActivePlayer());
  848. hub.ConnectActivePlayer();
  849. }
  850. void UpdateBtnForConnect()
  851. {
  852. SmartBowDeviceHub hub = SmartBowDeviceHub.ins;
  853. if (hub == null) return;
  854. if (hub.GetActivePlayer() == BluetoothPlayer.FIRST_PLAYER)
  855. {
  856. SmartBowSDK.BluetoothStatusEnum status1P = hub.GetSdkStatus(BluetoothPlayer.FIRST_PLAYER);
  857. if (bowStatus1P != status1P)
  858. {
  859. bowStatus1P = status1P;
  860. }
  861. if (bowStatus1P != SmartBowSDK.BluetoothStatusEnum.Connected)
  862. {
  863. isNext = false;
  864. }
  865. else if (!isNext && bowStatus1P == SmartBowSDK.BluetoothStatusEnum.Connected)
  866. {
  867. if (TryAdvanceToPostConnectStep(hub))
  868. {
  869. isNext = true;
  870. ShowStep(3);
  871. }
  872. }
  873. }
  874. else if (hub.GetActivePlayer() == BluetoothPlayer.SECOND_PLAYER)
  875. {
  876. SmartBowSDK.BluetoothStatusEnum status2P = hub.GetSdkStatus(BluetoothPlayer.SECOND_PLAYER);
  877. if (bowStatus2P != status2P)
  878. {
  879. bowStatus2P = status2P;
  880. }
  881. if (bowStatus2P != SmartBowSDK.BluetoothStatusEnum.Connected)
  882. {
  883. isNext = false;
  884. }
  885. if (!isNext && bowStatus2P == SmartBowSDK.BluetoothStatusEnum.Connected)
  886. {
  887. if (TryAdvanceToPostConnectStep(hub))
  888. {
  889. isNext = true;
  890. ShowStep(3);
  891. }
  892. }
  893. }
  894. }
  895. public void resetAimDevice()
  896. {
  897. BLEAimhandler aim = SmartBowDeviceHub.ins?.Aim;
  898. if (aim == null) return;
  899. aim.onCreateAimDeviceInfoById();
  900. if (!aim.SideSlipOverwritesDeviceInfo)
  901. {
  902. Debug.Log("[SideSlip] 直接连接,沿用已记录 MAC bInitMac:" + aim.aimDeviceInfo.bInitMac);
  903. return;
  904. }
  905. // 切换设备页已写入 type 并清空 MAC,此处仅结束覆盖标记
  906. aim.ClearSideSlipOverwriteFlag();
  907. Debug.Log("[SideSlip] 切换选设备连接 bInitMac:" + aim.aimDeviceInfo.bInitMac);
  908. }
  909. public void resetAimType()
  910. {
  911. BLEAimhandler aim = SmartBowDeviceHub.ins?.Aim;
  912. if (aim == null) return;
  913. aim.onCreateAimDeviceInfoById();
  914. if (!aim.SideSlipOverwritesDeviceInfo)
  915. return;
  916. if (aim.GetTempDeviceType() != -1)
  917. aim.SetAimDeviceType(aim.GetTempDeviceType());
  918. }
  919. #endregion
  920. static class SideSlipUIBuilder
  921. {
  922. const string BuilderMaskBg = "BG";
  923. const string BuilderRight = "Right";
  924. const string BuilderBtnClose = "BtnClose";
  925. const string BuilderContent = "Content";
  926. const string BuilderBottomBar = "BottomBar";
  927. const string BuilderBtnStep = "BtnStep";
  928. static readonly Color MaskColor = new Color(0f, 0f, 0f, 0.55f);
  929. static readonly Color PlaceholderTextColor = new Color(0.35f, 0.35f, 0.35f, 1f);
  930. public static void EnsureHierarchy(Transform root)
  931. {
  932. if (root == null)
  933. return;
  934. var layer = root.gameObject.layer;
  935. EnsureMask(root, layer);
  936. var right = EnsureRight(root, layer);
  937. if (right == null)
  938. return;
  939. EnsureBtnClose(right, layer);
  940. EnsureContent(right, layer);
  941. EnsureBottomBar(right, layer);
  942. }
  943. static void EnsureMask(Transform root, int layer)
  944. {
  945. var bg = root.Find(BuilderMaskBg);
  946. if (bg == null)
  947. {
  948. var go = CreateRect(root, BuilderMaskBg, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f),
  949. Vector2.zero, Vector2.zero, layer);
  950. bg = go.transform;
  951. }
  952. var image = bg.GetComponent<Image>();
  953. if (image == null)
  954. image = bg.gameObject.AddComponent<Image>();
  955. image.color = MaskColor;
  956. image.raycastTarget = true;
  957. if (bg.GetComponent<Button>() == null)
  958. {
  959. var btn = bg.gameObject.AddComponent<Button>();
  960. btn.transition = Selectable.Transition.None;
  961. btn.targetGraphic = image;
  962. }
  963. }
  964. static RectTransform EnsureRight(Transform root, int layer)
  965. {
  966. var right = root.Find(BuilderRight) as RectTransform;
  967. if (right != null)
  968. return right;
  969. var go = CreateRect(root, BuilderRight, new Vector2(1f, 0f), new Vector2(1f, 1f),
  970. new Vector2(1f, 0.5f), Vector2.zero, new Vector2(900f, 0f), layer);
  971. right = go.GetComponent<RectTransform>();
  972. var image = go.AddComponent<Image>();
  973. image.color = Color.white;
  974. image.raycastTarget = true;
  975. return right;
  976. }
  977. static void EnsureBtnClose(RectTransform right, int layer)
  978. {
  979. if (right.Find(BuilderBtnClose) != null)
  980. return;
  981. var go = CreateRect(right, BuilderBtnClose, new Vector2(0f, 1f), new Vector2(0f, 1f),
  982. new Vector2(0f, 1f), new Vector2(48f, -48f), new Vector2(72f, 72f), layer);
  983. var image = go.AddComponent<Image>();
  984. image.color = new Color(1f, 1f, 1f, 0.9f);
  985. image.raycastTarget = true;
  986. var btn = go.AddComponent<Button>();
  987. btn.targetGraphic = image;
  988. var labelGo = CreateRect(go.transform, "Label", Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f),
  989. Vector2.zero, Vector2.zero, layer);
  990. var tmp = labelGo.AddComponent<TextMeshProUGUI>();
  991. tmp.text = "×";
  992. tmp.fontSize = 48;
  993. tmp.alignment = TextAlignmentOptions.Center;
  994. tmp.color = Color.black;
  995. tmp.raycastTarget = false;
  996. }
  997. static void EnsureContent(RectTransform right, int layer)
  998. {
  999. var content = right.Find(BuilderContent) as RectTransform;
  1000. if (content == null)
  1001. {
  1002. var go = CreateRect(right, BuilderContent, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f),
  1003. Vector2.zero, Vector2.zero, layer);
  1004. content = go.GetComponent<RectTransform>();
  1005. content.offsetMin = new Vector2(32f, 140f);
  1006. content.offsetMax = new Vector2(-32f, -120f);
  1007. }
  1008. EnsureStepPlaceholder(content, "Step_0", "步骤 1 — 内容待开发", layer);
  1009. EnsureStepPlaceholder(content, "Step_1", "步骤 2 — 内容待开发", layer);
  1010. }
  1011. static void EnsureStepPlaceholder(RectTransform content, string stepName, string placeholder, int layer)
  1012. {
  1013. if (content.Find(stepName) != null)
  1014. return;
  1015. var go = CreateRect(content, stepName, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f),
  1016. Vector2.zero, Vector2.zero, layer);
  1017. var tmp = go.AddComponent<TextMeshProUGUI>();
  1018. tmp.text = placeholder;
  1019. tmp.fontSize = 36;
  1020. tmp.alignment = TextAlignmentOptions.TopLeft;
  1021. tmp.color = PlaceholderTextColor;
  1022. tmp.raycastTarget = false;
  1023. go.SetActive(stepName == "Step_0");
  1024. }
  1025. static void EnsureBottomBar(RectTransform right, int layer)
  1026. {
  1027. if (right.Find(BuilderBottomBar) != null)
  1028. return;
  1029. var bar = CreateRect(right, BuilderBottomBar, new Vector2(0f, 0f), new Vector2(1f, 0f),
  1030. new Vector2(0.5f, 0f), new Vector2(0f, 48f), new Vector2(0f, 120f), layer);
  1031. var btnGo = CreateRect(bar.transform, BuilderBtnStep, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
  1032. new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(420f, 88f), layer);
  1033. var img = btnGo.AddComponent<Image>();
  1034. img.color = new Color(0.2f, 0.55f, 1f, 1f);
  1035. img.raycastTarget = true;
  1036. var btn = btnGo.AddComponent<Button>();
  1037. btn.targetGraphic = img;
  1038. var labelGo = CreateRect(btnGo.transform, "Text (TMP)", Vector2.zero, Vector2.one,
  1039. new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero, layer);
  1040. var tmp = labelGo.AddComponent<TextMeshProUGUI>();
  1041. tmp.text = "切换步骤";
  1042. tmp.fontSize = 32;
  1043. tmp.alignment = TextAlignmentOptions.Center;
  1044. tmp.color = Color.white;
  1045. tmp.raycastTarget = false;
  1046. }
  1047. static GameObject CreateRect(Transform parent, string name, Vector2 anchorMin, Vector2 anchorMax,
  1048. Vector2 pivot, Vector2 anchoredPosition, Vector2 sizeDelta, int layer)
  1049. {
  1050. var go = new GameObject(name, typeof(RectTransform));
  1051. go.layer = layer;
  1052. var rt = go.GetComponent<RectTransform>();
  1053. rt.SetParent(parent, false);
  1054. rt.anchorMin = anchorMin;
  1055. rt.anchorMax = anchorMax;
  1056. rt.pivot = pivot;
  1057. rt.anchoredPosition = anchoredPosition;
  1058. rt.sizeDelta = sizeDelta;
  1059. rt.localScale = Vector3.one;
  1060. rt.localRotation = Quaternion.identity;
  1061. return go;
  1062. }
  1063. }
  1064. }
  1065. }