This commit is contained in:
陈思海 2025-03-12 20:59:12 +08:00
parent c9bc460ad7
commit 9d941a179d
77 changed files with 3508 additions and 1 deletions

View File

@ -6,7 +6,8 @@
"GUID:5553d74549d54e74cb548b3ab58a8483",
"GUID:75b6f2078d190f14dbda4a5b747d709c",
"GUID:a19b414bea3b97240a91aeab9a8eab36",
"GUID:198eb6af143bbc4488e2779d96697e06"
"GUID:198eb6af143bbc4488e2779d96697e06",
"GUID:80ecb87cae9c44d19824e70ea7229748"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 20aaf5daa7c6f1144be40271786f5aca
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c55a613d7aa2a27448539712f0cf3875
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
namespace AlicizaX.UI.RecyclerView
{
public class Adapter<T> : IAdapter
{
protected RecyclerView recyclerView;
protected List<T> list;
protected Action<T> onItemClick;
protected int choiceIndex = -1;
public int ChoiceIndex
{
get => choiceIndex;
set
{
SetChoiceIndex(value);
}
}
public Adapter(RecyclerView recyclerView) : this(recyclerView, new List<T>(), null)
{
}
public Adapter(RecyclerView recyclerView, List<T> list) : this(recyclerView, list, null)
{
}
public Adapter(RecyclerView recyclerView, List<T> list, Action<T> onItemClick)
{
this.recyclerView = recyclerView;
this.list = list;
this.onItemClick = onItemClick;
this.recyclerView.Adapter = this;
}
public virtual int GetItemCount()
{
return list == null ? 0 : list.Count;
}
public virtual int GetRealCount()
{
return GetItemCount();
}
public virtual string GetViewName(int index)
{
return "";
}
public virtual void OnBindViewHolder(ViewHolder viewHolder, int index)
{
if (index < 0 || index >= GetItemCount()) return;
T data = list[index];
viewHolder.BindViewData(data);
viewHolder.BindItemClick(data, t =>
{
SetChoiceIndex(index);
onItemClick?.Invoke(data);
});
viewHolder.BindChoiceState(index == choiceIndex);
}
public virtual void NotifyDataChanged()
{
recyclerView.RequestLayout();
recyclerView.Refresh();
}
public virtual void SetList(List<T> list)
{
this.list = list;
recyclerView.Reset();
NotifyDataChanged();
}
public T GetData(int index)
{
if (index < 0 || index >= GetItemCount()) return default;
return list[index];
}
public void Add(T item)
{
list.Add(item);
NotifyDataChanged();
}
public void AddRange(IEnumerable<T> collection)
{
list.AddRange(collection);
NotifyDataChanged();
}
public void Insert(int index, T item)
{
list.Insert(index, item);
NotifyDataChanged();
}
public void InsertRange(int index, IEnumerable<T> collection)
{
list.InsertRange(index, collection);
NotifyDataChanged();
}
public void Remove(T item)
{
int index = list.IndexOf(item);
RemoveAt(index);
}
public void RemoveAt(int index)
{
if (index < 0 || index >= GetItemCount()) return;
list.RemoveAt(index);
NotifyDataChanged();
}
public void RemoveRange(int index, int count)
{
list.RemoveRange(index, count);
NotifyDataChanged();
}
public void RemoveAll(Predicate<T> match)
{
list.RemoveAll(match);
NotifyDataChanged();
}
public void Clear()
{
list.Clear();
NotifyDataChanged();
}
public void Reverse(int index, int count)
{
list.Reverse(index, count);
NotifyDataChanged();
}
public void Reverse()
{
list.Reverse();
NotifyDataChanged();
}
public void Sort(Comparison<T> comparison)
{
list.Sort(comparison);
NotifyDataChanged();
}
public void SetOnItemClick(Action<T> onItemClick)
{
this.onItemClick = onItemClick;
}
protected void SetChoiceIndex(int index)
{
if (index == choiceIndex) return;
if (choiceIndex != -1)
{
if (TryGetViewHolder(choiceIndex, out var viewHolder))
{
viewHolder.BindChoiceState(false);
}
}
choiceIndex = index;
if (choiceIndex != -1)
{
if (TryGetViewHolder(choiceIndex, out var viewHolder))
{
viewHolder.BindChoiceState(true);
}
}
}
private bool TryGetViewHolder(int index, out ViewHolder viewHolder)
{
viewHolder = recyclerView.ViewProvider.GetViewHolder(index);
return viewHolder != null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2fe0ab8191b4944db29445f6f796a50
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
namespace AlicizaX.UI.RecyclerView
{
public class GroupAdapter : Adapter<GroupData>
{
private readonly List<GroupData> showList = new();
private string groupViewName;
public GroupAdapter(RecyclerView recyclerView, string groupViewName) : base(recyclerView)
{
this.groupViewName = groupViewName;
}
public GroupAdapter(RecyclerView recyclerView, List<GroupData> list) : base(recyclerView, list)
{
}
public GroupAdapter(RecyclerView recyclerView, List<GroupData> list, Action<GroupData> onItemClick) : base(recyclerView, list, onItemClick)
{
}
public override int GetItemCount()
{
return showList.Count;
}
public override string GetViewName(int index)
{
return showList[index].viewName;
}
public override void OnBindViewHolder(ViewHolder viewHolder, int index)
{
if (index < 0 || index >= GetItemCount()) return;
GroupData data = showList[index];
viewHolder.BindViewData(data);
viewHolder.BindItemClick(data, t =>
{
if (data.viewName == groupViewName)
{
data.bExpand = !data.bExpand;
NotifyDataChanged();
}
else
{
SetChoiceIndex(index);
onItemClick?.Invoke(data);
}
});
}
public override void NotifyDataChanged()
{
foreach (var data in list)
{
CreateGroup(data.type);
}
var groupList = showList.FindAll(data => data.viewName == groupViewName);
for (int i = 0; i < groupList.Count; i++)
{
int index = showList.IndexOf(groupList[i]);
Collapse(index);
if (groupList[i].bExpand)
{
Expand(index);
}
}
foreach (var group in groupList)
{
if (list.FindAll(data => data.type == group.type).Count == 0)
{
showList.Remove(group);
}
}
base.NotifyDataChanged();
}
public override void SetList(List<GroupData> list)
{
showList.Clear();
base.SetList(list);
}
private void CreateGroup(int type)
{
var groupData = showList.Find(data => data.type == type && data.viewName == groupViewName);
if (groupData == null)
{
groupData = new GroupData(type, groupViewName, type.ToString());
showList.Add(groupData);
}
}
public void Expand(int index)
{
var expandList = list.FindAll(data => data.type == showList[index].type);
showList.InsertRange(index + 1, expandList);
}
public void Collapse(int index)
{
var collapseList = showList.FindAll(data => data.type == showList[index].type && data.viewName != groupViewName);
showList.RemoveRange(index + 1, collapseList.Count);
}
}
public class GroupData
{
public bool bExpand;
public int type;
public string viewName;
public string name;
public GroupData(int type, string viewName, string name)
{
this.type = type;
this.viewName = viewName;
this.name = name;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1afc560670c303d42b28cc143ed25a8f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,15 @@
namespace AlicizaX.UI.RecyclerView
{
public interface IAdapter
{
int GetItemCount();
int GetRealCount();
string GetViewName(int index);
void OnBindViewHolder(ViewHolder viewHolder, int index);
void NotifyDataChanged();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fc979439548585f4097747c34ed12270
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
namespace AlicizaX.UI.RecyclerView
{
public class LoopAdapter<T> : Adapter<T>
{
public LoopAdapter(RecyclerView recyclerView) : base(recyclerView)
{
}
public LoopAdapter(RecyclerView recyclerView, List<T> list) : base(recyclerView, list)
{
}
public LoopAdapter(RecyclerView recyclerView, List<T> list, Action<T> onItemClick) : base(recyclerView, list, onItemClick)
{
}
public override int GetItemCount()
{
return int.MaxValue;
}
public override int GetRealCount()
{
return list == null ? 0 : list.Count;
}
public override void OnBindViewHolder(ViewHolder viewHolder, int index)
{
index %= list.Count;
base.OnBindViewHolder(viewHolder, index);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 852ebd0fa448a374aad55404886bfaae
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
namespace AlicizaX.UI.RecyclerView
{
public class MixedAdapter : Adapter<MixedData>
{
public MixedAdapter(RecyclerView recyclerView) : base(recyclerView)
{
}
public MixedAdapter(RecyclerView recyclerView, List<MixedData> list) : base(recyclerView, list)
{
}
public MixedAdapter(RecyclerView recyclerView, List<MixedData> list, Action<MixedData> onItemClick) : base(recyclerView, list, onItemClick)
{
}
public override string GetViewName(int index)
{
return list[index].viewName;
}
}
public class MixedData
{
public string viewName;
public string name;
public string icon;
public int number;
public int percent;
public MixedData(string viewName, string name, string icon, int number, int percent)
{
this.viewName = viewName;
this.name = name;
this.icon = icon;
this.number = number;
this.percent = percent;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5c335290acc1c8b45aa87e6fc2b00b0c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,192 @@
using System;
public class EaseUtil
{
public static double EaseInSine(float x)
{
return 1 - Math.Cos(x * Math.PI / 2);
}
public static double EaseOutSine(float x)
{
return Math.Sin(x * Math.PI / 2);
}
public static double EaseInOutSine(float x)
{
return -(Math.Cos(Math.PI * x) - 1) / 2;
}
public static double EaseInQuad(float x)
{
return x * x;
}
public static double EaseOutQuad(float x)
{
return 1 - (1 - x) * (1 - x);
}
public static double EaseInOutQuad(float x)
{
return x < 0.5 ? 2 * x * x : 1 - Math.Pow(-2 * x + 2, 2) / 2;
}
public static double EaseInQuart(float x)
{
return Math.Pow(x, 4);
}
public static double EaseOutQuart(float x)
{
return 1 - Math.Pow(1 - x, 4);
}
public static double EaseInOutQuart(float x)
{
return x < 0.5 ? 8 * x * x * x * x : 1 - Math.Pow(-2 * x + 2, 4) / 2;
}
public static double EaseInCubic(float x)
{
return Math.Pow(x, 3);
}
public static double EaseOutCubic(float x)
{
return 1 - Math.Pow(1 - x, 3);
}
public static double EaseInOutCubic(float x)
{
return x < 0.5f ? 4 * x * x * x : 1 - Math.Pow(-2 * x + 2, 3) / 2;
}
public static double EaseInQuint(float x)
{
return Math.Pow(x, 5);
}
public static double EaseOutQuint(float x)
{
return 1 - Math.Pow(1 - x, 5);
}
public static double EaseInOutQuint(float x)
{
return x < 0.5f ? 16 * Math.Pow(x, 5) : 1 - Math.Pow(-2 * x + 2, 5) / 2;
}
public static double EaseInExpo(float x)
{
return x == 0 ? 0 : Math.Pow(2, 10 * x - 10);
}
public static double EaseOutExpo(float x)
{
return x == 1 ? 1 : 1 - Math.Pow(2, -10 * x);
}
public static double EaseInOutExpo(float x)
{
return x == 0 ? 0 :
x == 1 ? 1 :
x < 0.5 ? Math.Pow(2, 20 * x - 10) / 2 :
(2 - Math.Pow(2, -20 * x + 10)) / 2;
}
public static double EaseInCirc(float x)
{
return 1 - Math.Sqrt(1 - Math.Pow(x, 2));
}
public static double EaseOutCirc(float x)
{
return Math.Sqrt(1 - Math.Pow(x - 1, 2));
}
public static double EaseInOutCirc(float x)
{
return x < 0.5 ? (1 - Math.Sqrt(1 - Math.Pow(2 * x, 2))) / 2 : (Math.Sqrt(1 - Math.Pow(-2 * x + 2, 2)) + 1) / 2;
}
public static double EaseInBack(float x)
{
double c1 = 1.70158;
double c3 = c1 + 1;
return c3 * x * x * x - c1 * x * x;
}
public static double EaseOutBack(float x)
{
double c1 = 1.70158;
double c3 = c1 + 1;
return 1 + c3 * Math.Pow(x - 1, 3) + c1 * Math.Pow(x - 1, 2);
}
public static double EaseInOutBack(float x)
{
double c1 = 1.70158;
double c2 = c1 * 1.525;
return x < 0.5 ?
Math.Pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2) / 2 :
(Math.Pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
}
public static double EaseInElastic(float x)
{
double c4 = 2 * Math.PI / 3;
return x == 0 ? 0 : x == 1 ? 1 : -Math.Pow(2, 10 * x - 10) * Math.Sin((x * 10 - 10.75) * c4);
}
public static double EaseOutElastic(float x)
{
double c4 = 2 * Math.PI / 3;
return x == 0 ? 0 : x == 1 ? 1 : Math.Pow(2, -10 * x) * Math.Sin((x * 10 - 0.75) * c4) + 1;
}
public static double EaseInOutElastic(float x)
{
double c5 = 2 * Math.PI / 4.5;
return x == 0 ? 0 :
x == 1 ? 1 :
x < 0.5 ? -(Math.Pow(2, 20 * x - 10) * Math.Sin((20 * x - 11.125) * c5)) / 2 :
Math.Pow(2, -20 * x + 10) * Math.Sin((20 * x - 11.125) * c5) / 2 + 1;
}
public static double EaseInBounce(float x)
{
return 1 - EaseOutBounce(1 - x);
}
public static double EaseOutBounce(float x)
{
double n1 = 7.5625;
double d1 = 2.75;
if (x < 1 / d1)
{
return n1 * x * x;
}
else if (x < 2 / d1)
{
return n1 * (x -= (float)(1.5 / d1)) * x + 0.75;
}
else if (x < 2.5 / d1)
{
return n1 * (x -= (float)(2.25 / d1)) * x + 0.9375;
}
else
{
return n1 * (x -= (float)(2.625 / d1)) * x + 0.984375;
}
}
public static double EaseInOutBounce(float x)
{
return x < 0.5 ? (1 - EaseOutBounce(1 - 2 * x)) / 2 : (1 + EaseOutBounce(2 * x - 1)) / 2;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d42e2db77425447490cb9f68003e818b
timeCreated: 1741771999

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c2c36f0a0862000438099a52b9b5e69c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,121 @@
using System.Collections.Generic;
using UnityEngine;
namespace AlicizaX.UI.RecyclerView
{
public class CircleLayoutManager : LayoutManager
{
private float radius;
private new CircleDirection direction;
private float intervalAngle;
private float initalAngle;
public override RecyclerView RecyclerView
{
get => recyclerView;
set
{
recyclerView = value;
recyclerView.SetScroller(recyclerView.gameObject.AddComponent<CircleScroller>());
}
}
public CircleLayoutManager(CircleDirection direction = CircleDirection.Positive, float initalAngle = 0)
{
this.direction = direction;
this.initalAngle = initalAngle;
}
public override Vector2 CalculateContentSize()
{
Vector2 size = viewProvider.CalculateViewSize(0);
radius = (Mathf.Min(viewportSize.x, viewportSize.y) - Mathf.Min(size.x, size.y)) / 2f - Mathf.Max(padding.x, padding.y);
intervalAngle = adapter.GetItemCount() > 0 ? 360f / adapter.GetItemCount() : 0;
return viewportSize;
}
public override Vector2 CalculateContentOffset()
{
return Vector2.zero;
}
public override Vector2 CalculateViewportOffset()
{
return Vector2.zero;
}
public override void Layout(ViewHolder viewHolder, int index)
{
viewHolder.RectTransform.anchoredPosition3D = CalculatePosition(index);
}
public override Vector2 CalculatePosition(int index)
{
float angle = index * intervalAngle;
angle = direction == CircleDirection.Positive ? angle : -angle;
angle += initalAngle + ScrollPosition;
float radian = angle * (Mathf.PI / 180f);
float x = radius * Mathf.Sin(radian);
float y = radius * Mathf.Cos(radian);
return new Vector2(x, y);
}
public override int GetStartIndex()
{
return 0;
}
public override int GetEndIndex()
{
return adapter.GetItemCount() - 1;
}
public override bool IsFullVisibleStart(int index) => false;
public override bool IsFullInvisibleStart(int index) => false;
public override bool IsFullVisibleEnd(int index) => false;
public override bool IsFullInvisibleEnd(int index) => false;
public override bool IsVisible(int index) => true;
public override float IndexToPosition(int index)
{
float position = index * intervalAngle;
return -position;
}
public override int PositionToIndex(float position)
{
int index = Mathf.RoundToInt(position / intervalAngle);
return -index;
}
public override void DoItemAnimation()
{
List<ViewHolder> viewHolders = viewProvider.ViewHolders;
for (int i = 0; i < viewHolders.Count; i++)
{
float angle = i * intervalAngle + initalAngle;
angle = direction == CircleDirection.Positive ? angle + ScrollPosition : angle - ScrollPosition;
float delta = (angle - initalAngle) % 360;
delta = delta < 0 ? delta + 360 : delta;
delta = delta > 180 ? 360 - delta : delta;
float scale = delta < intervalAngle ? (1.4f - delta / intervalAngle) : 1;
scale = Mathf.Max(scale, 1);
viewHolders[i].RectTransform.localScale = Vector3.one * scale;
}
}
}
public enum CircleDirection
{
Positive,
Negative
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 158658be786595e46a209d2b2162b4e2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,124 @@
using UnityEngine;
namespace AlicizaX.UI.RecyclerView
{
public class GridLayoutManager : LayoutManager
{
private Vector2 cellSize;
public GridLayoutManager(int unit = 1)
{
this.unit = unit;
}
public override Vector2 CalculateContentSize()
{
cellSize = viewProvider.CalculateViewSize(0);
int row = Mathf.CeilToInt(adapter.GetItemCount() / (float)unit);
float len;
if (direction == Direction.Vertical)
{
len = row * (cellSize.y + spacing.y) - spacing.y;
return new Vector2(contentSize.x, len + padding.y * 2);
}
len = row * (cellSize.x + spacing.x) - spacing.x;
return new Vector2(len, contentSize.y + padding.x * 2);
}
public override Vector2 CalculatePosition(int index)
{
int row = index / unit;
int column = index % unit;
float x, y;
if (direction == Direction.Vertical)
{
x = column * (cellSize.x + spacing.x);
y = row * (cellSize.y + spacing.y) - ScrollPosition;
}
else
{
x = row * (cellSize.x + spacing.x) - ScrollPosition;
y = column * (cellSize.y + spacing.y);
}
return new Vector2(x + padding.x, y + padding.y);
}
public override Vector2 CalculateContentOffset()
{
float width, height;
if (alignment == Alignment.Center)
{
width = Mathf.Min(contentSize.x, viewportSize.x);
height = Mathf.Min(contentSize.y, viewportSize.y);
}
else
{
width = viewportSize.x;
height = viewportSize.y;
}
return new Vector2((width - cellSize.x) / 2, (height - cellSize.y) / 2);
}
public override Vector2 CalculateViewportOffset()
{
float width, height;
if (alignment == Alignment.Center)
{
width = Mathf.Min(contentSize.x, viewportSize.x);
height = Mathf.Min(contentSize.y, viewportSize.y);
}
else
{
width = viewportSize.x;
height = viewportSize.y;
}
return new Vector2((width - cellSize.x) / 2, (height - cellSize.y) / 2);
}
public override int GetStartIndex()
{
float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x;
int index = Mathf.FloorToInt(ScrollPosition / len) * unit;
return Mathf.Max(0, index);
}
public override int GetEndIndex()
{
float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x;
float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x;
int index = Mathf.FloorToInt((ScrollPosition + viewLength) / len) * unit;
return Mathf.Min(index, adapter.GetItemCount() - 1);
}
public override float IndexToPosition(int index)
{
int row = index / unit;
float len, viewLength, position;
if (direction == Direction.Vertical)
{
len = row * (cellSize.y + spacing.y);
viewLength = viewportSize.y;
position = len + viewLength > contentSize.y ? contentSize.y - viewportSize.y : len;
}
else
{
len = row * (cellSize.x + spacing.x);
viewLength = viewportSize.x;
position = len + viewLength > contentSize.x ? contentSize.x - viewportSize.x : len;
}
return position;
}
public override int PositionToIndex(float position)
{
float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x;
int index = Mathf.RoundToInt(position / len);
return index * unit;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7e4f9a076f276cd4a8db0055fd37e220
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,115 @@
using UnityEngine;
namespace AlicizaX.UI.RecyclerView
{
public interface ILayoutManager
{
/// <summary>
/// 滚动时,刷新整个页面的布局
/// </summary>
void UpdateLayout();
/// <summary>
/// 为 ViewHolder 设置布局
/// </summary>
/// <param name="viewHolder"></param>
/// <param name="index"></param>
void Layout(ViewHolder viewHolder, int index);
/// <summary>
/// 设置 Content 大小
/// </summary>
void SetContentSize();
/// <summary>
/// 计算 Content 的大小
/// </summary>
/// <returns></returns>
Vector2 CalculateContentSize();
/// <summary>
/// 计算第 index 个 ViewHolder 到顶部的距离
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
Vector2 CalculatePosition(int index);
/// <summary>
/// 计算 ViewHolder 相对于内容长度的偏移
/// </summary>
/// <returns></returns>
Vector2 CalculateContentOffset();
/// <summary>
/// 计算 ViewHolder 相对于视口的偏移
/// </summary>
/// <returns></returns>
Vector2 CalculateViewportOffset();
/// <summary>
/// 获取当前显示的第一个 ViewHolder 下标
/// </summary>
/// <returns></returns>
int GetStartIndex();
/// <summary>
/// 获取当前显示的最后一个 ViewHolder 下标
/// </summary>
/// <returns></returns>
int GetEndIndex();
/// <summary>
/// 数据下标转换成在布局中对应的位置
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
float IndexToPosition(int index);
/// <summary>
/// 在布局中的位置转换成数据下标
/// </summary>
/// <param name="position"></param>
/// <returns></returns>
int PositionToIndex(float position);
/// <summary>
/// 滚动时item 对应的动画
/// </summary>
void DoItemAnimation();
/// <summary>
/// 判断第一个 ViewHolder 是否完全可见
/// </summary>
/// <param name="index">数据的真实下标</param>
/// <returns></returns>
bool IsFullVisibleStart(int index);
/// <summary>
/// 判断第一个 ViewHolder 是否完全不可见
/// </summary>
/// <param name="index">数据的真实下标</param>
/// <returns></returns>
bool IsFullInvisibleStart(int index);
/// <summary>
/// 判定最后一个 ViewHolder 是否完全可见
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
bool IsFullVisibleEnd(int index);
/// <summary>
/// 判定最后一个 ViewHolder 是否完全不可见
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
bool IsFullInvisibleEnd(int index);
/// <summary>
/// 判定第 index ViewHolder是否可见
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
bool IsVisible(int index);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 09e4099cf54916a41b7da73277ab988a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,230 @@
using UnityEngine;
namespace AlicizaX.UI.RecyclerView
{
public abstract class LayoutManager : ILayoutManager
{
protected Vector2 viewportSize;
public Vector2 ViewportSize
{
get => viewportSize;
private set => viewportSize = value;
}
protected Vector2 contentSize;
public Vector2 ContentSize
{
get => contentSize;
private set => contentSize = value;
}
protected Vector2 contentOffset;
public Vector2 ContentOffset
{
get => contentOffset;
private set => contentOffset = value;
}
protected Vector2 viewportOffset;
public Vector2 ViewportOffset
{
get => viewportOffset;
private set => viewportOffset = value;
}
protected IAdapter adapter;
public IAdapter Adapter
{
get => adapter;
set => adapter = value;
}
protected ViewProvider viewProvider;
public ViewProvider ViewProvider
{
get => viewProvider;
set => viewProvider = value;
}
protected RecyclerView recyclerView;
public virtual RecyclerView RecyclerView
{
get => recyclerView;
set => recyclerView = value;
}
protected Direction direction;
public Direction Direction
{
get => direction;
set => direction = value;
}
protected Alignment alignment;
public Alignment Alignment
{
get => alignment;
set => alignment = value;
}
protected Vector2 spacing;
public Vector2 Spacing
{
get => spacing;
set => spacing = value;
}
protected Vector2 padding;
public Vector2 Padding
{
get => padding;
set => padding = value;
}
protected int unit = 1;
public int Unit
{
get => unit;
set => unit = value;
}
protected bool canScroll;
public bool CanScroll
{
get => canScroll;
set => canScroll = value;
}
public float ScrollPosition => recyclerView.GetScrollPosition();
public LayoutManager() { }
public void SetContentSize()
{
viewportSize = recyclerView.GetComponent<RectTransform>().rect.size;
contentSize = CalculateContentSize();
contentOffset = CalculateContentOffset();
viewportOffset = CalculateViewportOffset();
}
public void UpdateLayout()
{
foreach (var viewHolder in viewProvider.ViewHolders)
{
Layout(viewHolder, viewHolder.Index);
}
}
public virtual void Layout(ViewHolder viewHolder, int index)
{
Vector2 pos = CalculatePosition(index);
Vector3 position = direction == Direction.Vertical ?
new Vector3(pos.x - contentOffset.x, -pos.y + contentOffset.y, 0) :
new Vector3(pos.x - contentOffset.x, -pos.y + contentOffset.y, 0);
viewHolder.RectTransform.anchoredPosition3D = position;
}
public abstract Vector2 CalculateContentSize();
public abstract Vector2 CalculatePosition(int index);
public abstract Vector2 CalculateContentOffset();
public abstract Vector2 CalculateViewportOffset();
public abstract int GetStartIndex();
public abstract int GetEndIndex();
public abstract float IndexToPosition(int index);
public abstract int PositionToIndex(float position);
public virtual void DoItemAnimation() { }
public virtual bool IsFullVisibleStart(int index)
{
Vector2 vector2 = CalculatePosition(index);
float position = direction == Direction.Vertical ? vector2.y : vector2.x;
return position + GetOffset() >= 0;
}
public virtual bool IsFullInvisibleStart(int index)
{
Vector2 vector2 = CalculatePosition(index + unit);
float position = direction == Direction.Vertical ? vector2.y : vector2.x;
return position + GetOffset() < 0;
}
public virtual bool IsFullVisibleEnd(int index)
{
Vector2 vector2 = CalculatePosition(index + unit);
float position = direction == Direction.Vertical ? vector2.y : vector2.x;
float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x;
return position + GetOffset() <= viewLength;
}
public virtual bool IsFullInvisibleEnd(int index)
{
Vector2 vector2 = CalculatePosition(index);
float position = direction == Direction.Vertical ? vector2.y : vector2.x;
float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x;
return position + GetOffset() > viewLength;
}
public virtual bool IsVisible(int index)
{
float position, viewLength;
viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x;
Vector2 vector2 = CalculatePosition(index);
position = direction == Direction.Vertical ? vector2.y : vector2.x;
if (position + GetOffset() > 0 && position + GetOffset() <= viewLength)
{
return true;
}
vector2 = CalculatePosition(index + unit);
position = direction == Direction.Vertical ? vector2.y : vector2.x;
if (position + GetOffset() > 0 && position + GetOffset() <= viewLength)
{
return true;
}
return false;
}
protected virtual float GetFitContentSize()
{
float len;
if (direction == Direction.Vertical)
{
len = alignment == Alignment.Center ? Mathf.Min(contentSize.y, viewportSize.y) : viewportSize.y;
}
else
{
len = alignment == Alignment.Center ? Mathf.Min(contentSize.x, viewportSize.x) : viewportSize.x;
}
return len;
}
protected virtual float GetOffset()
{
return direction == Direction.Vertical ? -contentOffset.y + viewportOffset.y : -contentOffset.x + viewportOffset.x;
}
}
public enum Direction
{
Vertical = 1,
Horizontal = 2,
Custom = 10
}
public enum Alignment
{
Left,
Center,
Top
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 959827c7e00900b47844bd9482d829ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,102 @@
using UnityEngine;
namespace AlicizaX.UI.RecyclerView
{
public class LinearLayoutManager : LayoutManager
{
protected float lineHeight;
public LinearLayoutManager() { }
public override Vector2 CalculateContentSize()
{
Vector2 size = viewProvider.CalculateViewSize(0);
lineHeight = direction == Direction.Vertical ? size.y : size.x;
int index = adapter.GetItemCount();
float position;
if (direction == Direction.Vertical)
{
position = index * (lineHeight + spacing.y) - spacing.y;
return new Vector2(contentSize.x, position + padding.y * 2);
}
position = index * (lineHeight + spacing.x) - spacing.x;
return new Vector2(position + padding.x * 2, contentSize.y);
}
public override Vector2 CalculatePosition(int index)
{
float position;
if (direction == Direction.Vertical)
{
position = index * (lineHeight + spacing.y) - ScrollPosition;
return new Vector2(0, position + padding.y);
}
position = index * (lineHeight + spacing.x) - ScrollPosition;
return new Vector2(position + padding.x, 0);
}
public override Vector2 CalculateContentOffset()
{
float len = GetFitContentSize();
if (direction == Direction.Vertical)
{
return new Vector2(0, (len - lineHeight) / 2);
}
return new Vector2((len - lineHeight) / 2, 0);
}
public override Vector2 CalculateViewportOffset()
{
if (direction == Direction.Vertical)
{
return new Vector2(0, (viewportSize.y - lineHeight) / 2);
}
return new Vector2((viewportSize.x - lineHeight) / 2, 0);
}
public override int GetStartIndex()
{
float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x;
int index = Mathf.FloorToInt(ScrollPosition / len);
return Mathf.Max(0, index);
}
public override int GetEndIndex()
{
float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x;
float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x;
int index = Mathf.FloorToInt((ScrollPosition + viewLength) / len);
return Mathf.Min(index, adapter.GetItemCount() - 1);
}
public override float IndexToPosition(int index)
{
if (index < 0 || index >= adapter.GetItemCount()) return 0;
float len, viewLength, position;
if (direction == Direction.Vertical)
{
len = index * (lineHeight + spacing.y);
viewLength = viewportSize.y;
position = len + viewLength > contentSize.y ? contentSize.y - viewportSize.y : len;
}
else
{
len = index * (lineHeight + spacing.x);
viewLength = viewportSize.x;
position = len + viewLength > contentSize.x ? contentSize.x - viewportSize.x : len;
}
return position;
}
public override int PositionToIndex(float position)
{
float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x;
int index = Mathf.RoundToInt(position / len);
return index;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36d9270c4a025694397715d3c16cde7e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,136 @@
using UnityEngine;
namespace AlicizaX.UI.RecyclerView
{
public class MixedLayoutManager : LayoutManager
{
public MixedLayoutManager() { }
public override Vector2 CalculateContentSize()
{
int index = adapter.GetItemCount();
float position = 0;
for (int i = 0; i < index; i++)
{
position += GetLength(i);
}
return direction == Direction.Vertical ?
new Vector2(contentSize.x, position - spacing.y + padding.y * 2) :
new Vector2(position - spacing.x + padding.x * 2, contentSize.y);
}
public override Vector2 CalculatePosition(int index)
{
// TODO 优化点,将 position 定义成全局变量
float position = 0;
for (int i = 0; i < index; i++)
{
position += GetLength(i);
}
position -= ScrollPosition;
return direction == Direction.Vertical ? new Vector2(0, position + padding.y) : new Vector2(position + padding.x, 0);
}
public override Vector2 CalculateContentOffset()
{
Vector2 size = viewProvider.CalculateViewSize(0);
float len = GetFitContentSize();
if (direction == Direction.Vertical)
{
return new Vector2(0, (len - size.y) / 2);
}
return new Vector2((len - size.x) / 2, 0);
}
public override Vector2 CalculateViewportOffset()
{
Vector2 size = viewProvider.CalculateViewSize(0);
if (direction == Direction.Vertical)
{
return new Vector2(0, (viewportSize.y - size.y) / 2);
}
return new Vector2((viewportSize.x - size.x) / 2, 0);
}
public override int GetStartIndex()
{
float position = 0;
float contentPosition = ScrollPosition;
int itemCount = adapter.GetItemCount();
for (int i = 0; i < itemCount; i++)
{
position += GetLength(i);
if (position > contentPosition)
{
return Mathf.Max(0, i);
}
}
return 0;
}
public override int GetEndIndex()
{
float position = 0;
float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x;
int itemCount = adapter.GetItemCount();
for (int i = 0; i < itemCount; i++)
{
position += GetLength(i);
if (position > ScrollPosition + viewLength)
{
return Mathf.Min(i, adapter.GetItemCount() - 1); ;
}
}
return itemCount - 1;
}
private float GetLength(int index)
{
Vector2 size = viewProvider.CalculateViewSize(index);
if (index < adapter.GetItemCount() - 1)
{
size += spacing;
}
float len = direction == Direction.Vertical ? size.y : size.x;
return len;
}
public override float IndexToPosition(int index)
{
Vector2 position = CalculatePosition(index);
if (direction == Direction.Vertical)
{
position.y = Mathf.Max(0, position.y);
position.y = Mathf.Min(position.y, contentSize.y - viewportSize.y);
return position.y;
}
else
{
position.x = Mathf.Max(0, position.x);
position.x = Mathf.Min(position.x, contentSize.x - viewportSize.x);
return position.x;
}
}
public override int PositionToIndex(float position)
{
float len = 0;
int itemCount = adapter.GetItemCount();
for (int i = 0; i < itemCount; i++)
{
len += GetLength(i);
if (len >= position)
{
return i;
}
}
return 0;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d296ceb3ab7786f43a94281a728d5205
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace AlicizaX.UI.RecyclerView
{
public class PageLayoutManager : LinearLayoutManager
{
private float minScale;
public PageLayoutManager(float minScale = 0.9f)
{
this.minScale = minScale;
}
public override Vector2 CalculateContentSize()
{
Vector2 size = viewProvider.CalculateViewSize(0);
lineHeight = direction == Direction.Vertical ? size.y : size.x;
int index = adapter.GetItemCount();
float position;
if (direction == Direction.Vertical)
{
position = index * (lineHeight + spacing.y) - spacing.y;
position += viewportSize.y - lineHeight;
return new Vector2(contentSize.x, position + padding.y * 2);
}
position = index * (lineHeight + spacing.x) - spacing.x;
position += viewportSize.x - lineHeight;
return new Vector2(position + padding.x * 2, contentSize.y);
}
public override Vector2 CalculatePosition(int index)
{
float position;
if (direction == Direction.Vertical)
{
position = index * (lineHeight + spacing.y) - ScrollPosition;
return new Vector2(0, position + padding.y);
}
position = index * (lineHeight + spacing.x) - ScrollPosition;
return new Vector2(position + padding.x, 0);
}
public override Vector2 CalculateContentOffset()
{
return Vector2.zero;
}
public override Vector2 CalculateViewportOffset()
{
return Vector2.zero;
}
protected override float GetOffset()
{
float offset = direction == Direction.Vertical ? viewportSize.y - lineHeight : viewportSize.x - lineHeight;
return offset / 2;
}
public override int PositionToIndex(float position)
{
float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x;
float pos = IndexToPosition(recyclerView.CurrentIndex);
// 根据是前划还是后划,来加减偏移量
int index = position > pos ? Mathf.RoundToInt(position / len + 0.25f) : Mathf.RoundToInt(position / len - 0.25f);
return index;
}
public override void DoItemAnimation()
{
List<ViewHolder> viewHolders = viewProvider.ViewHolders;
for (int i = 0; i < viewHolders.Count; i++)
{
float viewPos = direction == Direction.Vertical ?
-viewHolders[i].RectTransform.anchoredPosition.y :
viewHolders[i].RectTransform.anchoredPosition.x;
float scale = 1 - Mathf.Min(Mathf.Abs(viewPos) * 0.0006f, 1f);
scale = Mathf.Max(scale, minScale);
viewHolders[i].RectTransform.localScale = Vector3.one * scale;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4fccdae6dae870c42bc27f435a94e621
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a22aa56c3da6d724e97aafd9f3e7ed67
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
public interface IMixedObjectFactory<T> where T : class
{
T Create(string typeName);
void Destroy(string typeName, T obj);
void Reset(string typeName, T obj);
bool Validate(string typeName, T obj);
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 30f89ee61eb3f0949b5300bd9b7cc577
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
using System;
public interface IMixedObjectPool<T> : IDisposable where T : class
{
T Allocate(string typeName);
void Free(string typeName, T obj);
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3f4c13a4827ebad4a9ff08e636fbc67e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,28 @@
public interface IObjectFactory<T> where T : class
{
/// <summary>
/// 创建对象
/// </summary>
/// <returns></returns>
T Create();
/// <summary>
/// 销毁对象
/// </summary>
/// <param name="obj"></param>
void Destroy(T obj);
/// <summary>
/// 重置对象
/// </summary>
/// <param name="obj"></param>
void Reset(T obj);
/// <summary>
/// 验证对象
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
bool Validate(T obj);
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 77a642084db01624c8e5876605195d49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,23 @@
using System;
public interface IObjectPool : IDisposable
{
/// <summary>
/// 从池子中分配一个可用对象,没有的话就创建一个
/// </summary>
/// <returns></returns>
object Allocate();
/// <summary>
/// 将对象回收到池子中去,如果池中的对象数量已经超过了 maxSize则直接销毁该对象
/// </summary>
/// <param name="obj"></param>
void Free(object obj);
}
public interface IObjectPool<T> : IObjectPool, IDisposable where T : class
{
new T Allocate();
void Free(T obj);
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dd12164a5eea20e41bf7f3b7704f4b33
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,5 @@
public interface IPooledObject
{
void Free();
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b174ccb64b3938c449d4a69a3262d8d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,101 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
public class MixedObjectPool<T> : IMixedObjectPool<T> where T : class
{
private const int DEFAULT_MAX_SIZE_PER_TYPE = 10;
private readonly ConcurrentDictionary<string, List<T>> entries;
private readonly ConcurrentDictionary<string, int> typeSize;
private readonly IMixedObjectFactory<T> factory;
private int defaultMaxSizePerType;
public MixedObjectPool(IMixedObjectFactory<T> factory) : this(factory, DEFAULT_MAX_SIZE_PER_TYPE)
{
}
public MixedObjectPool(IMixedObjectFactory<T> factory, int defaultMaxSizePerType)
{
this.factory = factory;
this.defaultMaxSizePerType = defaultMaxSizePerType;
if (defaultMaxSizePerType <= 0)
{
throw new ArgumentException("The maxSize must be greater than 0.");
}
entries = new ConcurrentDictionary<string, List<T>>();
typeSize = new ConcurrentDictionary<string, int>();
}
public T Allocate(string typeName)
{
if (entries.TryGetValue(typeName, out List<T> list) && list.Count > 0)
{
T obj = list[0];
list.RemoveAt(0);
return obj;
}
return factory.Create(typeName);
}
public void Free(string typeName, T obj)
{
if (obj == null) return;
if (!factory.Validate(typeName, obj))
{
factory.Destroy(typeName, obj);
return;
}
int maxSize = GetMaxSize(typeName);
List<T> list = entries.GetOrAdd(typeName, n => new List<T>());
if (list.Count >= maxSize)
{
factory.Destroy(typeName, obj);
return;
}
factory.Reset(typeName, obj);
list.Add(obj);
}
public int GetMaxSize(string typeName)
{
if (typeSize.TryGetValue(typeName, out int size))
{
return size;
}
return defaultMaxSizePerType;
}
public void SetMaxSize(string typeName, int value)
{
typeSize.AddOrUpdate(typeName, value, (key, oldValue) => value);
}
protected virtual void Clear()
{
foreach (var kv in entries)
{
string typeName = kv.Key;
List<T> list = kv.Value;
if (list == null || list.Count <= 0) continue;
list.ForEach(e => factory.Destroy(typeName, e));
list.Clear();
}
entries.Clear();
typeSize.Clear();
}
public void Dispose()
{
Clear();
GC.SuppressFinalize(this);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0a62e68f43eac4b419140191eb09ea56
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,108 @@
using System;
using System.Threading;
public class ObjectPool<T> : IObjectPool<T> where T : class
{
private int maxSize;
private int initialSize;
protected readonly T[] entries = null;
protected readonly IObjectFactory<T> factory;
public ObjectPool(IObjectFactory<T> factory) : this(factory, Environment.ProcessorCount * 2)
{
}
public ObjectPool(IObjectFactory<T> factory, int maxSize) : this(factory, 0, maxSize)
{
}
public ObjectPool(IObjectFactory<T> factory, int initialSize, int maxSize)
{
this.factory = factory;
this.initialSize = initialSize;
this.maxSize = maxSize;
this.entries = new T[maxSize];
if (maxSize < initialSize)
{
throw new ArgumentException("The maxSize must be greater than or equal to the initialSize.");
}
for (int i = 0; i < initialSize; i++)
{
entries[i] = factory.Create();
}
}
public int MaxSize => maxSize;
public int InitialSize => initialSize;
public virtual T Allocate()
{
for (var i = 0; i < entries.Length; i++)
{
T value = entries[i];
if (value == null) continue;
if (Interlocked.CompareExchange(ref entries[i], null, value) == value)
{
return value;
}
}
return factory.Create();
}
public virtual void Free(T obj)
{
if (obj == null) return;
if (!factory.Validate(obj))
{
factory.Destroy(obj);
return;
}
factory.Reset(obj);
for (var i = 0; i < entries.Length; i++)
{
if (Interlocked.CompareExchange(ref entries[i], obj, null) == null)
{
return;
}
}
factory.Destroy(obj);
}
object IObjectPool.Allocate()
{
return Allocate();
}
public void Free(object obj)
{
Free((T)obj);
}
protected virtual void Clear()
{
for (var i = 0; i < entries.Length; i++)
{
var value = Interlocked.Exchange(ref entries[i], null);
if (value != null)
{
factory.Destroy(value);
}
}
}
public void Dispose()
{
Clear();
GC.SuppressFinalize(this);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 30999e5e03e2b434996100b09960468f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,36 @@
using UnityEngine;
public class UnityComponentFactory<T> : IObjectFactory<T> where T : Component
{
private T template;
private Transform parent;
public UnityComponentFactory(T template, Transform parent)
{
this.template = template;
this.parent = parent;
}
public T Create()
{
T obj = Object.Instantiate(template, parent);
return obj;
}
public void Destroy(T obj)
{
Object.Destroy(obj.gameObject);
}
public void Reset(T obj)
{
obj.gameObject.SetActive(false);
obj.gameObject.transform.position = Vector3.zero;
obj.gameObject.transform.rotation = Quaternion.identity;
}
public bool Validate(T obj)
{
return true;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3e2e3a0444783a7469d494ad630dc705
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,35 @@
using UnityEngine;
public class UnityGameObjectFactory : IObjectFactory<GameObject>
{
protected GameObject template;
protected Transform parent;
public UnityGameObjectFactory(GameObject template, Transform parent)
{
this.template = template;
this.parent = parent;
}
public virtual GameObject Create()
{
return Object.Instantiate(template, parent);
}
public virtual void Reset(GameObject obj)
{
obj.SetActive(false);
obj.transform.position = Vector3.zero;
obj.transform.rotation = Quaternion.identity;
}
public virtual void Destroy(GameObject obj)
{
Object.Destroy(obj);
}
public virtual bool Validate(GameObject obj)
{
return true;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7373bce96f2a515499c52b060ad9e01e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,61 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UnityMixedComponentFactory<T> : IMixedObjectFactory<T> where T : Component
{
protected T template;
protected Transform parent;
protected List<T> list;
private Dictionary<string, T> dict = new();
public UnityMixedComponentFactory(T template, Transform parent)
{
this.template = template;
this.parent = parent;
}
public UnityMixedComponentFactory(List<T> list, Transform parent)
{
this.list = list;
this.parent = parent;
foreach (var data in list)
{
dict[data.name] = data;
}
}
public UnityMixedComponentFactory(Dictionary<string, T> dict, Transform parent)
{
this.dict = dict;
this.parent = parent;
}
public T Create(string typeName)
{
T obj = Object.Instantiate(dict[typeName], parent);
obj.transform.position = Vector3.zero;
obj.transform.rotation = Quaternion.identity;
return obj;
}
public void Destroy(string typeName, T obj)
{
Object.Destroy(obj.gameObject);
}
public void Reset(string typeName, T obj)
{
obj.gameObject.SetActive(false);
obj.transform.position = Vector3.zero;
obj.transform.rotation = Quaternion.identity;
}
public bool Validate(string typeName, T obj)
{
return true;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5c2d58d006be3fc458b8c6761d76bc88
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,40 @@
using UnityEngine;
public class UnityMixedGameObjectFactory : IMixedObjectFactory<GameObject>
{
protected GameObject template;
protected Transform parent;
public UnityMixedGameObjectFactory(GameObject template, Transform parent)
{
this.template = template;
this.parent = parent;
}
public GameObject Create(string typeName)
{
GameObject obj = Object.Instantiate(template, parent);
GameObject model = Object.Instantiate(Resources.Load<GameObject>("ObjectPools/" + typeName), obj.transform);
model.transform.position = Vector3.zero;
model.transform.rotation = Quaternion.identity;
return obj;
}
public void Destroy(string typeName, GameObject obj)
{
Object.Destroy(obj);
}
public void Reset(string typeName, GameObject obj)
{
obj.SetActive(false);
obj.transform.position = Vector3.zero;
obj.transform.rotation = Quaternion.identity;
}
public bool Validate(string typeName, GameObject obj)
{
return true;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 16c8995b85215c6458119d581eea60b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,369 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace AlicizaX.UI.RecyclerView
{
public class RecyclerView : MonoBehaviour
{
[SerializeField] private Direction direction;
public Direction Direction
{
get => direction;
set => direction = value;
}
[SerializeField] private Alignment alignment;
public Alignment Alignment
{
get => alignment;
set => alignment = value;
}
[SerializeField] private Vector2 spacing;
public Vector2 Spacing
{
get => spacing;
set => spacing = value;
}
[SerializeField] private Vector2 padding;
public Vector2 Padding
{
get => padding;
set => padding = value;
}
[SerializeField] private bool scroll;
public bool Scroll
{
get => scroll;
set => scroll = value;
}
[SerializeField] private bool snap;
public bool Snap
{
get => snap;
set => snap = value & scroll;
}
[SerializeField, Range(1f, 50f)] private float scrollSpeed = 7f;
public float ScrollSpeed
{
get => scrollSpeed;
set => scrollSpeed = value;
}
[SerializeField, Range(10f, 50f)] private float wheelSpeed = 30f;
public float WheeelSpeed
{
get => wheelSpeed;
set => wheelSpeed = value;
}
[SerializeField] private ViewHolder[] templates;
public ViewHolder[] Templates
{
get => templates;
set => templates = value;
}
private ViewProvider viewProvider;
private LayoutManager layoutManager;
private Scroller scroller;
private Scrollbar scrollbar;
private int startIndex, endIndex;
private int currentIndex;
public int CurrentIndex
{
get => currentIndex;
set => currentIndex = value;
}
public bool CanScroll => true;
private RectTransform content;
public RectTransform Content
{
get
{
if (content == null)
{
content = transform.GetChild(0).GetComponent<RectTransform>();
}
return content;
}
}
public ViewProvider ViewProvider
{
get
{
viewProvider ??= templates.Length > 1 ? new MixedViewProvider(this, templates) : new SimpleViewProvider(this, templates);
return viewProvider;
}
}
public Scroller Scroller
{
get
{
if (scroller == null)
{
if (scroll)
{
scroller = gameObject.AddComponent<Scroller>();
ConfigScroller();
}
}
return scroller;
}
}
public Scrollbar Scrollbar
{
get
{
if (scrollbar == null)
{
scrollbar = GetComponentInChildren<Scrollbar>();
if (scrollbar != null)
{
scrollbar.gameObject.SetActive(scroll);
scrollbar.onValueChanged.AddListener(OnScrollbarChanged);
scrollbar.gameObject.AddComponent<ScrollbarEx>().OnDragEnd = OnScrollbarDragEnd;
}
}
return scrollbar;
}
}
public IAdapter Adapter { get; set; }
public LayoutManager LayoutManager => layoutManager;
public Action<int> OnIndexChanged;
public Action OnScrollValueChanged;
private void OnValidate()
{
if (scroller != null)
{
scroller.ScrollSpeed = scrollSpeed;
scroller.WheelSpeed = wheelSpeed;
}
}
private void OnScrollChanged(float pos)
{
layoutManager.UpdateLayout();
if (Scrollbar != null)
{
Scrollbar.SetValueWithoutNotify(pos / Scroller.MaxPosition);
}
if (layoutManager.IsFullInvisibleStart(startIndex))
{
viewProvider.RemoveViewHolder(startIndex);
startIndex += layoutManager.Unit;
}
else if (layoutManager.IsFullVisibleStart(startIndex))
{
if (startIndex == 0)
{
// TODO Do something, eg: Refresh
}
else
{
startIndex -= layoutManager.Unit;
viewProvider.CreateViewHolder(startIndex);
}
}
if (layoutManager.IsFullInvisibleEnd(endIndex))
{
viewProvider.RemoveViewHolder(endIndex);
endIndex -= layoutManager.Unit;
}
else if (layoutManager.IsFullVisibleEnd(endIndex))
{
if (endIndex >= viewProvider.GetItemCount() - layoutManager.Unit)
{
// TODO Do something, eg: Load More
}
else
{
endIndex += layoutManager.Unit;
viewProvider.CreateViewHolder(endIndex);
}
}
// 使用滚动条快速定位时,刷新整个列表
if (!layoutManager.IsVisible(startIndex) || !layoutManager.IsVisible(endIndex))
{
Refresh();
}
layoutManager.DoItemAnimation();
OnScrollValueChanged?.Invoke();
}
private void OnMoveStoped()
{
if (Snap)
{
SnapTo();
}
}
private void OnScrollbarChanged(float ratio)
{
Scroller.ScrollToRatio(ratio);
}
private void OnScrollbarDragEnd()
{
if (Scroller.Position < Scroller.MaxPosition)
{
if (Snap)
{
SnapTo();
}
}
}
public void Reset()
{
viewProvider?.Reset();
if (scroller != null)
{
scroller.Position = 0;
}
if (scrollbar != null)
{
scrollbar.SetValueWithoutNotify(0);
}
}
public void SetLayoutManager(LayoutManager layoutManager)
{
this.layoutManager = layoutManager;
ViewProvider.Adapter = Adapter;
ViewProvider.LayoutManager = layoutManager;
this.layoutManager.RecyclerView = this;
this.layoutManager.Adapter = Adapter;
this.layoutManager.ViewProvider = viewProvider;
this.layoutManager.Direction = direction;
this.layoutManager.Alignment = alignment;
this.layoutManager.Spacing = spacing;
this.layoutManager.Padding = padding;
this.layoutManager.CanScroll = CanScroll;
}
public void SetScroller(Scroller newScroller)
{
if (!scroll) return;
if (scroller != null)
{
scroller.OnValueChanged.RemoveListener(OnScrollChanged);
scroller.OnMoveStoped.RemoveListener(OnMoveStoped);
Destroy(scroller);
}
scroller = newScroller;
ConfigScroller();
}
private void ConfigScroller()
{
scroller.ScrollSpeed = scrollSpeed;
scroller.WheelSpeed = wheelSpeed;
scroller.Snap = Snap;
scroller.OnValueChanged.AddListener(OnScrollChanged);
scroller.OnMoveStoped.AddListener(OnMoveStoped);
}
public void Refresh()
{
ViewProvider.Clear();
startIndex = layoutManager.GetStartIndex();
endIndex = layoutManager.GetEndIndex();
for (int i = startIndex; i <= endIndex; i += layoutManager.Unit)
{
ViewProvider.CreateViewHolder(i);
}
layoutManager.DoItemAnimation();
}
public void RequestLayout()
{
layoutManager.SetContentSize();
if (Scroller == null) return;
Scroller.Direction = direction;
Scroller.ViewSize = layoutManager.ViewportSize;
Scroller.ContentSize = layoutManager.ContentSize;
if (Scrollbar != null && Scroller.ContentSize != Vector2.zero)
{
if ((direction == Direction.Vertical && layoutManager.ContentSize.y <= layoutManager.ViewportSize.y) ||
(direction == Direction.Horizontal && layoutManager.ContentSize.x <= layoutManager.ViewportSize.x) ||
(direction == Direction.Custom))
{
Scrollbar.gameObject.SetActive(false);
}
else
{
Scrollbar.gameObject.SetActive(true);
Scrollbar.direction = direction == Direction.Vertical ?
Scrollbar.Direction.TopToBottom :
Scrollbar.Direction.LeftToRight;
Scrollbar.size = direction == Direction.Vertical ?
Scroller.ViewSize.y / Scroller.ContentSize.y :
Scroller.ViewSize.x / Scroller.ContentSize.x;
}
}
}
public float GetScrollPosition()
{
return Scroller ? Scroller.Position : 0;
}
public void ScrollTo(int index, bool smooth = false)
{
if (!scroll) return;
Scroller.ScrollTo(layoutManager.IndexToPosition(index), smooth);
if (!smooth)
{
Refresh();
}
index %= Adapter.GetItemCount();
index = index < 0 ? Adapter.GetItemCount() + index : index;
if (currentIndex != index)
{
currentIndex = index;
OnIndexChanged?.Invoke(currentIndex);
}
}
private void SnapTo()
{
var index = layoutManager.PositionToIndex(GetScrollPosition());
ScrollTo(index, true);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7efd8e83d2092b347952108134dc37eb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b79ba677d829b904794e3b4a09cb67c2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,61 @@
using UnityEngine;
using UnityEngine.EventSystems;
namespace AlicizaX.UI.RecyclerView
{
public class CircleScroller : Scroller
{
private Vector2 centerPosition;
private void Awake()
{
RectTransform rectTransform = GetComponent<RectTransform>();
Vector2 position = transform.position;
Vector2 size = rectTransform.sizeDelta;
if (rectTransform.pivot.x == 0)
{
centerPosition.x = position.x + size.x / 2f;
}
else if (rectTransform.pivot.x == 0.5f)
{
centerPosition.x = position.x;
}
else
{
centerPosition.x = position.x - size.x / 2f;
}
if (rectTransform.pivot.y == 0)
{
centerPosition.y = position.y + size.y / 2f;
}
else if (rectTransform.pivot.y == 0.5f)
{
centerPosition.y = position.y;
}
else
{
centerPosition.y = position.y - size.y / 2f;
}
}
internal override float GetDelta(PointerEventData eventData)
{
float delta;
if (Mathf.Abs(eventData.delta.x) > Mathf.Abs(eventData.delta.y))
{
delta = eventData.position.y > centerPosition.y ? eventData.delta.x : -eventData.delta.x;
}
else
{
delta = eventData.position.x < centerPosition.x ? eventData.delta.y : -eventData.delta.y;
}
return delta * 0.1f;
}
protected override void Elastic()
{
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 85235544e637a4a4f93ad88a5cfdac0a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,15 @@
using UnityEngine.Events;
namespace AlicizaX.UI.RecyclerView
{
public interface IScroller
{
float Position { get; set; }
void ScrollTo(float position, bool smooth = false);
}
public class ScrollerEvent : UnityEvent<float> { }
public class MoveStopEvent : UnityEvent { }
public class DraggingEvent : UnityEvent<bool> { }
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3928a884003099344b231a9d554dc5da
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,80 @@
using System;
using PrimeTween;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace AlicizaX.UI.RecyclerView
{
public class ScrollbarEx : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IPointerEnterHandler, IPointerExitHandler
{
private RectTransform handle;
private Scrollbar scrollbar;
public Action OnDragEnd;
private bool dragging;
private bool hovering;
private void Awake()
{
scrollbar = GetComponent<Scrollbar>();
handle = scrollbar.handleRect;
}
public void OnBeginDrag(PointerEventData eventData)
{
dragging = true;
}
public void OnEndDrag(PointerEventData eventData)
{
dragging = false;
if (!hovering)
{
if (scrollbar.direction == Scrollbar.Direction.TopToBottom ||
scrollbar.direction == Scrollbar.Direction.BottomToTop)
{
Tween.ScaleX(handle, 1f, 0.2f);
}
else
{
Tween.ScaleY(handle, 1f, 0.2f);
}
}
OnDragEnd?.Invoke();
}
public void OnPointerEnter(PointerEventData eventData)
{
hovering = true;
if (scrollbar.direction == Scrollbar.Direction.TopToBottom ||
scrollbar.direction == Scrollbar.Direction.BottomToTop)
{
Tween.ScaleX(handle, 2f, 0.2f);
}
else
{
Tween.ScaleY(handle, 2f, 0.2f);
}
}
public void OnPointerExit(PointerEventData eventData)
{
hovering = false;
if (!dragging)
{
if (scrollbar.direction == Scrollbar.Direction.TopToBottom ||
scrollbar.direction == Scrollbar.Direction.BottomToTop)
{
Tween.ScaleX(handle, 1f, 0.2f);
}
else
{
Tween.ScaleY(handle, 1f, 0.2f);
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 682cfe39b0fffe544be8d5c11eb369e5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,245 @@
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
namespace AlicizaX.UI.RecyclerView
{
public class Scroller : MonoBehaviour, IScroller, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler
{
protected float position;
public float Position { get => position; set => position = value; }
protected float velocity;
public float Velocity => velocity;
protected Direction direction;
public Direction Direction
{
get => direction;
set => direction = value;
}
/// <summary>
/// 内容所需要大小
/// </summary>
protected Vector2 contentSize;
public Vector2 ContentSize
{
get => contentSize;
set => contentSize = value;
}
/// <summary>
/// 所在 View 的真实大小
/// </summary>
protected Vector2 viewSize;
public Vector2 ViewSize
{
get => viewSize;
set => viewSize = value;
}
protected float scrollSpeed = 1f;
public float ScrollSpeed
{
get => scrollSpeed;
set => scrollSpeed = value;
}
protected float wheelSpeed = 30f;
public float WheelSpeed
{
get => wheelSpeed;
set => wheelSpeed = value;
}
protected bool snap;
public bool Snap
{
get => snap;
set => snap = value;
}
protected ScrollerEvent scrollerEvent = new();
protected MoveStopEvent moveStopEvent = new();
protected DraggingEvent draggingEvent = new();
public float MaxPosition => direction == Direction.Vertical ?
Mathf.Max(contentSize.y - viewSize.y, 0) :
Mathf.Max(contentSize.x - viewSize.x, 0);
public float ViewLength => direction == Direction.Vertical ? viewSize.y : viewSize.x;
public ScrollerEvent OnValueChanged { get => scrollerEvent; set => scrollerEvent = value; }
public MoveStopEvent OnMoveStoped { get => moveStopEvent; set => moveStopEvent = value; }
public DraggingEvent OnDragging { get => draggingEvent; set => draggingEvent = value; }
// 停止滑动的时间,但此时并未释放鼠标按键
private float dragStopTime = 0f;
public virtual void ScrollTo(float position, bool smooth = false)
{
if (position == this.position) return;
if (!smooth)
{
this.position = position;
OnValueChanged?.Invoke(this.position);
}
else
{
StopAllCoroutines();
StartCoroutine(MoveTo(position));
}
}
public virtual void ScrollToRatio(float ratio)
{
ScrollTo(MaxPosition * ratio, false);
}
public void OnBeginDrag(PointerEventData eventData)
{
OnDragging?.Invoke(true);
StopAllCoroutines();
}
public void OnEndDrag(PointerEventData eventData)
{
Inertia();
Elastic();
OnDragging?.Invoke(false);
}
public void OnDrag(PointerEventData eventData)
{
dragStopTime = Time.time;
velocity = GetDelta(eventData);
position += velocity;
OnValueChanged?.Invoke(position);
}
public void OnScroll(PointerEventData eventData)
{
StopAllCoroutines();
float rate = GetScrollRate() * wheelSpeed;
velocity = direction == Direction.Vertical ? -eventData.scrollDelta.y * rate : eventData.scrollDelta.x * rate;
position += velocity;
OnValueChanged?.Invoke(position);
Elastic();
}
internal virtual float GetDelta(PointerEventData eventData)
{
float rate = GetScrollRate();
return direction == Direction.Vertical ? eventData.delta.y * rate : -eventData.delta.x * rate;
}
private float GetScrollRate()
{
float rate = 1f;
if (position < 0)
{
rate = Mathf.Max(0, 1 - (Mathf.Abs(position) / ViewLength));
}
else if (position > MaxPosition)
{
rate = Mathf.Max(0, 1 - (Mathf.Abs(position - MaxPosition) / ViewLength));
}
return rate;
}
/// <summary>
/// 松手时的惯性滑动
/// </summary>
protected virtual void Inertia()
{
// 松手时的时间 离 停止滑动的时间 超过一定时间,则认为此次惯性滑动无效
if (!snap && (Time.time - dragStopTime) > 0.01f) return;
if (Mathf.Abs(velocity) > 0.1f)
{
StopAllCoroutines();
StartCoroutine(InertiaTo());
}
else
{
OnMoveStoped?.Invoke();
}
}
/// <summary>
/// 滑动到顶部/底部之后,松手时回弹
/// </summary>
protected virtual void Elastic()
{
if (position < 0)
{
StopAllCoroutines();
StartCoroutine(ElasticTo(0));
}
else if (position > MaxPosition)
{
StopAllCoroutines();
StartCoroutine(ElasticTo(MaxPosition));
}
}
IEnumerator InertiaTo()
{
float timer = 0f;
float p = position;
float v = velocity > 0 ? Mathf.Min(velocity, 100) : Mathf.Max(velocity, -100);
float duration = snap ? 0.1f : 1f;
while (timer < duration)
{
float y = (float)EaseUtil.EaseOutCirc(timer) * 40;
timer += Time.deltaTime;
position = p + y * v;
Elastic();
OnValueChanged?.Invoke(position);
yield return new WaitForEndOfFrame();
}
OnMoveStoped?.Invoke();
}
IEnumerator ElasticTo(float targetPos)
{
yield return ToPosition(targetPos, 7);
}
IEnumerator MoveTo(float targetPos)
{
yield return ToPosition(targetPos, scrollSpeed);
}
IEnumerator ToPosition(float targetPos, float speed)
{
float startPos = position;
float time = Time.deltaTime;
while (Mathf.Abs(targetPos - position) > 0.1f)
{
position = Mathf.Lerp(startPos, targetPos, time * speed);
OnValueChanged?.Invoke(position);
time += Time.deltaTime;
yield return new WaitForEndOfFrame();
}
position = targetPos;
OnValueChanged?.Invoke(position);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7b7de4cb3a1546e4a9ade6b8dbf8af92
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 194b0a1eb83729844850e1873dbc0118
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 72a222ebb6ae56346b65b78fa3d60143
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,19 @@
using AlicizaX.UI.RecyclerView;
using TMPro;
public sealed class SimpleViewHolder : ViewHolder
{
private TMP_Text simpleText;
public override void FindUI()
{
simpleText = transform.Find("SimpleText").GetComponent<TMP_Text>();
}
public override void BindViewData<T>(T data)
{
string text = data as string;
simpleText.text = text;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 846ae5a2cd8b619459ecbadfc91e58f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,59 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace AlicizaX.UI.RecyclerView
{
public abstract class ViewHolder : MonoBehaviour
{
private bool isStarted;
private RectTransform rectTransform;
public RectTransform RectTransform
{
get
{
if (rectTransform == null)
{
rectTransform = GetComponent<RectTransform>();
}
return rectTransform;
}
private set
{
rectTransform = value;
}
}
public string Name { get; set; }
public int Index { get; set; }
public Vector2 SizeDelta => RectTransform.sizeDelta;
public virtual void OnStart()
{
if (!isStarted)
{
isStarted = true;
FindUI();
}
}
public virtual void OnStop() { }
public abstract void FindUI();
public abstract void BindViewData<T>(T data);
public virtual void BindItemClick<T>(T data, Action<T> action)
{
if (TryGetComponent(out Button button))
{
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() => action?.Invoke(data));
}
}
public virtual void BindChoiceState(bool state) { }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9d43d46f3524d814fbb2fb2f03e3c610
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 789e3dbb330a44f42b73d50f128888e4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace AlicizaX.UI.RecyclerView
{
public class MixedViewProvider : ViewProvider
{
[SerializeField] private ViewHolder chatLeftViewHolder;
[SerializeField] private ViewHolder chatRightViewHolder;
private IMixedObjectPool<ViewHolder> objectPool;
private Dictionary<string, ViewHolder> dict = new();
public MixedViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates)
{
foreach (var template in templates)
{
dict[template.name] = template;
}
UnityMixedComponentFactory<ViewHolder> factory = new(dict, recyclerView.Content);
objectPool = new MixedObjectPool<ViewHolder>(factory);
}
public override ViewHolder GetTemplate(string viewName)
{
if (templates == null || templates.Length == 0)
{
throw new NullReferenceException("ViewProvider templates can not null or empty.");
}
return dict[viewName];
}
public override ViewHolder[] GetTemplates()
{
if (templates == null || templates.Length == 0)
{
throw new NullReferenceException("ViewProvider templates can not null or empty.");
}
return dict.Values.ToArray();
}
public override ViewHolder Allocate(string viewName)
{
var viewHolder = objectPool.Allocate(viewName);
viewHolder.gameObject.SetActive(true);
return viewHolder;
}
public override void Free(string viewName, ViewHolder viewHolder)
{
objectPool.Free(viewName, viewHolder);
}
public override void Reset()
{
Clear();
objectPool.Dispose();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ef7ee3380d8e82c4f832f1e2535fe795
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,51 @@
using System;
namespace AlicizaX.UI.RecyclerView
{
public sealed class SimpleViewProvider : ViewProvider
{
private readonly IObjectPool<ViewHolder> objectPool;
public SimpleViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates)
{
UnityComponentFactory<ViewHolder> factory = new(GetTemplate(), recyclerView.Content);
objectPool = new ObjectPool<ViewHolder>(factory, 100);
}
public override ViewHolder GetTemplate(string viewName = "")
{
if (templates == null || templates.Length == 0)
{
throw new NullReferenceException("ViewProvider templates can not null or empty.");
}
return templates[0];
}
public override ViewHolder[] GetTemplates()
{
if (templates == null || templates.Length == 0)
{
throw new NullReferenceException("ViewProvider templates can not null or empty.");
}
return templates;
}
public override ViewHolder Allocate(string viewName)
{
var viewHolder = objectPool.Allocate();
viewHolder.gameObject.SetActive(true);
return viewHolder;
}
public override void Free(string viewName, ViewHolder viewHolder)
{
objectPool.Free(viewHolder);
}
public override void Reset()
{
Clear();
objectPool.Dispose();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6100883160a6b841a1d7084de2dcd76
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,131 @@
using System.Collections.Generic;
using UnityEngine;
namespace AlicizaX.UI.RecyclerView
{
/// <summary>
/// 提供和管理 ViewHolder
/// </summary>
public abstract class ViewProvider
{
private readonly List<ViewHolder> viewHolders = new();
public IAdapter Adapter { get; set; }
public LayoutManager LayoutManager { get; set; }
public List<ViewHolder> ViewHolders => viewHolders;
protected RecyclerView recyclerView;
protected ViewHolder[] templates;
public ViewProvider(RecyclerView recyclerView, ViewHolder[] templates)
{
this.recyclerView = recyclerView;
this.templates = templates;
}
public abstract ViewHolder GetTemplate(string viewName);
public abstract ViewHolder[] GetTemplates();
public abstract ViewHolder Allocate(string viewName);
public abstract void Free(string viewName, ViewHolder viewHolder);
public abstract void Reset();
public void CreateViewHolder(int index)
{
for (int i = index; i < index + LayoutManager.Unit; i++)
{
if (i > Adapter.GetItemCount() - 1) break;
string viewName = Adapter.GetViewName(i);
var viewHolder = Allocate(viewName);
viewHolder.OnStart();
viewHolder.Name = viewName;
viewHolder.Index = i;
viewHolders.Add(viewHolder);
LayoutManager.Layout(viewHolder, i);
Adapter.OnBindViewHolder(viewHolder, i);
}
}
public void RemoveViewHolder(int index)
{
for (int i = index; i < index + LayoutManager.Unit; i++)
{
if (i > Adapter.GetItemCount() - 1) break;
int viewHolderIndex = GetViewHolderIndex(i);
if (viewHolderIndex < 0 || viewHolderIndex >= viewHolders.Count) return;
var viewHolder = viewHolders[viewHolderIndex];
viewHolders.RemoveAt(viewHolderIndex);
viewHolder.OnStop();
Free(viewHolder.Name, viewHolder);
}
}
/// <summary>
/// 根据数据的下标获取对应的 ViewHolder
/// </summary>
/// <param name="index">数据的下标</param>
/// <returns></returns>
public ViewHolder GetViewHolder(int index)
{
foreach (var viewHolder in viewHolders)
{
if (viewHolder.Index == index)
{
return viewHolder;
}
}
return null;
}
/// <summary>
/// 根据数据的下标获取 ViewHolder 的下标
/// </summary>
/// <param name="index">数据的下标</param>
/// <returns></returns>
public int GetViewHolderIndex(int index)
{
for (int i = 0; i < viewHolders.Count; i++)
{
if (viewHolders[i].Index == index)
{
return i;
}
}
return -1;
}
public void Clear()
{
foreach (var viewHolder in viewHolders)
{
Free(viewHolder.Name, viewHolder);
}
viewHolders.Clear();
}
/// <summary>
/// 计算 ViewHolder 的尺寸
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public Vector2 CalculateViewSize(int index)
{
Vector2 size = GetTemplate(Adapter.GetViewName(index)).SizeDelta;
return size;
}
public int GetItemCount()
{
return Adapter == null ? 0 : Adapter.GetItemCount();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b1c2cd7d2729fb4488ffd0e25e0fbf43
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: