com.alicizax.unity.ui.exten.../Runtime/RecyclerView/Adapter/ItemRender.cs

817 lines
30 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using UnityEngine.EventSystems;
namespace AlicizaX.UI
{
/// <summary>
/// 定义 ItemRender 的基础绑定与解绑协议。
/// </summary>
internal interface IItemRender
{
/// <summary>
/// 将指定数据绑定到当前渲染实例。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
void Bind(object data, int index);
/// <summary>
/// 更新当前渲染实例的选中状态。
/// </summary>
/// <param name="selected">是否处于选中状态。</param>
void UpdateSelection(bool selected);
/// <summary>
/// 清理当前渲染实例上的绑定状态。
/// </summary>
void Unbind();
}
/// <summary>
/// 定义带强类型数据绑定能力的 ItemRender 协议。
/// </summary>
/// <typeparam name="TData">列表数据类型。</typeparam>
internal interface ITypedItemRender<in TData> : IItemRender
2026-03-30 15:40:00 +08:00
{
/// <summary>
/// 使用强类型数据执行绑定。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
void BindData(TData data, int index);
2026-03-30 15:40:00 +08:00
}
/// <summary>
/// 提供 ItemRender 的公共基类,封装框架内部的绑定生命周期入口。
/// </summary>
public abstract class ItemRenderBase : IItemRender
{
/// <summary>
/// 将渲染实例附加到指定的视图持有者。
/// </summary>
/// <param name="viewHolder">目标视图持有者。</param>
/// <param name="recyclerView">所属的 RecyclerView。</param>
/// <param name="adapter">当前使用的适配器。</param>
/// <param name="selectionHandler">选中项变更回调。</param>
internal abstract void Attach(ViewHolder viewHolder, RecyclerView recyclerView, IAdapter adapter, Action<int> selectionHandler);
/// <summary>
/// 将渲染实例从当前视图持有者上分离。
/// </summary>
internal abstract void Detach();
/// <summary>
/// 以对象形式绑定数据。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
internal abstract void BindObject(object data, int index);
/// <summary>
/// 更新内部记录的选中状态。
/// </summary>
/// <param name="selected">是否处于选中状态。</param>
internal abstract void UpdateSelectionInternal(bool selected);
/// <summary>
/// 清理当前绑定产生的临时状态。
/// </summary>
internal abstract void UnbindInternal();
/// <summary>
/// 由框架内部调用,将对象数据绑定到当前渲染实例。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
void IItemRender.Bind(object data, int index)
{
BindObject(data, index);
}
/// <summary>
/// 由框架内部调用,更新当前渲染实例的选中状态。
/// </summary>
/// <param name="selected">是否处于选中状态。</param>
void IItemRender.UpdateSelection(bool selected)
{
UpdateSelectionInternal(selected);
}
/// <summary>
/// 由框架内部调用,清理当前渲染实例的绑定状态。
/// </summary>
void IItemRender.Unbind()
{
UnbindInternal();
}
}
/// <summary>
/// 提供带强类型数据与视图持有者的列表项渲染基类。
/// </summary>
/// <typeparam name="TData">列表数据类型。</typeparam>
/// <typeparam name="THolder">视图持有者类型。</typeparam>
public abstract class ItemRender<TData, THolder> : ItemRenderBase, IItemInteractionHost, ITypedItemRender<TData>
where THolder : ViewHolder
{
/// <summary>
/// 当前持有者上的交互代理组件。
/// </summary>
private ItemInteractionProxy interactionProxy;
/// <summary>
/// 当前项被选中时的回调委托。
/// </summary>
private Action<int> selectionHandler;
/// <summary>
/// 上一次绑定到交互代理的交互标记。
/// </summary>
private ItemInteractionFlags cachedInteractionFlags;
/// <summary>
/// 标记交互代理是否已经完成当前配置绑定。
/// </summary>
private bool interactionBindingActive;
/// <summary>
/// 获取当前附加的强类型视图持有者。
/// </summary>
2026-03-30 15:40:00 +08:00
protected THolder Holder { get; private set; }
/// <summary>
/// 获取当前所属的 RecyclerView。
/// </summary>
protected RecyclerView RecyclerView { get; private set; }
/// <summary>
/// 获取当前所属的适配器。
/// </summary>
protected IAdapter Adapter { get; private set; }
/// <summary>
/// 获取当前绑定的数据对象。
/// </summary>
protected TData CurrentData { get; private set; }
/// <summary>
/// 获取当前绑定的数据索引。
/// </summary>
2026-03-30 15:40:00 +08:00
protected int CurrentIndex { get; private set; } = -1;
/// <summary>
/// 获取当前绑定的布局索引。
/// </summary>
protected int CurrentLayoutIndex { get; private set; } = -1;
/// <summary>
/// 获取当前项是否处于选中状态。
/// </summary>
2026-03-30 15:40:00 +08:00
protected bool IsSelected { get; private set; }
/// <summary>
/// 获取当前绑定版本号,用于校验异步回调是否仍然有效。
/// </summary>
protected uint CurrentBindingVersion { get; private set; }
/// <summary>
/// 获取当前渲染项支持的交互能力。
/// </summary>
public virtual ItemInteractionFlags InteractionFlags => ItemInteractionFlags.None;
/// <summary>
/// 由框架交互代理读取当前渲染项的交互能力。
/// </summary>
ItemInteractionFlags IItemInteractionHost.InteractionFlags => InteractionFlags;
/// <summary>
/// 获取键盘或手柄导航时采用的移动选项。
/// </summary>
protected virtual RecyclerNavigationOptions NavigationOptions => RecyclerNavigationOptions.Circular;
/// <summary>
/// 以对象形式绑定数据并执行强类型校验。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
internal override void BindObject(object data, int index)
{
if (data is not TData itemData)
{
throw new InvalidCastException(
$"ItemRender '{GetType().Name}' expected data '{typeof(TData).Name}', but got '{data?.GetType().Name ?? "null"}'.");
}
BindCore(itemData, index);
}
/// <summary>
/// 由框架内部调用,使用强类型数据执行绑定。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
void ITypedItemRender<TData>.BindData(TData data, int index)
{
BindCore(data, index);
}
/// <summary>
/// 更新内部选中状态并触发选中状态回调。
/// </summary>
/// <param name="selected">是否处于选中状态。</param>
internal override void UpdateSelectionInternal(bool selected)
{
2026-03-30 15:40:00 +08:00
EnsureHolder();
IsSelected = selected;
OnSelectionChanged(selected);
}
/// <summary>
/// 清理当前绑定数据关联的状态,并重置内部缓存。
/// </summary>
internal override void UnbindInternal()
{
if (Holder != null)
{
if (IsSelected)
{
IsSelected = false;
OnSelectionChanged(false);
}
OnClear();
if (interactionProxy != null)
{
interactionProxy.Clear();
interactionBindingActive = false;
cachedInteractionFlags = ItemInteractionFlags.None;
}
Holder.DataIndex = -1;
}
2026-03-30 15:40:00 +08:00
CurrentData = default;
CurrentIndex = -1;
CurrentLayoutIndex = -1;
CurrentBindingVersion = 0;
IsSelected = false;
}
/// <summary>
/// 判断指定绑定版本是否仍与当前持有者保持一致。
/// </summary>
/// <param name="bindingVersion">待校验的绑定版本号。</param>
/// <returns>版本号仍然有效时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
protected bool IsBindingCurrent(uint bindingVersion)
{
return Holder != null &&
CurrentBindingVersion != 0 &&
CurrentBindingVersion == bindingVersion &&
Holder.BindingVersion == bindingVersion;
}
/// <summary>
/// 执行一次完整的数据绑定流程。
/// </summary>
/// <param name="itemData">待绑定的强类型数据。</param>
/// <param name="index">当前数据索引。</param>
private void BindCore(TData itemData, int index)
{
EnsureHolder();
CurrentData = itemData;
CurrentIndex = index;
CurrentLayoutIndex = Holder.Index;
Holder.DataIndex = index;
CurrentBindingVersion = Holder.BindingVersion;
BindInteractionProxyIfNeeded();
OnBind(itemData, index);
}
/// <summary>
/// 每次当前持有者绑定到新的数据项时调用。
/// 仅用于执行数据驱动的界面刷新,例如文本、图片与状态更新。
/// 不要在此注册持有者级别的事件监听。
/// </summary>
/// <param name="data">当前绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
protected abstract void OnBind(TData data, int index);
/// <summary>
/// 当当前渲染实例附加到持有者实例时调用。
/// 这是持有者级生命周期,通常对同一组 render 与 holder 仅触发一次。
/// 适合执行一次性的持有者初始化,例如注册按钮监听或挂接可复用交互组件。
/// </summary>
protected virtual void OnHolderAttached()
{
interactionProxy = Holder.GetComponent<ItemInteractionProxy>();
if (interactionProxy == null)
{
interactionProxy = Holder.gameObject.AddComponent<ItemInteractionProxy>();
}
}
/// <summary>
/// 当当前渲染实例即将从持有者实例分离时调用。
/// 这是持有者级清理生命周期,通常对同一组 render 与 holder 仅触发一次。
/// 适合执行一次性的持有者清理,例如注销按钮监听或释放附加阶段缓存的引用。
/// </summary>
protected virtual void OnHolderDetached()
{
}
/// <summary>
/// 当当前项的选中状态发生变化时调用。
/// 仅应在此更新选中态相关的界面表现。
/// </summary>
/// <param name="selected">当前是否处于选中状态。</param>
protected virtual void OnSelectionChanged(bool selected)
{
}
/// <summary>
/// 每次当前数据绑定被清理时调用。
/// 这是绑定级清理生命周期,在复用过程中可能被多次触发。
/// 适合在此重置由当前绑定数据产生的临时界面状态。
/// </summary>
protected virtual void OnClear()
{
}
/// <summary>
/// 通知外部选中当前数据项。
/// </summary>
private void SelectCurrentItem()
{
if (CurrentIndex >= 0)
{
selectionHandler?.Invoke(CurrentIndex);
}
}
2026-03-30 15:40:00 +08:00
/// <summary>
/// 将当前渲染实例附加到指定持有者,并初始化上下文引用。
/// </summary>
/// <param name="viewHolder">目标视图持有者。</param>
/// <param name="recyclerView">所属的 RecyclerView。</param>
/// <param name="adapter">当前使用的适配器。</param>
/// <param name="selectionHandler">选中项变更回调。</param>
internal override void Attach(ViewHolder viewHolder, RecyclerView recyclerView, IAdapter adapter, Action<int> selectionHandler)
2026-03-30 15:40:00 +08:00
{
if (viewHolder == null)
{
throw new ArgumentNullException(nameof(viewHolder));
}
if (viewHolder is not THolder holder)
{
throw new InvalidOperationException(
$"RecyclerView item render '{GetType().FullName}' expects holder '{typeof(THolder).FullName}', but got '{viewHolder.GetType().FullName}'.");
}
Holder = holder;
RecyclerView = recyclerView;
Adapter = adapter;
this.selectionHandler = selectionHandler;
interactionBindingActive = false;
cachedInteractionFlags = ItemInteractionFlags.None;
OnHolderAttached();
}
/// <summary>
/// 将当前渲染实例从持有者上分离,并释放上下文引用。
/// </summary>
internal override void Detach()
{
if (Holder == null)
{
return;
}
OnHolderDetached();
interactionProxy?.Clear();
interactionProxy = null;
selectionHandler = null;
Holder = null;
RecyclerView = null;
Adapter = null;
CurrentData = default;
CurrentIndex = -1;
CurrentLayoutIndex = -1;
CurrentBindingVersion = 0;
IsSelected = false;
interactionBindingActive = false;
cachedInteractionFlags = ItemInteractionFlags.None;
2026-03-30 15:40:00 +08:00
}
/// <summary>
/// 确保当前渲染实例已经绑定有效的视图持有者。
/// </summary>
2026-03-30 15:40:00 +08:00
private void EnsureHolder()
{
if (Holder == null)
{
throw new InvalidOperationException(
$"RecyclerView item render '{GetType().FullName}' has not been initialized with a holder.");
}
}
/// <summary>
/// 按指定方向尝试移动焦点。
/// </summary>
/// <param name="direction">焦点移动方向。</param>
/// <returns>成功移动焦点时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
private bool MoveFocus(MoveDirection direction)
2026-03-30 15:40:00 +08:00
{
return RecyclerView != null && RecyclerView.NavigationController.TryMove(Holder, direction, NavigationOptions);
}
/// <summary>
/// 在需要时将当前渲染实例绑定到交互代理。
/// </summary>
private void BindInteractionProxyIfNeeded()
{
if (interactionProxy == null)
2026-03-30 15:40:00 +08:00
{
return;
}
2026-03-30 15:40:00 +08:00
ItemInteractionFlags interactionFlags = InteractionFlags;
if (interactionBindingActive && cachedInteractionFlags == interactionFlags)
{
return;
2026-03-30 15:40:00 +08:00
}
interactionProxy.Bind(this);
cachedInteractionFlags = interactionFlags;
interactionBindingActive = true;
}
/// <summary>
/// 由交互代理转发点击事件。
/// </summary>
/// <param name="eventData">点击事件数据。</param>
void IItemInteractionHost.HandlePointerClick(PointerEventData eventData)
{
SelectCurrentItem();
OnPointerClick(eventData);
}
/// <summary>
/// 由交互代理转发指针进入事件。
/// </summary>
/// <param name="eventData">指针事件数据。</param>
void IItemInteractionHost.HandlePointerEnter(PointerEventData eventData)
{
OnPointerEnter(eventData);
}
/// <summary>
/// 由交互代理转发指针离开事件。
/// </summary>
/// <param name="eventData">指针事件数据。</param>
void IItemInteractionHost.HandlePointerExit(PointerEventData eventData)
{
OnPointerExit(eventData);
}
/// <summary>
/// 由交互代理转发选中事件。
/// </summary>
/// <param name="eventData">选中事件数据。</param>
void IItemInteractionHost.HandleSelect(BaseEventData eventData)
{
OnItemSelected(eventData);
}
/// <summary>
/// 由交互代理转发取消选中事件。
/// </summary>
/// <param name="eventData">取消选中事件数据。</param>
void IItemInteractionHost.HandleDeselect(BaseEventData eventData)
{
OnItemDeselected(eventData);
}
/// <summary>
/// 由交互代理转发导航移动事件。
/// </summary>
/// <param name="eventData">导航事件数据。</param>
void IItemInteractionHost.HandleMove(AxisEventData eventData)
{
if (!OnMove(eventData))
{
MoveFocus(eventData.moveDir);
}
}
/// <summary>
/// 由交互代理转发开始拖拽事件。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
void IItemInteractionHost.HandleBeginDrag(PointerEventData eventData)
{
OnBeginDrag(eventData);
}
/// <summary>
/// 由交互代理转发拖拽事件。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
void IItemInteractionHost.HandleDrag(PointerEventData eventData)
{
OnDrag(eventData);
}
/// <summary>
/// 由交互代理转发结束拖拽事件。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
void IItemInteractionHost.HandleEndDrag(PointerEventData eventData)
{
OnEndDrag(eventData);
}
/// <summary>
/// 由交互代理转发提交事件。
/// </summary>
/// <param name="eventData">提交事件数据。</param>
void IItemInteractionHost.HandleSubmit(BaseEventData eventData)
{
SelectCurrentItem();
OnSubmit(eventData);
}
/// <summary>
/// 由交互代理转发取消事件。
/// </summary>
/// <param name="eventData">取消事件数据。</param>
void IItemInteractionHost.HandleCancel(BaseEventData eventData)
{
OnCancel(eventData);
}
/// <summary>
/// 当当前项收到点击事件时调用。
/// </summary>
/// <param name="eventData">点击事件数据。</param>
protected virtual void OnPointerClick(PointerEventData eventData)
{
}
/// <summary>
/// 当指针进入当前项时调用。
/// </summary>
/// <param name="eventData">指针事件数据。</param>
protected virtual void OnPointerEnter(PointerEventData eventData)
{
}
/// <summary>
/// 当指针离开当前项时调用。
/// </summary>
/// <param name="eventData">指针事件数据。</param>
protected virtual void OnPointerExit(PointerEventData eventData)
{
}
/// <summary>
/// 当当前项被 EventSystem 选中时调用。
/// </summary>
/// <param name="eventData">选中事件数据。</param>
protected virtual void OnItemSelected(BaseEventData eventData)
{
}
/// <summary>
/// 当当前项被 EventSystem 取消选中时调用。
/// </summary>
/// <param name="eventData">取消选中事件数据。</param>
protected virtual void OnItemDeselected(BaseEventData eventData)
{
}
/// <summary>
/// 当当前项收到导航移动事件时调用。
/// </summary>
/// <param name="eventData">导航事件数据。</param>
/// <returns>已自行处理导航事件时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
protected virtual bool OnMove(AxisEventData eventData)
{
return false;
}
/// <summary>
/// 当当前项开始被拖拽时调用。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
protected virtual void OnBeginDrag(PointerEventData eventData)
{
}
/// <summary>
/// 当当前项发生拖拽时调用。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
protected virtual void OnDrag(PointerEventData eventData)
{
}
/// <summary>
/// 当当前项结束拖拽时调用。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
protected virtual void OnEndDrag(PointerEventData eventData)
{
}
/// <summary>
/// 当当前项收到提交操作时调用。
/// </summary>
/// <param name="eventData">提交事件数据。</param>
protected virtual void OnSubmit(BaseEventData eventData)
{
}
/// <summary>
/// 当当前项收到取消操作时调用。
/// </summary>
/// <param name="eventData">取消事件数据。</param>
protected virtual void OnCancel(BaseEventData eventData)
{
2026-03-30 15:40:00 +08:00
}
}
/// <summary>
/// 负责解析、缓存并创建 ItemRender 定义。
/// </summary>
internal static class ItemRenderResolver
{
/// <summary>
/// 描述单个 ItemRender 的类型信息与创建方式。
/// </summary>
internal sealed class ItemRenderDefinition
{
/// <summary>
/// 初始化一份 ItemRender 定义。
/// </summary>
/// <param name="itemRenderType">渲染器运行时类型。</param>
/// <param name="holderType">对应的持有者类型。</param>
/// <param name="createInstance">渲染器实例创建委托。</param>
public ItemRenderDefinition(Type itemRenderType, Type holderType, Func<IItemRender> createInstance)
{
ItemRenderType = itemRenderType;
HolderType = holderType;
this.createInstance = createInstance;
}
/// <summary>
/// 获取渲染器运行时类型。
/// </summary>
public Type ItemRenderType { get; }
/// <summary>
/// 获取渲染器要求的持有者类型。
/// </summary>
public Type HolderType { get; }
/// <summary>
/// 用于创建渲染器实例的缓存委托。
/// </summary>
private readonly Func<IItemRender> createInstance;
/// <summary>
/// 创建并初始化一个可用的 ItemRender 实例。
/// </summary>
/// <param name="viewHolder">目标视图持有者。</param>
/// <param name="recyclerView">所属的 RecyclerView。</param>
/// <param name="adapter">当前使用的适配器。</param>
/// <param name="selectionHandler">选中项变更回调。</param>
/// <returns>已初始化完成的渲染器实例。</returns>
public IItemRender Create(ViewHolder viewHolder, RecyclerView recyclerView, IAdapter adapter, Action<int> selectionHandler)
{
if (viewHolder == null)
{
throw new ArgumentNullException(nameof(viewHolder));
}
if (!HolderType.IsInstanceOfType(viewHolder))
{
throw new InvalidOperationException(
$"RecyclerView item render '{ItemRenderType.FullName}' expects holder '{HolderType.FullName}', but got '{viewHolder.GetType().FullName}'.");
}
if (createInstance() is not ItemRenderBase itemRender)
2026-03-30 15:40:00 +08:00
{
throw new InvalidOperationException(
$"RecyclerView item render '{ItemRenderType.FullName}' could not be created.");
}
itemRender.Attach(viewHolder, recyclerView, adapter, selectionHandler);
2026-03-30 15:40:00 +08:00
return itemRender;
}
}
/// <summary>
/// ItemRender 定义缓存表,键为渲染器类型。
/// </summary>
private static readonly Dictionary<Type, ItemRenderDefinition> Definitions = new();
/// <summary>
/// 获取指定渲染器类型对应的定义,不存在时自动创建并缓存。
/// </summary>
/// <param name="itemRenderType">渲染器运行时类型。</param>
/// <returns>与该类型对应的渲染器定义。</returns>
public static ItemRenderDefinition GetOrCreate(Type itemRenderType)
{
if (itemRenderType == null)
{
throw new ArgumentNullException(nameof(itemRenderType));
}
if (Definitions.TryGetValue(itemRenderType, out ItemRenderDefinition definition))
{
return definition;
}
definition = CreateDefinition(itemRenderType);
Definitions[itemRenderType] = definition;
return definition;
}
/// <summary>
/// 为指定渲染器类型构建定义信息。
/// </summary>
/// <param name="itemRenderType">渲染器运行时类型。</param>
/// <returns>创建完成的渲染器定义。</returns>
private static ItemRenderDefinition CreateDefinition(Type itemRenderType)
{
if (itemRenderType.IsAbstract ||
itemRenderType.IsInterface ||
itemRenderType.ContainsGenericParameters ||
!typeof(IItemRender).IsAssignableFrom(itemRenderType))
{
throw new InvalidOperationException(
$"RecyclerView item render type '{itemRenderType.FullName}' is invalid.");
}
if (!TryGetHolderType(itemRenderType, out Type holderType))
{
throw new InvalidOperationException(
$"RecyclerView item render '{itemRenderType.FullName}' must inherit from ItemRender<TData, THolder>.");
}
ConstructorInfo constructor = itemRenderType.GetConstructor(
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
null,
2026-03-30 15:40:00 +08:00
Type.EmptyTypes,
null);
if (constructor == null)
{
throw new InvalidOperationException(
2026-03-30 15:40:00 +08:00
$"RecyclerView item render '{itemRenderType.FullName}' must have a parameterless constructor.");
}
return new ItemRenderDefinition(itemRenderType, holderType, CreateFactory(constructor));
}
/// <summary>
/// 尝试从渲染器继承链中解析对应的持有者类型。
/// </summary>
/// <param name="itemRenderType">渲染器运行时类型。</param>
/// <param name="holderType">解析得到的持有者类型。</param>
/// <returns>解析成功时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
private static bool TryGetHolderType(Type itemRenderType, out Type holderType)
{
for (Type current = itemRenderType; current != null && current != typeof(object); current = current.BaseType)
{
if (current.IsGenericType &&
current.GetGenericTypeDefinition() == typeof(ItemRender<,>))
{
Type[] arguments = current.GetGenericArguments();
holderType = arguments[1];
return true;
}
}
holderType = null;
return false;
}
/// <summary>
/// 基于无参构造函数创建渲染器实例工厂。
/// </summary>
/// <param name="constructor">渲染器的无参构造函数。</param>
/// <returns>用于创建渲染器实例的委托。</returns>
private static Func<IItemRender> CreateFactory(ConstructorInfo constructor)
{
NewExpression newExpression = Expression.New(constructor);
UnaryExpression convertExpression = Expression.Convert(newExpression, typeof(IItemRender));
return Expression.Lambda<Func<IItemRender>>(convertExpression).Compile();
}
}
}