520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)

文章目录

      • 一、前言
      • 二、最终效果
      • 三、读取CSDN粉丝列表数据
        • 1、分析粉丝列表页面结构
        • 2、爬数据
      • 四、Unity制作
        • 1、文件读取
        • 2、c#解析json
        • 3、UGUI循环复用列表
        • 4、头像的加载
        • 5、gif头像问题
        • 6、头像缓存
        • 7、界面制作
        • 8、比心,拉起浏览器访问博客
      • 五、结束语
      • 六、附录:工程源码
        • 1、CSDNFansPanel.cs
        • 2、FansItem.cs
        • 3、HeadIconLoader.cs
        • 4、RecyclingListView.cs
        • 5、RecyclingListViewItem.cs
        • 6、JSONConvert.cs

一、前言

点关注不迷路,持续输出Unity干货文章。
嗨,大家好,我是新发。
今天是2021年5月19日,明天就是5.20了(可能粉丝们看到这篇文章时已经5.20了),该表示表示了。我的粉丝数量马上突破两万了,我决定用Unity做一个Demo,给我这近两万粉丝比心。那么,开始吧~
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第1张图片

二、最终效果

最终Unity运行效果如下:
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第2张图片
点击比心,访问对应的博客主页:
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第3张图片
本文Demo工程已上传到CodeChina,感兴趣的同学可自行下载学习。
地址:https://codechina.csdn.net/linxinfa/UnityCSDNFansList
注:我使用的Unity版本:Unity 2020.1.14f1c1 (64-bit)

520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第4张图片

三、读取CSDN粉丝列表数据

1、分析粉丝列表页面结构

首先,分析一下CSDN粉丝页面的页面结构。在浏览器中按F12调试,可以看到粉丝名字的节点classsub-people-username
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第5张图片
进一步跟进到节点中,还可以看到粉丝博客url和头像的url
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第6张图片
部分粉丝写了简介,也一起读取下来吧。
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第7张图片

2、爬数据

开始爬数据…

注:爬虫我就不教大家啦,怕教坏小朋友。之前看新闻看到有程序员因为弄爬虫被抓的。

最后生成一个json文件,如下:
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第8张图片

四、Unity制作

1、文件读取

把上面生成的json文件放到Unity工程的Resources目录中。
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第9张图片
这样,我们就可以直接通过Resources.Load来读取文件。

var fansJsonStr = Resources.Load<TextAsset>("fans_list").text;

注:关于Unity文件目录结构以及Resources.Load读取文件的教程,可以参见我之前写的这篇文章:《学Unity的猫》——第五章:规范Unity的工程目录结构,
以及这篇文章:《Unity游戏开发——新发教你做游戏(三):3种资源加载方式》

2、c#解析json

读取了json文件内容后,我们需要对数据进行解析。Unity中大家常用的json解析库是LitJson,可以从GitHub找到源码。
GitHub地址:https://github.com/LitJSON/litjson
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第10张图片
不过我这边弄了一个迷你版的jsoin解析库,可以参见我之前写的这篇文章:《用C#实现一个迷你json库,无需引入dll(可直接放到Unity中使用)》
解析json的逻辑封装在JSONConvert类中,源码参见文章末尾。
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第11张图片

这样,我们就可以解析json数据了。

var fansJsonStr = Resources.Load<TextAsset>("fans_list").text;
// 解析json数据
var fansJsonArray = JSONConvert.DeserializeArray(fansJsonStr);
foreach (JSONObject dataItem in fansJsonArray)
{
     
    var fansName = (string)dataItem["name"];	// 昵称
    var fansIntro = (string)dataItem["intro"]; 	// 简介
    var fansBlogUrl = (string)dataItem["blog_url"];		// 博客地址
    var fansImageUrl = (string)dataItem["img_url"];		// 头像地址
	// ...
}

3、UGUI循环复用列表

由于粉丝数据达到近两万条,我们要在一个列表中显示这么多数据,如果创建近两万个ui item的话性能肯定是很差的,所以必须循环复用ui item
循环复用列表的原理其实就是,列表向上滑动时,当item超过显示区域的上边界时,把item移动到列表底部,重复使用item并更新itemui显示,向下滑动同理,把超过显示区域底部的item复用到顶部。
为了方便大家理解,我画成图,如下:
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第12张图片

注:循环列表的具体实现,我之前写过一篇教程,可以参见我之前写的这篇文章:《Unity UGUI实现循环复用列表,显示巨量列表信息,含Demo工程源码》

循环列表的逻辑封装在RecyclingListViewRecyclingListViewItem类中,源码参见文章末尾。
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第13张图片

