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