RectTransformUtility, or how to make a component that animates UI elements off the screen

RectTransformUtility, or how to make a component that animates UI elements off the screen


In the last article - Varieties of coordinates used in the Unity3d GUI I tried to briefly describe the types of coordinates in the Unity UI/RectTransform. Now I want to light up a little bit such a useful UI thing as the RectTransformUtility. Which is one of the main tools for calculating something in the UI in relation to something else.

A simple and easy task


There is a task - you need a component that animates UI element for the selected edge of the screen. The component should be purple where it is hierarchically, in which places the anchors are, what size of the screen, and in what place of the screen it is. The component must be able to remove the object in the 4th side (up, down, left, right) for a specified time.

Reflections

In principle, how can this be done? Find out the size of the screen in the coordinates of the object, move the object to the coordinate beyond the edge of the screen, and it seems to be the case with the hat. But there are a couple but:

How do I know the coordinates of the screen relative to the UI?

If you google in the forehead, then some sort of nonsense or not useful things, or even unanswered questions, are googling. The closest thing that comes up is when some kind of UI element follows the cursor, which just exists in screen coordinates.

  RectTransformUtility.ScreenPointToLocalPointInRectangle (canvas, new Vector2 (Input.mousePosition), null, out topRightLocalCoord);  

It is directly the RectTransformUtility and ScreenPointToLocalPointInRectangle. Here we get the local coordinates inside the rect (RectTransform), based on the position of the point on the screen.
In the current example, we find the local coordinates of the mouse cursor, we need to replace them with the edge of the screen:

  RectTransformUtility.ScreenPointToLocalPointInRectangle (canvas, new Vector2 (Screen.width, Screen.height), null, out topRightLocalCoord);  

And here we have received the coordinate of the upper right point of the screen, so that the object leaves the screen to the right, our object must be farther than this point + let's say the width of the rect or the specified indent.

So the first thing

We received local coordinates that are suitable for objects directly inside the canvas, if the rect that needs to be shifted lies in another rect, then its local coordinates will be considered relative to the parent, not the canvas. That is, these coordinates directly do not suit us.

There are two ways here , the first is to use global coordinates, which is why they are global. Or calculate the coordinates of the screen in the local coordinates of each rect separately.

Consider the first case - how to convert local coordinates to global ones.

Most googled methods use TransformPoint.

  transform.position = myCanvas.transform.TransformPoint (pos);  

So we convert local coordinates to global ones.

In my opinion, this is generally an extra step, since RectTransformUtility has a ScreenPointToWorldPointInRectangle method, which immediately returns a global position.

We need to shift the rect to the right edge of the screen, for this we take the X coordinate from the position found, and Y will leave the rect that we move so that it simply moves to the right.

  new Vector3 (topRightCoord.x + offset, rectTransform.position.y, 0);  

The resulting coordinate feed DoTween.

  rectTransform.DOMove (new Vector3 (correctedTargetRight.x, rectTransform.position.y, 0), timeForHiding);  

And hooray, the object is leaving to the right. But ...

Second nuance

Here we find out that in fact the positioning of the rect depends on the rect pivot.



Therefore, the object may dance with positioning depending on the pivot, plus the object may be very large, and the offset will not push it completely behind the screen, there will always be a chance that the piece will stick out.

That is, we need to tie the offset to offset, which will take into account the size of the rect + pivot.

The second nuance is to move the object by the size of the rect, you need to know the local or anchor coordinates, and we get the global coordinates. At once I will say that global coordinates cannot be taken and converted to local UI coordinates, or anchor ones.
I came up with the next crutch, we memorize the starting position of the rect, move it to the final global position, shift the anchor position by the size of the rect right, remember the global position that already takes into account the displacement taking into account the size of the object, and feed it to the dutvin, not forgetting to return it to the initial position

Code Example
  var targetRight = new Vector3 (topRightLocalCoord.x  , rectTransform.position.y, 0);
  rectTransform.position = targetRight;
  rectTransform.anchoredPosition + = rectTransform.sizeDelta;
  var correctedTargetRight = rectTransform.position;
  rectTransform.localPosition = startPoint;
  rectTransform.DOMove (new Vector3 (correctedTargetRight.x, rectTransform.position.y, 0), timeForHiding);  


It looks like a giant crutch, but this crutch allows you to synchronize global and other coordinates. It helps when there are objects in the interface that move relative to each other, and they are in different hierarchies. Well, plus so far this is the only way that I found to get the rect coordinates from the global ones.

At this point, we say no crutches, and back to the idea of ​​getting the screen size in local coordinates.

Second Way


The second way is to get the screen size for each rect separately, so we will know the local coordinates of the edges of the screen, regardless of canvas and hierarchy.

Third Nuance

  RectTransformUtility.ScreenPointToLocalPointInRectangle (rectTransform, new Vector2 (Screen.width, Screen.height), null, out topRightCoord);
  RectTransformUtility.ScreenPointToLocalPointInRectangle (rectTransform, new Vector2 (0, 0), null, out bottomScreenCoord);  