4、头像的加载

我们拿到的头像数据是一个https链接,我们可以通过UnityWebRequest来请求下载头像。
例:

string url = "https://profile.csdnimg.cn/6/6/4/3_m0_57622304";
var request = new UnityWebRequest(url);
downloadHandlerTexture = new DownloadHandlerTexture(true);
request.downloadHandler = downloadHandlerTexture;
request.SendWebRequest();
while(!request.isDone) 
{
     
	// 等待
	// 这里的while等待逻辑,可以改成协程yield return request.SendWebRequest();
}
if (string.IsNullOrEmpty(request.error))
{
     
	Texture2D tex2D = downloadHandlerTexture.texture;
	// TODO
}

注:关于UnityWebRequest的详细使用教程,可以参见我之前写的这篇文章:《长江后浪推前浪,UnityWebRequest替代WWW》

5、gif头像问题

运行过程中,我发现有不少头像加载出来是一个问号。
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第14张图片
我手动下载对应头像,发现它们其实是gif格式,比如这个:
在这里插入图片描述
虽然它看起来不会动,但它实际上是gif格式的,我们可以使用二进制查看器查看它的头部,可以看到是47 49 46 38 39 61,即GIF 89a格式。
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第15张图片
如果是JPG格式,则头两个字节是FF D8
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第16张图片
更多的二进制文件头如下:

文件格式 头部字节 尾部字节
JPG FF D8 FF D9
PNG 89 50 4E 47 0D 0A 1A 0A
GIF 89a 47 49 46 38 39 61
GIF 87a 47 49 46 37 39 61
TGA未压缩 00 00 02 00 00
TGA压缩 00 00 10 00 00
BMP 42 4D
PCX 0A
TIFF 4D 4D 或 49 49
ICO 00 00 01 00 01 00 20 20
CUR 00 00 02 00 01 00 20 20
IFF 46 4F 52 4D
ANI 52 49 4646

注:二进制查看器,推荐大家一个工具:Hex Editor,非常的轻巧,而且用它可以打开大型的文本文件。
HexEditor下载地址:https://hexeditor.en.softonic.com/
关于HexEditor可以参见我之前写的这篇文章:《超大文本文件怎么打开(使用Hex Editor)》

我们在工程中放一张默认的JPG头像:
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第17张图片
我们下载头像时,对头部二进制进行检测,判断是否是GIF,如果是GIF,则使用过默认头像显示。

var bytes = downloadHandlerTexture.data;
if(0x47 == bytes[0] && 0x49 == bytes[1] && 0x46 == bytes[2])
{
     
    // 是gif,显示默认头像

}

注,Unity中如果想要播放GIF也是可以的,感兴趣的同学可以参见我之前写的这篇文章:《Unity解析和显示/播放GIF图片,支持http url,支持本地file://,支持暂停、继续播放》

6、头像缓存

头像是动态加载的,加载过的头像,我们可以缓存起来,下次再显示时,可以不请求https了,直接从内存中读取。

/// 
/// 头像缓存
/// 
public class HeadCache
{
     
    static Dictionary<string, Texture2D> cache = new Dictionary<string, Texture2D>();

    public static void CacheTexture(string url, Texture2D tex)
    {
     
        if (!cache.ContainsKey(url))
            cache[url] = tex;
    }

    public static Texture2D GetFromCache(string url)
    {
     
        if (cache.ContainsKey(url))
            return cache[url];
        return null;
    }
}

上面的缓存,我只是缓存在内存中,如果你想写到本地磁盘中,可以使用EncodeToJPG转为二进制流再通过IO写到可写目录中。
例:

// using using System.IO;

// Texture2D tex;
string savePath = Application.persistentDataPath + "/head_icons";
var bytes = tex.EncodeToJPG();
using (FileStream fs = new FileStream(savePath, FileMode.Create))
{
     
    using (BinaryWriter bw = new BinaryWriter(fs))
    {
     
        bw.Write(bytes);
    }
}

7、界面制作

界面使用UGUI来制作。
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第18张图片
养成习惯,做好的界面保存成预设:
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第19张图片

注:关于预设的相关教程,可以参见我之前写的这两篇文章:《学Unity的猫——第八章:Unity预设文件,无限纸团喷射机》,《Unity2018.3.0.b1 版本的预设新工作流方式的使用体验》

8、比心,拉起浏览器访问博客

点击比心按钮,拉起浏览器访问博客,用的是Application.OpenURL接口。
例:

var blog_url = "https://blog.csdn.net/linxinfa";
UnityEngine.Application.OpenURL(blog_url);

