
简介一份基于Unity的UGUI ScrollRect组件实现的轮播图功能源码用于弥补UGUI系统未提供现成轮播组件的缺憾适用于游戏主界面、活动宣传页、商品展示等常见场景面向具备一定Unity基础、需要集成轮播效果的开发者。资源包共408个文件压缩后约3.47MB包含17个C#脚本、132张PNG图片素材、21个Asset配置和5个Unity场景脚本与美术资源分离便于查看替换且提供可直接运行的示例场景。功能上覆盖自动/手动切换、平滑动画、导航指示器并扩展无限轮播、鼠标悬停暂停、鼠标拖拽切换和竖向轮播等特性实现基于ScrollRect的滑动机制与HorizontalLayoutGroup/VerticalLayoutGroup排序配合DOTweenPro完成平滑移动工程基于Unity2020.3.28f1c1。目前已有1242人学习下载注释清晰、结构完整既可快速集成进现有项目也是理解UGUI滚动组件与动画协同工作的良好范例。1. 轮播图看着简单ScrollRect一拖就露馅Unity3d轮播图的回中判断是第一道坎做Unity3d轮播图很多人第一反应是拖一个ScrollRect然后把Image排进去但一运行就发现松手后图片停在一个不上不下的位置完全没有轮播该有的整页吸附感。更麻烦的是自动播放和手动拖拽一旦同时触发页面会来回抖最后只能靠一堆临时开关硬压下去。这份资源拆的是UnityUGUI轮播的完整实现从ScrollRect的归一化位置换算页码到无限轮播的边界处理再到自动播放与手势协同整体是一套能直接拖进项目的C#脚本集合。适合正在做商城Banner、公告位、横向分页列表的开发者尤其是不想重复造轮子、想一次性把轮播做稳的那批人。2. 先说滚动骨架为什么轮播图要用ScrollRect而不是手动挪Image2.1 轮播的本质是分页滑动ScrollRect替你管了输入与惯性轮播图的核心不只是定时翻页而是用户手滑时能精确感知每一页的边界。如果手动监听Drag事件去改Image的anchoredPosition要自己处理触点追踪、惯性模拟、多点触控和边界回弹还要考虑嵌套滚动容器时的事件冒泡工作量会迅速膨胀。ScrollRect是UGUI现成的滚动容器输入、惯性、裁剪都封装好了轮播只需要在此基础上加「回中判定」和「自动播放」两件事。实际项目里手动方案唯一站得住脚的场景是只有两三张图且完全不允许手势滑动纯定时切换一旦允许用户拖拽ScrollRect的性价比立刻体现。这份资源选择ScrollRect作为基底是合理的它把最难的输入处理和Clipping裁剪机制交给了引擎开发者只需要专注页面逻辑和滚动目标计算。2.2 回中判定的数学基础horizontalNormalizedPosition与页码换算// 轮播页码计算把ScrollRect的归一化滚动位置换算成当前页索引 public int GetCurrentPage() { float pos scrollRect.horizontalNormalizedPosition; // normalizedPosition范围是0~10是最左1是最右 float pageFloat pos * (pageCount - 1); int page Mathf.RoundToInt(pageFloat); return Mathf.Clamp(page, 0, pageCount - 1); }逻辑说明horizontalNormalizedPosition本质上是滚动条位置也就是content相对于viewport的偏移比例。总页数为pageCount时每一页所占的归一化跨度是1/(pageCount-1)所以把pos乘以(pageCount-1)再四舍五入得到的就是离当前位置最近的那一页。轮播的「回中吸附」就是基于这个索引去计算目标位置而不是靠肉眼调整Offset。参数说明pageCount必须和content子物体实际数量一致。如果后面做了首尾补页的无限轮播这里的pageCount仍指真实展示页content里的补页子物体要单独处理否则页码换算会整体错位一格。注意GetCurrentPage只适合在滚动停止后调用。滚动动画进行中normalizedPosition是中间值这个时候算页码会得到过渡页导致吸附目标错乱。2.3 Movement Type三档选择轮播场景哪个最合适Movement Type行为是否适合轮播原因Unrestricted可以超出边界是否带惯性取决于Inertia开关适合滚动位置完全由程序控制不受边界约束Elastic超出边界有回弹阻尼不太适合边缘回弹动画会和整页吸附叠加产生乱跳Clamped严格停在边界无法拖出勉强可用边界手感僵硬自动播放滚到边缘会顿住轮播我一般选Unrestricted同时关掉ScrollRect自带的Inertia否则松手后content会带着速度继续滑页面吸附的起点不受控制快速滑动时会连续跨页。关掉Inertia之后所有滚动过程统一交给自己的滚动协程驱动起点和终点完全精确这是轮播不「飘」的关键。提示Inertia不是必须关。如果产品要求「甩动翻页」的手感可以用scrollRect.velocity读出横向速度在OnEndDrag里决定目标是当前页加一还是减一这个方案在第4章会展开说明。3. 无限轮播怎么落地内容补页法与索引取模法的两套方案3.1 方案一首尾补页滚到伪边界时瞬移到真实边界所谓Unity3d无限轮播本质是「滚到边界时瞬移回中间状态的同一张图」。首尾补页法是在content头部复制原最后一页尾部复制原第一页这样content实际子物体数量是真实页数加二。用户在最左或最右看到的其实是复制品程序在滚动结束瞬间把位置改回真实的对应页因为视觉内容完全相同肉眼察觉不到瞬移。// 首尾补页的瞬移处理content里实际放了 realPageCount 2 个子物体 void TeleportToRealPage() { int fakeTotal realPageCount 2; if (currentIndex 0) { // 当前在fake第一页内容是最后一张图瞬移到真实最后一页 scrollRect.horizontalNormalizedPosition (float)realPageCount / (fakeTotal - 1); currentIndex realPageCount - 1; } else if (currentIndex fakeTotal - 1) { // 当前在fake最后一页内容是第一张图瞬移到真实第一页 scrollRect.horizontalNormalizedPosition 1f / (fakeTotal - 1); currentIndex 0; } }逻辑说明瞬移的关键是位置换算。fakeTotal是content实际页数fakeTotal-1是归一化分母。真实第一页的normalizedPosition是1/(fakeTotal-1)因为左侧还多了一个fake页真实最后一页的位置是realPageCount/(fakeTotal-1)。瞬移本身只是改位置不改视觉内容所以用户看到的画面是连续的。参数说明瞬移必须放在滚动动画完全结束之后执行最好在滚动协程里等while循环退出再调用TeleportToRealPage。如果在Update里每帧检查会因为ScrollRect自身的写入顺序冲突产生跳动感。realPageCount和fakeTotal是两个变量不要混用否则边界位置会差一页宽度。构造补页数据时还有一个容易漏的细节复制品的按钮事件如果绑定了原始下标要同步改成真实页的下标。否则用户点击最后一张复制品时打开的详情页是错的这个问题在真机上很难发现因为视觉上图片是对的。3.2 方案二取模映射不复制内容只改索引如果数据量很大或者图片来自网络复制内容的方案会浪费内存。这时可以走「三页复用」路线content里始终只放三五个页签子物体滚动结束后根据当前位置算出rawIndex再取模得到真实内容索引去刷新子物体的图片和数据这样滚动距离永远不超过一页宽度。// 取模映射法content只放真实页页码循环用余数实现 int GetVisualIndex(int rawIndex) { int pageCount realPageCount; int visual (rawIndex % pageCount pageCount) % pageCount; return visual; }逻辑说明rawIndex可以理解为一个永不回头的递增序号取模之后总落在0到pageCount-1之间。比如真实页数5rawIndex是12时visual算出是2对应第三张图。子物体在滚动结束后才换数据视觉上就形成了循环。参数说明C#的%运算符对负数会返回负余数比如-1 % 5结果是-1所以先加pageCount再取模保证visualIndex非负。这个方案适合数据量大的场景但子物体复用时要注意图片异步加载的取消逻辑避免上一张图的加载回调覆盖掉新内容。滚动结束后子物体定位的常见做法是把当前真实内容刷新到中间那个页签左右两个页签预加载前后页。这样用户从任意位置滑向相邻页时ScrollRect的实际滚动距离永远不超过一页宽度无限循环就退化成「每次都只滚一格」的简单问题了。3.3 两个方案的取舍与耗时控制首尾补页法代码量小、调试直观商品位只有三五张图时最稳动态复用方案省内存适合内容频繁变化的列表。我一般在小项目或Demo里用补页法线上内容系统用复用方案。耗时控制的核心原则是不要在Update里每帧计算页码和刷新UIOnEndDrag、自动播放定时器、滚动协程结束时各算一次就够。排查瞬移跳动还有一个实用手段在TeleportToRealPage前后各打印一次normalizedPosition正常情况下两次打印的差值应该正好是一页宽度对应的归一化量。如果差值明显偏大说明瞬移时机不对或者分母用错了。从那以后我做无限轮播时都会强制保留这两行Log直到验证通过才删掉。4. 自动播放和手势协同协程计时、拖拽打断与速度衔接4.1 定时自动翻页的协程骨架// 自动播放协程每隔interval秒向下一页滑动 IEnumerator AutoPlayLoop(float interval) { while (true) { yield return new WaitForSeconds(interval); if (isDragging || isAnimating) continue; ScrollToPage(currentIndex 1, 0.3f); } }逻辑说明协程是自动播放最直白的写法。每次等待固定时长后先检查有没有手势打断或上一次滚动动画是否还在运行再发起下一次滚动。isDragging由IBeginDrag和IEndDrag回调维护isAnimating由滚动协程进出时置位两个开关缺一不可。参数说明interval一般取3到5秒如果小于滚动动画时长会出现上个动画没结束又叠加下个动画的抖动。0.3f是滚动动画时长在这份资源里是用插值驱动normalizedPosition而不是直接赋值这样才能控制速度曲线。4.2 拖拽打断自动播放的事件接线public void OnBeginDrag(PointerEventData eventData) { isDragging true; StopCoroutine(autoPlayCoroutine); } public void OnEndDrag(PointerEventData eventData) { isDragging false; autoPlayCoroutine StartCoroutine(AutoPlayLoop(interval)); }逻辑说明用户手指一碰屏幕自动播放必须立刻停否则会出现手在拖、程序也在翻页的抢位置现象页面来回抖。OnEndDrag里重新启动协程时等待时长从完整interval重新计算这样用户看完一页后能至少停留完整时长而不是刚松手就自动翻页。参数说明如果产品希望用户松手后快速恢复自动播放可以把interval临时改短但要保证它仍大于滚动动画时长的两倍。autoPlayCoroutine要存成成员变量OnBeginDrag前先StopCoroutine防止连续快速拖拽时积累多个协程实例这是「松手后连翻两页」这种玄学问题的常见根源。4.3 手动翻页的终点修正松手后必须吸附到最近整页// 松手后吸附到最近整页从当前归一化位置反推目标页 void OnEndDrag(PointerEventData eventData) { if (scrollRect.velocity.x 300f) { // 快速右滑翻到下一页 ScrollToPage(currentIndex 1, 0.25f); return; } if (scrollRect.velocity.x -300f) { // 快速左滑翻到上一页 ScrollToPage(currentIndex - 1, 0.25f); return; } float currentPos scrollRect.horizontalNormalizedPosition; int targetPage Mathf.RoundToInt(currentPos * (pageCount - 1)); targetPage Mathf.Clamp(targetPage, 0, pageCount - 1); ScrollToPage(targetPage, 0.25f); }逻辑说明轮播和普通ScrollRect列表最大的区别就是松手后必须回到整页边界。如果用户拖了超过半页吸附到下一页不足半页吸回当前页。带速度判断是为了照顾快速甩动的手势速度超过300就直接按方向翻页避免缓慢吸附造成迟滞感。参数说明300这个阈值需要根据Canvas的参考分辨率调整。参考宽度750的项目里300比较合适如果分辨率更大阈值要同比放大。另外关掉Inertia之后velocity会立刻归零速度判断就失效了所以这个方案要保留Inertia并在OnEndDrag里读velocity两者是配套使用的。ScrollToPage内部还有一个细节协程开头要把isAnimating置true动画结束后置false并调用ChangePage刷新页码。如果ScrollToPage被快速重复调用要先StopCoroutine上一个滚动协程否则两个协程同时写normalizedPosition页面会在中途卡住。我把这个逻辑统一放在滚动协程里所有外部入口都走ScrollToPage而不是直接改位置。5. 避坑ScrollRect轮播常见的翻车现场与排查记录5.1 坑一轮播图显示不全或被裁剪错位现象运行时Banner上下被裁掉一块或第一页图片明显偏右拖动时背景露出来。原因ScrollRect的viewport没有挂RectMask2D或Mask裁剪区域不对content的anchor和pivot设置不合理滚动原点不在期望位置。解决先检查ScrollRect组件的viewport引用是否指向带Mask的节点再把content的pivot设为(0, 0.5)锚点对齐viewport左边缘最后确认每个页签子物体宽度与viewport宽度一致。Scene视图下直接拖content观察显示区域的边界在哪里是最快的定位手段。5.2 坑二快速滑动时页码连跳好几页现象用户猛一甩松手后从第3页直接跳到第6页中间页面一闪而过。原因ScrollRect默认开启Inertia松手后velocity继续推content吸附动画从很远的位置开始跨越了多个页面宽度。解决轮播场景下把Inertia关掉吸附动画完全由自己的滚动协程控制如果要保留惯性手感就用4.3的velocity阈值方案只允许跳一页不要用从任意位置吸附到最近整页的逻辑。排查时把OnEndDrag里的targetPage打Log确认它每次只变化1超过1就说明惯性没有处理好。5.3 坑三循环轮播最后一张到第一张会反向滚动一大截现象从最后一张切到第一张content明显往回滚了一大段距离或者滚到边界后卡住不动。原因首尾补页法里瞬移代码执行时机不对可能放在了滚动动画之前或者瞬移后没重置currentIndex导致ScrollRect认为自己还在尾巴上。解决瞬移必须放在滚动协程结束后执行同时把currentIndex改成真实页索引并触发ChangePage事件。调试技巧是打印瞬移前后的normalizedPosition正常情况下瞬移后位置应该落在0到1之间而且每次只偏移一页宽度如果瞬移后还是靠近边界说明fakeTotal和realPageCount混用了。5.4 坑四自动播放和手动拖拽打架页面来回抖现象手指还按在屏幕上页面就自己翻走了或者刚松手页面又立刻自动翻了一页。原因OnBeginDrag没有停掉自动播放协程或者停掉后OnEndDrag重复启动了多个协程实例。解决把协程引用存成成员变量启动前先StopCoroutine旧实例自动播放协程的while循环里每次也检查isDragging双重保险。排查时在协程第一次循环处加一行Debug.Log确认同一时刻只有一个自动播放协程在运行。这个坑最容易在真机上复现编辑器里反而不明显因为鼠标拖拽的节奏和手指差异很大。5.5 坑五指示器小圆点与当前页不同步现象手动拖动过程中指示器停在旧页码动画结束后才跟上首尾循环时指示器直接错乱。原因指示器更新写在滚动结束回调里而瞬移改位置时没有触发这个回调手动拖动过程中又想实时刷新指示器造成高频刷新掉帧。解决把页码变化统一收敛到一个ChangePage(int newPage)方法吸附结束、自动播放结束、瞬移结束都走这个方法。指示器、标题角标都订阅onPageChanged事件拖动过程中不实时刷新指示器只在滚动结束后刷新一次。6. 进阶让轮播图手感追上商业项目缓动曲线、指示器联动与点击翻页6.1 用AnimationCurve接管滚动过程// 用动画曲线驱动滚动实现「快起慢收」的轮播手感 IEnumerator ScrollToPage(int targetPage, float duration) { isAnimating true; StopCoroutine(scrollCoroutine); // 防止快速连点叠加动画 float startPos scrollRect.horizontalNormalizedPosition; float endPos (float)targetPage / (pageCount - 1); float elapsed 0f; while (elapsed duration) { elapsed Time.deltaTime; float t Mathf.Clamp01(elapsed / duration); float curveT scrollCurve.Evaluate(t); scrollRect.horizontalNormalizedPosition Mathf.Lerp(startPos, endPos, curveT); yield return null; } scrollRect.horizontalNormalizedPosition endPos; isAnimating false; ChangePage(targetPage); }逻辑说明normalizedPosition如果匀速变化观感是机械平移没有视觉重量。AnimationCurve在t0时给一个较快速度t接近1时减速模拟出「被拨动后自然停下」的感觉。参数说明scrollCurve在Inspector里配一条从(0,0)到(1,1)的曲线把两端坡度调得略陡一些效果更好时长在0.25到0.4秒之间观感最佳。StopCoroutine放在开头是为了防止用户快速连点时多个协程写同一位置。6.2 指示器联动与翻页事件商业项目里轮播图基本都带指示器。做法是把指示器做成独立组件订阅onPageChanged事件当前页高亮切换交给它自己处理。注意首尾补页法里瞬移那一次ChangePage的入参必须是真实页索引不能传fakeIndex如果用三页复用方案指示器更新要等数据换装完成后再执行否则内容还没切过去、圆点先亮了视觉错位。6.3 像素对齐、点击翻页与后台恢复轮播观感问题很大一部分出在「半像素」上。页签宽度尽量设成整数比如750而不是752.3否则边界会有模糊重影。另外主流App的Banner都支持点击左右区域翻页做法是在viewport上层覆盖两个透明Button各自响应左右半屏点击调用ScrollToPage(currentIndex ± 1, 0.3f)。还有一件事容易漏App切后台再回来Time.deltaTime可能跳过一大截协程计时会被错误触发常见做法是在OnApplicationPause里记录状态恢复后重置协程。从那以后我每次做完轮播都会强制走一遍「快滑、慢滑、循环首尾、切后台回来」四个动作确认没有跳页、没有抖动才交付这个习惯帮我挡住了不少线上问题。希望帮到你。本文还有配套的精品资源点击获取