In my current UI framework I use scroll which is based on separate camera + render texture so I can easily get notifies when item is in/out of scroll using unity’s standard OnBecameInvisible/OnBecameVisible.
What is the best way to do it for Scroll Rect - any thoughts? All I thought of as of now is to extend ScrollRect component and “send” some events to its children whet they’re out of viewport.
Maybe you can examine the object’s position (and size) and calculate whether it is visible or not? Try watching one of the objects’ RectTransform in the Inspector when you scroll it out of view, to work out what the values would need to be.
(My first thought was that there should be a built-in method for this sort of thing, but then I remembered that the system relies heavily on masking, so it’s only ever a straightforward calculation when the mask is rectangular.)
I have no idea which is best way, as I’m unable to find lots of information, however I’m solving same problem - I have really big list and I want to create items only when they become visible or nearly visible.
However, if you are interested, here is my solution:
first, a few extension methods:
public static Matrix4x4 TransformTo(this Transform from, Transform to)
{
return to.worldToLocalMatrix * from.localToWorldMatrix;
}
public static Rect RectRelativeTo(this RectTransform transform, Transform to)
{
Matrix4x4 matrix = transform.TransformTo(to);
Rect rect = transform.rect;
Vector3 p1 = new Vector2(rect.xMin, rect.yMin);
Vector3 p2 = new Vector2(rect.xMax, rect.yMax);
p1 = matrix.MultiplyPoint(p1);
p2 = matrix.MultiplyPoint(p2);
rect.xMin = p1.x;
rect.yMin = p1.y;
rect.xMax = p2.x;
rect.yMax = p2.y;
return rect;
}
TransformTo() calculates matrix which transforms points from one game object to another. I would like to avoid this, however I couldn’t find any other solution as we just need to know rectangle dimensions and positions in single space.
RectRelativeTo() just calculates position (and size) of rectangle of a game object “from point of view” of another game object.
Main code is:
private RectTransform VisibleRect = null;
private Transform Parent = null;
private float DistanceToRecalcVisibility = 300.0f;
private float DistanceMarginForLoad = 400.0f;
private float DistanceMarginForPreload = 1500.0f;
private float LastPos = Mathf.Infinity;
void Update()
{
if(Mathf.Abs(LastPos - Parent.transform.localPosition.y) >= DistanceToRecalcVisibility)
{
LastPos = Parent.transform.localPosition.y;
Rect checkRect = VisibleRect.rect;
Rect preloadCheckRect = VisibleRect.rect;
checkRect.yMin -= DistanceMarginForLoad;
checkRect.yMax += DistanceMarginForLoad;
preloadCheckRect.yMin -= DistanceMarginForPreload;
preloadCheckRect.yMax += DistanceMarginForPreload;
for(int i = 0; i < Parent.childCount; ++i)
{
RectTransform itemTransform = Parent.GetChild(i) as RectTransform;
Rect rect = itemTransform.RectRelativeTo(VisibleRect);
if(itemTransform)
{
UnityEngine.UI.Image img = itemTransform.GetComponent<UnityEngine.UI.Image>();
if(rect.Overlaps(checkRect))
{
img.color = Color.green;
}
else if(rect.Overlaps(preloadCheckRect))
{
img.color = Color.blue;
}
else
{
img.color = Color.red;
}
}
}
}
}
where
- VisibleRect - is UI rect which defines visible rectangle, in out case it’s scroll rect.
- Parent - is parent of all children. It is scrolled object by the scroll rect.
- DistanceToRecalcVisibility - only when parent is moved by this distance, whole thing is recalculated. This is makes it more performance only.
- DistanceMarginForLoad - distance from edges to be also loaded. It must be at least same as DistanceToRecalcVisibility, otherwise user will see not-yet-loaded parts during scrolling.
- DistanceMarginForPreload - this can be used to pre-load item (for example in separate thread).
This solution check visibility of each child in reference rectangle, but in order to save some power, it repeats only after user moves the view by certain value. (When I think about it, I should also call it when screen resolution changes.)
Notes:
- children all present all, but for the non-loaded items, they are just empty placeholders, because of layout. They are empty until they are close enough.
- the code expects the scale to be same between all three objects (child, parent, visible rectangle)
- the code could be optimized more if we would cache transformation matrix between parent and visible rectangle and transform child rectangle by just adding position of the child (because it’s relative to it’s parent)
I would love to not use Update, but some event-reaction, but I don’t know the way…
Thanks for your solution (especially with expending preload rect and having recalculation when preloaded buffer is partially used), I love it. I made some changes:
[SerializeField]
private Camera camera;
[SerializeField]
private ScrollRect scrollRect;
private const float DistanceToRecalcVisibility = 400.0f;
private const float DistanceMarginForLoad = 600.0f;
private float lastPos = Mathf.Infinity;
private void Start() {
this.scrollRect.onValueChanged.AddListener((newValue) => {
if (Mathf.Abs(this.lastPos - this.scrollRect.content.transform.localPosition.y) >= DistanceToRecalcVisibility) {
lastPos = this.scrollRect.content.transform.localPosition.y;
Vector2 scrollRectPosition = RectTransformUtility.WorldToScreenPoint(this.camera, this.scrollRect.transform.position);
RectTransform scrollRectTransform = this.scrollRect.GetComponent<RectTransform>();
float checkRectMinY = scrollRectPosition.y + scrollRectTransform.rect.yMin - DistanceMarginForLoad;
float checkRectMaxY = scrollRectPosition.y + scrollRectTransform.rect.yMax + DistanceMarginForLoad;
foreach (Transform child in this.scrollRect.content) {
Vector2 childPosition = RectTransformUtility.WorldToScreenPoint(this.camera, child.position);
// uncomment lines bellow if you set DistanceMarginForLoad less than height of single element
//RectTransform childRectTransform = child.GetComponent<RectTransform>();
//float childMinY = childPosition.y + childRectTransform.rect.yMin;
//float childMaxY = childPosition.y + childRectTransform.rect.yMax;
//if (childMaxY >= checkRectMinY && childMinY <= checkRectMaxY) {
if (childPosition.y >= checkRectMinY && childPosition.y <= checkRectMaxY) {
child.GetComponent<Image>().color = Color.blue;
} else {
child.GetComponent<Image>().color = Color.green;
}
}
}
});
}
Basically instead of Update I am using onValueChanged event and instead of converting everything myself, I am using WorldToScreenPoint method (I converted ScrollRect to visual screen coordinates and later convert all children as well). Since we use buffer, you can remove code which takes in an account child’s yMin/yMax. I added them in case someone for some reason will want to have 0 preload region.
BTW you can also use hidden elements instead of creating new ones =) Creating is always more time consuming than updating contents of old one + you save lots of memory. I should give it a try as well.
UPDATE: get rid of camera, I think this one is better:
this.scrollRect.onValueChanged.AddListener((newValue) => {
if (Mathf.Abs(this.lastPos - this.scrollRect.content.transform.localPosition.y) >= BigList.DistanceToRecalcVisibility) {
lastPos = this.scrollRect.content.transform.localPosition.y;
RectTransform scrollTransform = this.scrollRect.GetComponent<RectTransform>();
float checkRectMinY = scrollTransform.rect.yMin - DistanceMarginForLoad;
float checkRectMaxY = scrollTransform.rect.yMax + DistanceMarginForLoad;
foreach (Transform child in this.scrollRect.content) {
RectTransform childTransform = child.GetComponent<RectTransform>();
Vector3 positionInWord = childTransform.parent.TransformPoint(childTransform.localPosition);
Vector3 positionInScroll = scrollTransform.InverseTransformPoint(positionInWord);
float childMinY = positionInScroll.y + childTransform.rect.yMin;
float childMaxY = positionInScroll.y + childTransform.rect.yMax;
if (childMaxY >= checkRectMinY && childMinY <= checkRectMaxY) {
child.GetComponent<Image>().color = Color.blue;
} else {
child.GetComponent<Image>().color = Color.green;
}
}
}
});