点击比心按钮,效果如下:
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第20张图片
可以看到,拉起的是系统默认的浏览器,事实上,我们也可以做内置浏览器。

注:关于Unity的浏览器插件,可以参见我之前写的这些文章:
《Unity内嵌浏览器插件(Android、iOS、Windows)》
《新发的无聊小发明——PC端自制迷你浏览器给Unity调用(Windows窗体应用/WebBrowser/EXE)》
《unity内置浏览器插件UniWebView的使用(支持Android,ios,Mac)》
《新发的日常小实验——使用c# winfrom窗体应用制作浏览器,实现c#与html js交互》

五、结束语

感谢所有粉丝的支持。
末了,喜欢Unity的同学,不要忘记点击关注,如果有什么Unity相关的技术难题,也欢迎留言或私信~

六、附录:工程源码

工程总共6个脚本。
520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)_第21张图片

1、CSDNFansPanel.cs

using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
using UnityEngine.UI;


public class CSDNFansPanel : MonoBehaviour
{
     
    public RecyclingListView scrollList;
    /// 
    /// 列表数据
    /// 
    private List<FansData> list = new List<FansData>();



    private void Start()
    {
     
        ReadJson();

        // 列表item更新回调
        scrollList.ItemCallback = PopulateItem;

        // 设置数据,此时列表会执行更新
        scrollList.RowCount = list.Count;
    }

    private void ReadJson()
    {
     
        var fansList = Resources.Load<TextAsset>("fans_list").text;
        var jsonArray = JSONConvert.DeserializeArray(fansList);
        int index = jsonArray.Count;
        foreach (JSONObject dataItem in jsonArray)
        {
     
            FansData fans = new FansData();
            fans.Name = (string)dataItem["name"];
            fans.Intro = (string)dataItem["intro"];
            fans.BlogUrl = (string)dataItem["blog_url"];
            fans.ImageUrl = (string)dataItem["img_url"];
            fans.Row = index;
            list.Add(fans);
            --index;
        }
    }

    /// 
    /// item更新回调
    /// 
    /// 复用的item对象
    /// 行号
    private void PopulateItem(RecyclingListViewItem item, int rowIndex)
    {
     
        var child = item as FansItem;
        child.Data = list[rowIndex];
    }
}

2、FansItem.cs

using UnityEngine.Networking;
using UnityEngine.UI;

public class FansItem : RecyclingListViewItem
{
     
    // 昵称
    public Text nameText;
    // 简介
    public Text introText;
    // 比心按钮
    public Button btn;

    public HeadIconLoader iconLoader;


    public Text rowText;

    private FansData data;
    public FansData Data
    {
     
        get {
      return data; }
        set
        {
     
            data = value;
            nameText.text = data.Name;
            rowText.text = $"第{data.Row}位粉丝";
            introText.text = data.Intro;
            iconLoader.LoadIcon(data.ImageUrl);
        }
    }

    private void Start()
    {
     
        // 比心按钮
        btn.onClick.AddListener(() => 
        {
     
            UnityEngine.Application.OpenURL(data.BlogUrl);
        });
    }
}


public struct FansData
{
     
    public string Name;
    public string Intro;
    public string BlogUrl;
    public string ImageUrl;
    public int Row;
}

3、HeadIconLoader.cs

using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using System.Collections.Generic;

public class HeadIconLoader : MonoBehaviour
{
     
    public RawImage headIcon;
    public Texture2D defaultTexture;

    private UnityWebRequest request;
    private DownloadHandlerTexture downloadHandlerTexture;
    private bool loading = false;
    private string headUrl;

    public void LoadIcon(string url)
    {
     
        headUrl = url;
        var tex = HeadCache.GetFromCache(url);
        if(null != tex)
        {
     
            headIcon.texture = tex;
            loading = false;
            return;
        }
        

        if (null != request)
        {
     
            request.Dispose();
        }
        headIcon.texture = null;
        request = new UnityWebRequest(url);
        downloadHandlerTexture = new DownloadHandlerTexture(true);
        request.downloadHandler = downloadHandlerTexture;
        loading = true;
        request.SendWebRequest();
    }

    private void Update()
    {
     
        if (loading && request.isDone)
        {
     
            loading = false;
            if (string.IsNullOrEmpty(request.error))
            {
     
                var bytes = downloadHandlerTexture.data;
                if(0x47 == bytes[0] && 0x49 == bytes[1] && 0x46 == bytes[2])
                {
     
                    // 是gif,显示默认头像
                    headIcon.texture = defaultTexture;
                    HeadCache.CacheTexture(headUrl, defaultTexture);
                }
                else
                {
     
                    headIcon.texture = downloadHandlerTexture.texture;
                    HeadCache.CacheTexture(headUrl, downloadHandlerTexture.texture);
                }
            }
        }
    }
}

