Duilib性能優(yōu)化——列表控件

Duilib中本來就有列表控件CListUI,但是它不適用于數(shù)據(jù)量較大的情況:

  • 每一個(gè)item都會(huì)在內(nèi)存中有對(duì)應(yīng)的控件實(shí)例,浪費(fèi)內(nèi)存。
  • 列表每一次layout都會(huì)處理全部的項(xiàng)目,浪費(fèi)時(shí)間
  • 接口設(shè)計(jì)不夠靈活,難以做到數(shù)據(jù)與視圖分離
    (簡單的說,就是老子不喜歡)
    做過Android開發(fā)的肯定知道RecyclerView,這里也可以使用跟RecyclerView一樣的思路來優(yōu)化,簡單說一下就是這樣的:
  • 內(nèi)存里面只維護(hù)可視區(qū)域的控件,滾動(dòng)時(shí)重用這些控件,為它們綁定不同的數(shù)據(jù)
  • 數(shù)據(jù)與視圖之間的交互通過一個(gè)Adapter類進(jìn)行,業(yè)務(wù)方面只需要實(shí)現(xiàn)Adapter的幾個(gè)主要接口:取總項(xiàng)目數(shù)、創(chuàng)建新視圖、綁定某個(gè)條目的數(shù)據(jù)到視圖就可以完成最基本的顯示功能
    大致實(shí)現(xiàn)
    這里控件從CContainerUI繼承,我們主要完成布局的邏輯。
    首先定義一下Adapter的接口,列表將通過它獲取數(shù)據(jù):
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;          // 用于計(jì)算每個(gè)列表項(xiàng)的尺寸
    bool m_data_updated;                  //  是否需要強(qiáng)制刷新數(shù)據(jù)
    std::map<CControlUI*, int> m_itemview_index_map;    // 緩存每個(gè)view所綁定的項(xiàng)目序號(hào)

    int m_first_visible_index;               //  第一個(gè)可見view對(duì)應(yīng)的index
    int m_first_itemview_top_offset;    //  第一個(gè)可見view的top偏移量

    int m_line_height;          // 滾動(dòng)一行時(shí)所滾動(dòng)的高度
    int m_total_height;         // 整個(gè)列表需要占用的高度
    int m_available_height;  // 列表的可見部分高度
    int m_ScrollY;                 // 列表自己維護(hù)的垂直方向的滾動(dòng)
}

接下來就是布局邏輯,總體流程是這樣的:通過可用尺寸與總的列表項(xiàng)數(shù)量等計(jì)算出是否需要滾動(dòng)條、可見的列表項(xiàng)數(shù)量,之后根據(jù)垂直方向的滾動(dòng)偏移量對(duì)可見的列表項(xiàng)進(jìn)行布局,并將其綁定到對(duì)應(yīng)的列表項(xiàng)數(shù)據(jù):

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);
}

滾動(dòng)條的位置,滾動(dòng)范圍等信息的計(jì)算,因?yàn)楦淖儩L動(dòng)條控件位置時(shí)會(huì)導(dǎo)致父控件更新,所以為了避免死循環(huán),在這里用m_bScrollProcess判斷了是否正在處理滾動(dòng)條的邏輯中;這里還涉及到一個(gè)情況,假如滾動(dòng)條位置已經(jīng)在最底部,此時(shí)如果用戶刪除了某些列表項(xiàng),或者縮小窗口使列表可用區(qū)域變小,此時(shí)會(huì)造成顯示的數(shù)據(jù)區(qū)域不對(duì),因此需要在布局列表項(xiàng)之前先處理m_scrollY,確保不發(fā)生溢出;其他如果說還有什么特別的地方的話,大概就是要考慮一下垂直水平兩個(gè)方向的滾動(dòng)條互相之間的影響吧,邏輯如下:

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;
}

根據(jù)SetPos中計(jì)算出的item_view_count維護(hù)一個(gè)子控件列表,這個(gè)值是根據(jù)當(dāng)前列表高度與子項(xiàng)目的高度計(jì)算出的,由于有可能出現(xiàn)首尾兩個(gè)控件都只顯示一部分的情況,所以要多預(yù)留一個(gè)位置;雖然這里只有分配的邏輯沒有釋放的邏輯,但是也不影響實(shí)際使用:

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;
}

接下來是核心部分,根據(jù)變量m_scrollY中保存的列表可見區(qū)域的Y軸偏移量計(jì)算出當(dāng)前狀態(tài)下應(yīng)該顯示哪些項(xiàng)目,并進(jìn)行排版;force_update是為了給更新數(shù)據(jù)、或者列表可見范圍增大時(shí)使用。

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);
    }
  }
}

然后是滾動(dòng)邏輯的處理,需要重寫一下SetScrollPos,LineDown,PageDown等這一系列的函數(shù),處理成把m_ScrollY修改成對(duì)應(yīng)值就可以了,因?yàn)槲覀冇凶约旱囊惶着虐孢壿?。我是覺得 Duilib的CScrollbarUI滾起來不爽(有個(gè)定時(shí)器延時(shí)的邏輯),直接把滾動(dòng)條都重寫了一份。這個(gè)并不復(fù)雜,就不放代碼了吧;
雖然數(shù)據(jù)展示已經(jīng)實(shí)現(xiàn)了,但是實(shí)際應(yīng)用中很少會(huì)有純展示的需求,多少都會(huì)需要響應(yīng)一些事件。為了實(shí)現(xiàn)一些統(tǒng)一的事件,例如選中列表中項(xiàng)目、雙擊列表中項(xiàng)目等,我們可以定義一個(gè)通用的ListItem類,在里面實(shí)現(xiàn)一些通用事件的處理,比如發(fā)送DUI_MSGTYPE_ITEMCLICK等通知;當(dāng)然直接用HorizontalUI當(dāng)列表項(xiàng)也是可以的。
其他的話還有一些表頭,列寬拖拽之類的特性,由于沒有生產(chǎn)上的需求,就先不實(shí)現(xiàn)了,思路大致介紹到這里,這個(gè)實(shí)現(xiàn)其實(shí)目前也比較粗糙,完整代碼就不放了,有需要的話根據(jù)上面放的代碼應(yīng)該足夠自己抄一份了,沒準(zhǔn)還能抄得比我寫的更好吧哈哈。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,115評(píng)論 25 709
  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,211評(píng)論 3 119
  • 我是如何鼓勵(lì)孩子做家務(wù)的? 我孩子多,我對(duì)這方面有經(jīng)驗(yàn)的,了解孩子的一些特點(diǎn),孩子從小對(duì)外界,或物、人啊就有很強(qiáng)的...
    _紫霞閱讀 168評(píng)論 0 0
  • 第一,父母抱著“我是為你好” ,沒有界限~孩子期待父母理解,父母希望孩子聽話~愛在心里口難開,容易被彼此道德綁架~...
    從媯妤到瀾依閱讀 572評(píng)論 0 0
  • 一: 老舍的長篇小說: 《駱駝祥子》《四世同堂》 短篇小說:《馬褲先生》《微神》《柳家大院》《抱 孫》 豐子愷...
    曼谷123閱讀 246評(píng)論 0 0

友情鏈接更多精彩內(nèi)容