Winform 加载千万张图片的实践(一) RecycleView的实现

好长时间,已经没有过自我的总结,着实因为新入职的这家公司实习期的事情安排很多,第一次的跳槽,总是感觉有些矛盾,不过人生还是得求变啊,就当是丰富了人生的经历了。

说回正题,这次要实现的是,千万张图片的无卡顿加载。起因是在当前这家公司的项目中,有一个图片加载的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);
        }


         这样的话,我们的基本功能就已经完成了,为了实现控件的封装,使其不仅仅用于仅展示一个相册,我们加入了适配器模式,并且完善了滚轮和滚动条的联动,控件放大缩小的事件处理等,下一篇 图片的加载处理。
     

相关完整源代码地址如下 下载地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值