/// 
/// 头像缓存
/// 
public class HeadCache
{
     
    static Dictionary<string, Texture2D> cache = new Dictionary<string, Texture2D>();

    public static void CacheTexture(string url, Texture2D tex)
    {
     
        if (!cache.ContainsKey(url))
            cache[url] = tex;
    }

    public static Texture2D GetFromCache(string url)
    {
     
        if (cache.ContainsKey(url))
            return cache[url];
        return null;
    }
}

4、RecyclingListView.cs

using System;
using UnityEngine;
using UnityEngine.UI;

/// 
/// 循环复用列表
/// 
[RequireComponent(typeof(ScrollRect))]
public class RecyclingListView : MonoBehaviour
{
     
    [Tooltip("子节点物体")]
    public RecyclingListViewItem ChildObj;
    [Tooltip("行间隔")]
    public float RowPadding = 15f;
    [Tooltip("事先预留的最小列表高度")]
    public float PreAllocHeight = 0;

    public enum ScrollPosType
    {
     
        Top,
        Center,
        Bottom,
    }


    public float VerticalNormalizedPosition
    {
     
        get => scrollRect.verticalNormalizedPosition;
        set => scrollRect.verticalNormalizedPosition = value;
    }


    /// 
    /// 列表行数
    /// 
    protected int rowCount;

    /// 
    /// 列表行数,赋值时,会执行列表重新计算
    /// 
    public int RowCount
    {
     
        get => rowCount;
        set
        {
     
            if (rowCount != value)
            {
     
                rowCount = value;
                // 先禁用滚动变化
                ignoreScrollChange = true;
                // 更新高度
                UpdateContentHeight();
                // 重新启用滚动变化
                ignoreScrollChange = false;
                // 重新计算item
                ReorganiseContent(true);
            }
        }
    }

    /// 
    /// item更新回调函数委托
    /// 
    /// 子节点对象
    /// 行数
    public delegate void ItemDelegate(RecyclingListViewItem item, int rowIndex);

    /// 
    /// item更新回调函数委托
    /// 
    public ItemDelegate ItemCallback;

    protected ScrollRect scrollRect;
    /// 
    /// 复用的item数组
    /// 
    protected RecyclingListViewItem[] childItems;

    /// 
    /// 循环列表中,第一个item的索引,最开始每个item都有一个原始索引,最顶部的item的原始索引就是childBufferStart
    /// 由于列表是循环复用的,所以往下滑动时,childBufferStart会从0开始到n,然后又从0开始,以此往复
    /// 如果是往上滑动,则是从0到-n,再从0开始,以此往复
    /// 
    protected int childBufferStart = 0;
    /// 
    /// 列表中最顶部的item的真实数据索引,比如有一百条数据,复用10个item,当前最顶部是第60条数据,那么sourceDataRowStart就是59(注意索引从0开始)
    /// 
    protected int sourceDataRowStart;

    protected bool ignoreScrollChange = false;
    protected float previousBuildHeight = 0;
    protected const int rowsAboveBelow = 1;

    protected virtual void Awake()
    {
     
        scrollRect = GetComponent<ScrollRect>();
        ChildObj.gameObject.SetActive(false);
    }


    protected virtual void OnEnable()
    {
     
        scrollRect.onValueChanged.AddListener(OnScrollChanged);
        ignoreScrollChange = false;
    }

    protected virtual void OnDisable()
    {
     
        scrollRect.onValueChanged.RemoveListener(OnScrollChanged);
    }


    /// 
    /// 供外部调用,强制刷新整个列表,比如数据变化了,刷新一下列表
    /// 
    public virtual void Refresh()
    {
     
        ReorganiseContent(true);
    }

    /// 
    /// 供外部调用,强制刷新整个列表的局部item
    /// 
    /// 开始行
    /// 数量
    public virtual void Refresh(int rowStart, int count)
    {
     
        // only refresh the overlap
        int sourceDataLimit = sourceDataRowStart + childItems.Length;
        for (int i = 0; i < count; ++i)
        {
     
            int row = rowStart + i;
            if (row < sourceDataRowStart || row >= sourceDataLimit)
                continue;

            int bufIdx = WrapChildIndex(childBufferStart + row - sourceDataRowStart);
            if (childItems[bufIdx] != null)
            {
     
                UpdateChild(childItems[bufIdx], row);
            }
        }
    }

