当我们在Unity Editor里创建一个Scroll View的时候含有ScrollRect的对象,它下面还有三个子对象,两个含有ScrollBar组件的子对象是作为滚动条,一个Viewport用于限定显示区域。我们可以为Viewport下面的Content对象添加组件(例如Image)或者子对象。点击运行,我们就可以拖动Scroll View,并且看到里面的内容也跟着在滚动。本文就详细分析一下ScrollRect和ScrollBar的源码,了解一下它们是怎么动起来的。

按照惯例,附上UGUI源码下载地址。

首先介绍ScrollBar,它继承自Selectable,还继承了IBeginDragHandler, IDragHandler, IInitializePotentialDragHandler, ICanvasElement四个接口。

ScrollBar重写了OnEnable和OnDisable(调用时机参见Untiy3D组件小贴士(一)OnEnabled与OnDisabled)方法。OnEnable里会找到m_HandleRect父对象的RectTransform组件作为m_ContainerRect。如果所示:

1号对应的是ScrollBar,2号对应的是m_ContainerRect,3号对应的是m_HandleRect。

然后OnEnable会重新设置当前值(value),并刷新表现,即根据当前Value设置m_HandleRect的anchorMin和anchorMax,体现出来就是滚动条的位置发生了变化。

而OnDisable只是调用DrivenRectTransformTracker类型的m_Tracker的Clear方法。(参考https://docs.unity3d.com/462/Documentation/ScriptReference/DrivenRectTransformTracker.html。)

ScrollBar还重写了Selectable的OnPointerDown方法,设置isPointerDownAndNotDragging为true,使用协程调用了ClickRepeat方法,判断点击事件是否在m_HandleRect外面(一定在Scrollbar里面),如果在外面,就将事件坐标转换到m_HandleRect的本地坐标系里,然后调整value,直到点击事件在m_HandleRect里面。

重写的OnPointerUp方法里面,设置isPointerDownAndNotDragging为false。

另外还重写了Selectable的OnMove、FindSelectableOnLeft、FindSelectableOnRight、FindSelectableOnUp和FindSelectableOnDown方法。当方向键按下并与ScrollBar的方向一致时,便不在导航到下一个Selectable,而是修改value值(加减stepSize),即移动滚动条。

OnBeginDrag是继承自IBeginDragHandler接口的方法,这个方法里记录了拖拽的起始点(m_HandleRect内相对center的坐标)。

OnDrag是继承自IDragHandler接口的方法,这个方法会调用UpdateDrag方法。

        void UpdateDrag(PointerEventData eventData){if (eventData.button != PointerEventData.InputButton.Left)return;if (m_ContainerRect == null)return;Vector2 localCursor;if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(m_ContainerRect, eventData.position, eventData.pressEventCamera, out localCursor))return;Vector2 handleCenterRelativeToContainerCorner = localCursor - m_Offset - m_ContainerRect.rect.position;Vector2 handleCorner = handleCenterRelativeToContainerCorner - (m_HandleRect.rect.size - m_HandleRect.sizeDelta) * 0.5f;float parentSize = axis == 0 ? m_ContainerRect.rect.width : m_ContainerRect.rect.height;float remainingSize = parentSize * (1 - size);if (remainingSize <= 0)return;switch (m_Direction){case Direction.LeftToRight:Set(handleCorner.x / remainingSize);break;case Direction.RightToLeft:Set(1f - (handleCorner.x / remainingSize));break;case Direction.BottomToTop:Set(handleCorner.y / remainingSize);break;case Direction.TopToBottom:Set(1f - (handleCorner.y / remainingSize));break;}}

这个方法会计算出m_HandleRect左下角的坐标,根据Direction与剩下的尺寸(就是可滑动区域的尺寸)作比,计算出value。

OnInitializePotentialDrag方法是继承自IInitializePotentialDragHandler的方法,它将拖拽事件的useDragThreshold设为true。(这个值为true之后,判断拖拽事件开始时会加入一个对于移动距离的阈值判断)

另外,ScrollBar定义了一个onValueChanged的事件,我们可以在编辑器里添加事件监听。在Set方法里,这个事件可能会被发送出去。

下面我们将ScrollRect。它继承自UIBehaviour,另外还集成了IInitializePotentialDragHandler, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler, ICanvasElement, ILayoutElement, ILayoutGroup这些接口。

OnEnable方法里添加了m_HorizontalScrollbar和m_VerticalScrollbar的onValueChanged事件的监听(用于监听滚动条的value变化,以调整内容的位置)。并将自己注册到CanvasUpdateRegistry的Layout序列中去(参考UGUI内核大探究(六)CanvasUpdateRegistry)。

OnDisable方法将自己从CanvasUpdateRegistry中移除,并移除了两个ScrollBar的监听。设置m_HasRebuiltLayout为false,清除m_Tracker,设置m_Velocity(横纵速度)为0(在LateUpdate中被调用,用于将超出边界的内容移动回来),并通知LayoutRebuilder需要重建Layout(参考UGUI内核大探究(十)Layout与Fitter)。

IsActive除了调用了基类的有效性判断(对象有效并组件激活),还and了m_Content(内容)不为null。

