Duilib性能优化——列表控件

Duilib中本来就有列表控件CListUI,但是它不适用于数据量较大的情况:

  • 每一个item都会在内存中有对应的控件实例,浪费内存。
  • 列表每一次layout都会处理全部的项目,浪费时间
  • 接口设计不够灵活,难以做到数据与视图分离
    (简单的说,就是老子不喜欢)
    做过Android开发的肯定知道RecyclerView,这里也可以使用跟RecyclerView一样的思路来优化,简单说一下就是这样的:
  • 内存里面只维护可视区域的控件,滚动时重用这些控件,为它们绑定不同的数据
  • 数据与视图之间的交互通过一个Adapter类进行,业务方面只需要实现Adapter的几个主要接口:取总项目数、创建新视图、绑定某个条目的数据到视图就可以完成最基本的显示功能
    大致实现
    这里控件从CContainerUI继承,我们主要完成布局的逻辑。
    首先定义一下Adapter的接口,列表将通过它获取数据:
class CXListUIDelegate {
  public:
      virtual size_t GetItemCount() = 0;
      virtual CControlUI* CreateItemView() = 0;
      virtual void OnBindItemView(CControlUI* view, size_t index) = 0;
};

解释一下接下来定义的成员变量:

class CXListUI : public CContainerUI {
  ......      
  private:
    CXListUIDelegate* m_Delegate;
    CControlUI* m_HiddenItem;          // 用于计算每个列表项的尺寸
    bool m_data_updated;                  //  是否需要强制刷新数据
    std::map<CControlUI*, int> m_itemview_index_map;    // 缓存每个view所绑定的项目序号

    int m_first_visible_index;               //  第一个可见view对应的index
    int m_first_itemview_top_offset;    //  第一个可见view的top偏移量

    int m_line_height;          // 滚动一行时所滚动的高度
    int m_total_height;         // 整个列表需要占用的高度
    int m_available_height;  // 列表的可见部分高度
    int m_ScrollY;                 // 列表自己维护的垂直方向的滚动
}

接下来就是布局逻辑,总体流程是这样的:通过可用尺寸与总的列表项数量等计算出是否需要滚动条、可见的列表项数量,之后根据垂直方向的滚动偏移量对可见的列表项进行布局,并将其绑定到对应的列表项数据:

void CXTreeUI::SetPos(RECT rc) {
  CControlUI::SetPos(rc);
  if (!m_Delegate) return;
  rc = m_rcItem;

  rc.left += m_rcInset.left;
  rc.top += m_rcInset.top;
  rc.right -= m_rcInset.right;
  rc.bottom -= m_rcInset.bottom;
  if (m_pVerticalScrollBar && m_pVerticalScrollBar->IsVisible()) rc.right -= m_pVerticalScrollBar->GetFixedWidth();
  if (m_pHorizontalScrollBar && m_pHorizontalScrollBar->IsVisible()) rc.bottom -= m_pHorizontalScrollBar->GetFixedHeight();

  SIZE szAvailable = { rc.right - rc.left, rc.bottom - rc.top };
  m_available_width = szAvailable.cx;
  m_available_height = szAvailable.cy;

  size_t item_view_count = ceil(double(m_available_height) / m_HiddenItem->GetFixedHeight()) + 1;
  if (m_Delegate->GetItemCount() < item_view_count)
    item_view_count = m_Delegate->GetItemCount();

  m_total_height = m_Delegate->GetItemCount() * m_HiddenItem->GetFixedHeight();
  int width_required = m_Delegate->GetItemCount() == 0 ? 0 : m_HiddenItem->GetFixedWidth();
  ProcessScrollBar(szAvailable, width_required, m_total_height);

  bool force_update = ProcessVisibleItems(item_view_count);
  UpdateSubviews(rc, force_update || m_data_updated);
}

滚动条的位置,滚动范围等信息的计算,因为改变滚动条控件位置时会导致父控件更新,所以为了避免死循环,在这里用m_bScrollProcess判断了是否正在处理滚动条的逻辑中;这里还涉及到一个情况,假如滚动条位置已经在最底部,此时如果用户删除了某些列表项,或者缩小窗口使列表可用区域变小,此时会造成显示的数据区域不对,因此需要在布局列表项之前先处理m_scrollY,确保不发生溢出;其他如果说还有什么特别的地方的话,大概就是要考虑一下垂直水平两个方向的滚动条互相之间的影响吧,逻辑如下:

void CXTreeUI::ProcessScrollBar(SIZE szAvailable, int cxRequired, int cyRequired)
{
  if (m_bScrollProcess)
    return;

  m_bScrollProcess = true;
  if (szAvailable.cy < cyRequired && m_pVerticalScrollBar) {
    RECT rcScrollBarPos = { m_rcItem.right - m_pVerticalScrollBar->GetFixedWidth(), 
        m_rcItem.top, 
        m_rcItem.right, 
        m_rcItem.bottom };
   if (szAvailable.cx < cxRequired && m_pHorizontalScrollBar)
       rcScrollBarPos.bottom -= m_pHorizontalScrollBar->GetFixedHeight();
    m_pVerticalScrollBar->SetPos(rcScrollBarPos);
    if (m_ScrollY > cyRequired - szAvailable.cy) {
      m_ScrollY = cyRequired - szAvailable.cy;
      m_pVerticalScrollBar->SetScrollPos(m_ScrollY);
    }
    m_pVerticalScrollBar->SetScrollRange(cyRequired - szAvailable.cy);
  }
  else {
      if (m_pVerticalScrollBar)
          m_pVerticalScrollBar->SetVisible(false);
  }

  if (szAvailable.cx < cxRequired && m_pHorizontalScrollBar) {
    RECT rcScrollBarPos = { m_rcItem.left, 
        m_rcItem.bottom -  m_pHorizontalScrollBar->GetFixedHeight(),
        m_rcItem.right,
        m_rcItem.bottom};
    if (szAvailable.cy < cyRequired && m_pVerticalScrollBar)
        rcScrollBarPos.right -= m_pVerticalScrollBar->GetFixedWidth();
    m_pHorizontalScrollBar->SetPos(rcScrollBarPos);
    if (m_ScrollX > cxRequired - szAvailable.cx) {
        m_ScrollX = cxRequired - szAvailable.cx;
        m_pHorizontalScrollBar->SetScrollPos(m_ScrollX);
    }
    m_pHorizontalScrollBar->SetScrollRange(cxRequired - szAvailable.cx);
  }
  else {
      if (m_pHorizontalScrollBar)
          m_pHorizontalScrollBar->SetVisible(false);
  }

  m_bScrollProcess = false;
}

根据SetPos中计算出的item_view_count维护一个子控件列表,这个值是根据当前列表高度与子项目的高度计算出的,由于有可能出现首尾两个控件都只显示一部分的情况,所以要多预留一个位置;虽然这里只有分配的逻辑没有释放的逻辑,但是也不影响实际使用:

bool CXTreeUI::ProcessVisibleItems(int item_view_count) {
  if (m_items.GetSize() != item_view_count) {
    if (m_items.GetSize() < item_view_count) {
      for (int i = m_items.GetSize(); i != item_view_count; ++i) {
        CControlUI *pControl = m_Delegate->CreateItemView();
        if (m_pManager != NULL) m_pManager->InitControls(pControl, this);
        m_items.Add(pControl);
      }
    }
    return true;
  }
  return false;
}

接下来是核心部分,根据变量m_scrollY中保存的列表可见区域的Y轴偏移量计算出当前状态下应该显示哪些项目,并进行排版;force_update是为了给更新数据、或者列表可见范围增大时使用。

void CXTreeUI::UpdateSubviews(RECT rc, bool force_update) {
  int item_view_height = m_HiddenItem->GetFixedHeight();
  int item_view_width = m_HiddenItem->GetFixedWidth();

  int scroll_posY = (m_pVerticalScrollBar && m_pVerticalScrollBar->IsVisible()) ? m_ScrollY : 0;
  int first_visible_index = scroll_posY / item_view_height;
  int itemview_pos_top = scroll_posY % item_view_height;

  if (m_first_visible_index == first_visible_index && m_first_itemview_top_offset == itemview_pos_top && !force_update) {
    return;
  }

  m_first_visible_index = first_visible_index;
  m_first_itemview_top_offset = itemview_pos_top;

  if (m_first_itemview_top_offset > 0)
    m_first_itemview_top_offset = -m_first_itemview_top_offset;
  for (int i = 0; i != m_items.GetSize(); ++i) {
    CControlUI *pControl = static_cast<CControlUI*>(m_items.GetAt(i));
    if (first_visible_index + i >= m_Delegate->GetItemCount()) {
      pControl->SetVisible(false);
      continue;
    }
    pControl->SetVisible(true);
    RECT rcCtrl = { rc.left - m_ScrollX,
      rc.top + m_first_itemview_top_offset,
      item_view_width == 0 ? rc.right : rc.left + item_view_width - m_ScrollX,
      rc.top + m_first_itemview_top_offset + item_view_height };
    pControl->SetPos(rcCtrl);
    m_first_itemview_top_offset += item_view_height;
    if (m_data_updated || m_itemview_index_map.find(pControl) == m_itemview_index_map.end() ||
      m_itemview_index_map[pControl] != first_visible_index + i) {
      m_itemview_index_map[pControl] = first_visible_index + i;
      m_Delegate->OnBindItemView(pControl, first_visible_index + i);
    }
  }
}

然后是滚动逻辑的处理,需要重写一下SetScrollPos,LineDown,PageDown等这一系列的函数,处理成把m_ScrollY修改成对应值就可以了,因为我们有自己的一套排版逻辑。我是觉得 Duilib的CScrollbarUI滚起来不爽(有个定时器延时的逻辑),直接把滚动条都重写了一份。这个并不复杂,就不放代码了吧;
虽然数据展示已经实现了,但是实际应用中很少会有纯展示的需求,多少都会需要响应一些事件。为了实现一些统一的事件,例如选中列表中项目、双击列表中项目等,我们可以定义一个通用的ListItem类,在里面实现一些通用事件的处理,比如发送DUI_MSGTYPE_ITEMCLICK等通知;当然直接用HorizontalUI当列表项也是可以的。
其他的话还有一些表头,列宽拖拽之类的特性,由于没有生产上的需求,就先不实现了,思路大致介绍到这里,这个实现其实目前也比较粗糙,完整代码就不放了,有需要的话根据上面放的代码应该足够自己抄一份了,没准还能抄得比我写的更好吧哈哈。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容