
🐾 个人主页 🐾
阿松爱睡觉,横竖醒不来 🏅你可以不屠龙,但不能不磨剑🗡
文章目录
- 一、前言
- 二、实现思路
- 三、UI结构与工具方法
- [四、截图工具 ScreenShot](#四、截图工具 ScreenShot)
- 五、计算可视区域边界
- 六、分段滚动截图
-
- [(一)入口 StartShotTexture](#(一)入口 StartShotTexture)
- [(二)递归截图 ShotTexture](#(二)递归截图 ShotTexture)
- 七、拼接成长图并保存
- 八、注意事项
- 九、结语
- 十、整体代码
一、前言
最近项目里做了个保存长页面的功能:界面上一个 Scroll View,里面内容老长(数据报表、战绩详情之类的),一屏压根看不全。现在想做一下就是点一下保存或者分享,能把整个列表从头到尾拼成一张长图。
上网搜了一下,Unity 并没有现成的长截图 API 可以用:手机系统的长截图只能截系统自己的界面,够不着 App 里 UGUI 画出来的内容;Application.CaptureScreenshot 倒是能截图,但是只能截当前这一屏,而且这个方法已经废弃了。所以没辙,只能自己动手撸一个。
思路说白了就是个"笨办法":把列表一屏一屏往下滚,每滚一屏截一张图,全截完之后再把每张图里有效的部分裁出来,按顺序拼成一张大图。原理一句话就能说完,但是细节不少------等帧结束、边界计算、最后一屏的零头、拼接顺序------每一步处理不好,拼出来就是花的或者歪的。
这篇文章就把完整代码和每一步的道理捋一遍,初学者可以跟着抄,进阶的看思路就行。
那么话不多说,直接开始今天的内容吧!
二、实现思路
(一)核心原理
分三大步:分段滚动截图 → 裁剪每张图的有效区域 → 按顺序拼接。
从列表顶部开始,每次把 content 往下搬一屏的高度,搬完之后等帧结束截一张全屏图存起来,直到把列表滚到底。最后拿这三张图,把每张图里属于列表内容的区域裁出来,从最后一张开始往图片底部贴,一张长图就拼好了。
为什么要从最后一张开始贴?因为 Texture2D 的像素原点在左下角 ,SetPixels(0, 0, ...) 贴的是图片最底下那一块。列表最底部的内容在最后一张图里,所以最后一张要贴在长图的最底部,一路往上贴,第一张(列表顶部)自然就在最上面。顺序反了,长图就是倒的。
(二)几个关键点
- 截图必须等帧结束 。UI 是把当前帧渲染完才显示到屏幕上的,不等帧结束就截,截到的还是上一帧的旧画面,几屏拼起来能看出错位和花屏。所以核心截图方法要用协程 +
WaitForEndOfFrame。 - 用专门的相机渲染到 RenderTexture 。
ReadPixels读的是相机渲染出来的画面,所以场景里要放一个专门干活的截图相机,截图的时候把它的targetTexture设成新建的 RenderTexture,渲染完再把像素读成 Texture2D。 - 截图前先把碍事的 UI 藏起来 。返回按钮、保存按钮、分享按钮,这些不属于长图的内容,截图期间先
SetActive(false),截完再恢复。 - 最后一屏可能不足一页 。内容高度不一定刚好是屏高的整数倍,剩下的"零头"(代码里的
exHeight)要单独处理,不然要么少截一截,要么重复截一段。 - 屏幕坐标和布局坐标要换算 。Viewport 在屏幕上实际占的像素高度,受 Canvas 缩放影响,和它在布局里的高度不是一回事,裁图的时候要用一个比例(代码里的
ratio)换算。 - 截图期间界面不能动。截图回调是异步的,好几屏要好几帧才能截完,这期间如果列表数据刷新了、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 区分。相册名和文件名按自己项目的规范改。
八、注意事项
- 隐藏 UI 不要引起布局重排 。如果界面用了 Layout Group 或者 Content Size Fitter 这类自动排版,
SetActive(false)会让后面的元素自动补位,Viewport 的边界就变了,拼出来会错位。这种情况下可以考虑把要藏的元素改成透明(Image / Text 的color.a = 0),而不是禁用物体。 - Scrollbar 记得一起藏。如果 ScrollRect 带了滚动条,它会贴在列表右边、每一屏都在,长图右边会多出一条。截图期间一并隐藏掉。
- 截图期间别让列表内容变化。数据刷新、Item 复用、动画没停,都会让页面之间对不上。截之前停掉刷新,或者先把数据定格。
- 防重复点击。整个截图过程要跑好几帧,期间按钮虽然藏了,还是建议加个标志位,防止截图流程没走完又重新进一遍。
- 内存。代码里是每屏截一张全屏 Texture2D 全部攥在手里,截到最后才统一裁。列表特别长(几十屏)的时候,可以考虑截一张裁一张,裁完就把全屏图销毁,能省下不少内存。
- 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);
}
