可拖动的循环列表

基本的可拖动列表

首先想想如何去实现一个可以拖动的列表,因此我们可以在表现层抽象出两种东西:

  • 可以拖动。
  • 含有很多个 Item 格子。

针对这两点我们逐步去实现。

  • 首先我们得创建很多个格子吧,就使用最普通的 image。然后其被包裹在一个 Panel 当中,此处 Panel 也会挂载上 Mask 来做遮罩。
  • 接着就是我们拖动的实现,可以想到 unity 当中有两个关于拖动的接口:IBeginDragHandler, IDragHandler。接着我们需要记录两个变量,分别是:拖动时最后一次鼠标的位置当前鼠标位置距离上一次拖动时的偏移值。我们只需要在 OnBeginDragOnDrag 当中记录上一次拖动鼠标的位置,然后在 OnDrag 当中一直使用得到的偏移值去更新每个 Item 的 transform 即可。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    private Vector2 _lastPosition;
    private float _offset;

    public void OnBeginDrag(PointerEventData eventData)
    {
    _lastPosition = eventData.position;
    }
    public void OnDrag(PointerEventData eventData)
    {
    _offset = eventData.position.y - _lastPosition.y;
    _lastPosition = eventData.position;
    for (int i = 0; i < itemList.Count; i++)
    {
    itemList[i].transform.position += Vector3.up * _offset;
    }
    }
    这样就基本实现了一个可拖动的列表。

循环列表

为了实现无限滑动,其肯定是一个循环的列表。例如在 Mask 遮罩范围内,其是有 n 个格子,那我们实际是会需要 n + 1 个格子,额外的格子是被放在了 Mask 之外,其是来填充滑动时的缺口。我们需要初始化的时候记录一下第一个 Item ui 的 y 轴起始位置每个格子的高度,我们从 offset 的值不一样来分为两种滑动行为。

向上滑:

  • 此时 offset 大于 0。
  • 判断当前第一个格子的 y 是否大于初始位置,如果大于,说明此时最后出现了空缺,需要将列表最后一个格子调整到倒数第二个格子的后面,使用每个格子的高度来定位。
  • 继续判断当前第一个格子的 y 是否大于初始位置加上格子高度,如果大于,说明此时最后空缺的格子已经大于了一格。那么此时需要把第一个格子贴到最后一个格子后面,数据层顺序也是,然后数据层把每个格子向前移一位。

向下滑:

  • 此时 offset 小于 0。
  • 判断当前第一个格子的 y 是否小于初始位置,如果小于,说明此时最前面出现了空缺,需要将列表最后一个格子调整到第一个格子的前面,使用每个格子的高度来定位。
  • 继续判断当前第一个格子的 y 是否小于初始位置减去格子高度,如果小于,说明此时前面空缺的格子已经大于了一格。那么此时需要把最后一个格子贴到第一个格子前面,数据层顺序也是,然后数据层把每个格子向后移一位。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public void OnDrag(PointerEventData eventData)
{
_offset = eventData.position.y - _lastPosition.y;
_lastPosition = eventData.position;
for (int i = 0; i < itemList.Count; i++)
{
itemList[i].transform.position += Vector3.up * _offset;
}
if (_offset > 0)
{
// 向上滑动
if (itemList[0].transform.localPosition.y >= _firstItemY) // 此处是判断第一个物品当前和初始的相对位置
{
// 将最后一个物品贴到倒数第二个的后面
itemList[^1].transform.localPosition = itemList[^2].transform.localPosition - Vector3.up * _size;
}
if (itemList[0].transform.localPosition.y >= _firstItemY + _size)
{
// 如果向上位移已经超出了一个格子,就将第一个 Item 移动到最后一个
Item tmp = itemList[0];
tmp.transform.localPosition = itemList[^1].transform.localPosition - Vector3.up * _size;
// 更新数据层的顺序
for (int i = 1; i < itemList.Count; i++)
{
itemList[i - 1] = itemList[i];
}
itemList[^1] = tmp;
}

} else if (_offset < 0)
{
// 向下滑动
if (itemList[0].transform.localPosition.y <= _firstItemY)
{
itemList[^1].transform.localPosition = itemList[0].transform.localPosition + Vector3.up * _size;
}
if (itemList[0].transform.localPosition.y <= _firstItemY - _size)
{
// 如果向上位移已经超出了一个格子,就将第一个 Item 移动到最后一个
Item tmp = itemList[^1];
tmp.transform.localPosition = itemList[0].transform.localPosition + Vector3.up * _size;
// 更新数据层的顺序
for (int i = itemList.Count - 1; i >= 1; i--)
{
itemList[i] = itemList[i - 1];
}
itemList[0] = tmp;
}
}
}

数据绑定

首先我们要明确一个问题:我们的无限列表表现层显示 n 个格子,实际上是有 n + 1 个格子,那假如我们的数据列表,一共有 m 个(此处 m > n)。那我们也得在表现层做和数据层数量相当的格子吗?这显然是不合理的,Mask 遮罩过后依然会导致其渲染,也就耗费大量的性能时间去渲染无用的格子。因此此处的做法应该是维护一个数据下标的区间,去做一个循环绑定。