    /// 
    /// 供外部调用,强制刷新整个列表的某一个item
    /// 
    public virtual void Refresh(RecyclingListViewItem item)
    {
     

        for (int i = 0; i < childItems.Length; ++i)
        {
     
            int idx = WrapChildIndex(childBufferStart + i);
            if (childItems[idx] != null && childItems[idx] == item)
            {
     
                UpdateChild(childItems[i], sourceDataRowStart + i);
                break;
            }
        }
    }

    /// 
    /// 清空列表
    /// 
    public virtual void Clear()
    {
     
        RowCount = 0;
    }


    /// 
    /// 供外部调用,强制滚动列表,使某一行显示在列表中
    /// 
    /// 行号
    /// 目标行显示在列表的位置:顶部,中心,底部
    public virtual void ScrollToRow(int row, ScrollPosType posType)
    {
     
        scrollRect.verticalNormalizedPosition = GetRowScrollPosition(row, posType);
    }

    /// 
    /// 获得归一化的滚动位置,该位置将给定的行在视图中居中
    /// 
    /// 行号
    /// 
    public float GetRowScrollPosition(int row, ScrollPosType posType)
    {
     
        // 视图高
        float vpHeight = ViewportHeight();
        float rowHeight = RowHeight();
        // 将目标行滚动到列表目标位置时,列表顶部的位置
        float vpTop = 0;
        switch (posType)
        {
     
            case ScrollPosType.Top:
                {
     
                    vpTop = row * rowHeight;
                }
                break;
            case ScrollPosType.Center:
                {
     
                    // 目标行的中心位置与列表顶部的距离
                    float rowCentre = (row + 0.5f) * rowHeight;
                    // 视口中心位置
                    float halfVpHeight = vpHeight * 0.5f;

                    vpTop = Mathf.Max(0, rowCentre - halfVpHeight);
                }
                break;
            case ScrollPosType.Bottom:
                {
     
                    vpTop = (row+1) * rowHeight - vpHeight;
                }
                break;
        }


        // 滚动后,列表底部的位置
        float vpBottom = vpTop + vpHeight;
        // 列表内容总高度
        float contentHeight = scrollRect.content.sizeDelta.y;
        // 如果滚动后,列表底部的位置已经超过了列表总高度,则调整列表顶部的位置
        if (vpBottom > contentHeight)
            vpTop = Mathf.Max(0, vpTop - (vpBottom - contentHeight));

        // 反插值,计算两个值之间的Lerp参数。也就是value在from和to之间的比例值
        return Mathf.InverseLerp(contentHeight - vpHeight, 0, vpTop);
    }

    /// 
    /// 根据行号获取复用的item对象
    /// 
    /// 行号
    protected RecyclingListViewItem GetRowItem(int row)
    {
     
        if (childItems != null &&
            row >= sourceDataRowStart && row < sourceDataRowStart + childItems.Length &&
            row < rowCount)
        {
     
            // 注意这里要根据行号计算复用的item原始索引
            return childItems[WrapChildIndex(childBufferStart + row - sourceDataRowStart)];
        }

        return null;
    }

    protected virtual bool CheckChildItems()
    {
     
        // 列表视口高度
        float vpHeight = ViewportHeight();
        float buildHeight = Mathf.Max(vpHeight, PreAllocHeight);
        bool rebuild = childItems == null || buildHeight > previousBuildHeight;
        if (rebuild)
        {
     

            int childCount = Mathf.RoundToInt(0.5f + buildHeight / RowHeight());
            childCount += rowsAboveBelow * 2;

            if (childItems == null)
                childItems = new RecyclingListViewItem[childCount];
            else if (childCount > childItems.Length)
                Array.Resize(ref childItems, childCount);

            // 创建item
            for (int i = 0; i < childItems.Length; ++i)
            {
     
                if (childItems[i] == null)
                {
     
                    var item = Instantiate(ChildObj);
                    childItems[i] = item;
                }
                childItems[i].RectTransform.SetParent(scrollRect.content, false);
                childItems[i].gameObject.SetActive(false);
            }

            previousBuildHeight = buildHeight;
        }

        return rebuild;
    }


    /// 
    /// 列表滚动时,会回调此函数
    /// 
    /// 归一化的位置
    protected virtual void OnScrollChanged(Vector2 normalisedPos)
    {
     
        if (!ignoreScrollChange)
        {
     
            ReorganiseContent(false);
        }
    }

