从需求到思路到实现去谈一个实习期间遇到的问题。
主要需求是有三个界面满足严格先后关系(先设置年龄、月份、姓名),除了年龄设置界面,其他两个界面都可以返回上一级。
思路整理:由于操作有明确的先后关系,这里准备使用一个队列来存储。又由于有返回上一次操作的情况,那么外部需要有控制队头元素的权限。
那么可以是优先级队列、或者双端队列。由于项目工程使用的是.net 4.X framework,PirorityQueue是.net 6.0提供的原生方法,而且对于队列中的操作并没有太复杂的流程控制,不太必要用可以自定义ICompare方法的,比如优先队列或者最小堆等等,
所以准备选择双端队列(可以支持队头元素入队);
然后,还需要对执行后的操作做一个缓存,因为有返回的操作。
核心逻辑:
1.正常流程:事务从队尾入队,从队头出队一次,压入缓存栈,执行当前事务。
2.返回上一级:从栈中Pop两次,依次从【队头】入队(第一次的操作应当是队列中下一次需要执行的操作,第二次的操作,是返回后应当刷新的界面操作),出队一次(完成页面跳转到上一次操作的页面,且事务队列队头为跳转前的本界面)
首先是双端队列的实现,这里是用链表做的。
public class DoubleEndQueue<T>
{
public int Count => count;
private LinkedNode<T> first;
private LinkedNode<T> last;
private int count;
///
/// 队首入队
///
///
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++;
}
///
/// 队尾入队
///
///
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++;
}
///
/// 队首出队,返回出队元素
///
///
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;
}
///
/// 队尾出队,返回出队元素
///
///
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;
}
///
/// 清空队列
///
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;
}
}
顶层界面脚本
public class LoginSettingUI_Oversea : UIWindow
事务声明
private DoubleEndQueue<Action> _affairExcuteAffairQueue; // 存放下一步的操作
private Stack<Action> _affairBufferStack; //缓存点击后的操作
具体事务
主要操作,执行当前事务(队头),回滚事务,即返回上一级页面
顶层界面(控制流程)
*【注】TEngine框架的UI模块的生命周期,
internal void InternalCreate()
{
if (_isCreate == false)
{
_isCreate = true;
ScriptGenerator(); //获取层级列表组件
BindMemberProperty();//绑定UI成员元素
RegisterEvent();//注册事件
OnCreate();//类比MonoBehavior的Start()方法
}
}
下面是 LoginSettingUI_Oversea 的流程:
ScriptGenerator();不展示了,获取组件。
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事件:接受子界面通过点击广播的事件内容,字符串格式同样返回,这里可能是年、月、姓名;
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事件:执行队头事务,队头元素出队。
///
/// 执行当前事务
///
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界面射线检测的组件)禁用了,刷新界面后再开启即可。
///
/// 返回上一级
///
private void OnClickBackBtn()
{
if (_affairBufferStack.Count > 1)
{
//从缓存中读取回滚后需要执行的下一步操作以及当前待刷新页面的操作
for (int i = 0; i < 2; i++)
{
_affairExcuteAffairQueue.EnqueueAtFirst(_affairBufferStack.Pop());
}
//回滚上一步(刷新页面)
CommitAffair();
}
else
{
Debug.LogError("Cannot revert.");
}
}
public override void OnCreate()
{
base.OnCreate();
InitConfig();//初始化配置
InitAffairQueue();//初始化事务队列
CommitAffair();//执行一次事务
}
分配内存、导入需要展示的各类信息(这部分抽离出来方便,后面配置表更新后,直接改为读表)
///
/// 初始化配置数据
///
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;
}
下面开始初始化任务队列,任务入队。
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);
}
}
在本流程中,一个事务对应一张界面的创建
///
/// 设置用户年份
///
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());
}
}
///
/// 设置用户月份
///
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]);
}
}
///
/// 设置用户姓名
///
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 被初始化不同值。
在实际的时候,使用了一个对象池存放
private List<UIWidget> _uiWidgetsBuffer;
创建后,加入池子,切换页面的时候,从中取gameobject.刷新字段即可,如果个数不够追加创建。(不是本文重点,就不显示代码了,牵扯了很多数据,拿出来显得很混乱)
最后在脚本OnDestroy时销毁。
///
/// 清空动态加载组件缓存
///
private void DestoryWidgetsBuffer()
{
if (_uiWidgetsBuffer.Count != 0)
{
foreach (var item in _uiWidgetsBuffer)
{
DestroyUIWidget(item);
}
_uiWidgetsBuffer.Clear();
}
}