oynix

于无声处听惊雷,于无色处见繁花

StaggeredAdapter列表适配器

当使用ScrollView显示列表数据时,如果数据很少,一般有多少条数据就会创建多少个ItemView,然后一股脑将这些ItemView挂到ScrollView下的Content下;但是当数据量很大时,一下全部创建时压力就会给足到内存这边,这个时候需要动态复用ItemView,合理使用有限资源。

其实网上已经有一些现成的轮子,对于滑动、复用这些基本需求,都已经实现了。如果只有这些基本需求,就没有必要重复造轮子。但是除了这些基本的需求,还有些动画相关的,所以就需要一个可定制化更高的适配器,来应对层出的需求。

一、简介

1. 支持的功能/效果

  • 复用ItemView支持
  • 多类型ItemView支持
  • ItemView出现动画支持
  • 列表单ItemView刷新支持
  • 列表全ItemView刷新支持
  • 高亮效果支持
  • 滑动动画支持
  • 横向/纵向支持
  • 等宽/等高瀑布流支持

2. 实现原理

原理并不复杂,只是细节多些。想要复用ItemView,无非就是监听ScrollView的滑动状态,根据当前的滑动位置来实时更新各个ItemView的状态。至于滑动动画,则是通过DOTween,改变可滑动区域Content的anchorX或者anchorY坐标来实现。

对于每个列表,ItemView至多只会创建填满一个ScrollView时的数量。这些ItemView交替使用,即可展示所有的Data。

3. ViewHolder

ViewHolder是和ItemView在实例数量上是一一对应的关系,对于创建的每个ItemView,都会创建一个ViewHolder与其关联。ViewHolder,顾名思义,其中维护着该ItemView中的所有View。每次需要更新ItemView的显示情况时,则需要将需要展示的Data传入,然后ViewHolder根据Data更新自身之中的View即可。

ViewHolder的职责很单一,只是根据Data更新View的状态,至于其他的逻辑操作,比如ItemView中的按钮点击,以及点击之后的操作等,通通交给外面组件处理。

上面说到,每个ItemView实例对应一个ViewHolder实例。此外,每种ItemView,对应一种ViewHolder,意思即为,当一个列表有多个类型的ItemView时,这时就需要创建多个类型的ViewHolder。

例如,一个商店列表,会有金币样式CoinItemView、钻石样式DiamondItemView、礼包样式BundleItemView,那么就需要有CoinViewHolder、DiamondViewHolder以及BundleViewHolder。

4. Adapter

当Data的数量和ItemView的数量不匹配时,要想通过有限的ItemView展示超出其数量的Data,这个时候就需要有个组件在其中将这二者调和,使其可正常工作,而这个事就是适配器在做。

适配器里维护着所有的ItemView、ViewHolder以及Data;同时监听着ScrollView的滑动状态,根据滑动状态取出合适的Data,并找出空闲的ItemView,使其展示出Data并放到列表的恰当位置;此外还要提供操作列表的方法,如果更新数据、滑动到指定位置,等。

由于对于所有列表,有一些操作和处理是通用的,故将其抽取到一个公共类中,即为StaggeredAdapter,使用时只需继承此类,并实现特有的方法即可。

二、使用示例

在这种模式下,我们可以在意识中不再关注整个列表,而是把精力集中到ItemView上,对于列表要做的事情,就是根据数据正确的显示ItemView中的各个元素。

1. 创建Prefab

如图,在ScrollView的Content下创建一个名为HItemViewIgnore的ItemView。这个命名无要求,只要不重复即可,我的这个是因为要自动生成部分代码,所以要符合其中的一些规则。

其中,这个ItemView有5个元素,包括2个背景图,3个文字标签。

2. 创建ItemData

即为ItemView需要显示的数据,示例中比较简单,只需要展示学号、名字和分数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ItemData
{
public int Id;
public string Name;
public int Score;

public ItemData(int id, string n, int s)
{
Id = id;
Name = n;
Score = s;
}
}

3. 创建ViewHolder

如上所言,ViewHolder的作用就是维护ItemView中的所有元素,并且根据传入的Data,来正确地显示各个组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HorizontalViewHolder : ViewHolder
{
private readonly HItemViewIgnore _views;

public HorizontalViewHolder(int type, RectTransform view) : base(type, view)
{
_views = HItemViewIgnore.Get(view);
}

public void Bind(ItemData d)
{
_views.No_tmp.text = $"No:{d.Id}";
_views.Name_tmp.text = $"Name:{d.Name}";
_views.Score_tmp.text = $"Score:{d.Score}";
_views.BgFlunk_tr.SetActive(d.Score < 60);
}
}