数据初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public List<string> data = new();

// 标记当前使用的是哪些数据
private int startIndex = 0;
private int endIndex = 0;

private void Awake()
{
// 数据的字符串列表中一共有十个元素
for (int i = 0; i < 10; i++)
{
data.Add((i + 1).ToString());
}

_size = itemList[0].GetComponent<RectTransform>().rect.size.y;
_firstItemY = itemList[0].transform.localPosition.y;

for (int i = 0; i < itemList.Count; i++)
{
itemList[i].GetComponentInChildren<TextMeshProUGUI>().text = data[i];
}
// 初始化我们维护的下标区间
startIndex = 0;
endIndex = itemList.Count - 2;
}

数据的循环

在滑动过程中,要求调整数据,那么就是去调整我们维护的下标区间。在上面循环列表的例子里面,我们是将此滑动列表向上滑或者向下滑当中分别判断了两次(分别是开始滑动和滑过了一个格子)。比如向上滑,当我们开始滑动的时候,得去显示补到最后一个位置的数字了,那么此时我们就得去更新一下 endIndex 下标。

1
2
3
4
5
6
endIndex++;
if (endIndex > data.Count - 1)
{
endIndex = 0;
}
itemList[^1].GetComponentInChildren<TextMeshProUGUI>().text = data[endIndex];

当我们滑过第一个格子的时候,就证明其已经看不到了,然后就需要调整 startIndex 的下标

1
2
3
4
5
startIndex++;
if (startIndex > data.Count - 1)
{
startIndex = 0;
}

向下滑同理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 开始滑动的时候
startIndex--;
if (startIndex < 0)
{
startIndex = data.Count - 1;
}
itemList[^1].GetComponentInChildren<TextMeshProUGUI>().text = data[startIndex];

// 滑到看不到最上面的格子的时候
endIndex--;
if (endIndex < 0)
{
endIndex = data.Count - 1;
}

此时我们再运行,会发现:列表数据在滑动的时候一直在变。这是由于我们在滑动的时候,其会不断地去刷新那个数据,导致数据项一直在变。此时我们可以新创建一个字段,其标明当前 Item 是否已经滑动过。

1
private bool _isMovedItem = false;

接着做两件事:

  • 在向上滑和向下滑的 if 当中判断当前是不是 !_isMoveItem 的,满足条件才能滑动,并且滑动过后改变其状态,避免二次改变数据。
  • 在滑过一个格子之后,再去改变其状态,使得 _isMoveItem = false,保证下一次又可以滑动。
    此时再运行,我们还会发现一个问题:下标访问的时候报错了
    这个问题是由于,当我们往上滑动了列表,但是没有完全滑过一个格子。上一次我们已经改变了某个 index 对吧,但是此时我们如果往下滑,我们得去判断一下上一次滑动的方向吧,如果上一次是往上的话,得去还原一下其已经改变的 index,因此这里就需要再添加一个字段来存储上一次滑动的方向:
    1
    private bool _sliderUp = false;

在向上滑或者向下滑开始的时候,再去对其进行一个判断,如果上一次是往反方向话就得去还原其 index 状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 向上滑动时
if (!_sliderUp && _isMovedItem)
{
startIndex++;
_isMovedItem = false;
}
_sliderUp = true;

// 向下滑动时
if (_sliderUp && _isMovedItem)
{
endIndex--;
_isMovedItem = false;
}
_sliderUp = false;

自此,数据的绑定已经完成。

目前阶段的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
public void OnDrag(PointerEventData eventData)
{
_offset = eventData.position.y - _lastPosition.y;
_lastPosition = eventData.position;

for (int i = 0; i < itemList.Count; i++)
{
itemList[i].transform.position += Vector3.up * _offset;
}

if (_offset > 0)
{
if (!_sliderUp && _isMovedItem)
{
startIndex++;
_isMovedItem = false;
}
_sliderUp = true;

// 向上滑动
if (itemList[0].transform.localPosition.y >= _firstItemY && !_isMovedItem) // 此处是判断第一个物品当前和初始的相对位置
{
_isMovedItem = true;
// 将最后一个物品贴到倒数第二个的后面
itemList[^1].transform.localPosition = itemList[^2].transform.localPosition - Vector3.up * _size;

endIndex++;
if (endIndex > data.Count - 1)
{
endIndex = 0;
}
itemList[^1].GetComponentInChildren<TextMeshProUGUI>().text = data[endIndex];
}
else
{
if (itemList[0].transform.localPosition.y >= _firstItemY + _size)
{
_isMovedItem = false;
// 如果向上位移已经超出了一个格子,就将第一个 Item 移动到最后一个
Item tmp = itemList[0];
tmp.transform.localPosition = itemList[^1].transform.localPosition - Vector3.up * _size;
// 更新数据层的顺序
for (int i = 1; i < itemList.Count; i++)
{
itemList[i - 1] = itemList[i];
}
itemList[^1] = tmp;

startIndex++;
if (startIndex > data.Count - 1)
{
startIndex = 0;
}
}
}

}
else if (_offset < 0)
{
if (_sliderUp && _isMovedItem)
{
endIndex--;
_isMovedItem = false;
}
_sliderUp = false;

// 向下滑动
if (itemList[0].transform.localPosition.y <= _firstItemY && !_isMovedItem)
{
_isMovedItem = true;
itemList[^1].transform.localPosition = itemList[0].transform.localPosition + Vector3.up * _size;

startIndex--;
if (startIndex < 0)
{
startIndex = data.Count - 1;
}
itemList[^1].GetComponentInChildren<TextMeshProUGUI>().text = data[startIndex];
}
else
{
if (itemList[0].transform.localPosition.y <= _firstItemY - _size)
{
_isMovedItem = false;
// 如果向上位移已经超出了一个格子,就将第一个 Item 移动到最后一个
Item tmp = itemList[^1];
tmp.transform.localPosition = itemList[0].transform.localPosition + Vector3.up * _size;
// 更新数据层的顺序
for (int i = itemList.Count - 1; i >= 1; i--)
{
itemList[i] = itemList[i - 1];
}
itemList[0] = tmp;

endIndex--;
if (endIndex < 0)
{
endIndex = data.Count - 1;
}
}
}
}
}