OnRectTransformDimensionsChange(当RectTransform维度改变时)调用了SetDirty方法,通知LayoutRebuilder需要重建Layout。

OnInitializePotentialDrag(IInitializePotentialDragHandler)里设置m_Velocity为0。

OnBeginDrag(IBeginDragHandler)里设置将拖拽事件点转换为viewRect坐标系内的点赋值给m_PointerStartLocalCursor。将m_Content.anchoredPosition赋值给m_ContentStartPosition。并设置m_Dragging为true。

图中,1为ScrollRect,2为viewRect,3为content。

OnEndDrag(IEndDragHandler)设置m_Dragging为false。

OnDrag(IDragHandler)会根据m_PointerStartLocalCursor和m_ContentStartPosition计算出m_Content新的anchoredPosition。

OnScroll(IScrollHandler)是用于接收鼠标滚动的方法。这个方法根据滚动距离计算出m_Content的位置。

Rebuild是继承自ICanvasElement(参考UGUI内核大探究(六)CanvasUpdateRegistry),它在重建Layout的时候被调用。在Prelayout(预布局)阶段会调用UpdateCachedData(更新缓存数据,包括m_HorizontalScrollbarRect横向滚动条、m_VerticalScrollbarRect纵向滚动条、m_HSliderExpand是否支持横向滑动展开、m_VSliderExpand是否支持纵向滑动展开、m_HSliderHeight横向滚动条高度、m_VSliderWidth纵向滚动条宽度)。在PostLayout(后布局)阶段会更新边界、重置滚动条、保存旧数据(m_PrevPosition保存content的位置、m_PrevViewBounds保存view的边界、m_PrevContentBounds保存content的边界)。

ScrollRect还继承了ILayoutGroup接口,需要实现SetLayoutHorizontal和SetLayoutVertical两个方法。

