利用队列控制UI界面流程跳转【用户设置界面】

前言

从需求到思路到实现去谈一个实习期间遇到的问题。

需求分析

主要需求是有三个界面满足严格先后关系(先设置年龄、月份、姓名),除了年龄设置界面,其他两个界面都可以返回上一级。
思路整理 :由于操作有明确的先后关系,这里准备使用一个队列来存储。又由于有返回上一次操作的情况,那么外部需要有控制队头元素的权限。

那么可以是优先级队列、或者双端队列。由于项目工程使用的是.net 4.X framework,PirorityQueue是.net 6.0提供的原生方法,而且对于队列中的操作并没有太复杂的流程控制,不太必要用可以自定义ICompare方法的,比如优先队列或者最小堆等等,

所以准备选择双端队列(可以支持队头元素入队);

然后,还需要对执行后的操作做一个缓存,因为有返回的操作。

核心逻辑

1.正常流程:事务从队尾入队,从队头出队一次,压入缓存栈,执行当前事务。

2.返回上一级:从栈中Pop两次,依次从【队头】入队(第一次的操作应当是队列中下一次需要执行的操作,第二次的操作,是返回后应当刷新的界面操作),出队一次(完成页面跳转到上一次操作的页面,且事务队列队头为跳转前的本界面)

代码部分

工具类

首先是双端队列的实现,这里是用链表做的。

csharp 复制代码
  public class DoubleEndQueue<T>
    {
        public int Count => count;
        
        private LinkedNode<T> first;
        private LinkedNode<T> last;
        private int count;

        /// <summary>
        /// 队首入队
        /// </summary>
        /// <param name="value"></param>
        public void EnqueueAtFirst(T value)
        {
            LinkedNode<T> node = new LinkedNode<T>(value);
            if (first == null)
            {
                first = node;
                last = node;
            }
            else
            {
                first.Previous = node;
                node.Next = first;
                first = node;
            }
            count++;
        }
        /// <summary>
        /// 队尾入队
        /// </summary>
        /// <param name="value"></param>
        public void EnqueueAtLast(T value)
        {
            LinkedNode<T> node = new LinkedNode<T>(value);
            if (last == null)
            {
                first = node;
                last = node;
            }
            else
            {
                last.Next = node;
                node.Previous = last;
                last = node;
            }
            count++;
        }
        /// <summary>
        /// 队首出队,返回出队元素
        /// </summary>
        /// <returns></returns>
        public T DequeueAtFirst()
        {
            if (count == 0)
                new Exception("Dequeue is empty!");
            T value = first.Value;
            first = first.Next;
            if (first != null)
                first.Previous = null;
            count--;
            return value;
        }
        
        /// <summary>
        /// 队尾出队,返回出队元素
        /// </summary>
        /// <returns></returns>
        public T DequeueAtLast()
        {
            if (count == 0)
                new Exception("Dequeue is empty!");
            T value = last.Value;
            last = last.Previous;
            if (last != null)
                last.Next = null;
            count--;
            return value;
        }
        /// <summary>
        /// 清空队列
        /// </summary>
        public void Clear()
        {
            first = last = null;
            count = 0;
        }
        
    }
 
    public class LinkedNode<T>
    {
        public T Value { get; set; }
        public LinkedNode<T> Previous { get; set; }
        public LinkedNode<T> Next { get; set; }
        public LinkedNode(T value)
        {
            Value = value;
        }
    }

主界面脚本

顶层界面脚本

csharp 复制代码
public class LoginSettingUI_Oversea : UIWindow

事务声明

csharp 复制代码
private DoubleEndQueue<Action> _affairExcuteAffairQueue; // 存放下一步的操作
private Stack<Action> _affairBufferStack; //缓存点击后的操作

具体事务

主要操作,执行当前事务(队头),回滚事务,即返回上一级页面

顶层界面(控制流程)

*【注】TEngine框架的UI模块的生命周期,

csharp 复制代码
 internal void InternalCreate()
        {
            if (_isCreate == false)
            {
                _isCreate = true;
                ScriptGenerator(); //获取层级列表组件
                BindMemberProperty();//绑定UI成员元素
                RegisterEvent();//注册事件
                OnCreate();//类比MonoBehavior的Start()方法
            }
        }

下面是 LoginSettingUI_Oversea 的流程:

ScriptGenerator();不展示了,获取组件。

注册事件

csharp 复制代码
public override void RegisterEvent()
{
	base.RegisterEvent();
	AddUIEvent<string>(CommonWidgetDefine.LoginOptMsg,UpLoadOptMsg);
    AddUIEvent(CommonWidgetDefine.ExcuteRegisterAffair,CommitAffair);
    AddUIEvent(CommonWidgetDefine.ClickLocked, () =>
    {
	    _graphicRaycaster = gameObject.GetComponent<GraphicRaycaster>();
	    _graphicRaycaster.enabled = false;
    });
}

LoginOptMsg事件:接受子界面通过点击广播的事件内容,字符串格式同样返回,这里可能是年、月、姓名;

csharp 复制代码
	private void UpLoadOptMsg(string message)
		{
			if (string.IsNullOrWhiteSpace(message)) return ;
			const string number = "^[0-9]*$";
			const string month = "[A-Z][a-z][a-z]";
			Regex num = new Regex(number);
			Regex mon = new Regex(month);

			if (num.IsMatch(message))
			{
				UserYear = message;
			}
			else if (mon.IsMatch(message))
			{
				UserMonth = message;
			}
			else
			{
				
			}
			//Upload the message to Server.
		}

ExcuteRegisterAffair事件:执行队头事务,队头元素出队。

csharp 复制代码
    	/// <summary>
		/// 执行当前事务
		/// </summary>
		private void CommitAffair()
		{
			if (_affairExcuteAffairQueue.Count != 0)
			{
				//缓存当前操作
				var curAffairProcess = _affairExcuteAffairQueue.DequeueAtFirst();
				_affairBufferStack.Push(curAffairProcess);
				//执行操作
				ExcuteAffair(curAffairProcess);
			}
			else
			{
				Debug.LogError("Registration process error.");
			}
		}

ClickLocked事件:操作锁定信号,需求表明:点击后0.5跳转,且期间界面不可点击。这里把

GraphicRaycaster(UI界面射线检测的组件)禁用了,刷新界面后再开启即可。

其他事件

csharp 复制代码
    	/// <summary>
		/// 返回上一级
		/// </summary>
		private void OnClickBackBtn()
		{
			if (_affairBufferStack.Count > 1)
			{
				//从缓存中读取回滚后需要执行的下一步操作以及当前待刷新页面的操作
				for (int i = 0; i < 2; i++)
				{
					_affairExcuteAffairQueue.EnqueueAtFirst(_affairBufferStack.Pop());
				}
				//回滚上一步(刷新页面)
				CommitAffair();
			}
			else
			{
				Debug.LogError("Cannot revert.");
			}
		}
		

开始流程

csharp 复制代码
	public override void OnCreate()
		{
			base.OnCreate();
			InitConfig();//初始化配置
			InitAffairQueue();//初始化事务队列
			CommitAffair();//执行一次事务
		}

初始化配置信息

分配内存、导入需要展示的各类信息(这部分抽离出来方便,后面配置表更新后,直接改为读表)

csharp 复制代码
        /// <summary>
	    /// 初始化配置数据
	    /// </summary>
		private void InitConfig()
		{
			_graphicRaycaster = gameObject.GetComponent<GraphicRaycaster>();
			_affairExcuteAffairQueue = new DoubleEndQueue<Action>();
			_affairBufferStack = new Stack<Action>(TotolProcess);
			_uiWidgetsBuffer = new List<UIWidget>(TotolProcess);
			_pointWidgetsList = new List<LoginPointWidget>();
			InitConfigurationData();
		}
		private void InitConfigurationData()
		{
			_monthDic ??= new Dictionary<int, string>(MonthItemCounts)
			{
				{1,"Jan"},
				{2,"Feb"},
				{3,"Mar"},
				{4,"Apr"},
				{5,"May"},
				{6,"Jun"},
				{7,"Jul"},
				{8,"Aug"},
				{9,"Sept"},
				{10,"Oct"},
				{11,"Nov"},
				{12,"Dec"},
			};
			_topReminderList ??= new List<string>();
			_topReminderList.Add("Please select your child's birth year");
			_topReminderList.Add("Please select your child's birth month");
			_topReminderList.Add("May I ask your child's name is ......");
			_curYear =  DateTime.Now.Year;
		}

初始化任务队列

下面开始初始化任务队列,任务入队。

csharp 复制代码
		private void InitAffairQueue()
		{	
			_affairBufferStack.Clear();
			_affairExcuteAffairQueue.Clear();
			_affairExcuteAffairQueue.EnqueueAtLast(ProcessSetYear);
			_affairExcuteAffairQueue.EnqueueAtLast(ProcessSetMonth);
			_affairExcuteAffairQueue.EnqueueAtLast(ProcessSetName);
			
			var num = _affairExcuteAffairQueue.Count;
			for (int i = 0; i <num ; i++)
			{
				var curWidget =  CreateWidgetByPath<LoginPointWidget>(m_goPointRoot.transform, "LoginPointWidget");
				_pointWidgetsList.Add(curWidget);
			}
		}