Objects can be located anywhere on the screen, in contrast to the canvas that covers the entire screen. Therefore, the distances to the left and right edges of the screen may differ significantly. In the case of kanvas, we would only have enough of the upper right edge, and minus the upper right it would be the upper left. In this case, you need to get the lower left and upper right points separately, as shown in the example code.

Fourth Point

The local coordinate is the offset relative to the center of the parent, when the rect is nested in another rect, which occupies a small part of the canvas, then we need a coordinate that takes into account both offsets, well, everything is simple.

  ((Vector3) bottomLeftCoord + rectTransform.localPosition)  

add the vectors and get the coordinate we need. It turns out to be more complicated than with the global coordinates, but now we can carry out any calculations related to the size of the rect. And calmly finally add compensation without crutches.

  (Vector3) topRightCoord + rectTransform.localPosition + (new Vector3 ((rectTransform.sizeDelta.x * rectTransform.pivot.x) + rectTransform.sizeDelta.x, 0, 0));   

This is what the coordinate for the shift to the right looks like with the compensation of the width of the rect and the offset for the screen by the width of the rect, there is no possibility to set the offset, I plan to finish it a little later, but I think it will be interesting for someone to try to write it.

Findings


  1. For UI elements, it's better to use local or anchor coordinates, and you should try to understand them. Global coordinates can be used for special cases, but they do not make it possible to work comfortably, for example, with the size of rektov and in many other micro episodes.
  2. You need to look at the RectTransformUtility, it has a lot of useful functionality for the UI, all calculations related to the position of something inside and around the rect are done through it.

Well, the component itself, if anyone wants to play with it, it will need DoTween:

Component
  using DG.Tweening;
 using UnityEngine;

 public enum Direction {DEFAULT, RIGHT, LEFT, TOP, BOTTOM}

 public class HideBeyondScreenComponent: MonoBehaviour
 {
  [SerializeField] private Direction direction;
  [SerializeField] private float timeForHiding = 1;
  [SerializeField] private float offset = 50;
  private Vector3 startPoint;
  private RectTransform rectTransform;
  private Vector2 topRightCoord;
  private Vector2 bottomLeftCoord;

  private void Start ()
  {
  rectTransform = transform as RectTransform;
  startPoint = rectTransform.localPosition;
  RectTransformUtility.ScreenPointToLocalPointInRectangle (rectTransform, new Vector2 (Screen.width, Screen.height), null, out topRightCoord);
  RectTransformUtility.ScreenPointToLocalPointInRectangle (rectTransform, new Vector2 (0, 0), null, out bottomLeftCoord);
  Hide ();
  }

  public void Show ()
  {
  rectTransform.DOLocalMove (startPoint, timeForHiding);
  }

  public void Hide ()
  {
  switch (direction)
  {
  case Direction.LEFT:
  rectTransform.DOLocalMove (new Vector3 (EndPosition (Direction.LEFT) .x, rectTransform.localPosition.y, 0), timeForHiding);
  break;
  case Direction.RIGHT:
  rectTransform.DOLocalMove (new Vector3 (EndPosition (Direction.RIGHT) .x, rectTransform.localPosition.y, 0), timeForHiding);
  break;
  case Direction.TOP:
  rectTransform.DOLocalMove (new Vector3 (rectTransform.localPosition.x, EndPosition (Direction.TOP) .y, 0), timeForHiding);
  break;
  case Direction.BOTTOM:
  rectTransform.DOLocalMove (new Vector3 (rectTransform.localPosition.x, EndPosition (Direction.BOTTOM) .y, 0), timeForHiding);
  break;
  }
  }

  private Vector3 NegativeCompensation ()
  {
  Return new Vector2 ((- rectTransform.sizeDelta.x - offset) + rectTransform.sizeDelta.x * rectTransform.pivot.x,
  (-rectTransform.sizeDelta.y -offset) + rectTransform.sizeDelta.y * rectTransform.pivot.y);
  }

  private Vector3 PositiveCompensation ()
  {
  Return new Vector2 ((rectTransform.sizeDelta.x * rectTransform.pivot.x) + offset,
  (rectTransform.sizeDelta.y * rectTransform.pivot.y) + offset);
  }

  private Vector2 EndPosition (Direction direction)
  {
  switch (direction)
  {
  case Direction.LEFT:
  return ((Vector3) bottomLeftCoord + rectTransform.localPosition) + NegativeCompensation ();
  case Direction.RIGHT:
  return (Vector3) topRightCoord + rectTransform.localPosition + PositiveCompensation ();
  case Direction.TOP:
  return ((Vector3) topRightCoord + rectTransform.localPosition) + PositiveCompensation ();
  case Direction.BOTTOM:
  return ((Vector3) bottomLeftCoord + rectTransform.localPosition) + NegativeCompensation ();
  }

  return startPoint;
  }
 }  

Source text: RectTransformUtility, or how to make a component that animates UI elements off the screen