    /// 
    /// 重新计算列表内容
    /// 
    /// 是否要清空列表重新计算
    protected virtual void ReorganiseContent(bool clearContents)
    {
     

        if (clearContents)
        {
     
            scrollRect.StopMovement();
            scrollRect.verticalNormalizedPosition = 1;
        }

        bool childrenChanged = CheckChildItems();
        // 是否要更新整个列表
        bool populateAll = childrenChanged || clearContents;


        float ymin = scrollRect.content.localPosition.y;

        // 第一个可见item的索引
        int firstVisibleIndex = (int)(ymin / RowHeight());


        int newRowStart = firstVisibleIndex - rowsAboveBelow;

        // 滚动变化量
        int diff = newRowStart - sourceDataRowStart;
        if (populateAll || Mathf.Abs(diff) >= childItems.Length)
        {
     

            sourceDataRowStart = newRowStart;
            childBufferStart = 0;
            int rowIdx = newRowStart;
            foreach (var item in childItems)
            {
     
                UpdateChild(item, rowIdx++);
            }

        }
        else if (diff != 0)
        {
     
            int newBufferStart = (childBufferStart + diff) % childItems.Length;

            if (diff < 0)
            {
     
                // 向前滑动
                for (int i = 1; i <= -diff; ++i)
                {
     
                    // 得到复用item的索引
                    int wrapIndex = WrapChildIndex(childBufferStart - i);
                    int rowIdx = sourceDataRowStart - i;
                    UpdateChild(childItems[wrapIndex], rowIdx);
                }
            }
            else
            {
     
                // 向后滑动
                int prevLastBufIdx = childBufferStart + childItems.Length - 1;
                int prevLastRowIdx = sourceDataRowStart + childItems.Length - 1;
                for (int i = 1; i <= diff; ++i)
                {
     
                    int wrapIndex = WrapChildIndex(prevLastBufIdx + i);
                    int rowIdx = prevLastRowIdx + i;
                    UpdateChild(childItems[wrapIndex], rowIdx);
                }
            }

            sourceDataRowStart = newRowStart;

            childBufferStart = newBufferStart;

        }

    }

    private int WrapChildIndex(int idx)
    {
     
        while (idx < 0)
            idx += childItems.Length;

        return idx % childItems.Length;
    }

    /// 
    /// 获取一行的高度,注意要加上RowPadding
    /// 
    private float RowHeight()
    {
     
        return RowPadding + ChildObj.RectTransform.rect.height;
    }

    /// 
    /// 获取列表视口的高度
    /// 
    private float ViewportHeight()
    {
     
        return scrollRect.viewport.rect.height;
    }

    protected virtual void UpdateChild(RecyclingListViewItem child, int rowIdx)
    {
     
        if (rowIdx < 0 || rowIdx >= rowCount)
        {
     
            child.gameObject.SetActive(false);
        }
        else
        {
     
            if (ItemCallback == null)
            {
     
                Debug.Log("RecyclingListView is missing an ItemCallback, cannot function", this);
                return;
            }

            // 移动到正确的位置
            var childRect = ChildObj.RectTransform.rect;
            Vector2 pivot = ChildObj.RectTransform.pivot;
            float ytoppos = RowHeight() * rowIdx;
            float ypos = ytoppos + (1f - pivot.y) * childRect.height;
            float xpos = 0 + pivot.x * childRect.width;
            child.RectTransform.anchoredPosition = new Vector2(xpos, -ypos);
            child.NotifyCurrentAssignment(this, rowIdx);

            // 更新数据
            ItemCallback(child, rowIdx);

            child.gameObject.SetActive(true);
        }
    }

    /// 
    /// 更新content的高度
    /// 
    protected virtual void UpdateContentHeight()
    {
     
        // 列表高度
        float height = ChildObj.RectTransform.rect.height * rowCount + (rowCount - 1) * RowPadding;
        // 更新content的高度
        var sz = scrollRect.content.sizeDelta;
        scrollRect.content.sizeDelta = new Vector2(sz.x, height);
    }

    protected virtual void DisableAllChildren()
    {
     
        if (childItems != null)
        {
     
            for (int i = 0; i < childItems.Length; ++i)
            {
     
                childItems[i].gameObject.SetActive(false);
            }
        }
    }
}

5、RecyclingListViewItem.cs

using UnityEngine;

/// 
/// 列表item,你自己写的列表item需要继承该类
/// 
[RequireComponent(typeof(RectTransform))]
public class RecyclingListViewItem : MonoBehaviour
{
     

    private RecyclingListView parentList;

    /// 
    /// 循环列表
    /// 
    public RecyclingListView ParentList
    {
     
        get => parentList;
    }

    private int currentRow;
    /// 
    /// 行号
    /// 
    public int CurrentRow
    {
     
        get => currentRow;
    }

