【Unity开发】Unity长截图功能实现

🐾 个人主页 🐾

阿松爱睡觉,横竖醒不来 🏅你可以不屠龙,但不能不磨剑🗡


文章目录

一、前言

最近项目里做了个保存长页面的功能:界面上一个 Scroll View,里面内容老长(数据报表、战绩详情之类的),一屏压根看不全。现在想做一下就是点一下保存或者分享,能把整个列表从头到尾拼成一张长图。

上网搜了一下,Unity 并没有现成的长截图 API 可以用:手机系统的长截图只能截系统自己的界面,够不着 App 里 UGUI 画出来的内容;Application.CaptureScreenshot 倒是能截图,但是只能截当前这一屏,而且这个方法已经废弃了。所以没辙,只能自己动手撸一个。

思路说白了就是个"笨办法":把列表一屏一屏往下滚,每滚一屏截一张图,全截完之后再把每张图里有效的部分裁出来,按顺序拼成一张大图。原理一句话就能说完,但是细节不少------等帧结束、边界计算、最后一屏的零头、拼接顺序------每一步处理不好,拼出来就是花的或者歪的。

这篇文章就把完整代码和每一步的道理捋一遍,初学者可以跟着抄,进阶的看思路就行。

那么话不多说,直接开始今天的内容吧!

二、实现思路

(一)核心原理

分三大步:分段滚动截图 → 裁剪每张图的有效区域 → 按顺序拼接。

从列表顶部开始,每次把 content 往下搬一屏的高度,搬完之后等帧结束截一张全屏图存起来,直到把列表滚到底。最后拿这三张图,把每张图里属于列表内容的区域裁出来,从最后一张开始往图片底部贴,一张长图就拼好了。

为什么要从最后一张开始贴?因为 Texture2D 的像素原点在左下角 ,SetPixels(0, 0, ...) 贴的是图片最底下那一块。列表最底部的内容在最后一张图里,所以最后一张要贴在长图的最底部,一路往上贴,第一张(列表顶部)自然就在最上面。顺序反了,长图就是倒的。

(二)几个关键点

  1. 截图必须等帧结束 。UI 是把当前帧渲染完才显示到屏幕上的,不等帧结束就截,截到的还是上一帧的旧画面,几屏拼起来能看出错位和花屏。所以核心截图方法要用协程 + WaitForEndOfFrame。
  2. 用专门的相机渲染到 RenderTexture 。ReadPixels 读的是相机渲染出来的画面,所以场景里要放一个专门干活的截图相机,截图的时候把它的 targetTexture 设成新建的 RenderTexture,渲染完再把像素读成 Texture2D。
  3. 截图前先把碍事的 UI 藏起来 。返回按钮、保存按钮、分享按钮,这些不属于长图的内容,截图期间先 SetActive(false),截完再恢复。
  4. 最后一屏可能不足一页 。内容高度不一定刚好是屏高的整数倍,剩下的"零头"(代码里的 exHeight)要单独处理,不然要么少截一截,要么重复截一段。
  5. 屏幕坐标和布局坐标要换算 。Viewport 在屏幕上实际占的像素高度,受 Canvas 缩放影响,和它在布局里的高度不是一回事,裁图的时候要用一个比例(代码里的 ratio)换算。
  6. 截图期间界面不能动。截图回调是异步的,好几屏要好几帧才能截完,这期间如果列表数据刷新了、UI 布局重排了,拼出来必错位。这个下面注意事项里还会细说。

(三)为什么不用别的方案

肯定有同学会问:为什么不把整个 Content 一股脑渲染到一张超大的 RenderTexture 上,一步到位?

原因有两个:一是纹理尺寸有上限 ,大部分设备最大纹理也就 4096 或者 8192,列表稍微长一点就超了;二是内存爆炸,一张 1080×20000 的纹理,光内存就 80 多 MB,性能稍微差一点的手机直接寄了。

分段拼接就没有这两个问题,每张图都是正常的一屏大小,拼接的时候也是按块搬运,内存可控,列表多长都不怕。缺点就是慢一点、代码麻烦一点,但是稳。

