多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unity ScrollRect长图截图全攻略:分段捕获、拼接算法与避坑实践

Unity ScrollRect长图截图全攻略:分段捕获、拼接算法与避坑实践 简介面向在Unity中实现滚动列表内容长图截取与本地保存的开发者这份资源围绕连续截图、图像拼接与文件导出三个关键环节组织。压缩包共2000个文件以946个MD说明文档、527个bin数据文件、155个txt和99个json配置为主体同时包含Unity项目的asset/meta资源、C#脚本、PNG示例图以及少量XML、C头文件等其中MD笔记与脚本可直接用于阅读改造bin、cache、json等数据文件有助于对照工程运行状态便于按目录快速定位配置、源码与缓存信息整体约716.55MB。目前已有163人学习使用。借助其中逐帧滚动、纹理拼接、边界对齐与性能优化相关思路可以对照场景文件和脚本理解位置控制、协程截图、长图合成与本地持久化保存的实现方式若需导出PDF也可在此工程基础上继续扩展图像渲染与页面合并模块。适合正在处理UI滚动区域导出长图、或需要在此基础上扩展PDF导出能力的Unity开发者。1. 为什么滚动列表截图是个常规但棘手的需求长图导出的三种实现路线在 Unity 里做 Scroll View 连续截图并保存本地本质上是在解决一个矛盾Texture2D.ReadPixels只能捕捉屏幕上当前可见的像素而滚动列表的内容往往超出屏幕高度。你要是直接截屏得到的只是视口里的那一小块不是用户想要的整张列表。这个需求常见于抽卡记录导出、商城道具列表分享、排行榜/成就墙截图、数字孪生项目的报表面板导出——用户想要一张能直接发微信的长图。我先给结论实现路线有三条。第一条是逐帧滚动 分段截图 内存拼接纯 Unity 原生 API 就能做通用性最好后面要讲的方案就以它为主。第二条是抓取 ScrollRect 的content的 RenderTexture 快照适合内容本身在一个独立相机里渲染的情况但和 ScrollRect 的默认渲染管线整合很麻烦。第三条是修改 Canvas 的scaleFactor把内容压进屏幕一次截完快但会糊而且对嵌套滚动和自适应高度支持很差。绝大多数项目最后都会走第一条。本篇会把第一条路从原理讲到参数调整再讲拼接算法和那些不跑一遍绝对发现不了的坑——比如滚动没停稳就截图导致接缝错位、Canvas 用了 Overlay 渲染模式截出来全黑、图片尺寸超过 8192 被压缩后拼接崩溃。我会结合自己做过的一个商城道具列表导出功能把每一步的关键代码和调参思路完整写出来。2. 截图前的坐标换算为什么 ScrollRect 的本地坐标不能直接当像素坐标用2.1 像素单位与 Canvas 缩放因子的换算想截长图第一步不是写截图函数而是把 ScrollRect 的世界坐标、本地坐标和像素坐标之间的换算关系搞清楚。你通过RectTransformUtility.WorldToScreenPoint拿到的屏幕坐标单位是屏幕像素而 ScrollView 里 content 的rect.height单位是画布单位。两者之间隔着一个Canvas.scaleFactor。比如一个 1080x1920 的屏幕Canvas 的 reference resolution 设为 1080x1920scaleFactor是 1但如果你的 Canvas 用了Scale With Screen Size且 reference resolution 是 720x1280适配到 1080p 的屏幕上时scaleFactor约为 1.5。你在做分段截图时每次滚动的偏移量必须乘以scaleFactor否则截出来的图是错位且模糊的。我用一个具体例子说明。假设 content 的rect.height是 3000 画布单位viewport 的可见高度是 600 画布单位scaleFactor是 2。那么整张长图的高度应该是 6000 像素而不是 3000 像素。ReadPixels的宽度和高度参数必须填像素值。常见的翻车写法是直接用content.rect.height作为ReadPixels的高度截图生成一张 3000 像素的图但实际显示内容占 6000 像素于是图被纵向压扁一半。我一般会在截图前打印出这三者的值content.rect.height、scaleFactor、Screen.height确认换算关系后再往下走。2.2 用 RectTransform 边界计算 content 在屏幕内的可见区间换算完单位后还需要准确地知道当前 content 的哪一段落在屏幕里。这里不能直接读content.anchoredPosition.y因为 anchoredPosition 受 pivot 和 anchor 的影响尤其在 ScrollRect 的 content 常被设置成顶部对齐、pivot 在 (0.5, 1)它的 anchoredPosition.y 是负值而且这个负值的大小不一定恰好等于滚动偏移量。更稳妥的做法是算出 content 的四个角的世界坐标再转换到屏幕坐标。RectTransform contentRect scrollRect.content; RectTransform viewportRect scrollRect.viewport; Vector3[] corners new Vector3[4]; contentRect.GetWorldCorners(corners); float contentTopInScreen RectTransformUtility.WorldToScreenPoint(canvas.worldCamera, corners[1]).y; float contentBottomInScreen RectTransformUtility.WorldToScreenPoint(canvas.worldCamera, corners[0]).y; float viewportTopInScreen RectTransformUtility.WorldToScreenPoint(canvas.worldCamera, viewportRect.position).y;这套代码的逻辑是GetWorldCorners 返回四个按顺序排列的角点索引 0 是左下索引 1 是左上索引 2 是右上索引 3 是右下。取 corners[1] 和 corners[0] 就拿到了 content 顶部和底部的世界坐标再转成屏幕坐标。这样不管你 content 的 pivot 怎么设置、anchor 怎么摆拿到的都是真实屏幕位置。viewport 的顶部则直接用它的中心点位置转屏幕坐标。有了这三组值就能算出当前可见区域相对 content 顶部的像素偏移float visibleRatioOffset (contentTopInScreen - viewportTopInScreen) / Screen.height;这个visibleRatioOffset就是你从 content 顶部往下滚动了的比例范围在 0 到 1 之间。后面做分段截图时需要用这个比例来定位第一段应该截哪一块。2.3 计算总段数与每段的截图高度分段高度是截图方案里最需要调的核心参数。理想情况下每段高度恰好等于 viewport 的像素高度截完一段滚一段最终拼接时不重不漏。但实际操作中如果每次滚动的距离和截图高度之间有亚像素级误差接缝处就会有一条细线或微小的内容重叠。我的做法是引入一个重叠量每段截图高度比滚动步进大 2 到 4 像素。举例来说viewport 像素高度为 1200那么每次截图高 1203 像素滚动 1200 像素拼接时从第 2 段开始裁掉顶部 3 像素再并进长图。这样重叠区域可以容忍滚动误差拼接处不会出现内容断裂。float viewportPixelHeight viewportRect.rect.height * canvas.scaleFactor; float overlapPixels 3f; float stepPixels viewportPixelHeight; float capturePixelHeight viewportPixelHeight overlapPixels; int totalSegments Mathf.CeilToInt((contentPixelHeight - viewportPixelHeight) / stepPixels) 1;totalSegments的算法要留意用总高度减去视口高度再除以步进向上取整再加 1。加 1 是因为最后一段要保证截到 content 的底部边缘即使它不足一个完整的视口高度。如果你不做这个加 1最后一段会漏掉底部一小截内容。拼接时还要注意最后一段的实际高度可能小于capturePixelHeight需要单独记录每段实际截出来的像素高度不能想当然地按统一高度拼接。3. 落地实现用 Coroutine 驱动滚动、分段截图与 Texture2D 拼接3.1 核心架构协程驱动逐帧滚动截图分段截图必须等到滚动动画完全停下来再执行ReadPixels。如果你在调用ScrollRect.verticalNormalizedPosition的下一帧立刻截图ScrollRect 的弹性动画还在进行中截出来的画面是上一帧和这一帧的中间态拼接后必然错位。常见做法是用StopMovement()先强制停止 ScrollRect 的惯性滚动然后手动设置verticalNormalizedPosition让 content 停在一个精确位置再等一帧渲染完成最后截图。我一般会在协程里维护一个循环先滚动到目标位置 → yield 一帧 → 执行截图 → 取像素 → 滚动到下一段的位置。这个流程必须在一个协程里串行完成不能用异步回调或 Update 里的状态机因为后续拼接依赖截图完成的先后顺序。public IEnumerator CaptureScrollViewLongImage(ScrollRect scrollRect, Canvas canvas, ActionTexture2D onComplete) { RectTransform viewport scrollRect.viewport; RectTransform content scrollRect.content; // 记录初始滚动位置方便结束后恢复 float startNormalizedPos scrollRect.verticalNormalizedPosition; // 1. 先滚到顶部确保从 content 最顶部开始截 scrollRect.StopMovement(); scrollRect.verticalNormalizedPosition 1f; yield return null; // 2. 计算所有分段参数 float contentPixelHeight content.rect.height * canvas.scaleFactor; float viewportPixelHeight viewport.rect.height * canvas.scaleFactor; float stepPixels viewportPixelHeight; float capturePixelHeight viewportPixelHeight 3f; int totalSegments Mathf.CeilToInt((contentPixelHeight - viewportPixelHeight) / stepPixels) 1; // 3. 逐段截图 ListTexture2D segments new ListTexture2D(); float totalCapturedHeight 0f; for (int i 0; i totalSegments; i) { float targetY 1f - i * (stepPixels / contentPixelHeight); scrollRect.verticalNormalizedPosition Mathf.Clamp01(targetY); yield return null; Texture2D segment CaptureSegment(canvas, viewport, capturePixelHeight); int actualHeight segment.height; segments.Add(segment); totalCapturedHeight actualHeight; } // 4. 拼接所有分段 Texture2D finalImage CombineSegments(segments, Mathf.RoundToInt(contentPixelHeight)); // 5. 恢复初始滚动位置 scrollRect.verticalNormalizedPosition startNormalizedPos; onComplete?.Invoke(finalImage); }这里有两个参数需要解释。targetY 1f - i * (stepPixels / contentPixelHeight)是把像素偏移换算成verticalNormalizedPosition的公式。verticalNormalizedPosition的取值 1 代表顶部0 代表底部所以从顶部开始就要用 1 减去偏移量。另一个参数是协程里yield return null的位置——只等一帧是不够的如果 Canvas 的 pre-render 队列里有遗留的 UI 脏标记一帧之后画面还没刷新完稳妥做法是连续等两帧。我会在后续的避坑章节单独说这个。3.2 用 RenderTexture 配合 ReadPixels 截取 UI 内容注意 Canvas 渲染模式分段截图函数CaptureSegment的实现本质上是先把 UI 画面渲染进一个 RenderTexture然后用ReadPixels把像素读出来。这里最关键的适配条件是 Canvas 的 Render Mode 必须是Screen Space - Camera或Screen Space - Overlay这两种模式下 UI 会被渲染到屏幕相机或最终屏幕合成器上可以通过Screen相关的 API 直接捕获。如果你的 Canvas 是World Space那 ScrollRect 的位置和视角旋转会让截出来的像素区域变得不可预测这种情况请直接改用针对 World Space UI 的专用截图方案本文不展开。private Texture2D CaptureSegment(Canvas canvas, RectTransform viewport, float captureHeightPixels) { float scaleFactor canvas.scaleFactor; int x Mathf.RoundToInt(viewport.position.x - viewport.rect.width * 0.5f * scaleFactor); int y Mathf.RoundToInt(viewport.position.y - viewport.rect.height * 0.5f * scaleFactor); int width Mathf.RoundToInt(viewport.rect.width * scaleFactor); int height Mathf.RoundToInt(captureHeightPixels); // 把 y 坐标从以屏幕左下角为原点转成 ReadPixels 期望的以屏幕左下角为原点 // 注意这里 viewport.position 是画布坐标系下的中心点要转换到屏幕像素坐标 Rect screenRect RectTransformUtility.WorldToScreenRect(canvas.worldCamera, viewport); RenderTexture rt RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32); // 把当前屏幕内容拷贝到 RenderTexture // 这一步需要有 Camera最简单的是直接把 Screen 内容拷贝 // 但更可控的做法是临时改 Canvas 的 renderMode把 UI 渲染到这个 RT 里 // 下面给出一种常见但踩坑过的写法 ... }这里我需要坦白说一个容易踩的重坑很多人想直接把ReadPixels作用在当前的 BackBuffer 上但ReadPixels在 Overlay 模式下读的是整个屏幕。你不能直接用一个子矩形去读而要先把整个屏幕内容拷贝到 RT再从 RT 的指定区域ReadPixels。更干净的做法是在截图期间把 Canvas 的renderMode切换成Screen Space - Camera指定一个专门渲染 UI 的相机把相机的 targetTexture 设成你准备好的 RenderTexture。截完最后一帧再恢复原状。这样就不会受到屏幕分辨率、DPI 或同时存在多个相机的干扰。我最后实际项目里采取的就是这一套截出来的清晰度和一致性比 Overlay 方案好很多。3.3 拼接长图SetPixels 的高效填充与内存细节拼接看起来简单但处理不好很容易爆内存。如果你有 20 段 1080x1203 的截图直接Texture2D一个个 Load 进内存峰值内存轻松超过 200MB在移动平台上直接卡死甚至被系统杀进程。我的做法是不要保留所有的分段 Texture2D 对象而是把每段的像素数据用GetPixels32读出来存成Color32[]数组拼完后把数组一次性填入最终的长图 Texture2D。这样分段 Texture2D 可以立即Destroy内存占用从 O(n) 降到 O(1) 加一个分段数组的临时开销。private Texture2D CombineSegments(ListTexture2D segments, int finalHeightPixels) { int width segments[0].width; Color32[] finalPixels new Color32[width * finalHeightPixels]; int yOffset 0; foreach (Texture2D seg in segments) { Color32[] segPixels seg.GetPixels32(); int segHeight seg.height; // 如果当前段超过了最终高度截断 if (yOffset segHeight finalHeightPixels) { segHeight finalHeightPixels - yOffset; } for (int y 0; y segHeight; y) { // 将分段像素拷贝到最终像素数组的对应区域 // 注意 Unity 的纹理坐标y 轴从底部开始 int srcStart y * width; int destStart (yOffset y) * width; System.Array.Copy(segPixels, srcStart, finalPixels, destStart, width); } yOffset segHeight; Object.Destroy(seg); } Texture2D final new Texture2D(width, finalHeightPixels, TextureFormat.RGBA32, false); final.SetPixels32(finalPixels); final.Apply(false, false); return final; }拼接函数里有一个值得强调的边界判断最后一段的segHeight必须做finalHeightPixels - yOffset的截断。如果你不做这个保护最后一段的实际高度比预期小拷贝时数组越界会直接抛异常。另外SetPixels32之后必须调用Apply(false, false)第一个参数设为 false 表示不生成 mipmap第二个参数设为 false 表示不压缩考虑到你要把图保存成 PNG这两项必须关掉否则后续EncodeToPNG可能会因为纹理格式不匹配而报错或者输出错误图片。3.4 保存到本地PNG 编码与沙盒路径拼接完成之后保存到本地移动端和 PC 端路径策略会有不同。移动端必须写入Application.persistentDataPathAndroid 上写Application.dataPath或StreamingAssets是写不进去的。PC 端我一般让玩家选择保存路径用File.WriteAllBytes即可。以下是我验证过、可以直接抄的保存函数public static string SaveTextureToPNG(Texture2D texture, string fileName, bool isMobile true) { string directory isMobile ? Application.persistentDataPath : System.IO.Path.Combine(Application.dataPath, ../Screenshots); if (!System.IO.Directory.Exists(directory)) { System.IO.Directory.CreateDirectory(directory); } string fullPath System.IO.Path.Combine(directory, fileName); byte[] pngData texture.EncodeToPNG(); System.IO.File.WriteAllBytes(fullPath, pngData); // 释放纹理内存 if (texture ! null) { Object.Destroy(texture); } return fullPath; }这段代码有一个细节EncodeToPNG返回的byte[]在移动端可能非常大几 MB 到几十 MB 不等写入完成前不要提前释放texture否则pngData也读不到有效数据。另外保存路径里的Directory.CreateDirectory是必需的很多第一次跑的人会把路径直接写成某个不存在的目录导致 IO 异常报错这个异常在 PC 端还能排查在 Android 上晦涩到你根本想不到是目录问题。4. 性能参数怎么调分段步进、重叠像素、协程频率和内存峰值控制4.1 步进高度选择为什么不用像素级步进而用视口高度根据我的经验每段步进取 100% 视口高度是最稳妥的基准值。有些人为了追求每段更小把步进改成 50% 视口高度这样确实能减少单次截图之间的滚动位移误差但段数翻倍内存和耗时也翻倍。你要理解步进长度从 100% 缩到 50% 的收益分段数增加每段的纹理高度减半单段GetPixels32的临时数组变小内存峰值确实会下降但拼接时循环次数变多、总耗时变长。在 PC 上这个差异感受不明显在低端 Android 上步进 100% 和 50% 的耗时差距可以到 40%。如果 content 的总高度在 10000 像素以内步进直接用 100% 视口高度如果超过 15000 像素我建议步进改成 80% 视口高度并开一个段间延迟——每截一段让出两帧避免 UI 渲染线程过载。超过 20000 像素的场景直接拆分成多个长图吧别指望一张图把滚动列表整个尾到头都装下。移动端一张 1080 宽、20000 高的 PNG 解码本身就会吃几百 MB 内存体验很糟。4.2 重叠像素的调节逻辑2px 还是 5px重叠像素主要用于抵消滚动位置的取整误差。当verticalNormalizedPosition设置到某个值后ScrollRect 内部会把它换算成 content 的 anchoredPosition这个换算结果直接 float 取整可能有 1 像素左右的跳动。再加上ReadPixels的坐标取整截出来的图在边界处错位个 2 到 3 像素是常见的。我用在 1080p 下的经验值是重叠 3 像素2K 分辨率下要加到 5 像素4K 下 8 像素。有一个简单的验证方式截完长图后把相邻两段在拼接处放大 200% 看有没有水平撕裂线有就加大重叠量没有就减到刚好消失即可。注意重叠量加大会让拼接代码里裁剪顶部像素的逻辑多一个开销但几乎可以忽略。4.3 避免每帧截图加帧间隔减少渲染压力很多人第一次写这个功能会在 Update 里每帧截图一段结果滚动动画没停截图结果全是糊的。正确方案是严格按照协程一帧一段来跑。但如果 content 特别长、段数特别多一帧一段的节奏会导致 UI 卡顿因为截图时ReadPixels是同步操作会阻塞主线程几十毫秒。我的做法是在每段之间插入一个yield return new WaitForSeconds(0.02f)强制让出一段时间片。这个 0.02 秒在 50 段的截图任务里只增加 1 秒总时长但用户的其它 UI 操作不会完全卡死。4.4 内存控制的上限值分析与优化这里有一个可以照着估算的公式单段截图内存约为width * capturePixelHeight * 4字节。以 1080 宽、1203 高为例单段约为 5.2MBGetPixels32又会复制一份等大的Color32[]约 5.2MB拼接时的最终大图数组是 1080 * 10000 * 4 ≈ 43MB。看起来不高但如果你在拼接时把所有分段纹理都保留着20 段就是 100MB 起步。所以上一节强调的边拼边销毁非常重要。我还会在截图循环里用Resources.UnloadUnusedAssets()吗不会那个函数很耗性能直接靠Object.Destroy并引用置空就可以了。5. 必须避开的五个坑从截图全黑到滚动位置恢复失败5.1 Canvas 是 Overlay 模式ReadPixels 一片黑现象执行截图后生成的纹理是全黑的或者只有背景没有 UI。 原因Screen Space - Overlay模式下UI 被渲染在分离的 overlay 层ReadPixels在默认的 back buffer 上读不到这一层。这个坑在编辑器里不容易触发因为 Game 视图的后处理管线会做额外合成但打包到真机上必现。 解决把 Canvas 临时切换为Screen Space - Camera并且给它单独分配一个截图用相机相机的targetTexture指向你的 RenderTexture然后等一帧再执行ReadPixels。截完全部段落后再恢复 Canvas 和相机的原状。注意恢复时机要放在协程的finally块里防止中途异常导致 UI 一直处于被修改的状态。5.2 滚动没停稳就截图拼接处错位现象长图里每条列表项都在接缝处有上下错位严重时内容重复或缺失。 原因设置verticalNormalizedPosition后ScrollRect 的惯性或弹性动画没有立即停止画面仍在移动中就执行了ReadPixels。 解决设置verticalNormalizedPosition之前必须先调用scrollRect.StopMovement()把惯性速度清零然后在yield return null后再加一个yield return null或者等待一帧后检查scrollRect.velocity.magnitude小于 0.01 再继续。我最终用的是等速度归零再加一帧的双保险策略实测零错位。5.3 内容底部少了最后一段现象长图的最底部缺少一小截内容看起来像被截断了。 原因totalSegments计算时没有正确按Mathf.CeilToInt加上最后不足一段的高度对应的段数或者循环内verticalNormalizedPosition在接近 0 时被 Clamp 到 0导致最后一段滚动不完全截出来的内容不是预期的那一段。 解决采用我在 2.3 节给出的totalSegments计算式同时最后一段不要依赖verticalNormalizedPosition的精确值而是直接调用content.anchoredPosition new Vector2(content.anchoredPosition.x, 0)把 content 强制钉到底部然后截图。两种做法在最终效果上无差别但后者更保险。5.4 拼接后图片无法打开或尺寸异常现象EncodeToPNG生成的 PNG 文件在相册/图片查看器里打不开或者长图被系统压缩成小图。 原因最常见的是最终长图的Texture2D尺寸超过了设备的纹理上限。移动端 GLES 通常要求纹理宽高不大于 8192 或 4096。一个 1080 宽、9000 高的纹理在部分设备上直接创建失败EncodeToPNG返回空数组。另外还有可能是TextureFormat用了不支持压缩编码的格式比如RGBAFloatPNG 编码器无法处理。 解决在创建最终长图前先检查SystemInfo.maxTextureSize如果finalHeightPixels超出限制则按最大允许高度进行等比缩放后再拼接。或者直接放弃单张超长图改成输出多张标准高度比如 4000px的图再让用户用第三方拼接工具。这个妥协方案我在数字孪生项目里用过客户完全可以接受。5.5 截图完成后 ScrollRect 卷回顶部现象截图跑完后界面内容跳回了列表顶部用户当前浏览位置丢了。 原因协程结束时没有恢复scrollRect.verticalNormalizedPosition为初始值。 解决在协程开始前保存scrollRect.verticalNormalizedPosition结束位置写在finally块里恢复。有一种边缘情况如果用户点击导出按钮后立刻又手动滚动列表协程里的恢复操作会和用户的手势冲突。我的做法是截图过程中置空scrollRect的拖拽输入通过scrollRect.enabled false禁用截图结束后再启用并恢复位置。简单粗暴但有效。6. 提升截图质量的进阶技巧关闭后处理特效、固定分辨率与对比验证截图质量不只是拼接算法的事渲染环境的一致性更重要。我遇到过三次自己 PC 上截出来很漂亮、真机上截出来发灰的案例最后定位都是不同后处理特效叠加导致的。UI 后处理链条里常见的 Bloom、Color Grading、Vignette 都会让同一帧内容在不同设备上呈现不同颜色。截图这种需要如实记录的场景我会在截图开始前把相机上的PostProcessing组件临时禁用截完再恢复。列表里如果有MaskableGraphic的阴影特效或 Canvas Group 的透明度动画也要在截图中保证它们都处于静止状态。另外一个经验是固定截图用分辨率。如果你依赖Screen.width和Screen.height在 PC 窗口全屏切换、多显示器缩放变化时截图尺寸会跳动拼接时宽度不一致直接报错。我的做法是给截图流程指定一个固定分辨率例如1280x720或1920x1080通过临时改 Game 视图分辨率或直接设置RenderTexture尺寸来实现。这一点在需要给客户交付统一尺寸长图时尤其重要。曾经有一次我在 Windows 上截出 1920 宽的图客户在 Mac 上看没问题但放大到 Retina 屏就发虚就是因为固定分辨率太低后来我统一输出 2x 分辨率的长图来兼顾所有场景。做完截图、拼接、保存全流程后做一次对比验证我总结了三个可以量化的检查项检查项预期值失败时的修改方向拼接段落数 计算段数严格相等检查totalSegments算法与循环条件长图高度 content像素高度误差不大于 3px检查scaleFactor与 重叠量设置相邻段内容无错位/重叠放大200%无撕裂线增大重叠像素或增加滚动稳定帧每次调整参数后跑一遍这三个检查能快速定位问题出在坐标换算、滚动时序还是纹理创建阶段。截图过程中最破坏体验的其实是不可见——你不知道哪一段出了错直到看到最终长图。我后来会加一个调试模式把每一段已经截到的分段图临时存到沙盒里出错时能直接定位是哪一段出问题而不用把整个长图导出来看。结尾我想说Scroll View 长图截图这个功能代码量不大但处处是细节。如果只记住一件事那就是先用 StopMovement 把滚动打死再截图这一条能解决你一半以上的错位问题。剩下的就是 Canvas 模式和纹理尺寸这两个系统级约束提前规划好不要在项目后期才想起来做长图导出功能。希望我的这些血泪经验能帮你少走弯路一次跑通这个功能。本文还有配套的精品资源点击获取
返回列表