如何在Unity UI的ScrollRect中滚动到特定元素?

34
我使用Unity 5.1为移动游戏构建了一个注册表单。 为此,我使用了Unity UI组件:ScrollRect + Autolayout(垂直布局)+ Text(标签)+ Input Field。 这部分工作正常。
但是,在键盘打开时,所选字段在键盘下方。是否有一种编程方式可以滚动表单以将所选字段带入视图?
我尝试使用ScrollRect.verticalNormalizedPosition进行滚动,它可用于滚动某些内容,但我无法使所选字段出现在预期位置。
谢谢您的帮助!

为了更清晰明了,更新了语法。 - Justin Pearce
14个回答

80

我将给你提供一段代码片段,因为我觉得有帮助。希望这能帮到你!

protected ScrollRect scrollRect;
protected RectTransform contentPanel;

public void SnapTo(RectTransform target)
{
    Canvas.ForceUpdateCanvases();

    contentPanel.anchoredPosition =
            (Vector2)scrollRect.transform.InverseTransformPoint(contentPanel.position)
            - (Vector2)scrollRect.transform.InverseTransformPoint(target.position);
}

2
谢谢。这就是解决方案。 :-) - Florent Morin
2
我不得不添加一个偏移量,因为某些锚点的原因,但除此之外,这真的很有帮助。 - Almo
1
这对我来说几乎起作用了,但没有在X轴上居中(而是在左侧)。正如Almo所说,这可能是一个锚点问题。Peter Morris下面的结果有效。 - Eli
3
我花费了6个小时在这上面,结果真的这么简单吗?我需要时刻记得在浪费时间在forum.unity.com之前先去Stack Overflow查找答案。 - Terry Torres
非常感谢您提供的解决方案。 - Gaurang s
你,我的朋友,是传奇。 - 68060

20

对我来说,这些建议都没有起作用,以下代码可以解决问题

这是扩展的代码

using UnityEngine;
using UnityEngine.UI;

namespace BlinkTalk
{
    public static class ScrollRectExtensions
    {
        public static Vector2 GetSnapToPositionToBringChildIntoView(this ScrollRect instance, RectTransform child)
        {
            Canvas.ForceUpdateCanvases();
            Vector2 viewportLocalPosition = instance.viewport.localPosition;
            Vector2 childLocalPosition   = child.localPosition;
            Vector2 result = new Vector2(
                0 - (viewportLocalPosition.x + childLocalPosition.x),
                0 - (viewportLocalPosition.y + childLocalPosition.y)
            );
            return result;
        }
    }
}

以下是我如何使用它来滚动内容的直接子元素以便让其显示在视图中:

    private void Update()
    {
        MyScrollRect.content.localPosition = MyScrollRect.GetSnapToPositionToBringChildIntoView(someChild);
    }

1
我很惊讶这对我起作用了,因为我正在尝试滚动到嵌套元素(所以我猜localPosition应该是0,0,但我错了)。看起来使用localPosition而不是anchoredPosition可以绕过所有的RectTransform技巧。 - Eli
你有一个小的演示如何使用它吗?当在更新中使用时,一切都像疯了一样。我已经将这个调用移动到OnPointerClick方法中,但是当我点击输入字段时,它看起来正在滚动到它,但是然后键盘弹出并且回去了。 - Makalele

9

虽然 @maksymiuk 的回答是最正确的,因为它通过 InverseTransformPoint() 函数正确考虑了锚点、中心和其他所有因素,但对于我来说它仍不能直接使用 - 对于垂直滚动条,它也会改变其 X 位置。因此,我只需更改以检查是否启用垂直或水平滚动,并且如果未启用,则不更改它们的轴。

public static void SnapTo( this ScrollRect scroller, RectTransform child )
{
    Canvas.ForceUpdateCanvases();

    var contentPos = (Vector2)scroller.transform.InverseTransformPoint( scroller.content.position );
    var childPos = (Vector2)scroller.transform.InverseTransformPoint( child.position );
    var endPos = contentPos - childPos;
    // If no horizontal scroll, then don't change contentPos.x
    if( !scroller.horizontal ) endPos.x = contentPos.x;
    // If no vertical scroll, then don't change contentPos.y
    if( !scroller.vertical ) endPos.y = contentPos.y;
    scroller.content.anchoredPosition = endPos;
}