三、UI结构与工具方法

UI 结构其实就一个 Scroll View ,没有什么特别的,画面能够使用鼠标进行滚动就OK

代码里用到了几个项目里封装好的方法,先说明一下,大家有自己的实现直接替换就行,不影响逻辑:

代码里的写法 作用
GetChild("Bottom/Title") 按路径查找子物体,Transform 的扩展方法
scrollRect.Move(position) 把 content 移动到指定位置
go.transform.DelayTime(Action) 延时执行,封装了协程
ScreenShot.GetPhotoPath(".png") 获取图片保存路径
AlbumManager.Instance.SaveImageToGallery() 保存图片到手机相册
APIManager.ShareImageMessage(path) 调起分享
TipsUI_01.ShowMsg("xxx") 飘字提示

其中 Move 方法和每个项目的 ScrollRect 配置有关,这里给一个最常见的写法(Content 顶部锚定时,向下滚动 anchoredPosition.y 是负值,所以取负):

csharp 复制代码
public static void Move(this ScrollRect scrollRect, int position)
{
    // 把 content 移动到指定位置,position 是距顶部的偏移量
    scrollRect.content.anchoredPosition = new Vector2(0, -position);
}

四、截图工具 ScreenShot

先写最底层的东西:截一张图。用协程等当前帧结束,把截图相机渲染到 RenderTexture,再把像素读成 Texture2D,通过回调交出去。上层只管调,什么时候截完了回调里通知。

csharp 复制代码
/// <summary>
/// 截一张图,截图完成后通过回调返回
/// </summary>
public void StartShotTexture2(Rect shotRect, Action<Texture2D> callback)
{
    StartCoroutine(CaptureScreenshotTexture(shotRect, callback));
}

IEnumerator CaptureScreenshotTexture(Rect rect, Action<Texture2D> callback)
{
    // 一定要等到帧结束,等当前帧的UI全部渲染完,不然截到的是旧画面
    yield return new WaitForEndOfFrame();

    // 新建 RenderTexture 作为截图相机的渲染目标,32 是深度缓冲
    RenderTexture rt = new RenderTexture((int)Screen.width, (int)Screen.height, 32);
    CameraScreenShot.targetTexture = rt;
    CameraScreenShot.Render();
    RenderTexture.active = rt;

    // 创建一个空纹理,读取像素信息并存储为纹理数据
    Texture2D screenShot = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGB24, false);
    screenShot.ReadPixels(rect, 0, 0);
    screenShot.Apply();
    texture = screenShot;
    CameraScreenShot.targetTexture = null;
    RenderTexture.active = null;
    Destroy(rt);

    // 回调交出去,上层接着干后续的活
    callback.Invoke(screenShot);
}

CameraScreenShot 是挂在场景里的一个相机,专门用来截图(可以用主相机,也可以单独建一个),需要截到的内容都要在它的 Culling Mask 里。注意截完之后要把 targetTexture 和 RenderTexture.active 都置空,RenderTexture 也要记得销毁,不然很吃内存。

这里截的都是全屏(Rect(0, 0, Screen.width, Screen.height)),先拿整屏,具体裁哪里后面拼接的时候再算。

哦对了,还有就是如果你的 Canvas 是 Screen Space - Overlay (UI 不经过任何相机,直接画在屏幕上),那这套"相机 + RenderTexture"的方法是抓不到 UI 的,因为相机渲染的时候压根不知道 UI 的存在。这种情况下最简单的做法是省掉相机,等帧结束之后直接 ReadPixels 读屏幕后备缓冲,一样能读到 UI。两种写法都行,项目里用的是相机渲染,所以后面按这个讲。

五、计算可视区域边界

拼接的时候要知道:每一张全屏图里,哪一段才是列表的有效内容。比如屏幕顶部有标题栏、底部有按钮区(截图时虽然藏了,位置还占着),有效区域只是中间 Viewport 这一块。所以先把 Viewport 的四个顶点从世界坐标换算成屏幕坐标,拿到它在屏幕上的上下边界。

RectBoundary 就是个装着四个边界值的小结构体:

