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

Posted

技术标签:

【中文标题】如何使用 Unity UI 滚动到 ScrollRect 中的特定元素?【英文标题】:How to scroll to a specific element in ScrollRect with Unity UI? 【发布时间】:2015-06-10 19:53:17 【问题描述】:

我使用 Unity 5.1 为手机游戏构建了一个注册表单。 为此,我使用 Unity UI 组件:ScrollRect + Autolayout(垂直布局)+ Text(标签)+ Input Field。 这部分工作正常。

但是,当打开键盘时,所选字段在键盘下方。有没有办法以编程方式滚动表单以显示所选字段?

我尝试过使用ScrollRect.verticalNormalizedPosition,滚动部分效果很好,但是我无法让选定的字段出现在我想要的位置。

感谢您的帮助!

【问题讨论】:

为清晰起见更新了语法。 【参考方案1】:

我将给你一个我的代码 sn-p,因为我觉得能帮上忙。希望这会有所帮助!

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);

【讨论】:

谢谢。这就是解决方案。 :-) 由于一些锚点,我不得不添加一个偏移量,但否则这是一个很大的帮助/。 这几乎对我有用,但没有以 X 为中心(而是在左侧)。正如阿尔莫所说,这可能是一个锚定问题。下面彼得莫里斯的结果奏效了。 我花了6个小时做这个,真的这么简单吗?在forum.unity.com上浪费时间之前,我需要始终记得检查*** 非常感谢您提供此解决方案【参考方案2】:

没有任何建议对我有用,以下代码有效

这是扩展名

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);
    

【讨论】:

我很惊讶这对我有用,因为我有嵌套的元素,我试图滚动到(所以我猜 localPosition 会是 0,0,虽然我错了)。不过,看起来使用 localPosition 而不是 anchoredPosition 可以绕过所有 RectTransform hokery pokery。 有小demo如何使用吗?当使用更新时,一切都像疯了一样。我已将此调用移至 OnPointerClick 方法,但是当我单击输入字段时,它看起来正在滚动到它,但随后键盘弹出并返回。【参考方案3】:

尽管@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;

【讨论】:

有人能指出我的回答有什么问题吗?它改进了接受的答案,如果您的滚动只是水平或垂直,这不起作用,我的改进适用于这些情况。 请不要只删除我的评论,你会让这里变成一个充满敌意的环境。 欢迎来到*** 我不知道是谁投了反对票,但对我来说,它只是导致禁用的滚动方向疯狂地跳来跳去。也许如果您使用“endPos.x = scroller.content.anchoredPosition.x”(对于 y 也是如此)? Alice 对 endPos.x 和 endPos.y 的细微改动对我来说非常适合。【参考方案4】:

这是我将选定对象夹在 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,试着将你的内边距设置为 10 或 20。这是一个很好的实现。它帮助了我。【参考方案5】:

我这个问题的版本的前提条件:

我想滚动到的element 应该完全可见(间隙最小) elementscrollRect 内容的直接子代 如果element 已经完全可见,则保持 scoll 位置 我只关心垂直尺寸

这对我来说最有效(感谢其他灵感):

// 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;

【讨论】:

【参考方案6】:

Peter Morris 对具有“弹性”运动类型的 ScrollRects 的回答变体。令我困扰的是,滚动矩形一直在为边缘情况(第一个或最后几个元素)设置动画。希望有用:

/// <summary>
/// Thanks to https://***.com/a/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://***.com/a/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);

【讨论】:

【参考方案7】:

如果有人想要平滑滚动(使用 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;
    

【讨论】:

【参考方案8】:

是的,这可以使用编码垂直滚动,请试试这个代码:

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

当我开发聊天功能时,这段代码对我来说很好用。

【讨论】:

【参考方案9】:

width表示滚动矩形中childern的宽度(假设所有childern宽度相同),spacing表示childern之间的间距,index表示您要到达的目标元素

public float getSpecificItem (float pWidth, float pSpacing,int pIndex) 
    return (pIndex * pWidth) - pWidth + ((pIndex - 1) * pSpacing);

【讨论】:

【参考方案10】:

垂直调整:

[SerializeField]
private ScrollRect _scrollRect;

private void ScrollToCurrentElement()

    var siblingIndex = _currentListItem.transform.GetSiblingIndex();

    float pos = 1f - (float)siblingIndex / _scrollRect.content.transform.childCount;

    if (pos < 0.4)
    
        float correction = 1f / _scrollRect.content.transform.childCount;
        pos -= correction;
    

    _scrollRect.verticalNormalizedPosition = pos;
 

【讨论】:

【参考方案11】:

这是我为解决此问题而创建的。将此行为放置在可以通过控制器 UI 导航选择的每个按钮上。 它支持完全嵌套的子对象。 它只支持垂直滚动,但可以很容易地适应水平滚动。

    using UnityEngine;
    using System;
    using UnityEngine.EventSystems;
    using UnityEngine.UI;
    
    namespace Legend
    
        public class ScrollToOnSelect: MonoBehaviour, ISelectHandler
        
            ScrollRect scrollRect;
            RectTransform target;
    
            void Start()
            
                scrollRect = GetComponentInParent<ScrollRect>();
                target = (RectTransform)this.transform;
            
    
            Vector3 LocalPositionWithinAncestor(Transform ancestor, Transform target)
            
                var result = Vector3.zero;
                while (ancestor != target && target != null)
                
                    result += target.localPosition;
                    target = target.parent;
                
                return result;
            
    
            public void EnsureScrollVisible()
            
                Canvas.ForceUpdateCanvases();
    
                var targetPosition = LocalPositionWithinAncestor(scrollRect.content, target);
                var top = (-targetPosition.y) - target.rect.height / 2;
                var bottom = (-targetPosition.y) + target.rect.height / 2;
    
                var topMargin = 100; // this is here because there are headers over the buttons sometimes
    
                var result = scrollRect.content.anchoredPosition;
                if (result.y > top - topMargin)
                    result.y = top - topMargin;
                if (result.y + scrollRect.viewport.rect.height < bottom)
                    result.y = bottom - scrollRect.viewport.rect.height;
    
                //Debug.Log($"targetPosition target.rect.size top bottom scrollRect.content.anchoredPosition->result");
    
                scrollRect.content.anchoredPosition = result;
            
    
            public void OnSelect(BaseEventData eventData)
            
                if (scrollRect != null)
                    EnsureScrollVisible();
            
        
    

【讨论】:

【参考方案12】:

简单又完美

            var pos = 1 - ((content.rect.height / 2 - target.localPosition.y) / content.rect.height);
            scrollRect.normalizedPosition = new Vector2(0, pos);

【讨论】:

以上是关于如何使用 Unity UI 滚动到 ScrollRect 中的特定元素?的主要内容,如果未能解决你的问题,请参考以下文章

Unity Rect Mask 2d 溢出

Unity3D 新 UI 系统和列表视图

[Unity学习]使用ScrollRect实现自动滚动到底部显示实时消息,并在拖动的时候取消自动滚动,再次手动滑到底部,又继续自动滚动

[Unity学习]使用ScrollRect实现自动滚动到底部显示实时消息,并在拖动的时候取消自动滚动,再次手动滑到底部,又继续自动滚动

[Unity学习]使用ScrollRect实现自动滚动到底部显示实时消息,并在拖动的时候取消自动滚动,再次手动滑到底部,又继续自动滚动

Unity - 将后期处理效果应用于所有 UI 元素