在开发中有时候会碰到需要展示tips说明信息的功能,比如打开道具列表,点击相应的道具并展示该道具的属性等详细信息,这时就有可能会出现tips的UI超出屏幕边界的问题。
?
因此这里做一个自适应的道具tips
首先需要设计tips的UI。
因为需要UI随着文本的长度自适应,确保始终能够显示全部的文本信息,因此Text的RectTransform也需要随着文本高度自动增加height,这里就会用到preferredHeight。
同时在Text自适应后,其外部的背景bg也需要根据Text来自动改变。
为了满足以上的两个需求,这里设计Tips的prefab为如下结构:
?
?
从以上结构可知,Tips相对于TipsBg是自适应的,并且保持上下左右完全对称。注意这里设置TipsBg的pivot为“0,1”,默认以鼠标点击位置为起点来显示tips:
??
这样的结构就可以实现一个效果:当改变TipsBg的RectTransform的宽高时,由于Tips的sizeDelta会始终保持如上的数值不变,因此Tips的宽高也会自动的改变,。
所以只需要在运行时改变TipsBg的RectTransform的宽高即可。
那么如何设置TipsBg的宽高呢?
当为Text设置文本内容后,text.preferredHeight指的是针对该内容,组件Text希望RectTransform能够提供的height。如果rect.height == text.preferredHeight,则该文本可以完全显示出来
这样就可以计算出TipsBg期望的高度:
float yHeight = tipsTxt.preferredHeight + Mathf.Abs(tipsTxtRect.sizeDelta.y); //获取tipsTxt的理想height,并计算tipsBg的目标height
tipsBgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, yHeight);
默认情况下不需要改变TipsBg的宽度,所以宽度暂时不需要重新设置
注意:在设置RectTransform的宽高时使用UGUI自带的方法:SetSizeWithCurrentAnchors。该方法会以UI当前的pivot为起点来设置宽高,并且在设置完成后会自动根据当前的anchors分布来调整RectTransform面板中的显示数据。
而且在获取UI对象的position时也是该UI的pivot在世界空间下的坐标,而不是该UI四个anchor交叉点的坐标。
基于pivot这样的特性,在设置TipsBg的position时,就可以在改变pivot的基础上,直接将鼠标点击位置赋值给TipsBg,而不需要另外计算TipsBg超出边界的数值,从而计算TipsBg的目标position。
在设置完TipsBg的宽高后就有可能会出现tips边界超出屏幕的问题,
那么如何检测该tips是否超出了屏幕边界并调整位置呢?
基于pivot的特性,直接把鼠标位置赋值给TipsBg.position,因此就可以很容易计算出UI是否超出了屏幕边界:
//通过设置TipsBg的pivot,而不是position,来避免超出屏幕边界
if (Input.mousePosition.x + tipsBgRect.rect.width <= Screen.width) //默认向右显示
tipsBgPivot.x = 0; //当该UI在X轴上的最大值依然在屏幕以内时
else
tipsBgPivot.x = 1;
if (Input.mousePosition.y - tipsBgRect.rect.height >= 0) //默认向下显示
tipsBgPivot.y = 1;
else
tipsBgPivot.y = 0;
运行时效果如下:红点处为鼠标点击位置
通过改变TipsBg的pivot来调整UI位置,使其不超出屏幕边界
虽然以上方法可以避免一部分超出边界的情况,但如果文本过长,屏幕过小,调整之后依然会超出边界,该如何呢?
首先需要获取UI对象四个角的坐标值:rectTransform.GetWorldCorners。该方法获取到的坐标点是以左下角为起点,顺时针方向依次输出。通过检测各个点的坐标即可知道是否超出了边界。
当依然超出边界时,有两种方式:一是改变TipsBg的宽度值,使得Tips的text.preferredHeight降低,从而避免;如果宽度已无法再调整,此时就只能改变Text的字体大小来改变text.preferredHeight
//虽然设置pivot后可以避免超出边界的情况,但如果文本过长,依然有可能会超出边界
//但这里只考虑垂直方向上依然超出边界的情况,水平方向则不考虑——因为水平方向的宽度可以自主设定
//解决办法:当Y轴方向超出边界时,改变TipsBg的宽度,从而降低tipsTxt.preferredHeight
//获取tipsBgRect的四角的坐标
tipsBgRect.GetWorldCorners(worldCornersPos);
//从屏幕左下角开始顺时针查看各个corner的Y轴坐标值
if (worldCornersPos[0].y < 0 || worldCornersPos[1].y > Screen.height) //说明超出边界
{
//Debug.Log("<color=yellow> " + width + " " + fontSize + " " + allTips[index] + " </color>");
if(width < Screen.width * 0.5f) //当宽度尚且可以继续调节时
{
tipsBgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Mathf.Min(width + 100, Screen.width * 0.5f)); //宽度最大不超过屏幕的一半
//当tipsTxtRect的宽度改变后,其tipsTxt.preferredHeight也会即时改变——这一点很方便,很重要
tipsBgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, tipsTxt.preferredHeight + Mathf.Abs(tipsTxtRect.sizeDelta.y));
//继续下一次检测
AutoAdjustTipsBounds(Mathf.Min(width + 100, Screen.width * 0.5f), fontSize);
}
else //宽度已无法再调节,此时需要改变字体大小
{
tipsTxt.fontSize = fontSize - 1;
tipsBgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Screen.width * 0.5f);
tipsBgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, tipsTxt.preferredHeight + Mathf.Abs(tipsTxtRect.sizeDelta.y));
//继续下一次检测
AutoAdjustTipsBounds(Screen.width * 0.5f, fontSize - 1);
}
}
通过以上递归方法的检测,就可以一直调整到完全显示在屏幕内部的效果
完整代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using System.Text;
public class TipsAdjust : MonoBehaviour
{
Text tipsTxt;
RectTransform tipsTxtRect, tipsBgRect;
string[] allTips; //所有的tips信息
int index = 0; //对应的tip索引
GameObject go; //为方便显示点击的位置,这里用红点代替
float oldWidth = 0; //TipsBg的RectTransform原本的宽度
int oldFontSize = 0; //text组件的字体大小——可能用于自适应调整
Vector2 tipsBgPivot = Vector2.zero;
Vector3[] worldCornersPos = new Vector3[4];
void Start()
{
//获取组件对象
GameObject tipsObj = GameObject.Find("Tips");
tipsTxtRect = tipsObj.GetComponent<RectTransform>();
tipsTxt = tipsObj.GetComponent<Text>();
tipsBgRect = this.GetComponent<RectTransform>();
//初始化文本
string content = File.ReadAllText(Application.dataPath + "/Resources/Poems.txt");
allTips = content.Split(new string[] { "##" }, System.StringSplitOptions.None);
//注意:使用string.Split分割string时如果使用string类型的参数,需要用上述的模式
//为方便显示鼠标点击位置,这里使用红点代替
go = GameObject.Instantiate(Resources.Load("Prefabs/RedPoint")) as GameObject;
go.transform.parent = this.transform.parent; //如果不设置parent,则新实例化的obj会脱离canvas层级而存在,因此不会显示在画面上
go.SetActive(false); //暂时不显示
oldWidth = tipsBgRect.rect.width; //存储原始宽度值
oldFontSize = tipsTxt.fontSize; //字体原始大小
}
void Update()
{
if (Input.GetMouseButtonDown(0))
OnItemBtnClick();
}
void OnItemBtnClick()
{
//为tips赋值
++index;
if (index >= allTips.Length) index = 0;
tipsTxt.text = allTips[index];
//设置Tips的宽高
//由于Tips相对于TipsBg是自适应的,因此这里只需要计算TipsBg的宽高,Tips就会自适应达到效果
tipsTxt.fontSize = oldFontSize; //针对自适应有可能会改变字体大小,所以这里重新设置
tipsBgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, oldWidth); //自适应有可能会改变原有宽度,所以这里重新设置
float yHeight = tipsTxt.preferredHeight + Mathf.Abs(tipsTxtRect.sizeDelta.y); //获取tipsTxt的理想height,并计算tipsBg的目标height
tipsBgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, yHeight);
//注意:
//由于TipsBg和Tips之间UI的设计,因此tips的rect是肯定小于TipsBg的,
//所以可以明确知道tipsTxtRect.sizeDelta.y < 0。这里为了计算方便,直接使用Maths.Abs
//为了让效果更明显,这里直接展示鼠标点击位置
go.transform.position = Input.mousePosition;
if (!go.activeInHierarchy) go.SetActive(true);
AutoAdjustTipsBounds(oldWidth, oldFontSize);
}
//检测是否超出屏幕边界,并自适应调整位置
void AutoAdjustTipsBounds(float width, int fontSize)
{
//通过设置TipsBg的pivot,而不是position,来避免超出屏幕边界
if (Input.mousePosition.x + tipsBgRect.rect.width <= Screen.width) //默认向右显示
tipsBgPivot.x = 0; //当该UI在X轴上的最大值依然在屏幕以内时
else
tipsBgPivot.x = 1;
if (Input.mousePosition.y - tipsBgRect.rect.height >= 0) //默认向下显示
tipsBgPivot.y = 1;
else
tipsBgPivot.y = 0;
//设置TipsBg的pivot以及position,由于TipsBg和Tips之间自适应的关系,Tips也会自动实现效果
tipsBgRect.position = Input.mousePosition;
tipsBgRect.pivot = tipsBgPivot;
//虽然设置pivot后可以避免超出边界的情况,但如果文本过长,依然有可能会超出边界
//但这里只考虑垂直方向上依然超出边界的情况,水平方向则不考虑——因为水平方向的宽度可以自主设定
//解决办法:当Y轴方向超出边界时,改变TipsBg的宽度,从而降低tipsTxt.preferredHeight
//获取tipsBgRect的四角的坐标
tipsBgRect.GetWorldCorners(worldCornersPos);
//从屏幕左下角开始顺时针查看各个corner的Y轴坐标值
if (worldCornersPos[0].y < 0 || worldCornersPos[1].y > Screen.height) //说明超出边界
{
//Debug.Log("<color=yellow> " + width + " " + fontSize + " " + allTips[index] + " </color>");
if(width < Screen.width * 0.5f) //当宽度尚且可以继续调节时
{
tipsBgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Mathf.Min(width + 100, Screen.width * 0.5f)); //宽度最大不超过屏幕的一半
//当tipsTxtRect的宽度改变后,其tipsTxt.preferredHeight也会即时改变——这一点很方便,很重要
tipsBgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, tipsTxt.preferredHeight + Mathf.Abs(tipsTxtRect.sizeDelta.y));
//继续下一次检测
AutoAdjustTipsBounds(Mathf.Min(width + 100, Screen.width * 0.5f), fontSize);
}
else //宽度已无法再调节,此时需要改变字体大小
{
tipsTxt.fontSize = fontSize - 1;
tipsBgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Screen.width * 0.5f);
tipsBgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, tipsTxt.preferredHeight + Mathf.Abs(tipsTxtRect.sizeDelta.y));
//继续下一次检测
AutoAdjustTipsBounds(Screen.width * 0.5f, fontSize - 1);
}
}
}
}
?以上就实现了道具tips不超出边界的需求。运行效果如下:?
? ? ? ? ? ? ? ? ? ? ? ??
完整项目链接如下:AutoAdjustTipsProject.zip-Unity3D文档类资源-CSDN下载
PS:
1.UGUI中rectTransform.sizeDelta 和 rectTransform.rect.size的区别:
“rect.size”代表的是该UI对象的真实宽高,不论瞄点如何设置;
“sizeDelta”表示UI对象的宽高比对应的anchor矩形大或者小多少
验证:
如图,Test01的四个锚点都在中心处,Test02的四个锚点则分散在四个角落:
?此时使用如下代码分别输出两个对象的rect.size 和 sizeDelta:
GameObject test01 = GameObject.Find("Test01");
GameObject test02 = GameObject.Find("Test02");
RectTransform rect01 = test01.GetComponent<RectTransform>();
RectTransform rect02 = test02.GetComponent<RectTransform>();
Debug.Log("<color=green> rect01: " + rect01.sizeDelta + " " + rect01.rect.size + " </color>");
Debug.Log("<color=green> rect02: " + rect02.sizeDelta + " " + rect02.rect.size + " </color>");
?运行结果如下:
?从以上结果可知:
1.当UI对象的四个anchor汇聚在一处时(不论是否汇聚在中心,只要四个锚点不分散即可),此时sizeDelta与rect.size相同
2.当在代码中手动设定UI对象的sizeDelta时:
rect02.sizeDelta = new Vector2(-50, -50);
Debug.Log("<color=green> rect02: @@@@@@@@@@ " + rect02.sizeDelta + " " + rect02.rect.size + " </color>");
运行结果如下:
而此时Test02对象的RectTransform:
Game视图中的结果为:
总结:
1.当需要获取UI对象的真实宽高时使用“rect.size”,而不要使用“sizeDelta”
2.如果要设置UI对象的宽高,则使用rect.SetSizeWithCurrentAnchors,不要直接的设置rect.size,该变量不具备set属性
rect02.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 200); //设置宽度
rect02.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 300); //设置高度
Game视图效果以及Test02的RectTransform变化如下:
??
注意:
1.当使用SetSizeWithCurrentAnchors设置UI对象的宽高时,是以当前UI对象的pivot为中心点来设置的,例如以上Test02的pivot为“0.5,0.5”,由于屏幕空间以左下角为原点,X向右递增,Y向上递增,所以“0.5,0.5”代表的是矩形屏幕的正中心点。在此基础上拉伸UI对象的宽和高
倘若设置Test02的pivot为“0,0.5”,则代表以屏幕左侧边缘的正中为基准点来拉伸UI对象的宽高:
?
pivot 和 anchor是两个不同的属性,这里并没有用到anchor来影响UI对象的宽高显示
2.设置了UI对象的宽高后,由于SetSizeWithCurrentAnchors还需要考虑该UI对象的anchor,在设置size后,UGUI会自动根据UI对象的size和anchor来设置其rectTransform相应的数值
3.UGUI中pivot 和 anchor是完全不同的两个属性,两者的作用也不相同,由上述测试知晓
4.sizeDelta实际代表的意义:
当四个锚点分散开时:
sizeDelta.x = rect.x - anchorRectangle.x;
sizeDelta.y = rect.y - anchorRectangle.y;
所以当sizeDelta.x <?0 时代表rect的宽度小于原有的矩形宽度,而在UGUI的RectTransform视图中,却是站在该UI对象的角度,超出anchor矩形的使用负数,在anchor以内的则用正数表示。
如此就会出现当sizeDelta.x < 0,而RectTransform中数值为正数的情况:
? ?
PS: "Left","Right"的数值不一定是对半分,这里只是因为设置的tipsBg为上下左右对称的设计才如此;但sizeDelta.x代表的一定是UI实际宽度与anchor矩形宽度的总差值
5.RectTransform.rect.width 和 rect.size.x是等效的,都是获取rect的宽度,但RectTransform.rect.x是获取rect的X坐标,这个不能和rect.size.x弄混
2.如何获取UI对象四角的坐标值:
在获取UI对象四角的坐标值时有两种方式:一种是Unity自带的方法rectTransform.GetWorldCorners,另一种则需要根据该UI对象的pivot和rect.size来分别计算——这种比较麻烦,因此不同的pivot计算方式不同
//方式一:
Vector3[] worldCornersPos = new Vector3[4];
tipsImageRect.GetWorldCorners(worldCornersPos);
foreach(var temp in worldCornersPos)
{
Debug.Log("<color=blue> " + temp + " </color>");
}
//方式二:根据不同的pivot用不同的方式计算四角的坐标值
//情况一:当pivot为“0, 0.5”时
GameObject test03 = GameObject.Find("Test03");
RectTransform test03Rect = test03.GetComponent<RectTransform>();
test03Rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 200);
test03Rect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 300);
Vector3[] test03WorldCornersPos = new Vector3[4];
test03Rect.GetWorldCorners(test03WorldCornersPos);
foreach (var temp2 in test03WorldCornersPos)
{
Debug.Log("<color=yellow> " + temp2 + " " + test03.transform.position + " </color>");
}
//这里为了方便比较两种方式的差异,所以先使用方式一输出结果
//从结果可以看出,如果使用方式二主动计算,需要根据pivot的设置变换不同的计算过程,这样其实没有必要。所以这里推荐使用Unity自带的方法来获取corner数值
以上测试中“Test03”的pivot为“0,0.5”,运行结果如下:
??
?
?总结:
1.从以上数值的输出来看,推荐使用UGUI自带的"GetWorldCorners"来获取UI对象四角的坐标值。并且从数值的输出顺序也可以看出,以屏幕空间左下角为起点,顺时针方向输出四角的值
?2.从以上输出结果可以看出,在获取UI对象坐标时得到的是以pivot为中心点显示的坐标(0, 249, 0),而并不是UI对象自身四角交叉线的重合点坐标,由此可见pivot的重要性,在设置UI对象坐标和宽高时都会用到
|