2
有人能指出我的答案哪里有问题吗?它对已接受的答案进行了改进,如果您的滚动只是水平或只是垂直的话,已接受的答案无法正常工作,而我的改进则适用于这些情况。 - vedranm
3
请不要仅仅删除我的评论,这会让这个环境变得非常敌对。 - vedranm
4
欢迎来到 Stack Overflow。 - Commodore Yournero
1
我不知道是谁给它点了踩,但对我来说,它只是导致禁用滚动方向疯狂跳动。也许如果你使用 "endPos.x = scroller.content.anchoredPosition.x"(y 同理)会更好? - Alice Purcell
这对我来说非常完美,Alice 对 endPos.x 和 endPos.y 进行了轻微的更改。 - Mike

7

这是我将选定对象夹紧到ScrollRect的方法:

private ScrollRect scrollRect;
private RectTransform contentPanel;

public void ScrollReposition(RectTransform obj)
{
    var objPosition = (Vector2)scrollRect.transform.InverseTransformPoint(obj.position);
    var scrollHeight = scrollRect.GetComponent<RectTransform>().rect.height;
    var objHeight = obj.rect.height;

    if (objPosition.y > scrollHeight / 2)
    {
        contentPanel.localPosition = new Vector2(contentPanel.localPosition.x,
            contentPanel.localPosition.y - objHeight - Padding.top);
    }

    if (objPosition.y < -scrollHeight / 2)
    {
        contentPanel.localPosition = new Vector2(contentPanel.localPosition.x,
contentPanel.localPosition.y + objHeight + Padding.bottom);
    }
}

什么是 Padding.top 和 Padding.bottom? - Alice Purcell
Alice,尝试将你的填充设置为10或20。这是一个很好的实现。它帮助了我。 - Nolex

6

我解决这个问题的先决条件:

  • 我想要滚动到的 element 应该完全可见(仅具有最小间隙)
  • elementscrollRect 的内容的直接子级
  • element 已完全可见,则保持滚动位置不变
  • 我只关心垂直方向

以下是对我最有效的解决方案(感谢其他启发):

// ScrollRect scrollRect;
// RectTransform element;
// Fully show `element` inside `scrollRect` with at least 25px clearance
scrollArea.EnsureVisibility(element, 25);

使用这个扩展方法:

public static void EnsureVisibility(this ScrollRect scrollRect, RectTransform child, float padding=0)
{
    Debug.Assert(child.parent == scrollRect.content,
        "EnsureVisibility assumes that 'child' is directly nested in the content of 'scrollRect'");

    float viewportHeight = scrollRect.viewport.rect.height;
    Vector2 scrollPosition = scrollRect.content.anchoredPosition;

    float elementTop = child.anchoredPosition.y;
    float elementBottom = elementTop - child.rect.height;

    float visibleContentTop = -scrollPosition.y - padding;
    float visibleContentBottom = -scrollPosition.y - viewportHeight + padding;

    float scrollDelta =
        elementTop > visibleContentTop ? visibleContentTop - elementTop :
        elementBottom < visibleContentBottom ? visibleContentBottom - elementBottom :
        0f;

    scrollPosition.y += scrollDelta;
    scrollRect.content.anchoredPosition = scrollPosition;
}

3

对于滚动类型为“弹性”的ScrollRect的Peter Morris答案的一种变体。我很困扰滚动矩形在边界情况下(前几个或最后几个元素)仍然保持动画效果。希望这有用:

/// <summary>
/// Thanks to https://dev59.com/110a5IYBdhLWcg3wD08e#50191835
/// </summary>
/// <param name="instance"></param>
/// <param name="child"></param>
/// <returns></returns>
public static IEnumerator BringChildIntoView(this UnityEngine.UI.ScrollRect instance, RectTransform child)
{
    Canvas.ForceUpdateCanvases();
    Vector2 viewportLocalPosition = instance.viewport.localPosition;
    Vector2 childLocalPosition = child.localPosition;
    Vector2 result = new Vector2(
        0 - (viewportLocalPosition.x + childLocalPosition.x),
        0 - (viewportLocalPosition.y + childLocalPosition.y)
    );
    instance.content.localPosition = result;

    yield return new WaitForUpdate();

    instance.horizontalNormalizedPosition = Mathf.Clamp(instance.horizontalNormalizedPosition, 0f, 1f);
    instance.verticalNormalizedPosition = Mathf.Clamp(instance.verticalNormalizedPosition, 0f, 1f);
}

这会引入一个帧延迟。

更新:这里有一个版本,它不会引入帧延迟,而且还考虑了内容的缩放:

/// <summary>
/// Based on https://dev59.com/110a5IYBdhLWcg3wD08e#50191835
/// </summary>
/// <param name="instance"></param>
/// <param name="child"></param>
/// <returns></returns>
public static void BringChildIntoView(this UnityEngine.UI.ScrollRect instance, RectTransform child)
{
    instance.content.ForceUpdateRectTransforms();
    instance.viewport.ForceUpdateRectTransforms();

    // now takes scaling into account
    Vector2 viewportLocalPosition = instance.viewport.localPosition;
    Vector2 childLocalPosition = child.localPosition;
    Vector2 newContentPosition = new Vector2(
        0 - ((viewportLocalPosition.x * instance.viewport.localScale.x) + (childLocalPosition.x * instance.content.localScale.x)),
        0 - ((viewportLocalPosition.y * instance.viewport.localScale.y) + (childLocalPosition.y * instance.content.localScale.y))
    );

    // clamp positions
    instance.content.localPosition = newContentPosition;
    Rect contentRectInViewport = TransformRectFromTo(instance.content.transform, instance.viewport);
    float deltaXMin = contentRectInViewport.xMin - instance.viewport.rect.xMin;
    if(deltaXMin > 0) // clamp to <= 0
    {
        newContentPosition.x -= deltaXMin;
    }
    float deltaXMax = contentRectInViewport.xMax - instance.viewport.rect.xMax;
    if (deltaXMax < 0) // clamp to >= 0
    {
        newContentPosition.x -= deltaXMax;
    }
    float deltaYMin = contentRectInViewport.yMin - instance.viewport.rect.yMin;
    if (deltaYMin > 0) // clamp to <= 0
    {
        newContentPosition.y -= deltaYMin;
    }
    float deltaYMax = contentRectInViewport.yMax - instance.viewport.rect.yMax;
    if (deltaYMax < 0) // clamp to >= 0
    {
        newContentPosition.y -= deltaYMax;
    }

    // apply final position
    instance.content.localPosition = newContentPosition;
    instance.content.ForceUpdateRectTransforms();
}

/// <summary>
/// Converts a Rect from one RectTransfrom to another RectTransfrom.
/// Hint: use the root Canvas Transform as "to" to get the reference pixel positions.
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <returns></returns>
public static Rect TransformRectFromTo(Transform from, Transform to)
{
    RectTransform fromRectTrans = from.GetComponent<RectTransform>();
    RectTransform toRectTrans = to.GetComponent<RectTransform>();

    if (fromRectTrans != null && toRectTrans != null)
    {
        Vector3[] fromWorldCorners = new Vector3[4];
        Vector3[] toLocalCorners = new Vector3[4];
        Matrix4x4 toLocal = to.worldToLocalMatrix;
        fromRectTrans.GetWorldCorners(fromWorldCorners);
        for (int i = 0; i < 4; i++)
        {
            toLocalCorners[i] = toLocal.MultiplyPoint3x4(fromWorldCorners[i]);
        }

        return new Rect(toLocalCorners[0].x, toLocalCorners[0].y, toLocalCorners[2].x - toLocalCorners[1].x, toLocalCorners[1].y - toLocalCorners[0].y);
    }

    return default(Rect);
}

3

如果有人想要实现平滑滚动(使用lerp),可以参考以下内容。

[SerializeField]
private ScrollRect _scrollRectComponent; //your scroll rect component
[SerializeField]
RectTransform _container; //content transform of the scrollrect
private IEnumerator LerpToChild(RectTransform target)
{
    Vector2 _lerpTo = (Vector2)_scrollRectComponent.transform.InverseTransformPoint(_container.position) - (Vector2)_scrollRectComponent.transform.InverseTransformPoint(target.position);
    bool _lerp = true;
    Canvas.ForceUpdateCanvases();

    while(_lerp)
    {
        float decelerate = Mathf.Min(10f * Time.deltaTime, 1f);
        _container.anchoredPosition = Vector2.Lerp(_scrollRectComponent.transform.InverseTransformPoint(_container.position), _lerpTo, decelerate);
        if (Vector2.SqrMagnitude((Vector2)_scrollRectComponent.transform.InverseTransformPoint(_container.position) - _lerpTo) < 0.25f)
        {
            _container.anchoredPosition = _lerpTo;
            _lerp = false;
        }
        yield return null;
    }
}

2
简单易用且完美运行。
            var pos = 1 - ((content.rect.height / 2 - target.localPosition.y) / content.rect.height);
            scrollRect.normalizedPosition = new Vector2(0, pos);

天才。其他解决方案会打断用户的进一步滚动。 - AndyDeveloper

2
我对提供的答案有问题,所以我自己写了一个,不关心你的滚动设置(锚点、枢轴、水平、垂直、目标不是内容变换的直接子级等)。我通过将目标子矩形转换为视口中的本地空间,并检查它是否超出视口来实现这一点。如果超出了,就将内容朝着那个方向移动足够多,使目标子级完全可见。这比一些答案总是将目标强制定位到滚动条顶部要好。
public static void KeepChildInScrollViewPort(ScrollRect scrollRect, RectTransform child, Vector2 margin)
{
    Canvas.ForceUpdateCanvases();

    // Get min and max of the viewport and child in local space to the viewport so we can compare them.
    // NOTE: use viewport instead of the scrollRect as viewport doesn't include the scrollbars in it.
    Vector2 viewPosMin = scrollRect.viewport.rect.min;
    Vector2 viewPosMax = scrollRect.viewport.rect.max;

    Vector2 childPosMin = scrollRect.viewport.InverseTransformPoint(child.TransformPoint(child.rect.min));
    Vector2 childPosMax = scrollRect.viewport.InverseTransformPoint(child.TransformPoint(child.rect.max));

    childPosMin -= margin;
    childPosMax += margin;

    Vector2 move = Vector2.zero;

    // Check if one (or more) of the child bounding edges goes outside the viewport and
    // calculate move vector for the content rect so it can keep it visible.
    if (childPosMax.y > viewPosMax.y) {
        move.y = childPosMax.y - viewPosMax.y;
    }
    if (childPosMin.x < viewPosMin.x) {
        move.x = childPosMin.x - viewPosMin.x;
    }
    if (childPosMax.x > viewPosMax.x) {
        move.x = childPosMax.x - viewPosMax.x;
    }
    if (childPosMin.y < viewPosMin.y) {
        move.y = childPosMin.y - viewPosMin.y;
    }

    // Transform the move vector to world space, then to content local space (in case of scaling or rotation?) and apply it.
    Vector3 worldMove = scrollRect.viewport.TransformDirection(move);
    scrollRect.content.localPosition -= scrollRect.content.InverseTransformDirection(worldMove);
}

嗨,你好伙计 xD - Петър Петров

1

是的,可以使用编程实现垂直滚动,请尝试以下代码:

//Set Scrollbar Value - For Displaying last message of content
Canvas.ForceUpdateCanvases ();
verticleScrollbar.value = 0f;
Canvas.ForceUpdateCanvases ();

这段代码在我开发聊天功能时运行良好。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接