简介:这是一份面向 Unity 开发者的完整工程资源,适合有一定 GUI 与脚本编程基础、希望实现 Scroll View 滚动列表内容连续截图并合成为长图保存到本地的开发者。资源围绕滚动列表长图导出的实际需求,覆盖协程驱动滚动位置、逐帧调用屏幕捕捉 API、利用 Texture2D 拼接合成完整长图、处理图片边缘对齐等关键环节,同时给出导出 PDF 的扩展思路和性能优化建议,能帮助开发者理解连续截图与图像处理的核心原理。包内包含 Unity 工程文件、C# 源码、场景配置、文本说明等资料,便于对照学习。资源共 2000 个文件,以 md 文档、bin 二进制资源、txt 说明和 cs 脚本为主,并含 asset、meta、png 等工程文件,压缩包整体约 716.55MB,文件类型较为完整。目前已有 162 人学习,适合用作功能参考或进一步改造为项目内的截图导出工具。
1. Unity Scroll View 连续截图:滚动列表变长图,先搞清四个字“裁掉的内容”
做 Unity 项目时,最常被产品追着要的功能之一,就是把排行榜、聊天记录、订单流水这类 Scroll View 列表一键导出成长图。很多人第一反应是调ScreenCapture.CaptureScreenshot,按下按钮截一张图就完事——但截出来的只有当前视口里那一屏,列表下面滚不到的内容全没了,产品看一眼就会打回来说“榜单有 120 行,你这图里只有 8 行”。Scroll View 连续截图的难点,本质上不是截图 API 怎么调,而是 Scroll View 的 viewport 把超出可视区域的 UI 全部裁掉了,而截图工具只能截“屏幕上看得到的像素”。所以这篇要解决的核心问题就一个:怎么把被 viewport 裁掉的内容一段一段捞回来,再拼成一张完整长图保存到本地。适合在 Canvas 上做界面导出、生成分享图的 Unity 开发,内容不依赖具体业务 UI,排行榜、背包、聊天列表都能直接套。
2. 截图方案选型:分段拼接 vs 离屏渲染,差别在透明通道和全屏闪烁
2.1 分段拼接:代码量少,坑全在对齐
最常见、也最容易跑通的做法,是写一个协程,把 ScrollRect 的 Content 按固定步长往下滚一段、截一张图,滚到底后把若干张截图拼成一张长图。这个方案的核心依据是:Unity 的 UI 只要在 Canvas 下且没有被 viewport 裁剪,就会正常渲染到屏幕上,所以当 Content 滚动到某个位置时,当前可视区域的内容是可截取的。
分段拼接的好处是代码不依赖任何平台 API,Editor 下和真机都能跑,也不需要额外相机。缺点是拼接时像素对齐要自己做,遇到 ScrollRect 自带弹性回弹、惯性滑动时,Content 位置不稳定会直接截出重复行或丢行。另外ScreenCapture.CaptureScreenshotAsTexture()截的是整个屏幕,如果 Canvas 是 Screen Space - Camera 模式,会把世界相机渲染的东西也截进来,后面裁剪逻辑就得多处理一层。
2.2 离屏渲染:适合透明背景,但解决不了 viewport 裁剪
离屏渲染的做法是做一个单独的相机,把要截的 UI 放到指定 Layer,相机 TargetTexture 指向一张 RenderTexture,最后ReadPixels读出像素。很多人以为离屏渲染可以“绕过屏幕”,直接截到 Scroll View 全部内容——这是误解。Scroll View 的 viewport 裁剪是靠RectMask2D或者Mask在 Shader 层面做裁剪的,这个裁剪和屏幕还是离屏没有关系,裁剪区域外的 UI 根本不会提交像素。所以离屏渲染也得先处理 viewport 问题,要么临时把 viewport 拉大到 Content 大小,要么把 Mask 禁用掉。
那离屏渲染的优势在哪?一是能截透明背景的 PNG,分段拼接截的是屏幕,屏幕背景色会带进图里,透明通道基本拿不到;二是截图分辨率可以自定义,不依赖设备屏幕尺寸;三是不会像CaptureScreenshot在部分 Android 机型上出现“闪烁一帧”的现象,因为离屏渲染发生在 RenderTexture 上,不经过屏幕呈现。
2.3 选型条件:按导出场景决定,不按喜好决定
我一般这样判断:如果导出图最终要贴到聊天、社交平台,或者要覆盖在别的背景上,必须要有透明通道,走离屏渲染;如果只是保存到本地、发朋友圈、打印,分段拼接更快,性能也更容易控。还有第三个维度的参考——列表内容是不是动态加载的。如果 Scroll View 用的是按需实例化 Item(比如只实例化可视区附近的数据),分段拼接会因为滚动触发数据重排,截出来的内容可能前一张和后一张之间少了几个 Item。这种情况必须先确认数据源是否一次性全量加载,是虚列表就得先把实例化范围放开,或者走离屏渲染配合禁用 Mask 的方式。
选型时还要看 Editor 还是真机调试。Editor 下CaptureScreenshotAsTexture截的像素分辨率跟随 Game 视图,如果 Game 视图和 Build 分辨率不一致,导出图上字会发虚。真机上这个问题不明显,但 Android 上连续调用截图 API 确实有概率触发 Surface 重绘,表现为导出过程中屏幕闪一下。我自己做的时候,Editor 调试用分段拼接,发版给用户的功能走离屏渲染,两套代码共用拼接和保存逻辑。
3. 协程截图脚本落地:滚动步进、重叠像素与长图拼接参数
3.1 初始化参数:先把关键尺寸算清楚
先强调一个前提:下面的脚本适用于 Content 在 Scroll View 内一次性完整实例化的场景,Content 的 pivot 建议保持 (0.5, 0.5),anchor 保持 stretch。脚本做的事是:记录起始位置 -> 按步长滚动 -> 截屏 -> 继续滚动 -> 截到底 -> 拼接 -> 保存。核心是步长和帧数的计算,步长决定了截多少张图,也决定了拼接时是否会有遗漏。
using System.Collections; using System.IO; using UnityEngine; using UnityEngine.UI; public class ScrollViewLongCapture : MonoBehaviour { [Header("绑定")] public ScrollRect scrollRect; public Canvas rootCanvas; [Header("截图参数")] public float startDelay = 0.2f; public int overlapPixels = 8; // 相邻两张图重叠的像素行数 public float stepFactor = 0.9f; // 每次滚动 viewport 高度的 90% public bool restorePosition = true; // 截完恢复原滚动位置 [Header("输出")] public string fileName = "scroll_capture.png"; private Texture2D[] slices; private Vector2 originalPos; public void StartCapture() { StartCoroutine(CaptureSequence()); } private IEnumerator CaptureSequence() { yield return new WaitForEndOfFrame(); RectTransform content = scrollRect.content; RectTransform viewport = scrollRect.viewport; originalPos = content.anchoredPosition; // 内容总高度和可视区高度,单位是 Canvas 像素 float contentHeight = content.rect.height; float viewportHeight = viewport.rect.height; if (contentHeight <= viewportHeight) { Debug.LogWarning("内容没有超出可视区,不需要分段截图"); yield break; } // 实际能滚动的距离 float maxScroll = Mathf.Max(0f, contentHeight - viewportHeight); // 每次滚动多少像素,控制截图张数 float stepPx = viewportHeight * stepFactor; // 需要截多少张,最后一张可能不满一屏 int frameCount = Mathf.CeilToInt(maxScroll / stepPx) + 1; slices = new Texture2D[frameCount]; // 每张截图之间让 UI 稳定一帧,避免半渲染状态 for (int i = 0; i < frameCount; i++) { float targetY = Mathf.Min(maxScroll, i * stepPx); ApplyScroll(targetY); scrollRect.StopMovement(); yield return new WaitForEndOfFrame(); slices[i] = CaptureCurrentFrame(); } if (restorePosition) { ApplyScroll(0f); scrollRect.StopMovement(); } Texture2D combined = CombineSlices(slices, overlapPixels); SaveTexture(combined, fileName); Debug.Log($"保存完成, 尺寸 {combined.width} x {combined.height}"); } private void ApplyScroll(float y) { if (scrollRect.verticalNormalizedPosition < 0f) { // 防止 elastic 模式下越界 scrollRect.velocity = Vector2.zero; } Vector2 pos = scrollRect.content.anchoredPosition; pos.y = y; scrollRect.content.anchoredPosition = pos; } }stepFactor是控制截图像素重叠量的关键参数,0.9 意味着每次滚动可视区高度的 90%,相邻两张图之间自然有 10% 的内容重复。这样即使滚动定位有 1~2 像素误差,拼接时也能通过重叠区域对齐,避免丢行。overlapPixels是拼接时真正裁剪掉的像素行数,和stepFactor是两个维度:step 控制“拍多少张”,overlap 控制“拼的时候裁掉多少重复行”。如果 overlap 设 0,两张图边界会紧贴,浮点取整误差会造成黑线或内容错位。overlap 设 8~16 像素,拼接逻辑会从下一张图第 overlap 行开始取,留出误差缓冲。
3.2 截图与拼接:CaptureScreenshotAsTexture 和 SetPixels 的配合
截图和拼接是这套方案里最容易出幺蛾子的两个环节。截屏这里我用ScreenCapture.CaptureScreenshotAsTexture(),它返回一张整个屏幕的 Texture2D,直接同步可用,不像CaptureScreenshot(path)还要等文件系统异步写完,适合连续多次调用。但要注意它截的是整个屏幕,如果你的 Canvas 是 Overlay 模式且是全屏 UI,不需要裁剪;如果不是全屏,需要按 viewport 的屏幕坐标裁一刀。
private Texture2D CaptureCurrentFrame() { Texture2D full = ScreenCapture.CaptureScreenshotAsTexture(); Rect viewRect = GetViewportScreenRect(); int w = Mathf.FloorToInt(viewRect.width); int h = Mathf.FloorToInt(viewRect.height); Texture2D crop = new Texture2D(w, h, TextureFormat.RGBA32, false); Color[] pixels = full.GetPixels( Mathf.FloorToInt(viewRect.x), Mathf.FloorToInt(viewRect.y), w, h); crop.SetPixels(pixels); crop.Apply(); Destroy(full); return crop; } private Rect GetViewportScreenRect() { // 把 viewport 的世界坐标转屏幕坐标,Overlay 模式下中心点即屏幕坐标 Vector3[] corners = new Vector3[4]; scrollRect.viewport.GetWorldCorners(corners); if (rootCanvas.renderMode == RenderMode.ScreenSpaceOverlay) { return new Rect(corners[0].x, corners[0].y, corners[2].x - corners[0].x, corners[2].y - corners[0].y); } // ScreenSpaceCamera 模式需要转换到屏幕坐标 Vector2 min = RectTransformUtility.WorldToScreenPoint(rootCanvas.worldCamera, corners[0]); Vector2 max = RectTransformUtility.WorldToScreenPoint(rootCanvas.worldCamera, corners[2]); // WorldToScreenPoint 以左下角为原点,和 Texture2D.GetPixels 的 y 方向一致 return Rect.MinMaxRect(min.x, min.y, max.x, max.y); }这里最容易被忽略的是坐标原点方向。WorldToScreenPoint返回的坐标以屏幕左下角为原点,y 向上;Texture2D.GetPixels的 y 参数也是从图片底部开始算。两个方向一致,直接传就行。如果你习惯了 UI 坐标从左上角往下,到这里会翻车——我之前就是没换算,裁出来的图整体上下颠倒。坐标换算完,拼接就是纯像素操作:第一张图整段拿来,之后每张图从overlapPixels行开始取,因为重叠区在上一张图里已经截过了。
private Texture2D CombineSlices(Texture2D[] slices, int overlap) { int width = slices[0].width; int height = slices[0].height; int totalHeight = height; for (int i = 1; i < slices.Length; i++) { totalHeight += slices[i].height - overlap; } Texture2D result = new Texture2D(width, totalHeight, TextureFormat.RGBA32, false); int yOffset = 0; for (int i = 0; i < slices.Length; i++) { Texture2D s = slices[i]; int startY = (i == 0) ? 0 : overlap; int copyHeight = s.height - startY; Color[] block = s.GetPixels(0, startY, width, copyHeight); result.SetPixels(0, yOffset, width, copyHeight, block); yOffset += copyHeight; // 每拼一张就释放,不然内存峰值会很高 Destroy(s); } result.Apply(); return result; }拼接逻辑的overlap不是简单地把两张图重叠部分“盖在一起”,而是从上一张的尾部开始,下一张从overlap行往后取。这样即使滚动定位有误差,只要误差小于 overlap 行,最终长图里就不会出现内容缺失或重复。拼接完的result是一张超长图,宽度等于 viewport 宽度,高度是内容区总高。这里要提醒一点:SetPixels是按行从下往上排的,所以 result 的底部是截图序列的第 0 张,对应滚动起始位置,符合直觉,不用额外翻转。
3.3 保存 PNG 与关键参数速查
保存用EncodeToPNG()再写文件,这一步在 PC、Android、iOS 上通用。我习惯把保存路径暴露成参数,Editor 下用Application.dataPath同级目录,真机用Application.persistentDataPath,避免用户没有权限写外部存储。
private void SaveTexture(Texture2D tex, string name) { string dirPath = Application.persistentDataPath + "/CaptureOutput/"; if (!Directory.Exists(dirPath)) { Directory.CreateDirectory(dirPath); } string filePath = Path.Combine(dirPath, name); File.WriteAllBytes(filePath, tex.EncodeToPNG()); #if UNITY_EDITOR Debug.Log("截图已保存到: " + filePath); #endif }几个参数的经验值:列表项高度如果很小(比如 50 像素以内),步长建议设0.85f,overlap 设16f,因为行高小的时候 1 像素误差就可能导致文字被切半;普通排行榜 Item 高度 80~150 像素,stepFactor = 0.9f, overlap = 8f足够;内容高度超过 5000 像素时,建议把 step 调大到 0.95,减少截图张数,否则拼接循环里GetPixels的拷贝量会吃掉大量内存。帧率方面,一次 8000 像素高的列表,步长 0.9、viewport 高度 800,大约要截 12 张左右,协程逐帧执行,2 秒内能跑完,用户不会觉得卡。
4. 画质与文件细节:DPI 写入、透明背景、内存峰值控制
4.1 重叠像素的选择和边界情况
overlap 选 8 还是 16,核心看两步之间实际位移的误差。ScrollRect 的anchoredPosition是 float,设置值会被赋值到 Transform,但在不同机型上,UI 重建和 Canvas 顶点数据更新可能差一帧。WaitForEndOfFrame保证的是“这一帧渲染已经完成”,但 Canvas 的脏标记刷新有时会延迟到下一帧。如果你的 Item 里带富文本、图集多、或者有ContentSizeFitter,强烈建议 overlap 设 16 以上,反正多截的像素会在拼接时裁掉,不会造成内容重复。只有一种情况 overlap 不能覆盖——滚动一步跨越了超过一个 Item 的高度,也就是 Item 高度大于viewportHeight * (1 - stepFactor)。这种时候截图之间会整个丢掉一个 Item,改 overlap 没用,必须调小 stepFactor。
4.2 把 DPI 写进 PNG:EncodeToPNG 的隐藏短板
Texture2D.EncodeToPNG()生成的 PNG 文件默认不写 pHYs 块,也就是没有 DPI 信息。Windows 照片查看器和微信发送原图一般没关系,但你要把长图交给打印店、或者让用户在 Word/PDF 里插入图片,DPI 缺失会导致图片被按 96dpi 解读,打印出来尺寸完全不对。Unity 没有官方 API 写 DPI,但 PNG 格式允许在 IHDR 之后插入一个 pHYs chunk 来声明像素密度。常见的做法是,保存后用二进制方式往 PNG 文件里插入这个 chunk。
public static void WritePngWithDpi(Texture2D tex, string path, int dpi = 300) { byte[] png = tex.EncodeToPNG(); byte[] result = InsertPhysChunk(png, dpi); File.WriteAllBytes(path, result); } private static byte[] InsertPhysChunk(byte[] png, int dpi) { int idatIndex = FindChunk(png, "IDAT"); uint ppm = (uint)Mathf.RoundToInt(dpi / 0.0254f); // 像素/米 byte[] chunkData = new byte[9]; WriteUIntBE(chunkData, 0, ppm); WriteUIntBE(chunkData, 4, ppm); chunkData[8] = 1; // 单位是米 using var ms = new MemoryStream(); ms.Write(png, 0, idatIndex); WriteUIntBE(ms, 9); // pHYs 数据长度 9 ms.Write(new byte[] { (byte)'p', (byte)'H', (byte)'Y', (byte)'s' }, 0, 4); ms.Write(chunkData, 0, 9); byte[] crcInput = new byte[13]; System.Buffer.BlockCopy(new byte[] { (byte)'p', (byte)'H', (byte)'Y', (byte)'s' }, 0, crcInput, 0, 4); System.Buffer.BlockCopy(chunkData, 0, crcInput, 4, 9); WriteUIntBE(ms, Crc32(crcInput)); ms.Write(png, idatIndex, png.Length - idatIndex); return ms.ToArray(); } private static int FindChunk(byte[] png, string name) { // PNG 签名 8 字节, 之后是 IHDR 块, 从第 8 字节开始遍历块 int offset = 8; byte[] typeName = new byte[4]; while (offset + 8 <= png.Length) { int len = (png[offset] << 24) | (png[offset + 1] << 16) | (png[offset + 2] << 8) | png[offset + 3]; System.Array.Copy(png, offset + 4, typeName, 0, 4); string s = System.Text.Encoding.ASCII.GetString(typeName); if (s == name) { return offset; } offset += 12 + len; // 跳到下一个 chunk } return -1; } private static uint Crc32(byte[] data) { uint crc = 0xFFFFFFFF; foreach (byte b in data) { crc ^= b; for (int i = 0; i < 8; i++) { uint mask = 0u - (crc & 1u); crc = (crc >> 1) ^ (0xEDB88320u & mask); } } return ~crc; }ppm的计算是dpi / 0.0254f,因为 1 英寸等于 0.0254 米,300dpi 对应的像素密度是 11811。这个值和 Windows 图片属性里显示的 300dpi 是一致的。Crc32是 PNG 规范里指定的 CRC-32 算法,多项式 0xEDB88320 是反射形式,uint 运算在 C# 里要用0u - (crc & 1u)来构造掩码,直接写- (crc & 1)会溢出报错。这段代码抄下来就能用,但注意它只适用于标准 PNG,Unity 的 EncodeToPNG 输出都是标准 8 位 RGBA PNG,没有遇到兼容性问题。
4.3 透明背景和内存峰值控制
透明背景需求必须走离屏渲染方案,分段拼接截屏幕拿不到透明通道。离屏渲染时注意 Canvas 的 Sorting Layer 和相机 culling mask 要配对,相机只渲染目标 UI 的 Layer。渲染到 RenderTexture 后,ReadPixels的 Rect 必须从 (0,0) 开始读整个 RenderTexture,读出来就是透明背景。然后你仍然要处理 viewport 裁剪的问题:一个比较省事的做法是截图前把 viewport 的 sizeDelta 临时设置成 content 的大小,并关掉RectMask2D组件,等截完再恢复。这样 RenderTexture 里就是完整的列表内容,不需要滚动,直接一次截完。代价是 UI 会短暂地不裁剪,用户能看到列表“溢出”一帧。由于相机渲染不经过屏幕,实际上用户看不到这个瞬间,只是内存峰值会比较高——一张 1080x8000 的透明 PNG,运行时内存至少 35MB 起步,大列表要做好分批处理或临时降低分辨率。
5. 避坑:Scroll View 连续截图踩过的五个坑
5.1 截图之间出现明显的黑线或白缝
现象:拼出来的长图每隔一段就有一条横贯的黑线,位置正好是两张截图的交界处。
原因:GetPixels的 y 参数取整误差。viewport 高度换算成像素后,如果出现小数,比如 735.6,Unity 会四舍五入截取,而滚动位移用的是 Canvas 坐标系的 float,和像素坐标系对不上,交界处就有一条没有内容覆盖的缝隙。
解决:拼接时不按整张图高度对齐,而是按上一张图的底部行号对齐。给overlapPixels留足余量后,从startY = overlap开始取下一段;同时在拼接循环里,把上一张图的最后 1 行丢弃,强制从下一张图的overlap行接上,缝隙就消失了。
5.2 ScrollView 弹性回弹导致内容错位
现象:连续滚动截图时,截到一半列表突然自己弹回去了,后面的截图内容和前面重复,长图里同一段数据出现了两次。
原因:ScrollRect开了movementType = Elastic,当 Content 滚到底部时,回弹动画会改变anchoredPosition。协程里设置 targetY 后没有立刻停住,回弹把位置拉走了。
解决:每次设置滚动位置后调用scrollRect.StopMovement(),把velocity清零;如果还不行,截图期间临时把movementType改成Clamped,截完恢复。回弹动画本质上是带动量的运动,anchoredPosition赋值后一帧内可能被回弹逻辑覆盖,StopMovement要在赋值后同帧调用。
5.3 截到半渲染状态的 UI,文字发虚或图标缺一角
现象:长图上某些 Item 的文字比实际模糊,或者 icon 只截了一半。
原因:WaitForEndOfFrame保证的是渲染管线结束,但 Canvas 的脏标记刷新有自己的调度。如果截图帧刚好在 Canvas 重建之后、GPU 提交之前,屏幕上的内容可能是上一帧的。
解决:在滚动位置设置之后,额外等一个yield return null再WaitForEndOfFrame,让 Canvas 有时间处理布局变更。对于带ContentSizeFitter的 Item,要等两帧,因为 SizeFitter 的布局计算是异步的。
5.4 ScrollRect 的 OnValueChanged 回调在截图期间被反复触发
现象:列表项里如果监听了ScrollRect.onValueChanged做加载更多、隐藏悬浮按钮等操作,截图期间这些回调大量触发,有的还会打断 UI 状态。
原因:每截一张图就要滚动一次,每次滚动位置变化都会触发一次onValueChanged事件。如果你的业务逻辑在回调里做了耗时操作,截图协程会被卡顿,甚至会因为重入而中断。
解决:截图开始前用一个 bool 标志位屏蔽业务回调,截完再恢复。比移除监听更安全的是在回调函数最前面判断_captureInProgress,这样业务逻辑不感知截图这个过程,只跳过自己。注意恢复时不要手动触发一个假的回调来刷新 UI,直接让按钮状态自然刷新即可。
5.5 长图过大,Android 上打开照片应用一片黑
现象:拼接完的 PNG 在电脑上看没问题,但传到手机相册打开,图片显示全黑,或者提示图片损坏。
原因:部分 Android 机型的图片解码器对超大图片(尤其高度超过 8000 像素以上)解码内存峰值会超过应用可用内存,系统直接放弃解码,返回黑色或者解析失败。
解决:导出长图前先按目标用途决定尺寸——发微信群聊的长图建议高度控制在 8000 像素以内,宽度保持原样;如果内容确实很长,分段导出多张图,或者在拼接时做一次缩放。Unity 里缩放Texture2D用ScaleTexture逐像素采样性能较差,可以把 Texture 画到 RenderTexture 上再ReadPixels,GPU 扛得住,代码也简单。
6. 从长图到 PDF 与验证:两条转换路和一个像素级自查习惯
导出的长图往往不是终点,不少项目要求“保存成 PDF,方便用户打印或归档”。Unity 本身没有 PDF 输出能力,需要走平台能力或者第三方库。我这边实践过两条路:一条是 iOS 上直接调用原生UIGraphicsPDFRenderer,把长图按 A4 比例切割后每页渲染一张图,全程不需要额外库,但只在 iOS 端有效,而且要在 Xcode 工程里加原生桥接文件。另一条是导出 PNG 后,接入 PDFsharp 之类的 .NET 库在 Windows 或服务器端合成 PDF,适合做多平台导出中心。从 Unity 侧看,最省事的做法是先把长图按 A4 竖版比例切成多段,每段生成一个 PDF 页面。切割算法其实就是复用前面拼接的部分逻辑,把长图按固定高度分页,分页之间留一点重叠避免表格线被切断。
验证工作往往是这部分最容易跳过的。导出完成后,我会在代码里加一个自检:按固定行号取原列表里的数据项,在长图对应坐标读取像素颜色,判断是否和期望的 Item 背景色一致。这个验证不能替人眼检查排版,但能快速发现拼接错位、丢行这类严重问题。我自己的习惯是,每调整一次滚动步长或者 overlap 参数,就导出一次小图,用 PS 或者看图工具把相邻两张截图的拼接边界放大到 400% 检查一遍文字是否连续。这个动作看起来繁琐,但确实救过我几次——有一次就是 Item 高度取了带小数的值,步进累积误差到了第 5 张图直接错开 3 个像素,文字被截断,光看缩略图完全看不出来。从那以后我每次改 UI 尺寸,都会强制重新跑一遍快速导出 + 放大验证的流程。这套坑踩完之后再看这类需求,其实就一句话:Scroll View 连续截图不是截不截得出来的问题,而是滚动位置控制、重叠像素策略、裁剪边界处理三者之间能不能对齐的问题,参数调稳了,剩下的就是按业务场景选分段拼接还是离屏渲染。希望这篇能帮你少走这一段弯路。
本文还有配套的精品资源,点击获取