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)還能抄得比我寫的更好吧哈哈。