    private RectTransform rectTransform;
    public RectTransform RectTransform
    {
     
        get
        {
     
            if (rectTransform == null)
                rectTransform = GetComponent<RectTransform>();
            return rectTransform;
        }
    }

    private void Awake()
    {
     
        rectTransform = GetComponent<RectTransform>();
    }

    /// 
    /// item更新事件响应函数
    /// 
    public virtual void NotifyCurrentAssignment(RecyclingListView v, int row)
    {
     
        parentList = v;
        currentRow = row;
    }


}

6、JSONConvert.cs

using System.Collections.Generic;
using System.Text;


public static class JSONConvert
{
     
    #region Global Variables
    private static char[] _charary;
    private static int _aryend;

    #endregion

    #region JSON Deserialization

    /// 
    /// Convert string to JSONObject
    /// 
    /// 
    /// 
    private static JSONObject DeserializeSingletonObject(ref int left)
    {
     
        JSONObject localjson = new JSONObject();
        while (left <= _aryend)
        {
     
            char c = _charary[left];
            if (c == ' ' || c == '\r' || c == '\n' || c == '\t')  //skip empty char
            {
     
                left++;
                continue;
            }
            if (c == ',')
            {
     
                left++;
                continue;
            }
            char r = '\0';
            if (c == '\"' || c == '\'')     //beginning of key
            {
     
                left++;
                r = c;
            }
            else if (c == '}')      //end of JSONObject
            {
     
                left++;
                break;
            }
            int column = left;
            if (r == '\0')
            {
     
                while (_charary[column] != ':') column++;
            }
            else
            {
     
                while (!(_charary[column] == r && _charary[column - 1] != '\\' && _charary[column + 1] == ':')) column++;
            }
            string key = new string(_charary, left, column - left);         //get the key
            if (r == '\0')
                left = column + 1;
            else
                left = column + 2;
            c = _charary[left];
            while (c == ' ' || c == '\r' || c == '\n' || c == '\t')  //skip empty char
            {
     
                left++;
                c = _charary[left];
            }
            if (c == '\"' || c == '\'')     //if value is string
            {
     
                left++;
                int strend = left;
                while (_charary[strend] != c || _charary[strend - 1] == '\\') strend++;
                localjson[key] = new string(_charary, left, strend - left);
                left = strend + 1;
            }
            else if (c == '{') // JSONObject
            {
     
                left++;
                localjson[key] = DeserializeSingletonObject(ref left);
            }
            else if (c == '[')     //JSONArray
            {
     
                left++;
                localjson[key] = DeserializeSingletonArray(ref left);
            }
            else
            {
     
                //other class, such as boolean, int
                //all are converted to string, it can be enriched if in need
                int comma = left;
                char co = _charary[comma];
                while (co != ',' && co != '}')
                {
     
                    comma++;
                    co = _charary[comma];
                }
                int em = comma - 1;
                co = _charary[em];
                while (co == ' ' || co == '\r' || co == '\n' || co == '\t')
                {
     
                    em--;
                    co = _charary[em];
                }
                localjson[key] = new string(_charary, left, em - left + 1);
                left = comma;
            }
        }
        return localjson;
    }

    /// 
    /// Convert string to JSONArray
    /// 
    /// 
    /// 
    private static JSONArray DeserializeSingletonArray(ref int left)
    {
     
        JSONArray jsary = new JSONArray();
        while (left <= _aryend)
        {
     
            char c = _charary[left];
            if (c == ' ' || c == '\r' || c == '\n' || c == '\t')  //skip empty char
            {
     
                left++;
                continue;
            }
            if (c == ',')
            {
     
                left++;
                continue;
            }
            if (c == ']')
            {
     
                left++;
                break;
            }
            if (c == '{') //JSONObject
            {
     
                left++;
                jsary.Add(DeserializeSingletonObject(ref left));
            }
            else if (c == '[')     //JSONArray
            {
     
                left++;
                jsary.Add(DeserializeSingletonArray(ref left));
            }
            else if (c == '\"' || c == '\'')            //string
            {
     
                left++;
                int strend = left;
                while (_charary[strend] != c || _charary[strend - 1] == '\\') strend++;
                jsary.Add(new string(_charary, left, strend - left));
                left = strend + 1;
            }
            else
            {
     
                //other class, such as boolean, int
                //all are converted to string, it can be enriched if in need
                int comma = left;
                char co = _charary[comma];
                while (co != ',' && co != ']')
                {
     
                    comma++;
                    co = _charary[comma];
                }
                int em = comma - 1;
                co = _charary[em];
                while (co == ' ' || co == '\r' || co == '\n' || co == '\t')
                {
     
                    em--;
                    co = _charary[em];
                }
                jsary.Add(new string(_charary, left, em - left + 1));
                left = comma;
            }
        }
        return jsary;
    }