csharp 复制代码
public class RectBoundary
{
    public float Down;   // 下边界的屏幕Y
    public float Up;     // 上边界的屏幕Y
    public float Left;   // 左边界的屏幕X
    public float Right;  // 右边界的屏幕X
}
csharp 复制代码
/// <summary>
/// 获取UI在屏幕上的边界
/// </summary>
public static RectBoundary GetBoundary(this ScrollRect self, RectTransform rect, Canvas canvas)
{
    var posArray = GetUIScreenPosition(self, rect, canvas);

    // GetWorldCorners 的四个角:0左下 1左上 2右上 3右下
    float contentDown = posArray[0].y;    // 可视区域下边界的屏幕Y
    float contentUp = posArray[1].y;      // 可视区域上边界的屏幕Y
    float contentLeft = posArray[0].x;    // 左边界
    float contentRight = posArray[2].x;   // 右边界

    RectBoundary boundary = new RectBoundary
    {
        Down = contentDown,
        Up = contentUp,
        Left = contentLeft,
        Right = contentRight,
    };
    return boundary;
}

/// <summary>
/// 获取UI的四个顶点屏幕坐标
/// </summary>
public static Vector3[] GetUIScreenPosition(this ScrollRect self, RectTransform rectTransform, Canvas canvas)
{
    Vector3[] corners = new Vector3[4];
    rectTransform.GetWorldCorners(corners);
    Vector3[] screenPosArray = new Vector3[4];
    for (int i = 0; i < 4; i++)
    {
        screenPosArray[i] = canvas.worldCamera.WorldToScreenPoint(corners[i]);
    }
    return screenPosArray;
}

RectTransform.GetWorldCorners 拿到的四个角顺序是固定的:0 左下、1 左上、2 右上、3 右下 。屏幕坐标系的 Y 是从屏幕底部往上数的,所以 Up(上边界)一定比 Down(下边界)大,两者相减就是 Viewport 在屏幕上占的像素高度:

csharp 复制代码
pagPeixSize = (int)(viewBoundary.Up - viewBoundary.Down); // 一页的屏幕像素高

顺带提醒一句:这个写法里用了 canvas.worldCamera,只适用于 Screen Space - Camera 和 World Space 的 Canvas。如果是 Screen Space - Overlay,worldCamera 是 null,会直接报空引用,那种情况下 Overlay 的世界坐标和屏幕坐标基本一致,直接用 GetWorldCorners 的结果就行。

有了边界之后,裁图的规则就三条(后面拼接会用到):

第几张 裁剪区域 高度
第一张 从 Down 一直到屏幕顶部 Screen.height - Down
中间每张 从 Down 到 Up,正好一页 pagPeixSize
最后一张 从 Down 往上裁零头的高度 ratio * pagPeixSize

第一张比中间每张多裁的那一块(Down 到屏幕顶部),一般是标题栏这类固定在顶部的区域,拼进来的长图第一页就带着标题,和手机上长截图的效果一样。

六、分段滚动截图

先把用到的成员变量放这里,都是这个类的字段(类名随便起,能挂到按钮上就行):

csharp 复制代码
public class LongScreenShot : MonoBehaviour
{
    private List<Texture2D> texture2Ds = new List<Texture2D>(); // 每一页的截图
    private RectBoundary viewBoundary;   // 可视区域的屏幕边界
    private int exHeight;                // 不足一屏的零头(布局单位)
    private int pagPeixSize;             // 一页内容在屏幕上的像素高
    private float ratio;                 // 布局高度换算成屏幕像素的比例
    private bool isShare;                // true-调起分享 false-保存相册

    // ... 下面还有
}

(一)入口 StartShotTexture

截图的总入口,挂到保存/分享按钮上,进来先干三件事:藏按钮、拿组件、算参数,算完直接开截。

