AnchorOverride.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine;
  5. using UnityEngine.EventSystems;
  6. #pragma warning disable 0649 // never assigned warning
  7. namespace TheraBytes.BetterUi
  8. {
  9. #if UNITY_2018_3_OR_NEWER
  10. [ExecuteAlways]
  11. #else
  12. [ExecuteInEditMode]
  13. #endif
  14. [HelpURL("https://documentation.therabytes.de/better-ui/AnchorOverride.html")]
  15. [AddComponentMenu("Better UI/Layout/Anchor Override", 30)]
  16. public class AnchorOverride : UIBehaviour, IResolutionDependency
  17. {
  18. public enum Modus
  19. {
  20. AutoUpdateInstantOnStart,
  21. AutoUpdateAlwaysAnimate,
  22. ManualUpdate,
  23. }
  24. [Serializable]
  25. public class AnchorReference
  26. {
  27. public enum ReferenceLocation
  28. {
  29. Disabled,
  30. Center,
  31. Pivot,
  32. LowerLeft,
  33. UpperRight,
  34. }
  35. [SerializeField] RectTransform reference;
  36. [SerializeField] ReferenceLocation minX;
  37. [SerializeField] ReferenceLocation maxX;
  38. [SerializeField] ReferenceLocation minY;
  39. [SerializeField] ReferenceLocation maxY;
  40. public RectTransform Reference { get { return reference; } set { reference = value; } }
  41. public ReferenceLocation MinX { get { return minX; } }
  42. public ReferenceLocation MaxX { get { return maxX; } }
  43. public ReferenceLocation MinY { get { return minY; } }
  44. public ReferenceLocation MaxY { get { return maxY; } }
  45. }
  46. [Serializable]
  47. public class AnchorReferenceCollection : IScreenConfigConnection
  48. {
  49. [SerializeField] List<AnchorReference> elements = new List<AnchorReference>();
  50. public List<AnchorReference> Elements { get { return elements; } }
  51. [SerializeField]
  52. string screenConfigName;
  53. public string ScreenConfigName { get { return screenConfigName; } set { screenConfigName = value; } }
  54. }
  55. [Serializable]
  56. public class AnchorReferenceCollectionConfigCollection : SizeConfigCollection<AnchorReferenceCollection> { }
  57. [SerializeField]
  58. AnchorReferenceCollection anchorsFallback = new AnchorReferenceCollection();
  59. [SerializeField]
  60. AnchorReferenceCollectionConfigCollection anchorsConfigs = new AnchorReferenceCollectionConfigCollection();
  61. [SerializeField] Modus mode;
  62. [SerializeField] bool isAnimated;
  63. [SerializeField] float acceleration = 1;
  64. [SerializeField] float maxMoveSpeed = 0.05f;
  65. [SerializeField] float snapThreshold = 0.002f;
  66. AnchorReferenceCollection currentAnchors;
  67. public AnchorReferenceCollection CurrentAnchors
  68. {
  69. get
  70. {
  71. if (currentAnchors == null)
  72. {
  73. currentAnchors = anchorsConfigs.GetCurrentItem(anchorsFallback);
  74. }
  75. return currentAnchors;
  76. }
  77. }
  78. public Modus Mode { get { return mode; } set { mode = value; } }
  79. public bool IsAnimated { get { return isAnimated; } set { isAnimated = value; } }
  80. public float AnimationAcceleration { get { return acceleration; } set { acceleration = value; } }
  81. public float AnimationMaxMoveSpeed { get { return maxMoveSpeed; } set { maxMoveSpeed = value; } }
  82. public float AnimationSnapThreshold { get { return snapThreshold; } set { snapThreshold = value; } }
  83. public bool IsCurrentlyAnimating { get { return IsCurrentlyAnimating; } }
  84. Canvas canvas;
  85. RectTransform RectTransform { get { return this.transform as RectTransform; } }
  86. DrivenRectTransformTracker rectTransformTracker = new DrivenRectTransformTracker();
  87. float currentVelocity = 0;
  88. // instantUpdate doesn't work if triggered before the UI is fully initialized.
  89. // This counter enforces instantUpdates for the specified number of frames as a workaround.
  90. int instantApplyFrames;
  91. bool isCurrentlyAnimating;
  92. protected override void OnEnable()
  93. {
  94. base.OnEnable();
  95. bool instantUpdate =
  96. #if UNITY_EDITOR
  97. !Application.isPlaying || currentAnchors == null ||
  98. #endif
  99. mode == Modus.AutoUpdateInstantOnStart || (mode == Modus.ManualUpdate && !isAnimated);
  100. currentAnchors = anchorsConfigs.GetCurrentItem(anchorsFallback);
  101. instantApplyFrames = instantUpdate ? 1 : 0;
  102. UpdateAnchors(instantUpdate);
  103. }
  104. protected override void OnDisable()
  105. {
  106. base.OnDisable();
  107. rectTransformTracker.Clear();
  108. }
  109. public void OnResolutionChanged()
  110. {
  111. currentAnchors = anchorsConfigs.GetCurrentItem(anchorsFallback);
  112. UpdateAnchors(true);
  113. }
  114. public void SetAnchorReferenceTarget(RectTransform target,
  115. int index = -1, bool skipAnimation = true)
  116. {
  117. SetAnchorReferenceTarget(target, anchorsFallback, index, skipAnimation);
  118. foreach (var config in anchorsConfigs.Items)
  119. {
  120. SetAnchorReferenceTarget(target, config, index, skipAnimation);
  121. }
  122. }
  123. public void SetAnchorReferenceTarget(RectTransform target, string screenConfigName,
  124. int index = -1, bool skipAnimation = true)
  125. {
  126. if (screenConfigName == null || screenConfigName == ResolutionMonitor.Instance.FallbackName)
  127. {
  128. SetAnchorReferenceTarget(target, anchorsFallback, index, skipAnimation);
  129. }
  130. else
  131. {
  132. var config = anchorsConfigs.Items.FirstOrDefault(o => o.ScreenConfigName == screenConfigName);
  133. if (config == null)
  134. {
  135. Debug.LogErrorFormat("AnchorOverride.SetAnchorReferenceTarget() - Could not find config with name \"{0}\"", screenConfigName);
  136. return;
  137. }
  138. SetAnchorReferenceTarget(target, config, index, skipAnimation);
  139. }
  140. }
  141. public void SetAnchorReferenceTarget(RectTransform target, AnchorReferenceCollection referenceCollection,
  142. int index = -1, bool skipAnimation = true)
  143. {
  144. if (index < 0) // if -1, recursively call this method for every index
  145. {
  146. for (int i = 0; i < referenceCollection.Elements.Count; i++)
  147. {
  148. SetAnchorReferenceTarget(target, referenceCollection, i, skipAnimation);
  149. }
  150. return;
  151. }
  152. if (index >= referenceCollection.Elements.Count)
  153. {
  154. throw new IndexOutOfRangeException(string.Format("Trying to set target for index {0} in {1} which only has {2} elements.", index, referenceCollection.ScreenConfigName, referenceCollection.Elements.Count));
  155. }
  156. var config = referenceCollection.Elements[index];
  157. if (config == null)
  158. {
  159. throw new NullReferenceException(string.Format("Config at index {0} is null.", index));
  160. }
  161. config.Reference = target;
  162. if (skipAnimation && Mode != Modus.ManualUpdate && referenceCollection == CurrentAnchors)
  163. {
  164. UpdateAnchors(true);
  165. instantApplyFrames = 1;
  166. }
  167. }
  168. private void Update()
  169. {
  170. if (instantApplyFrames > 0)
  171. {
  172. UpdateAnchors(true);
  173. instantApplyFrames -= 1;
  174. return;
  175. }
  176. if (mode == Modus.ManualUpdate && !isCurrentlyAnimating)
  177. return;
  178. bool instantUpdate =
  179. #if UNITY_EDITOR
  180. !Application.isPlaying;
  181. #else
  182. false;
  183. #endif
  184. UpdateAnchors(instantUpdate);
  185. }
  186. public void UpdateAnchors(bool forceInstant)
  187. {
  188. if (!enabled)
  189. return;
  190. if (currentAnchors == null)
  191. {
  192. currentAnchors = anchorsConfigs.GetCurrentItem(anchorsFallback);
  193. }
  194. Vector2 anchorMin = RectTransform.anchorMin;
  195. Vector2 anchorMax = RectTransform.anchorMax;
  196. rectTransformTracker.Clear();
  197. foreach (AnchorReference a in currentAnchors.Elements)
  198. {
  199. Rect rect;
  200. if (TryGetAnchor(a, out rect))
  201. {
  202. if (a.MinX != AnchorReference.ReferenceLocation.Disabled)
  203. {
  204. anchorMin.x = GetAnchorPosition(a, rect, a.MinX).x;
  205. rectTransformTracker.Add(this, this.RectTransform, DrivenTransformProperties.AnchorMinX);
  206. }
  207. if (a.MaxX != AnchorReference.ReferenceLocation.Disabled)
  208. {
  209. anchorMax.x = GetAnchorPosition(a, rect, a.MaxX).x;
  210. rectTransformTracker.Add(this, this.RectTransform, DrivenTransformProperties.AnchorMaxX);
  211. }
  212. if (a.MinY != AnchorReference.ReferenceLocation.Disabled)
  213. {
  214. anchorMin.y = GetAnchorPosition(a, rect, a.MinY).y;
  215. rectTransformTracker.Add(this, this.RectTransform, DrivenTransformProperties.AnchorMinY);
  216. }
  217. if (a.MaxY != AnchorReference.ReferenceLocation.Disabled)
  218. {
  219. anchorMax.y = GetAnchorPosition(a, rect, a.MaxY).y;
  220. rectTransformTracker.Add(this, this.RectTransform, DrivenTransformProperties.AnchorMaxY);
  221. }
  222. }
  223. }
  224. if (isAnimated && !forceInstant)
  225. {
  226. float distMinX = Mathf.Abs(RectTransform.anchorMin.x - anchorMin.x);
  227. float distMinY = Mathf.Abs(RectTransform.anchorMin.y - anchorMin.y);
  228. float distMaxX = Mathf.Abs(RectTransform.anchorMax.x - anchorMax.x);
  229. float distMaxY = Mathf.Abs(RectTransform.anchorMax.y - anchorMax.y);
  230. float maxDist = Mathf.Max(distMinX, distMinY, distMaxX, distMaxY);
  231. if (maxDist <= snapThreshold)
  232. {
  233. currentVelocity = 0;
  234. RectTransform.anchorMin = anchorMin;
  235. RectTransform.anchorMax = anchorMax;
  236. isCurrentlyAnimating = false;
  237. return;
  238. }
  239. currentVelocity = Mathf.Clamp01(currentVelocity + acceleration * Time.unscaledDeltaTime);
  240. float maxMove = currentVelocity * maxDist / 2f;
  241. float scale = Mathf.Clamp01(maxMoveSpeed / maxMove);
  242. float amount = 0.5f * scale * currentVelocity;
  243. float minX = Mathf.Lerp(RectTransform.anchorMin.x, anchorMin.x, amount);
  244. float minY = Mathf.Lerp(RectTransform.anchorMin.y, anchorMin.y, amount);
  245. float maxX = Mathf.Lerp(RectTransform.anchorMax.x, anchorMax.x, amount);
  246. float maxY = Mathf.Lerp(RectTransform.anchorMax.y, anchorMax.y, amount);
  247. RectTransform.anchorMin = new Vector2(minX, minY);
  248. RectTransform.anchorMax = new Vector2(maxX, maxY);
  249. isCurrentlyAnimating = true;
  250. }
  251. else
  252. {
  253. RectTransform.anchorMin = anchorMin;
  254. RectTransform.anchorMax = anchorMax;
  255. isCurrentlyAnimating = false;
  256. }
  257. }
  258. private static Vector2 GetAnchorPosition(AnchorReference a, Rect rect, AnchorReference.ReferenceLocation location)
  259. {
  260. Vector2 localPos = new Vector2();
  261. switch (location)
  262. {
  263. case AnchorReference.ReferenceLocation.Center:
  264. localPos = rect.center;
  265. break;
  266. case AnchorReference.ReferenceLocation.Pivot:
  267. localPos = rect.min + new Vector2(a.Reference.pivot.x * rect.width, a.Reference.pivot.y * rect.height);
  268. break;
  269. case AnchorReference.ReferenceLocation.LowerLeft:
  270. localPos = rect.min;
  271. break;
  272. case AnchorReference.ReferenceLocation.UpperRight:
  273. localPos = rect.max;
  274. break;
  275. default:
  276. throw new NotImplementedException();
  277. }
  278. return localPos;
  279. }
  280. bool TryGetAnchor(AnchorReference anchorRef, out Rect anchorObject)
  281. {
  282. anchorObject = new Rect();
  283. if (anchorRef.Reference == null)
  284. return false;
  285. #if UNITY_EDITOR
  286. if (IsParentOf(anchorRef.Reference))
  287. {
  288. Debug.LogError("Anchor Override: referenced object cannot be a child. Reference is removed.");
  289. anchorRef.Reference = null;
  290. return false;
  291. }
  292. #endif
  293. Camera cam = null;
  294. if (canvas == null)
  295. {
  296. canvas = this.transform.GetComponentInParent<Canvas>();
  297. }
  298. if (canvas != null)
  299. {
  300. cam = canvas.worldCamera;
  301. }
  302. Rect screenRect = anchorRef.Reference.ToScreenRect(true, canvas);
  303. Vector2 min = screenRect.min;
  304. Vector2 max = screenRect.max;
  305. RectTransform parentRectTransform = this.transform.parent as RectTransform;
  306. Vector2 localPosMin, localPosMax;
  307. if (RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRectTransform, min, cam, out localPosMin))
  308. {
  309. if (RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRectTransform, max, cam, out localPosMax))
  310. {
  311. Vector2 size = parentRectTransform.rect.size;
  312. if (size.x == 0 || size.y == 0) // preven division by zero
  313. return false;
  314. Vector2 pp = parentRectTransform.pivot;
  315. localPosMin = new Vector2(pp.x + localPosMin.x / size.x, pp.y + localPosMin.y / size.y);
  316. localPosMax = new Vector2(pp.x + localPosMax.x / size.x, pp.y + localPosMax.y / size.y);
  317. anchorObject.min = localPosMin;
  318. anchorObject.size = localPosMax - localPosMin;
  319. return true;
  320. }
  321. }
  322. return false;
  323. }
  324. bool IsParentOf(Transform transform)
  325. {
  326. if (transform.parent == this.transform)
  327. return true;
  328. if (transform.parent == null)
  329. return false;
  330. return IsParentOf(transform.parent);
  331. }
  332. #if UNITY_EDITOR && UNITY_2017_2_OR_NEWER
  333. void OnDrawGizmos()
  334. {
  335. // Ensure continuous Update calls.
  336. if (!Application.isPlaying)
  337. {
  338. UnityEditor.EditorApplication.QueuePlayerLoopUpdate();
  339. UnityEditor.SceneView.RepaintAll();
  340. }
  341. }
  342. #endif
  343. }
  344. }
  345. #pragma warning restore 0649