    #endregion

    #region Public Interface

    /// 
    /// Get a JSONObject instance from char[]
    /// 
    /// 
    /// 
    public static JSONObject DeserializeCharToObject(char[] input)
    {
     
        _charary = input;
        _aryend = _charary.Length - 1;
        while (_aryend > 0)
            if (_charary[_aryend] != '}')
                _aryend--;
            else
                break;
        int start = 0;
        while (start < _aryend)
            if (_charary[start] != '{')
                start++;
            else
                break;
        start++;
        if (_aryend < start + 1)
            return null;
        return DeserializeSingletonObject(ref start);
    }

    /// 
    /// Get a JSONObject instance from string
    /// 
    /// 
    /// 
    public static JSONObject DeserializeObject(string input)
    {
     
        return DeserializeCharToObject(input.ToCharArray());     //The first char must be '{'
    }

    /// 
    /// Get a JSONArray instance from char[]
    /// 
    /// 
    /// 
    public static JSONArray DeserializeCharsToArray(char[] input)
    {
     
        _charary = input;
        _aryend = _charary.Length - 1;
        while (_aryend > 0)
            if (_charary[_aryend] != ']')
                _aryend--;
            else
                break;
        int start = 0;
        while (start < _aryend)
            if (_charary[start] != '[')
                start++;
            else
                break;
        start++;
        if (_aryend < start + 1)
            return null;
        return DeserializeSingletonArray(ref start);
    }

    /// 
    /// Get a JSONArray instance from string
    /// 
    /// 
    /// 
    public static JSONArray DeserializeArray(string input)
    {
     
        return DeserializeCharsToArray(input.ToCharArray());
    }
    /// 
    /// Serialize a JSONObject instance
    /// 
    /// 
    /// 
    public static string SerializeObject(JSONObject jsonObject)
    {
     
        StringBuilder sb = new StringBuilder();
        sb.Append("{");
        foreach (KeyValuePair<string, object> kvp in jsonObject)
        {
     
            if (kvp.Value is JSONObject)
            {
     
                sb.Append(string.Format("\"{0}\":{1},", kvp.Key, SerializeObject((JSONObject)kvp.Value)));
            }
            else if (kvp.Value is JSONArray)
            {
     
                sb.Append(string.Format("\"{0}\":{1},", kvp.Key, SerializeArray((JSONArray)kvp.Value)));
            }
            else if (kvp.Value is string)
            {
     
                sb.Append(string.Format("\"{0}\":\"{1}\",", kvp.Key, kvp.Value));
            }
            else if (kvp.Value is int || kvp.Value is long)
            {
     
                sb.Append(string.Format("\"{0}\":{1},", kvp.Key, kvp.Value));
            }
            else
            {
     
                sb.Append(string.Format("\"{0}\":\"{1}\",", kvp.Key, ""));
            }
        }
        if (sb.Length > 1)
            sb.Remove(sb.Length - 1, 1);
        sb.Append("}");
        return sb.ToString();
    }

    /// 
    /// Serialize a JSONArray instance
    /// 
    /// 
    /// 
    public static string SerializeArray(JSONArray jsonArray)
    {
     
        StringBuilder sb = new StringBuilder();
        sb.Append("[");
        for (int i = 0; i < jsonArray.Count; i++)
        {
     
            if (jsonArray[i] is JSONObject)
            {
     
                sb.Append(string.Format("{0},", SerializeObject((JSONObject)jsonArray[i])));
            }
            else if (jsonArray[i] is JSONArray)
            {
     
                sb.Append(string.Format("{0},", SerializeArray((JSONArray)jsonArray[i])));
            }
            else if (jsonArray[i] is string)
            {
     
                sb.Append(string.Format("\"{0}\",", jsonArray[i]));
            }
            else
            {
     
                sb.Append(string.Format("\"{0}\",", ""));
            }

        }
        if (sb.Length > 1)
            sb.Remove(sb.Length - 1, 1);
        sb.Append("]");
        return sb.ToString();
    }

    #endregion
}


public class JSONObject : Dictionary<string, object>
{
     
    public void put(string key, string value)
    {
     
        this[key] = value;
    }

    public void put(string key, int value)
    {
     
        this[key] = value.ToString();

    }
}


public class JSONArray : List<object>
{
      }

你可能感兴趣的:(Unity3D,unity,python,520,csdn,粉丝)