Duilib性能优化——列表控件

2018-09-27  本文已影响0人  丑角的晨歌

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

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当列表项也是可以的。
其他的话还有一些表头,列宽拖拽之类的特性,由于没有生产上的需求,就先不实现了,思路大致介绍到这里,这个实现其实目前也比较粗糙,完整代码就不放了,有需要的话根据上面放的代码应该足够自己抄一份了,没准还能抄得比我写的更好吧哈哈。

上一篇下一篇

猜你喜欢

热点阅读