csharp 复制代码
private void StartShotTexture()
{
    texture2Ds.Clear();
    // 截图期间把不相关的 UI 藏起来,截完再恢复
    GetChild("Top/BackButton").SetActive(false);
    GetChild("Bottom/SaveButton").SetActive(false);
    GetChild("Bottom/ShareButton").SetActive(false);

    // 拿到 Canvas 和 ScrollRect 相关组件
    Canvas canvas = GameObject.Find("CanvasUI").GetComponent<Canvas>();
    ScrollRect scrollRect = GetChild("Bottom/ListView").GetComponent<ScrollRect>();
    RectTransform viewport = (RectTransform)GetChild("Bottom/ListView/Viewport").transform;

    // 重置列表的位置,从头开始截
    scrollRect.content.anchoredPosition = new Vector2(0, 0);

    int position = 0;                                              // 当前滚动位置
    int pageSize = (int)viewport.rect.height;                      // 一页的高度(布局单位)
    int maxPosition = (int)(scrollRect.content.rect.height - pageSize); // 最大可滚动位置
    exHeight = 0;
    if (scrollRect.content.rect.height > pageSize)
    {
        // 不足一屏的零头,比如内容2500高一页1000,零头就是500
        exHeight = (int)(scrollRect.content.rect.height % pageSize);
    }

    // 可视区域边界 & 单位换算
    viewBoundary = scrollRect.GetBoundary(viewport, canvas);
    pagPeixSize = (int)(viewBoundary.Up - viewBoundary.Down);
    ratio = exHeight / viewport.rect.height;

    ShotTexture(scrollRect, position, pageSize, maxPosition, exHeight);
}

几个变量解释一下:

  • pageSize:一页的高度,直接用 Viewport 的布局高度,单位是布局单位(受 Canvas 缩放影响);
  • maxPosition:内容高度减去一页高度,也就是能滚到的最大位置;
  • exHeight:内容高度对一页高度取余,就是最后不足一屏的零头,没有零头就是 0;
  • ratio:零头占整页的比例,后面把零头从布局单位换算成屏幕像素用。

(二)递归截图 ShotTexture

核心方法,逻辑就四步:藏标题 → 滚到位置 → 截图 → 判断继续还是结束。因为截图是异步回调,所以用递归的方式一页一页往下走。

csharp 复制代码
private void ShotTexture(ScrollRect scrollRect, int position, int pageSize, int maxPosition, int _exHeight)
{
    // 从第二张开始把标题藏掉,不然每一页都会重复带上标题
    if (texture2Ds.Count > 0)
    {
        GetChild("Bottom/Title").SetActive(false);
    }

    // 移动到指定位置,然后截一张全屏图
    scrollRect.Move(position);
    ScreenShot.Instance.StartShotTexture2(new Rect(0, 0, Screen.width, Screen.height), texture =>
    {
        texture2Ds.Add(texture);

        // 每一页先存一份到本地,调试的时候可以肉眼检查每页截得对不对,发布前可以去掉
        byte[] bytes = texture.EncodeToPNG();
        File.WriteAllBytes($"{Application.dataPath}/{texture2Ds.Count}SavedScreen.png", bytes);
        Debug.Log($"xiao=====:{Application.dataPath + "/SavedScreen.png"}");

        if (position + pageSize > maxPosition)
        {
            // 再搬一页就到底了
            if (_exHeight > 0)
            {
                // 还有零头没截:把零头补上,让列表滚到底,再截最后一张
                position += _exHeight;
                ShotTexture(scrollRect, position, pageSize, maxPosition, 0);
            }
            else
            {
                // 全部截完,列表移回顶部,进入拼接
                scrollRect.Move(0);
                Debug.Log("截图结束");
                Save();
            }
        }
        else
        {
            // 还没截完,继续搬一页
            position += pageSize;
            ShotTexture(scrollRect, position, pageSize, maxPosition, _exHeight);
        }
    });
}

这里有个时序上的细节:搬动位置之后不能马上截图,要等截图回调里再判断下一页。因为 Move 只是改了 anchoredPosition,UI 要等到下一帧渲染才会更新,截图方法里等了 WaitForEndOfFrame,等协程醒过来的时候当前帧已经渲染完了,回调里拿到的就是这一页的画面,这个时序是对的。

