Unity UGUI屏幕适配详解:以悬浮窗为例
Unity UGUI屏幕适配详解:以悬浮窗为例
在Unity开发中,UGUI(Unity GUI)提供了强大的UI解决方案,但有时仍需手动处理屏幕适配问题。本文通过一个悬浮窗案例,详细介绍了如何在不同屏幕分辨率下实现UI元素的像素适配,包括技术原理和具体代码实现。
Unity的UGUI确实帮助开发者做了很多适配工作,但有时仍会遇到特殊需求,需要手动处理适配问题。例如,当需要实现一个类似iPhone的AssistiveTouch悬浮窗时,这个悬浮窗不能出屏,出屏后需要弹回到屏幕范围内,这时就需要考虑适配问题。
技术分析
处理这类问题时,需要考虑以下两点:
- UI资源的比例是什么?
- 要适配的屏幕比例又是什么?
假设UI资源是按照960x640(3:2)绘制的,而目标屏幕是1920x1080(16:9)。不能直接使用1920x1080的尺寸来限定运动范围,因为两种屏幕的比例不同。在960x640屏幕上(50,50)像素的点,在1920x1080屏幕上的位置是不同的。
但是,我们可以根据屏幕尺寸的差异来转换。具体思路是:如果用960 / 1920 = ? 宽的比乘以一个1920元素的尺寸或位置,那不就是等于960的尺寸和位置了。
但是,计算比例的方式必须与UGUI的适配方式保持一致,才能保证结果的准确性。如果UGUI是以高为适配的,你算得是宽的比,结果肯定是不对的。
这些重要的信息都来自于Canvas Scaler组件,它负责了UGUI中对各种尺寸屏幕的像素适配(这里我用的是Scale with Screen Size模式,自建单独的UI摄像机)。
代码实现
下面是一个悬浮球的完整实现代码:
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityEngine.Events;
public class UIBtnCloud : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerExitHandler, IBeginDragHandler, IDragHandler, IEndDragHandler
{
/// <summary>
/// 按下后超过这个时间则认定为"长按"
/// </summary>
public float interval = 0.1f;
/// <summary>
/// 是否只调用一次
/// </summary>
public bool invokeOnce = false;
// 按下标志
private bool isPointerDown = false;
// 记录时间
private float recordTime;
//是否已经调用过
private bool hadInvoke = false;
// 点击事件
public UnityEvent onClick = new UnityEvent();
// 按住事件
public UnityEvent onPress = new UnityEvent();
private RectTransform m_rectTransform;
private float m_scale;
private float m_minX, m_minY, m_maxX, m_maxY;
void Start()
{
m_rectTransform = gameObject.GetComponent<RectTransform>();
// 计算分辨率的比例
CanvasScaler cs = XXX.GetInstance().CanvasScaler;
m_scale = cs.matchWidthOrHeight == 1 ?
(float)Screen.height / cs.referenceResolution.y :
(float)Screen.width / cs.referenceResolution.x;
// 范围
m_minX = m_rectTransform.sizeDelta.x / 2 * m_scale;
m_minY = m_rectTransform.sizeDelta.y / 2 * m_scale;
m_maxX = Screen.width - m_minX;
m_maxY = Screen.height - m_minY;
}
void Update()
{
// 一次机会已用完
if (invokeOnce && hadInvoke) return;
// 按下
if (isPointerDown)
{
// 算按住
if ((Time.time - recordTime) > interval)
{
hadInvoke = true;
onPress.Invoke();
}
}
}
#region 处理点击
// 按下
void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
{
isPointerDown = true;
recordTime = Time.time;
}
// 抬起
void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
{
isPointerDown = false;
hadInvoke = false;
// 算点击
if ((Time.time - recordTime) < interval)
{
onClick.Invoke();
}
}
// 离开
void IPointerExitHandler.OnPointerExit(PointerEventData eventData)
{
isPointerDown = false;
hadInvoke = false;
}
#endregion 处理点击 -------------------------------------
#region 处理拖拽
private void SetRectTransformPos(Vector2 position, Camera camera)
{
Vector3 globalMousePos;
if (RectTransformUtility.ScreenPointToWorldPointInRectangle(m_rectTransform, position, camera, out globalMousePos))
{
m_rectTransform.position = globalMousePos;
}
}
// 开始拖拽
void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
{
}
// 拖拽中
void IDragHandler.OnDrag(PointerEventData eventData)
{
this.SetRectTransformPos(eventData.position, eventData.pressEventCamera);
}
// 结束拖拽
void IEndDragHandler.OnEndDrag(PointerEventData eventData)
{
// 位置
float x = m_rectTransform.position.x;
float y = m_rectTransform.position.y;
// 超出范围处理
if (x < m_minX)
{
x = m_minX;
}
else if (x > m_maxX)
{
x = m_maxX;
}
if (y < m_minY)
{
y = m_minY;
}
else if (y > m_maxY)
{
y = m_maxY;
}
Vector2 currentPosition = new Vector2(x, y);
// 设置坐标
this.SetRectTransformPos(currentPosition, eventData.pressEventCamera);
}
#endregion 处理拖拽 -------------------------------------
}
将以上脚本挂在普通按钮上即可使用。需要注意的是,代码中的CanvasScaler cs = XXX.GetInstance().CanvasScaler;
需要根据项目实际情况进行修改。
此外,这个实现没有添加差值效果,只是简单地将出屏的球拉回。如果需要更平滑的效果,可以自行添加差值动画。
总结
UGUI确实比NGUI好用,但同时也增加了使用的门槛,尤其是对于不了解差值动画概念的新手来说。不过,提高自己才是最重要的,不能总依赖别人。