构造函数中的view参数,即为ItemView,在这里指的是第一步中创建的额HItemViewIgnore。一般来说,拿到了RectTransform,就可以通过Find方法和GetComponent方法获取到下面挂着的所有的元素。因为这些都是样板代码,所以我是通过自动生成代码工具生成的,放在了HItemViewIgnore类中,通过其中的Get方法就可以获得实例,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class HItemViewIgnore
{
public Transform BgFlunk_tr;
public TMP_Text No_tmp;
public TMP_Text Name_tmp;
public TMP_Text Score_tmp;

public static HItemViewIgnore Get(Transform t)
{
var p = new HItemViewIgnore();
p.Bind(t);
return p;
}

private void Bind(Transform root)
{
BgFlunk_tr = root.Find("BgFlunk_tr").GetComponent<Transform>();
No_tmp = root.Find("No_tmp").GetComponent<TMP_Text>();
Name_tmp = root.Find("Name_tmp").GetComponent<TMP_Text>();
Score_tmp = root.Find("Score_tmp").GetComponent<TMP_Text>();
}
}

ViewHolder中的Bind方法的参数,即为需要展示的数据,这个参数由调用侧按需传入。这个示例中需要做的事情就是把3个参数展示出来,同时根据分数大小控制BgFlunk是否显示,不及格就显示,反之不显示。

如上,维护ViewHolder中的所有元素,并且可以根据Data正确显示,这就是ViewHolder需要做的所有的工作。

4. 创建Adapter

需要创建一个类,继承自StaggeredAdapter,同时实现其中的所有抽象方法。

关于其中涉及到的type参数,指的是这个列表下有几种类型的ViewHolder。通过重写GetItemType方法,可以指定每个ItemView的类型,同时通过GetItemName方法可以为每个类型指定对应的ItemView,这两个方法需要搭配使用。

比如示例中,只有一种类型的ItemView,所以GetItemType方法中,不管哪个position,都是固定返回0;而在GetItemName方法中,对于类型是0的,则是返回名为HItemViewIgnore的ItemView。

当有多个类型的时候,也可以在GetItemType按需返回其类型,然后在GetItemName中返回对应的ItemView的名字即可。

其他几个方法,都是固定的写法,代码如下:

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
public class HorizontalAdapter : StaggeredAdapter<HorizontalViewHolder>
{
private readonly Dictionary<int, string> _itemViewName = new()
{
{0, "HItemViewIgnore"}
};

private readonly List<ItemData> _data;

public HorizontalAdapter(List<ItemData> d, MonoBehaviour parentView, ScrollRect sr, bool scrollOrientationCenter = true) : base(parentView, sr, scrollOrientationCenter)
{
_data = d;
}

protected override int GetItemCount()
{
return _data.Count;
}

protected override int GetItemType(int position)
{
// var d = _data[position];
// if (...) return 0;
// if (...) return 1;
// if (...) return 2;
return 0;
}

protected override string GetItemName(int type)
{
return _itemViewName[type];
}

protected override HorizontalViewHolder OnCreateViewHolder(int type, RectTransform viewRoot)
{
return new HorizontalViewHolder(type, viewRoot);
}

protected override void OnBindView(int position, HorizontalViewHolder holder)
{
var d = _data[position];
holder.Bind(d);
}

protected override void OnItemVisible(bool repeat, int num, HorizontalViewHolder holder, bool completelyVisible)
{
holder.Root.SetActive(true);
}

protected override void OnItemInvisible(int position, HorizontalViewHolder holder)
{
holder.Root.SetActive(false);
}

protected override void OnItemHighlight(int position, HorizontalViewHolder holder)
{
}
}

5. 显示列表

造了一组随机数据,调用Show方法即可显示。

1
2
3
4
5
6
7
8
9
10
11
private void InitHorizontalList()
{
var data = new List<ItemData>();
for (var i = 0; i < 100; ++i)
{
data.Add(new ItemData(i, $"Student{i + 1}", Random.Range(30, 101)));
}

var adapter = new HorizontalAdapter(data, this, HScrollView_tr.GetComponent<ScrollRect>());
adapter.Show(true);
}

附录

详细的代码可查看Git仓库:
https://github.com/oynix/StaggeredAdapter

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2023/11/e288442792d6/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道