最后一张的处理是这样的:假设内容 2500 高一页 1000,前两张截完 position 是 1000,1000 + 1000 > 1500(maxPosition),这时候零头 500 还有,于是 position += 500 变成 1500,正好等于 maxPosition------也就是列表滚到底,内容底部和 Viewport 底部对齐。这个时候截出来的最后一张,底部那一小段(500 对应的屏幕像素)就是剩下的内容,上面多出来的部分是重复的,裁掉不要就行。

七、拼接成长图并保存

(一)先算总高度

拼接之前先知道长图要开多大,把每一张要裁的高度加起来就行:

csharp 复制代码
private void Save()
{
    // 先把 UI 恢复原样
    GetChild("Bottom/Title").SetActive(true);
    GetChild("Top/BackButton").gameObject.SetActive(true);
    GetChild("Bottom/SaveButton").gameObject.SetActive(true);
    GetChild("Bottom/ShareButton").gameObject.SetActive(true);

    int textureeight = 0;
    if (texture2Ds.Count >= 2)
    {
        for (int i = 0; i < texture2Ds.Count; i++)
        {
            // 第一张:从可视区域下边界一直裁到屏幕顶部(标题 + 一整页)
            if (i == 0)
            {
                textureeight += (int)(texture2Ds[i].height - viewBoundary.Down);
            }
            // 最后一张:有零头只算零头,否则算一整页
            else if (i == texture2Ds.Count - 1)
            {
                if (exHeight > 0)
                {
                    textureeight += (int)(ratio * pagPeixSize);
                }
                else
                {
                    textureeight += pagPeixSize;
                }
            }
            // 中间每张:一整页
            else
            {
                textureeight += pagPeixSize;
            }
        }
    }
    else
    {
        // 只有一张图,内容不满一屏,就是一次普通截图
        textureeight = Screen.height;
    }

    // ... 接着拼

(变量名 textureeight 是 texture height 拼习惯了,将就看,不影响逻辑)

(二)按顺序拼接

总高度算出来之后开一张同样宽度的空纹理,然后从最后一张图开始,一路往上贴:

csharp 复制代码
    Texture2D texture2D = new Texture2D(Screen.width, textureeight);
    int positionY = 0;

    // 倒序遍历:列表底部的内容贴在长图最底部,一路往上,第一张自然在最上面
    for (int i = texture2Ds.Count - 1; i >= 0; i--)
    {
        Texture2D texture = texture2Ds[i];
        int y = 0;
        int height = 0;

        if (texture2Ds.Count == 1)
        {
            // 只有一张,整张就是有效内容
            height = Screen.height;
        }
        else
        {
            // 每张图都从可视区域的下边界开始裁
            y = (int)viewBoundary.Down;

            // 最后一张:有零头就只裁零头的高度,正好是内容底部那一小段
            if (i == texture2Ds.Count - 1)
            {
                if (exHeight > 0)
                {
                    height = (int)(ratio * pagPeixSize);
                }
                else
                {
                    height = pagPeixSize;
                }
            }
            // 第一张:从下边界裁到屏幕顶部,把标题一起带进来
            else if (i == 0)
            {
                height = (int)(Screen.height - viewBoundary.Down);
            }
            // 中间每张:裁完整的一页
            else
            {
                height = pagPeixSize;
            }
        }

        // 从源图裁一块,贴到长图的 positionY 处,然后 positionY 往上挪
        Color[] colors = texture.GetPixels(0, y, Screen.width, height);
        texture2D.SetPixels(0, positionY, texture.width, height, colors);
        positionY += height;
    }
    texture2D.Apply();

    // 编码成 PNG 存到本地
    string path = ScreenShot.GetPhotoPath(".png");
    File.WriteAllBytes(path, texture2D.EncodeToPNG());

    // ... 接着保存或分享

GetPixels 和 SetPixels 的坐标原点都在左下角,x 向右、y 向上。源图从 y = Down 开始裁,裁出来的块整块贴到长图的 positionY 处,贴完 positionY 加上这一块的高度,下一块就贴在它上面。倒序遍历 + 从 0 开始贴,两个操作配合,顺序就对了。

(三)保存或分享

csharp 复制代码
    // 等一拍再保存/分享,避免和截图抢时间导致卡顿
    go.transform.DelayTime(() =>
    {
        if (isShare)
        {
            // 分享
            bool isSuccess = APIManager.ShareImageMessage(path);
            if (!isSuccess)
            {
                TipsUI_01.ShowMsg("分享失败");
            }
        }
        else
        {
            // 保存到相册
            bool isSuccess = AlbumManager.Instance.SaveImageToGallery(path, "APlusSports", "APlusSportsScreenshot.png");
            if (isSuccess)
            {
                TipsUI_01.ShowMsg("保存成功");
            }
            else
            {
                TipsUI_01.ShowMsg("保存失败");
            }
        }
#if UNITY_EDITOR
        // Editor 下直接打开看效果
        Application.OpenURL(path);
#endif
    });
}

