美文网首页
Duilib性能优化——列表控件

Duilib性能优化——列表控件

作者: 丑角的晨歌 | 来源:发表于2018-09-27 19:47 被阅读0次

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

    相关文章

      网友评论

          本文标题:Duilib性能优化——列表控件

          本文链接:https://www.haomeiwen.com/subject/pucxwftx.html