通过GUI封装了解高级UI插件原理
结果演示
这里我将成品打包,下面是下载地址:
https://download.csdn.net/download/BraveRunTo/15347380
目的
通过对Unity自带GUI的封装训练来更好的理解NGUI,UGUI的原理。
需求分析
我们需要提取所有控件的共性部分来实现封装。所有的控件都会含有一个位置信息Rect,一个内容信息GUIContent和样式信息GUIStyle(不填样式信息默认使用Unity的预设样式),所以我们对GUI的封装就是要封装这些共性部分,并通过一个父类来代表所有的控件。
由于位置信息Rect需要做UI自适应逻辑,而内容信息和样式信息属于可以直接面板赋值的字段,不需要处理额外逻辑,所以我们需要将Rect单独提成一个类来处理逻辑。这样体现了类的单一原则思想。
RectCalculator类
我们为Rect逻辑类起名为RectCalculator,意为Rect计算器,负责计算Rect方位,实现UI位置自适应。RectCalculator作为数据处理类不需要继承Monobehaviour。
在进行逻辑处理之前我们需要了解九宫格的概念。
九宫格:
九宫格就是将一块屏幕分成9个部分:中,左,右,上,下,左上,左下,右上,右下。每一个部分都一个相对UI屏幕原点的原点,此原点是通过屏幕宽和高计算出来的。如下图所示:
其中彩色方块中的坐标为每个相对原点的坐标信息,简称相对屏幕位置。NGUI,UGUI的UI自适应都用到了九宫格的概念。
除了屏幕有九宫格的概念,控件的中心点也有九宫格的概念,如下图:
这里由于当屏幕原点确定后,改变控件原点位置,视觉上控件会向相反的方向移动,所以这里的坐标信息都是负数。这里我们简称为控件原点偏移坐标
由屏幕九宫格和控件九宫格就组成了我们NGUI,UGUI中的锚点anchor和中心点Priot。这里我们直接得出控件坐标的计算公式:
控件坐标计算公式 = 相对屏幕坐标 + 控件原点偏移坐标 + 偏移位置。
代码:
所以我们的RectCalculator首先需要一个枚举来列出九宫格布局如下:
public enum AnchorPositionType
{
Center,
Up,
Down,
Left,
Right,
LeftUp,
RightUp,
LeftDown,
RightDown,
}
利用九宫格的计算公式我们可以书写以下代码:
//使用此特性来使关联此类的对象可以在inspector面板中看到此类的信息
[System.Serializable]
public class RectCaculator
{
//当前控件的Rect信息
private Rect _widgetRealRect = Rect.zero;
//控件锚点,控件原点相对位置
public AnchorPositionType widgetAnchor = AnchorPositionType.Center;
//父类锚点,这里是屏幕相对位置
public AnchorPositionType parentAnchor = AnchorPositionType.Center;
//控件偏移量
public Vector2 offset;
//计算完后的控件原点相对位置
private Vector2 widgetPosition;
//控件宽度
public float widgetWidth;
//控件高度
public float widgetHeight;
//屏幕宽度
private float screenWidth;
//屏幕高度
private float screenHeight;
//对外公开的Rect信息只读属性,外部直接调用此属性就可以得到当前控件的Rect信息
public Rect WidgetRealRect
{
get
{
//对控件的宽和高进行赋值
_widgetRealRect.width = widgetWidth;
_widgetRealRect.height = widgetHeight;
//计算控件原点相对位置
CalculateWidgetPos();
//计算控件坐标
CalculateParentPosition();
//返回计算结果
return _widgetRealRect;
}
}
/// <summary>
/// 计算控件原点相对位置
/// </summary>
private void CalculateWidgetPos()
{
switch (widgetAnchor)
{
case AnchorPositionType.Center:
widgetPosition.x = -widgetWidth / 2;
widgetPosition.y = -widgetHeight / 2;
break;
case AnchorPositionType.Down:
widgetPosition.x = -widgetWidth / 2;
widgetPosition.y = -widgetHeight;
break;
case AnchorPositionType.Up:
widgetPosition.x = -widgetWidth / 2;
widgetPosition.y = 0;
break;
case AnchorPositionType.Left:
widgetPosition.x = 0;
widgetPosition.y = -widgetHeight / 2;
break;
case AnchorPositionType.Right:
widgetPosition.x = -widgetWidth;
widgetPosition.y = -widgetHeight / 2;
break;
case AnchorPositionType.LeftUp:
widgetPosition.x = 0;
widgetPosition.y = 0;
break;
case AnchorPositionType.RightUp:
widgetPosition.x = -widgetWidth;
widgetPosition.y = 0;
break;
case AnchorPositionType.LeftDown:
widgetPosition.x = 0;
widgetPosition.y = -widgetHeight;
break;
case AnchorPositionType.RightDown:
widgetPosition.x = -widgetWidth;
widgetPosition.y = -widgetHeight;
break;
}
}
/// <summary>
/// 控件坐标计算公式 = 相对屏幕坐标 + 控件原点偏移坐标 + 偏移位置,
/// 根据此公式得出结果
/// </summary>
private void CalculateParentPosition()
{
screenWidth = Screen.width;
screenHeight = Screen.height;
switch (parentAnchor)
{
case AnchorPositionType.Center:
_widgetRealRect.x = screenWidth / 2 + widgetPosition.x + offset.x;
_widgetRealRect.y = screenHeight / 2 + widgetPosition.y + offset.y;
break;
case AnchorPositionType.Down:
_widgetRealRect.x = screenWidth / 2 + widgetPosition.x + offset.x;
_widgetRealRect.y = screenHeight + widgetPosition.y + offset.y;
break;
case AnchorPositionType.Up:
_widgetRealRect.x = screenWidth / 2 + widgetPosition.x + offset.x;
_widgetRealRect.y = 0 + widgetPosition.y + offset.y;
break;
case AnchorPositionType.Left:
_widgetRealRect.x = 0 + widgetPosition.x + offset.x;
_widgetRealRect.y = screenHeight / 2 + widgetPosition.y + offset.y;
break;
case AnchorPositionType.Right:
_widgetRealRect.x = screenWidth + widgetPosition.x + offset.x;
_widgetRealRect.y = screenHeight / 2 + widgetPosition.y + offset.y;
break;
case AnchorPositionType.LeftUp:
_widgetRealRect.x = 0 + widgetPosition.x + offset.x;
_widgetRealRect.y = 0 + widgetPosition.y + offset.y;
break;
case AnchorPositionType.RightUp:
_widgetRealRect.x = screenWidth + widgetPosition.x + offset.x;
_widgetRealRect.y = 0 + widgetPosition.y + offset.y;
break;
case AnchorPositionType.LeftDown:
_widgetRealRect.x = 0 + widgetPosition.x + offset.x;
_widgetRealRect.y = screenHeight + widgetPosition.y + offset.y;
break;
case AnchorPositionType.RightDown:
_widgetRealRect.x = screenWidth + widgetPosition.x + offset.x;
_widgetRealRect.y = screenHeight + widgetPosition.y + offset.y;
break;
}
}
}
ContentClass类
完成了Rect计算类,接下来我们就可以书写控件基类ContentClass了,当然这个名字起的不太好。
作为所有控件的基类,此类需要有Rect信息,内容信息,样式信息,和控件的绘制方法。
代码:
public abstract class ContentClass : MonoBehaviour
{
public RectCaculator rectCalculator;
public GUIContent content;
public GUIStyle style;
public bool useCustomStyle = false;
/// <summary>
/// UI创建类,Draw其实就是CreateUI成员的过程
/// </summary>
public void DrawGui()
{
if (useCustomStyle)
{
UseStyle();
}
else
{
DoNotUseStyle();
}
}
/// <summary>
/// 具体创建什么UI由子类决定
/// </summary>
protected abstract void UseStyle();
protected abstract void DoNotUseStyle();
}
所有属性都设置为public是为了可以在inspector面板看到,当然也可以使用特性来实现。
到这里我们就实现了GUI的基本封装,但还存在着问题:
- GUI绘制的顺序是不确定的。
- 编辑模式下不能看到GUI。
这里我们模仿NGUI和UGUI创建一个画布类或者叫UI根类来解决这两个问题。
UIRoot类
- 顺序不能确定,使用GetComponentsInChildren获得控件数组,从而有序绘制。
- [ExecuteAlways],此特性保证unity生命周期在编译模式下也可以被调用。
//此特性保证unity生命周期在编译模式下也可以被调用。
[ExecuteAlways]
public class UIRoot : MonoBehaviour
{
//子类控件ContentClass数组。这里利用了多态的思想。
private ContentClass[] _childUi;
private void Awake()
{
SreachChild();
}
//搜索子类中所有的控件
private void SreachChild()
{
_childUi = GetComponentsInChildren<ContentClass>();
}
private void OnGUI()
{
//如果当前为编辑模式,则持续搜索子类控件,来达到对于控件的实时控制
if (!Application.isPlaying)
{
SreachChild();
}
//按照搜索的结果依次调用绘制方法。
for (int i = 0; i < _childUi.Length; i++)
{
if (_childUi[i].gameObject.activeInHierarchy)
{
_childUi[i].DrawGui();
}
}
}
}
这样我们的自定义GUI封装,CGUI就完成了。