保存和分享走的是同一个入口,用 isShare 区分。相册名和文件名按自己项目的规范改。

八、注意事项

  1. 隐藏 UI 不要引起布局重排 。如果界面用了 Layout Group 或者 Content Size Fitter 这类自动排版,SetActive(false) 会让后面的元素自动补位,Viewport 的边界就变了,拼出来会错位。这种情况下可以考虑把要藏的元素改成透明(Image / Text 的 color.a = 0),而不是禁用物体。
  2. Scrollbar 记得一起藏。如果 ScrollRect 带了滚动条,它会贴在列表右边、每一屏都在,长图右边会多出一条。截图期间一并隐藏掉。
  3. 截图期间别让列表内容变化。数据刷新、Item 复用、动画没停,都会让页面之间对不上。截之前停掉刷新,或者先把数据定格。
  4. 防重复点击。整个截图过程要跑好几帧,期间按钮虽然藏了,还是建议加个标志位,防止截图流程没走完又重新进一遍。
  5. 内存。代码里是每屏截一张全屏 Texture2D 全部攥在手里,截到最后才统一裁。列表特别长(几十屏)的时候,可以考虑截一张裁一张,裁完就把全屏图销毁,能省下不少内存。
  6. Editor 和真机有差别 。Screen.width / height 在 Editor 里是 Game 窗口的分辨率,真机上是实际分辨率,只要 UI 适配没问题,这套计算就是成立的,但是调试的时候记得以真机为准。

九、结语

所以长截图的实现其实就是"滚一段、截一段、拼一段"的笨办法,没什么黑魔法。但是里面的细节------等帧结束、边界计算、零头处理、拼接顺序------每一个处理不好,拼出来就是花的、歪的或者重复的。建议大家照着代码自己敲一遍,比看十遍都管用,记得把注意事项里的坑过一遍。

如果在实践的过程中遇到什么问题,欢迎在评论区留言,我看到的话会及时回复。这期的内容就到这里,下期见,拜拜!


十、整体代码

球的麻袋,为了方便查看,我将所有的功能逻辑的代码都放到了这里,方便全面理解代码,不用跳来跳去的查看:

csharp 复制代码
private void StartShotTexture()
{
    texture2Ds.Clear();
    GetChild("Top/BackButton").SetActive(false);
    GetChild("Bottom/SaveButton").SetActive(false);
    GetChild("Bottom/ShareButton").SetActive(false);
    // 获取Canvas组件
    Canvas canvas = GameObject.Find("CanvasUI").GetComponent<Canvas>();
    ScrollRect scrollRect = GetChild("Bottom/ListView").GetComponent<ScrollRect>();
    RectTransform viewport = (RectTransform)GetChild("Bottom/ListView/Viewport").transform;
    //重置列表的位置
    scrollRect.content.anchoredPosition = new Vector2(0, 0);
    int position = 0;
    int pageSize = (int)viewport.rect.height;
    int maxPosition = (int)(scrollRect.content.rect.height - pageSize);
    exHeight = 0;//剩余的
    if (scrollRect.content.rect.height > pageSize)
    {
        exHeight = (int)(scrollRect.content.rect.height % pageSize);
    }
    viewBoundary = scrollRect.GetBoundary(viewport, canvas);//可见区域
    pagPeixSize = (int)(viewBoundary.Up - viewBoundary.Down);
    ratio = exHeight / viewport.rect.height;
    ShotTexture(scrollRect, position, pageSize, maxPosition, exHeight);
}