事务内容

在本流程中,一个事务对应一张界面的创建

csharp 复制代码
		/// <summary>
		/// 设置用户年份
		/// </summary>
		private void ProcessSetYear()
		{
			ClearWidgetsBuffer();
			m_btnBack.gameObject.SetActive(false);
			m_btnReminderMid.gameObject.SetActive(true);
			m_textReminderMid.text = String.Format("{0} or before", _curYear - YearItemCounts);
			m_textReminderTop.text = _topReminderList[0];
			SetPointColor(0);

			for (int i = 0; i < YearItemCounts; i++)
			{
				var curWidget = CreateWidgetByPath<LoginOptWidget>(m_goYearRoot.transform, "LoginOptWidget");
				_uiWidgetsBuffer.Add(curWidget);
				curWidget.DataInitialize((_curYear-i).ToString());
			}
		}
		
		/// <summary>
		/// 设置用户月份
		/// </summary>
		private void ProcessSetMonth()
		{
			ClearWidgetsBuffer();
			m_btnBack.gameObject.SetActive(true);
			_graphicRaycaster.enabled = true;
			m_btnReminderMid.gameObject.SetActive(false);
			m_textReminderTop.text = _topReminderList[1];
			SetPointColor(1);

			for (int i = 1; i <= MonthItemCounts; i++)
			{
			  var curWidget = CreateWidgetByPath<LoginOptWidget>(m_goMonthRoot.transform, "LoginOptWidget");
			  _uiWidgetsBuffer.Add(curWidget);
			  curWidget.DataInitialize(_monthDic[i]);
			}
		}
		
		/// <summary>
		/// 设置用户姓名
		/// </summary>
		private void ProcessSetName()
		{
			ClearWidgetsBuffer();
			m_btnBack.gameObject.SetActive(true);
			_graphicRaycaster.enabled = true;
			SetPointColor(2);
			m_textReminderTop.text = _topReminderList[2];
            
			var curWidget =  CreateWidgetByPath<LoginNameSettings>(m_goNameRoot.transform, "LoginNameSettings");
			_uiWidgetsBuffer.Add(curWidget);
		}

比如设置月份的界面

中间的横向布局的月份("Jan")选择按钮,是运行时创建的,类似的年份设置也会创建一系列按键,它们来源于同一个prefab,在创建时,m_textOpt.text 被初始化不同值。

在实际的时候,使用了一个对象池存放

csharp 复制代码
private List<UIWidget> _uiWidgetsBuffer;

创建后,加入池子,切换页面的时候,从中取gameobject.刷新字段即可,如果个数不够追加创建。(不是本文重点,就不显示代码了,牵扯了很多数据,拿出来显得很混乱)

最后在脚本OnDestroy时销毁。

csharp 复制代码
    	/// <summary>
		/// 清空动态加载组件缓存
		/// </summary>
		private void DestoryWidgetsBuffer()
		{
			if (_uiWidgetsBuffer.Count != 0)
			{
				foreach (var item in _uiWidgetsBuffer)
				{
					DestroyUIWidget(item);
				}
				_uiWidgetsBuffer.Clear();	
			}
		}
相关推荐
℡枫叶℡36 分钟前
Unity - Import Activity Window 资源导入诊断信息窗口
unity·资源导入诊断
TO_ZRG3 小时前
Unity 证书校验
unity·游戏引擎
mxwin4 小时前
Unity Shader 切线空间数据是如何计算出来的
unity·游戏引擎·shader
mxwin8 小时前
Unity Shader 法线贴图跟切线空间有什么关系
unity·游戏引擎·贴图·shader
mxwin8 小时前
Unity Shader 贴图和采样的关系 如何保证贴图清晰
unity·游戏引擎·贴图·shader
心前阳光10 小时前
Unity之使用火山引擎实现文字提问流式回复
unity·游戏引擎·火山引擎
mxwin13 小时前
Unity Shader 什么是球谐光照 原理是什么
unity·游戏引擎·shader
心前阳光13 小时前
Unity之使用火山引擎实现语音识别
unity·语音识别·火山引擎
心前阳光13 小时前
Unity之使用火山引擎实现流式语音合成
unity·游戏引擎·火山引擎