点关注不迷路,持续输出Unity
干货文章。
嗨,大家好,我是新发。
今天是2021年5月19日
,明天就是5.20
了(可能粉丝们看到这篇文章时已经5.20
了),该表示表示了。我的粉丝数量马上突破两万了,我决定用Unity
做一个Demo
,给我这近两万粉丝比心。那么,开始吧~
最终Unity
运行效果如下:
点击比心,访问对应的博客主页:
本文Demo
工程已上传到CodeChina
,感兴趣的同学可自行下载学习。
地址:https://codechina.csdn.net/linxinfa/UnityCSDNFansList
注:我使用的Unity
版本:Unity 2020.1.14f1c1 (64-bit)
。
首先,分析一下CSDN
粉丝页面的页面结构。在浏览器中按F12
调试,可以看到粉丝名字的节点class
是sub-people-username
。
进一步跟进到节点中,还可以看到粉丝博客url
和头像的url
。
部分粉丝写了简介,也一起读取下来吧。
开始爬数据…
注:爬虫我就不教大家啦,怕教坏小朋友。之前看新闻看到有程序员因为弄爬虫被抓的。
把上面生成的json
文件放到Unity
工程的Resources
目录中。
这样,我们就可以直接通过Resources.Load
来读取文件。
var fansJsonStr = Resources.Load<TextAsset>("fans_list").text;
注:关于
Unity
文件目录结构以及Resources.Load
读取文件的教程,可以参见我之前写的这篇文章:《学Unity的猫》——第五章:规范Unity的工程目录结构,
以及这篇文章:《Unity游戏开发——新发教你做游戏(三):3种资源加载方式》
读取了json
文件内容后,我们需要对数据进行解析。Unity
中大家常用的json
解析库是LitJson
,可以从GitHub
找到源码。
GitHub
地址:https://github.com/LitJSON/litjson
不过我这边弄了一个迷你版的jsoin
解析库,可以参见我之前写的这篇文章:《用C#实现一个迷你json库,无需引入dll(可直接放到Unity中使用)》
解析json
的逻辑封装在JSONConvert
类中,源码参见文章末尾。
这样,我们就可以解析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"]; // 头像地址
// ...
}
由于粉丝数据达到近两万条,我们要在一个列表中显示这么多数据,如果创建近两万个ui item
的话性能肯定是很差的,所以必须循环复用ui item
。
循环复用列表的原理其实就是,列表向上滑动时,当item
超过显示区域的上边界时,把item
移动到列表底部,重复使用item
并更新item
的ui
显示,向下滑动同理,把超过显示区域底部的item复用到顶部。
为了方便大家理解,我画成图,如下:
注:循环列表的具体实现,我之前写过一篇教程,可以参见我之前写的这篇文章:《Unity UGUI实现循环复用列表,显示巨量列表信息,含Demo工程源码》
循环列表的逻辑封装在RecyclingListView
和RecyclingListViewItem
类中,源码参见文章末尾。
我们拿到的头像数据是一个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》
运行过程中,我发现有不少头像加载出来是一个问号。
我手动下载对应头像,发现它们其实是gif
格式,比如这个:
虽然它看起来不会动,但它实际上是gif
格式的,我们可以使用二进制查看器查看它的头部,可以看到是47 49 46 38 39 61
,即GIF 89a
格式。
如果是JPG
格式,则头两个字节是FF D8
更多的二进制文件头如下:
文件格式 | 头部字节 | 尾部字节 |
---|---|---|
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
头像:
我们下载头像时,对头部二进制进行检测,判断是否是GIF
,如果是GIF
,则使用过默认头像显示。
var bytes = downloadHandlerTexture.data;
if(0x47 == bytes[0] && 0x49 == bytes[1] && 0x46 == bytes[2])
{
// 是gif,显示默认头像
}
注,
Unity
中如果想要播放GIF
也是可以的,感兴趣的同学可以参见我之前写的这篇文章:《Unity解析和显示/播放GIF图片,支持http url,支持本地file://,支持暂停、继续播放》
头像是动态加载的,加载过的头像,我们可以缓存起来,下次再显示时,可以不请求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);
}
}
注:关于预设的相关教程,可以参见我之前写的这两篇文章:《学Unity的猫——第八章:Unity预设文件,无限纸团喷射机》,《Unity2018.3.0.b1 版本的预设新工作流方式的使用体验》
点击比心按钮,拉起浏览器访问博客,用的是Application.OpenURL
接口。
例:
var blog_url = "https://blog.csdn.net/linxinfa";
UnityEngine.Application.OpenURL(blog_url);
点击比心按钮,效果如下:
可以看到,拉起的是系统默认的浏览器,事实上,我们也可以做内置浏览器。
注:关于
Unity
的浏览器插件,可以参见我之前写的这些文章:
《Unity内嵌浏览器插件(Android、iOS、Windows)》
《新发的无聊小发明——PC端自制迷你浏览器给Unity调用(Windows窗体应用/WebBrowser/EXE)》
《unity内置浏览器插件UniWebView的使用(支持Android,ios,Mac)》
《新发的日常小实验——使用c# winfrom窗体应用制作浏览器,实现c#与html js交互》
感谢所有粉丝的支持。
末了,喜欢Unity
的同学,不要忘记点击关注,如果有什么Unity
相关的技术难题,也欢迎留言或私信~
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];
}
}
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;
}
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;
}
}
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);
}
}
}
}
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;
}
}
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>
{
}