private void ShotTexture(ScrollRect scrollRect, int position, int pageSize, int maxPosition, int _exHeight)
{
    if (texture2Ds.Count > 0)
    {
        GetChild("Bottom/Title").SetActive(false);
    }
    scrollRect.Move(position);
    ScreenShot.Instance.StartShotTexture2(new Rect(0, 0, Screen.width, Screen.height), texture =>
    {
        texture2Ds.Add(texture);
        byte[] bytes = texture.EncodeToPNG();
        File.WriteAllBytes($"{Application.dataPath}/{texture2Ds.Count}SavedScreen.png", bytes);
        Debug.Log($"xiao=====:{Application.dataPath + "/SavedScreen.png"}");
        if (position + pageSize > maxPosition)
        {
            //有剩余部分
            if (_exHeight > 0)
            {
                position += _exHeight;
                //继续截图
                ShotTexture(scrollRect, position, pageSize, maxPosition, 0);
            }
            else
            {
                scrollRect.Move(0);
                //截图结束
                Debug.Log("截图结束");
                Save();
            }
        }
        else
        {
            position += pageSize;
            //继续截图
            ShotTexture(scrollRect, position, pageSize, maxPosition, _exHeight);
        }
    });
}

        private void Save()
        {
            GetChild("Bottom/Title").SetActive(true);
            GetChild("Top/BackButton").gameObject.SetActive(true);
            GetChild("Bottom/SaveButton").gameObject.SetActive(true);
            GetChild("Bottom/ShareButton").gameObject.SetActive(true);
            int textureeight = 0;

            if (texture2Ds.Count >= 2)
            {
                for (int i = 0; i < texture2Ds.Count; i++)
                {
                    //第一张
                    if (i == 0)
                    {
                        textureeight += (int)(texture2Ds[i].height - viewBoundary.Down);
                    }
                    //如果是最后一张
                    else if (i == texture2Ds.Count - 1)
                    {
                        if (exHeight > 0)
                        {
                            textureeight += (int)(ratio * pagPeixSize);
                        }
                        else
                        {
                            textureeight += pagPeixSize;
                        }
                    }
                    else
                    {
                        textureeight += pagPeixSize;
                    }
                }
            }
            else
            {
                textureeight = Screen.height;
            }

            Texture2D texture2D = new Texture2D(Screen.width, textureeight);
            int positionY = 0;

            for (int i = texture2Ds.Count - 1; i >= 0; i--)
            {
                Texture2D texture = texture2Ds[i];
                int y = 0;
                int height = 0;
                if (texture2Ds.Count == 1)
                {
                    height = Screen.height;
                }
                else
                {
                    y = (int)viewBoundary.Down;
                    //如果是最后一张
                    if (i == texture2Ds.Count - 1)
                    {
                        if (exHeight > 0)
                        {
                            height = (int)(ratio * pagPeixSize);
                        }
                        else
                        {
                            height = pagPeixSize;
                        }
                    }
                    //如果是第一张
                    else if (i == 0)
                    {
                        height = (int)(Screen.height - viewBoundary.Down);
                    }
                    else
                    {
                        height = pagPeixSize;
                    }
                }

                Color[] colors = texture.GetPixels(0, y, Screen.width, height);
                texture2D.SetPixels(0, positionY, texture.width, height, colors);
                positionY += height;
            }
            texture2D.Apply();
            string path = ScreenShot.GetPhotoPath(".png");
            File.WriteAllBytes(path, texture2D.EncodeToPNG());
            go.transform.DelayTime(() =>
            {
                //Application.OpenURL(path);
                if (isShare)
                {
                    bool isSuccess = APIManager.ShareImageMessage(path);
                    if (!isSuccess)
                    {
                        TipsUI_01.ShowMsg("分享失败");
                    }
                }
                else
                {
                    bool isSuccess = AlbumManager.Instance.SaveImageToGallery(path, "APlusSports", "APlusSportsScreenshot.png");
                    if (isSuccess)
                    {
                        TipsUI_01.ShowMsg("保存成功");
                    }
                    else
                    {
                        TipsUI_01.ShowMsg("保存失败");
                    }
                }
#if UNITY_EDITOR
                Application.OpenURL(path);
#endif
            });
        }

