好长时间,已经没有过自我的总结,着实因为新入职的这家公司实习期的事情安排很多,第一次的跳槽,总是感觉有些矛盾,不过人生还是得求变啊,就当是丰富了人生的经历了。
说回正题,这次要实现的是,千万张图片的无卡顿加载。起因是在当前这家公司的项目中,有一个图片加载的WInform控件,在加载几十张图片以后,界面会出现明显的卡顿,这种用户体验实在是让人难以接受,想要优化,又看不到源码,所以,不如重新写一个。
做过安卓开发的同学们应该知道,移动设备的资源总是十分有限的,在列表显示或者图片显示等方面,需要经过各种的处理,以避免OOM,我将要做的就是将安卓的相关思想引入到winform的程序中。
首先,看一下我们实现的效果,在一般列表显示内容时,常规的思路,一次性准备好列表的所有子项,然后将所有子项都显示出来,这样的问题显而易见,如果子项有一百个还好,如果一万个,先不说可能直接出现的OOM问题,先是将所有子项渲染到界面上,这个卡顿就是让人难以忍受的。
所以,我们要借鉴安卓的思路,假设当前界面最大的显示行数为5行,我们默认便加载7行,多余的两行在控件滚动的时候,来回的“复用”,如下图所示:
第一次列表加载显示时,1到5是当前用户可见的区域,6和7是不可见的区域。
当用户的滚轮或者滚动条向下滑动时,1这个显示区域将不可见,而6被用户可见,我们这时将不可见的1调整至最后
当用户的滚轮滑动或者滚动条继续下滑时,将会出现如下的情况,被我们重新复用加载的1和2又显示了出来
而当用户先上滑动时,我们会挪除当前的最后一行,然后将最后一行放到第一行的位置,来进行子项的展示,大体如下图:
在上图的思路下,我们加载的项目由可能的千万个子项目变成了固定的七个子项,控件的渲染自然会加快不少。
下来我们开始制作这样一个控件,我选择了FlowLayoutPanel加上VScrollBar的组合,如下图所示:
首先,我们加载一些照片到这个控件中,为了能够记住照片的顺序,我编写了下面的代码,将数字变成一张照片,得到了本地图片的列表。
/// <summary>
/// 获取图形的集合列表
/// </summary>
/// <returns></returns>
private List<string> GetImageList()
{
List<string> datas = new List<string>();
//string familyName, float emSize, GraphicsUnit unit
Font font = new Font("宋体", 32, GraphicsUnit.Pixel);
string path = @"D:\360downloads\Resource";
for (int i = 0; i < 200; i++)
{
string realPath = string.Concat(path, @"\", i.ToString(), ".png");
if (!File.Exists(realPath))
{
Bitmap bitmap = TextToBitmap(i.ToString(), font, Rectangle.Empty, Color.Black, Color.White);
bitmap.Save(realPath);
bitmap.Dispose();
}
datas.Add(realPath);
}
return datas;
}
/// <summary>
/// 把文字转换才Bitmap
/// </summary>
/// <param name="text"></param>
/// <param name="font"></param>
/// <param name="rect">用于输出的矩形,文字在这个矩形内显示,为空时自动计算</param>
/// <param name="fontcolor">字体颜色</param>
/// <param name="backColor">背景颜色</param>
/// <returns></returns>
private Bitmap TextToBitmap(string text, Font font, Rectangle rect, Color fontcolor, Color backColor)
{
Graphics g;
Bitmap bmp;
StringFormat format = new StringFormat(StringFormatFlags.NoClip);
if (rect == Rectangle.Empty)
{
bmp = new Bitmap(1, 1);
g = Graphics.FromImage(bmp);
//计算绘制文字所需的区域大小(根据宽度计算长度),重新创建矩形区域绘图
SizeF sizef = g.MeasureString(text, font, PointF.Empty, format);
int width = (int)(sizef.Width + 1);
int height = (int)(sizef.Height + 1);
rect = new Rectangle(0, 0, width, height);
bmp.Dispose();
bmp = new Bitmap(width, height);
}
else
{
bmp = new Bitmap(rect.Width, rect.Height);
}
g = Graphics.FromImage(bmp);
//使用ClearType字体功能
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
g.FillRectangle(new SolidBrush(backColor), rect);
g.DrawString(text, font, Brushes.Black, rect, format);
return bmp;
}
接下来,我们在控件的SizeChange的事件中,基于子项的宽高,结合控件的高度,算出我们这个控件目前能展示几行子项,每行展示几个子项,最大显示的行数等,代码如下,这里假设子项宽高为100,margin为3
_Column = flContent.Width / 106;
if (_Column < 1)
_Column = 1;
_InitRow = flContent.Height / 100 + CacheRowCount;
这些渲染前的准备做好以后,下来,我们将要给FlowLayoutpanel添加子项了,首先,我们先创建一个照片的子项,很简单,就是一个用户控件,里面放着一个PictureBox,添加一个公共的方法LoadImage,方法参数是url,在方法中从指定的url中加载图片并显示到PictureBox即可
下来定义一个初始化界面的方法NotifyDataSetChanged,在方法中,我们先添加_columns乘_row个控件,初始化滚动条的最大值,
/// <summary>
/// 更新界面
/// </summary>
public void NotifyDataSetChanged()
{
//获取当前可加载的个数
_CurrentRow = _InitRow;
int maxNum = _Column * _InitRow;
if (maxNum > _Adapter.GetItemCount())
maxNum = _Adapter.GetItemCount();
_MaxRow = (int)Math.Ceiling(_Adapter.GetItemCount() * 1f / _Column) + 2;
//初始化滚动条
if (_MaxRow - CacheRowCount > 0)
{
vSBar.Visible = true;
vSBar.Maximum = _MaxRow;
vSBar.Value = 0;
}
else
{
vSBar.Visible = false;
vSBar.Maximum = 0;
vSBar.Value = 0;
}
//清除旧的子项目
flContent.Controls.Clear();
List<string> urls =GetImageList();
for (int i = 0; i < maxNum; i++)
{
ItemImage itemImage = new ItemImage();
itemImage.LoadImage(urls[i]);
flContent.Controls.Add(itemImage);
}
}
这样我们便初始化好了显示项目,接下来,我们要实现复用子项目,在滚动条的数值改变事件中来进行判断处理,相关伪代码如下:
/// <summary>
/// 滚动条的滚动事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void VSBar_ValueChanged(object sender, EventArgs e)
{
//滚动条向下滚动
if (row > _CurrentRow)
{
//在集合中去掉第一行
//修改第一行的数据
//第一行加到最后,重新显示
}
//滚动条向上滚动
if (row < _CurrentRow)
{
//在集合中去掉最后行
//修改最后一行的数据
//最后一行加到集合最前面,重新显示
}
//滑动到指定位置
flContent.AutoScrollPosition = new Point(0, row > _InitRow ? row * 106 : 0);
}
这样的话,我们的基本功能就已经完成了,为了实现控件的封装,使其不仅仅用于仅展示一个相册,我们加入了适配器模式,并且完善了滚轮和滚动条的联动,控件放大缩小的事件处理等,下一篇 图片的加载处理。
相关完整源代码地址如下 下载地址