自定义UGUI界面抽象框架
使用UGUI来制作用户界面是很方便的,但如果每个界面都用原始的方法,独立写一个脚本对每个UI组件进行控制和操作,虽然一样能工作的很好,但对于后续拓展以及调试等工作会造成不小的麻烦,想象一下臃肿复杂的Start方法和Update方法,在需要修改的时候会非常痛苦。
因此一个比较抽象的,条理清晰的框架就显得很必要了,这种框架的设计方法有很多很多种,不同的人也会有不同的风格,但是基本思想都大同小异,无非就是将通用性强的功能抽象出来放入到基类里面,然后一层一层往下继承来具体描述特定的界面。
基于这样的思维,可以设计出简单而实用的抽象框架。
先从比较基础的方式入手,定义一个界面抽象基类如下
public abstract class BaseUIObject : MonoBehaviour {
private void Start() {
init();
}
protected void init() {
try {
loadViews();
setupTriggers();
} catch (Exception e) {
Debug.Log("ERROR in - " + gameObject.name);
Debug.Log(e.Message + "\n"+e.StackTrace);
}
}
protected virtual void loadViews() { // 载入视图组件
Debug.Log("No Implement for Function 'loadViews'! - " + getTagName());
}
protected virtual void setupTriggers() { // 设定界面事件
Debug.Log("No Implement for Function 'setupTriggers'! - " + getTagName());
}
public virtual void updateViews() { // 刷新
Debug.Log("No Implement for Function 'updateViews'! - " + getTagName());
}
public abstract string getTagName();
}
然后只要所有跟界面相关的控制类都继承这个基类便可以遵照一定的方法规律去实现功能,比如loadViews方法用于加载界面组件,setupTriggers方法用于设置各种组件的回调等等。
public class TestUIPanel : BaseUIObject {
private InputField userNameInput;
private InputField passWordInput;
private Button loginBtn;
protected override void loadViews() {
userNameInput = transform.Find("UserNameField").gameObject.GetComponent<InputField>();
passWordInput = transform.Find("PassWordField").gameObject.GetComponent<InputField>();
loginBtn = transform.Find("LoginButton").gameObject.GetComponent<Button>();
}
protected override void setupTriggers() {
loginBtn.onClick.AddListener(onLoginClick);
}
public override void updateViews() {
userNameInput.text = "";
passWordInput.text = "";
}
private void onLoginClick() {
LoginUtil.login(userNameInput.text, passWordInput.text);
}
public override string getTagName() {
return "TestPanel";
}
}
以上就是个很简单的登录界面,在使用了抽象框架之后整个界面功能代码的区分就很清晰了,哪一部分代码是干什么的,通过方法名就能一目了然。
当然这个框架不应止步于此,只要是对界面开发有帮助的特性都可以尝试引入,到最后的结果也肯定是多种多样的,比如接下来这个例子。
下面这个框架基于“事件驱动系统”,在作者的自定义事件驱动系统一文中有详细讲解整套系统如何设计。
public abstract class BaseUIObject : BaseEventObject {
protected string relatedObject; // 关联上级组件tag
// ------ 面板动画效果
protected bool isPlayingAnimate = false; // 是否正在播放动画
protected bool isExpanded = true;
protected float slideAmount = 0f;
protected Vector3 shrinkPosition;
protected Vector3 expandPosition;
// ------ 标志位
protected bool dataUpdateFlag = false;
protected bool postLayoutFlag = false;
protected bool needRearrange = false;
protected void Awake() {
preLoad();
}
protected override void init() {
try {
loadViews();
loadMembers();
setupTriggers();
postLoad();
} catch (Exception e) {
LogUtils.logNotice("ERROR in - " + gameObject.name);
LogUtils.logError(e.Message + "\n"+e.StackTrace);
}
}
protected virtual void preLoad() { // 预载入
LogUtils.logWarning("No Implement for Function 'preLoad'! - " + getTagName());
}
protected virtual void loadMembers() { // 载入成员数据
LogUtils.logWarning("No Implement for Function 'loadMembers'! - " + getTagName());
}
protected virtual void loadViews() { // 载入视图组件
LogUtils.logWarning("No Implement for Function 'loadViews'! - " + getTagName());
}
protected virtual void setupTriggers() { // 设定界面事件
LogUtils.logWarning("No Implement for Function 'setupTriggers'! - " + getTagName());
}
protected virtual void postLoad() { // 后处理
LogUtils.logWarning("No Implement for Function 'postLoad'! - " + getTagName());
}
public virtual void updateViews() { // 手动刷新
LogUtils.logWarning("No Implement for Function 'updateViews'! - " + getTagName());
}
protected T findComponent<T>(string path) {
// GameObject的拓展方法,用于根据路径直接寻找子对象的组件
return gameObject.FindComponent<T>(path);
}
protected GameObject findGameObject(string path) {
// gameObject的拓展方法,用于根据路径寻找子对象
return gameObject.FindObject(path);
}
protected virtual void checkForAnimation() {
if(isPlayingAnimate) {
if(isExpanded) {
slidePanelTo(expandPosition, shrinkPosition);
} else {
slidePanelTo(shrinkPosition, expandPosition);
}
}
}
public void setRelatedObject(string objTag) { // 设置关联上级组件tag
relatedObject = objTag;
}
public void notifyUpdate() {
if(!dataUpdateFlag) {
dataUpdateFlag = true;
}
}
public void slidePanel(bool isExpand) {
if(isExpand) {
// 展开操作
if(!isExpanded) {
if(!isPlayingAnimate) {
if(needRearrange) {
transform.SetAsLastSibling();
}
slideAmount = 0f;
beforeAnimate();
isPlayingAnimate = true;
}
}
} else {
// 收起操作
if(isExpanded) {
if(!isPlayingAnimate) {
slideAmount = 0f;
beforeAnimate();
isPlayingAnimate = true;
}
}
}
}
protected void slidePanelTo(Vector3 start, Vector3 target) {
if (transform.localPosition != target) {
transform.localPosition = Vector3.Lerp(start, target, slideAmount);
slideAmount += Time.deltaTime * 1.5f;
} else {
if (isPlayingAnimate) {
isExpanded = !isExpanded;
isPlayingAnimate = false;
afterAnimate();
}
}
}
protected virtual void afterAnimate() { }
protected virtual void beforeAnimate() { }
protected void onSlideClick(BaseEventData e) {
if(!isPlayingAnimate) {
slideAmount = 0f;
beforeAnimate();
isPlayingAnimate = true;
}
}
public bool isShowing() {
return isExpanded;
}
public bool isAnimating() {
return isPlayingAnimate;
}
}
上面的代码里封装了一个面板平移的动画实现,只要在子类的execute方法实现中调用checkForAnimation即可。这种类似的功能封装到基类对那些需要在子类中频繁使用的方法非常有用,能极大地降低冗余代码量(当然若使用类似DoTween之类的动画功能库实现起来更简单)。
要使用这个抽象框架,只需要简单地为某一个UI模块或者面板新建脚本,继承BaseUIObject抽象类,实现其中的抽象方法并且按需重写一部分加载方法即可。
public class TestUIPanel : BaseUIObject {
protected override void loadViews() {
// 加载界面元素
}
protected override void setupTriggers() {
// 设置触发器和回调等
}
protected override void handleEvent(MBaseEvent e) {
// 事件处理方法
}
public override string getTagName() {
// 返回该UI对象的TagName,用于事件分发
}
protected override void execute() {
// 每帧执行的方法主体
}
}
有这样的抽象框架,编写界面控制器的时候就可以统一标准,无论是编写还是后续测试,修改以及维护都会十分方便。