public static RectBoundary GetBoundary(this ScrollRect self,RectTransform rect, Canvas canvas)
{
    var posArray = GetUIScreenPosition(self, rect, canvas);

    //0  "(0.00, -0.83, 50.00)"
    //1  "(0.00, 400.63, 50.00)"
    //2  "(800.04, 400.63, 50.00)"
    //3  "(800.04, -0.83, 50.00)"
    float contentDown = posArray[0].y;
    float contentUp = posArray[1].y;
    float contentLeft = posArray[0].x;
    float contentRight = posArray[2].x;
    RectBoundary boundary = new RectBoundary
    {
        Down = contentDown,
        Up = contentUp,
        Left = contentLeft,
        Right = contentRight,
    };
    return boundary;
}

 /// <summary>
 /// 获取UI的四个顶点屏幕坐标
 /// </summary>
 /// <param name="rectTransform"></param>
 /// <returns></returns>
 public static Vector3[] GetUIScreenPosition(this ScrollRect self,RectTransform rectTransform, Canvas canvas)
 {
     Vector3[] corners = new Vector3[4];
     rectTransform.GetWorldCorners(corners);
     Vector3[] screenPosArray = new Vector3[4];
     for (int i = 0; i < 4; i++)
     {
         screenPosArray[i] = canvas.worldCamera.WorldToScreenPoint(corners[i]);
         //Debug.Log($"{rectTransform.name} 的屏幕坐标 {i} :{screenPosArray[i]}");
     }
     return screenPosArray;
 }


public void StartShotTexture2(Rect shotRect, Action<Texture2D> callback)
{
    Debug.Log("StartShotTexture 截一张图片: ");
    StartCoroutine(CaptureScreenshotTexture(shotRect, callback));
}


IEnumerator CaptureScreenshotTexture(Rect rect, Action<Texture2D> callback)
{
    Debug.Log("截图片:");
    yield return new WaitForEndOfFrame();

    RenderTexture rt = new RenderTexture((int)Screen.width, (int)Screen.height, 32);
    CameraScreenShot.targetTexture = rt;
    CameraScreenShot.Render();
    RenderTexture.active = rt;

    // 先创建一个的空纹理,大小可根据实现需要来设置
    Texture2D screenShot = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGB24, false);
    // 读取屏幕像素信息并存储为纹理数据
    screenShot.ReadPixels(rect, 0, 0);
    screenShot.Apply();
    texture = screenShot;
    CameraScreenShot.targetTexture = null;
    RenderTexture.active = null;
    Destroy(rt);
    Debug.Log("截图片 end:");
    callback.Invoke(screenShot);
}
相关推荐
小小数媒成员2 小时前
Rect Transform
游戏·unity·游戏引擎
_oP_i4 小时前
Windows 11 家庭中文版 25H2
c#
unicrom_深圳市由你创科技4 小时前
电池化成曲线MES追溯:关键点提取与验证
c#
老王IT4 小时前
C#语言基础语法介绍
开发语言·c#
人丰5 小时前
一次生产卡顿复盘:BI 数据抽取引发 PostgreSQL 长事务事故
数据库·c#
淡海水7 小时前
07-01-源码-.NET Core GC总览与架构
unity·架构·c#·.netcore·gc
weixin_416660079 小时前
Mermaid流程图导出到Word:SVG、PNG与清晰度如何选择?
开发语言·c#
true...10 小时前
虚幻里面设置物体显示、隐藏的两种方式
游戏引擎·虚幻
公子小六12 小时前
基于.NET的Windows窗体编程之WinForms自定义控件(3)
windows·c#·.net·音视频·winforms