水平版本

直接上代码,基本思想和垂直一样,注意 Horizontal Layout Group 排布以后记得 disable 或者 remove 掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
public class CustomHorizontalScrollView : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
public int numberCount = 10;
private List<string> data = new();

public RectTransform content;
public List<Item> itemList = new();

private Vector3 _lastPosition;
private float _offset;

// 单个 item 的宽
private float _size;
// 第一个 item 的初始值
private float _firstItemX;

private int _startIndex;
private int _endIndex;

private bool _isMovedItem = false;
private bool _isSliderLeft = false;

private void Awake()
{
for (int i = 0; i < numberCount; i++)
{
data.Add((i + 1).ToString());
}

_size = itemList[0].GetComponent<RectTransform>().rect.size.x;
_firstItemX = itemList[0].transform.localPosition.x;

for (int i = 0; i < itemList.Count; i++)
{
itemList[i].GetComponentInChildren<TextMeshProUGUI>().text = data[i];
}

_startIndex = 0;
_endIndex = itemList.Count - 2;
}


public void OnBeginDrag(PointerEventData eventData)
{
_lastPosition = eventData.position;
}

public void OnDrag(PointerEventData eventData)
{
_offset = eventData.position.x - _lastPosition.x;
_lastPosition = eventData.position;

for (int i = 0; i < itemList.Count; i++)
{
itemList[i].transform.position += Vector3.right * _offset;
}

// 向左移动
if (_offset < 0)
{
if (!_isSliderLeft && _isMovedItem)
{
_isMovedItem = false;
_startIndex++;
}
_isSliderLeft = true;

if (itemList[0].transform.localPosition.x <= _firstItemX && !_isMovedItem)
{
_isMovedItem = true;
// 将最后一个加到倒数第二个的后面
itemList[^1].transform.localPosition = itemList[^2].transform.localPosition + Vector3.right * _size;

_endIndex++;
if (_endIndex > data.Count - 1)
{
_endIndex = 0;
}
itemList[^1].GetComponentInChildren<TextMeshProUGUI>().text = data[_endIndex];
}
else
{
if (itemList[0].transform.localPosition.x <= _firstItemX - _size)
{
_isMovedItem = false;
Item tmp = itemList[0];
tmp.transform.localPosition = itemList[^1].transform.localPosition + Vector3.right * _size;

for (int i = 1; i < itemList.Count; i++)
{
itemList[i - 1] = itemList[i];
}
itemList[^1] = tmp;

_startIndex++;
if (_startIndex > data.Count - 1)
{
_startIndex = 0;
}
}
}

}
else if (_offset > 0) // 向右滑
{
if (_isSliderLeft && _isMovedItem)
{
_isMovedItem = false;
_endIndex--;
}
_isSliderLeft = false;

if (itemList[0].transform.localPosition.x >= _firstItemX && !_isMovedItem)
{
_isMovedItem = true;
// 将最后一个添加到第一个的前面
itemList[^1].transform.localPosition = itemList[0].transform.localPosition - Vector3.right * _size;

_startIndex--;
if (_startIndex < 0)
{
_startIndex = data.Count - 1;
}
itemList[^1].GetComponentInChildren<TextMeshProUGUI>().text = data[_startIndex];
}
else
{
if (itemList[0].transform.localPosition.x >= _firstItemX + _size)
{
_isMovedItem = false;
Item tmp = itemList[^1];
tmp.transform.localPosition = itemList[0].transform.localPosition - Vector3.right * _size;

for (int i = itemList.Count - 1; i >= 1; i--)
{
itemList[i] = itemList[i - 1];
}
itemList[0] = tmp;

_endIndex--;
if (_endIndex < 0)
{
_endIndex = data.Count - 1;
}
}
}
}
}

public void OnEndDrag(PointerEventData eventData)
{

}
}