SetLayoutHorizontal里,如果m_HSliderExpand或m_VSliderExpand为true,便强制立刻重建content的布局。然后根据m_VSliderExpand、vScrollingNeeded(content的高度大于view的高度)、m_HSliderExpand和hScrollingNeeded(content的宽度大于view的宽度)计算viewRect的sizeDelta、m_ViewBounds和m_ContentBounds。(关于sizeDelta是相对于父对象的尺寸,参考https://docs.unity3d.com/462/Documentation/ScriptReference/RectTransform-sizeDelta.html)

SetLayoutVertical里调用UpdateScrollbarLayout方法并更新m_ViewBounds和m_ContentBounds。

UpdateScrollbarLayout里将横向滚动条的宽度设置为与ScrollRect同样值(如果有纵向滚动条,减掉其宽度),将纵向滚动条的高度设置为与ScrollRect同样值(如果有横向滚动条,减掉其高度)。

ScrollRect还重写了LateUpdate,这个方法是每一帧都会被调用,在所有组件Update调用完之后。在这个方法里,调用EnsureLayoutHasRebuilt确保Layout已经被重建,调用UpdateScrollbarVisibility更新ScrollBar的可见性。接着UpdateBounds更新边界。如果m_Dragging为false,且content已经超出了可滚动范围(例如:content的最小点的x大于view的最小点的x),且m_Velocity速度不为0,便根据速度逐渐将content的坐标修正为合理的值。然后如果在拖动中且m_Inertia(惯性)便根据content的当前位置和m_PrevPosition计算出一个新的惯性速度m_Velocity。然后判断m_ViewBounds、m_ContentBounds、m_Content.anchoredPosition和旧数据不同,更新ScrollBar的位置,发送OnValueChanged(编辑器中可设置)并保存当前数据为旧数据。

最后看一下更新边界的方法:

        private void UpdateBounds(){m_ViewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);m_ContentBounds = GetBounds();if (m_Content == null)return;// Make sure content bounds are at least as large as view by adding padding if not.// One might think at first that if the content is smaller than the view, scrolling should be allowed.// However, that's not how scroll views normally work.// Scrolling is *only* possible when content is *larger* than view.// We use the pivot of the content rect to decide in which directions the content bounds should be expanded.// E.g. if pivot is at top, bounds are expanded downwards.// This also works nicely when ContentSizeFitter is used on the content.Vector3 contentSize = m_ContentBounds.size;Vector3 contentPos = m_ContentBounds.center;Vector3 excess = m_ViewBounds.size - contentSize;if (excess.x > 0){contentPos.x -= excess.x * (m_Content.pivot.x - 0.5f);contentSize.x = m_ViewBounds.size.x;}if (excess.y > 0){contentPos.y -= excess.y * (m_Content.pivot.y - 0.5f);contentSize.y = m_ViewBounds.size.y;}m_ContentBounds.size = contentSize;m_ContentBounds.center = contentPos;}private readonly Vector3[] m_Corners = new Vector3[4];private Bounds GetBounds(){if (m_Content == null)return new Bounds();var vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);var vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);var toLocal = viewRect.worldToLocalMatrix;m_Content.GetWorldCorners(m_Corners);for (int j = 0; j < 4; j++){Vector3 v = toLocal.MultiplyPoint3x4(m_Corners[j]);vMin = Vector3.Min(v, vMin);vMax = Vector3.Max(v, vMax);}var bounds = new Bounds(vMin, Vector3.zero);bounds.Encapsulate(vMax);return bounds;}

GetBounds方法是将m_Content的四个顶点的世界坐标转化为viewRect坐标,然后生成一个Bounds,其实就是m_Content相对于viewRect的位置以及尺寸(会在计算m_Content位置的时候用到)。 UpdateBounds会继续调整这个值,只有在Unity官方认为不合理的时候(content宽度或高度比view小)才会执行额外的调整,将Bounds的坐标和尺寸调整成合理的值(尺寸和view相同,位置根据pivot调整)。

UGUI内核大探究(十一)ScrollRect与ScrollBar相关推荐

  1. UGUI内核大探究(十二)Slider

    Slider是UGUI的一个组件,使用它可以实现滑动条,算是一个比较常用的组件,它与ScrollBar(参考UGUI内核大探究(十一)ScrollRect与ScrollBar)有些类似,但又不太相同. ...

  2. UGUI内核大探究(十三)Dropdown

    Dropdown(下拉框)可谓是UGUI的集大成者,在Unity Editor里新建一个Dropdown,会随之附赠Text(Label对象).Image(Arrow对象).ScrollRect(Te ...

  3. UGUI内核大探究(十六)InputField

    InputField是UGUI的重要组件,可以提供文本输入功能,是与用户交互的一个重要手段.我们可以在编辑器里,为OnValueChanged和OnEndEdit两个事件添加监听,这样就可以获得用户输 ...

  4. UGUI内核大探究(八)MaskableGraphic

    MaskableGraphic是UGUI的核心组件,它继承自Graphic.MaskableGraphic是一个抽象类,它的派生类有RawImage.Image.Text.顾名思义,MaskableG ...

  5. UGUI内核大探究(十八)Raycaster

    射线其实是属于事件系统,它在EventSystem/Raycasters目录下,有BaseRaycaster.PhysicsRaycaster和Physics2DRaycaster三个类,命名空间也是 ...

  6. UGUI内核大探究(二)执行事件

    UGUI内核大探究(一)EventSystem我们探究了事件系统,其中我们讲到EventSystem可以通过ExecuteEvents这个类来执行事件,那么事件是如何执行的呢?这里涉及到了两个文件Ev ...

  7. UGUI内核大探究(九)Image与RawImage

    Image组件是UGUI里最常用的组件(可能没有之一),我们知道其实还有一个RawImage组件.那么二者的区别是什么呢?之前的文章UGUI内核大探究(八)MaskableGraphic中我们提到过, ...

  8. UGUI内核大探究(一)EventSystem

    2019独角兽企业重金招聘Python工程师标准>>> UGUI是Unity3D官方推出的UI系统,为了更好的使用UGUI,我们就需要去了解它. UGUI代码开源,我们可以从bitb ...

  9. C++之指针探究(十一):函数名的本质和函数指针

    相关博文:C++之指针探究(十三):函数指针数组 相关博文:C++之指针探究(十二):指针.下标.数组及其作函数参数探究 相关博文:C++之指针探究(十一):函数名的本质和函数指针 相关博文:C++之 ...

最新文章

  1. freescale imx6 编译 linux ltib,TQIMX6Q技术分享——LTIB安装配置(转)
  2. 怎么让jsp中的按钮置灰不能使用_UI设计中的按钮设计规范
  3. Yii2 Pjax的简单使用
  4. 激光炸弹(BZOJ1218)
  5. python形参中的:*args和**kwargs区别
  6. SQL Server内存
  7. 爱快软路由拨号移动网络设置
  8. 我读“世界500强面试题”
  9. HTML下拉菜单(超详细):
  10. 西瓜视频中视频计划还有机会吗?
  11. 华为手机媒体音量自动静音_华为媒体音量自动静音
  12. underscore.js 报_is not defined解决方法
  13. 江苏省盐城中学信息竞赛队(YZOI)队规
  14. day1 704.二分查找 27.移除元素
  15. 区别:JDK,JRE,JVM,JIT
  16. 实例二:上海房屋2017年成交信息分析
  17. 哪个软件可以制作GIF动态图
  18. (01)JS大法好,JavaScript一统天下开篇
  19. 电脑显示服务器意外终止,Win7提示Dcom Server Process Launcher服务意外终止怎么办?...
  20. 机器学习笔记03-求导规则与梯度下降算法推导

热门文章

  1. 143-再谈mtx和lock_guard和unique_lock
  2. LM358呼吸灯(亲测能用,效果很棒~)
  3. jdk-9.0.4安装与环境变量配置
  4. socket局域网聊天demo
  5. Python-群发推广邮件
  6. 我的世界mod整合包java_我的世界1.7.10食物、匠魂与超能力向整合包(附JAVA)
  7. 上海租房房源数据分析(基于R的案例分析)(一)
  8. LCD液晶显示屏的特性与控制
  9. 万字技术干货 |YMatrix 高性能时序数据库引擎的技术实践
  10. vue 项目使用 openlayers根据半径绘制圆形、绘制多边形