用 Bolt 实现了一个表格控件:
1. 提供 Insert,Remove,Get,Set 接口,可以为表格增删数据;
2. 通过 ItemClass, ItemSetDataFunc 属性来指定显示数据所用的 itemObj;
3. 不会每个 data 都创建 itemObj 来显示, 只为需要显示的数据创建 itemObj;
4. 提供 AttachItemEvent 接口,可以监听 itemObj 向外发出的事件;
5. 根据屏幕大小,列数可以自适应;
6. 根据屏幕大小,每一列的宽度可以自适应;
gridview.xml
<xlue> <control class="GridView"> <attr_def> <attr name="ItemClass" type="string" /> <!-- 子控件类名 --> <attr name="ItemWidth" type="int" /> <!-- 子控件宽度 --> <attr name="ItemHeight" type="int" /> <!-- 子控件高度 --> <attr name="ItemSetDataFunc" type="string" /> <!-- 子控件 SetData 函数名,GridView 自动调用子控件的 SetData 函数来为子控件设定数据 --> <attr name="ColumnNum" type="int"> <default>1</default> </attr> <attr name="AutoColumnCount" type="bool"> <!-- 列数根据 GridView 控件宽度自适应, 为 true 则 ColumnNum, AutoColumnWidth 属性失效 --> <default>false</default> </attr> <attr name="AutoColumnWidth" type="bool"> <!-- 子控件的宽度根据 GridView 控件宽度自适应, 为 true 则 ItemWidth 属性失效 --> <default>false</default> </attr> <attr name="ColumnSpace" type="int"> <!-- 列与列之间的间隙 --> <default>0</default> </attr> <attr name="RowSpace" type="int"> <!-- 行与行之间的间隙 --> <default>0</default> </attr> <attr name="ScrollBarBkg" type="string" /> <!-- 滚动条背景 texture id --> <attr name="ScrollBarSliderNormal" type="string" /> <!-- 滚动条滑块 normal 态 texture id --> <attr name="ScrollBarSliderHover" type="string" /> <!-- 滚动条滑块 hover 态 texture id --> <attr name="ScrollBarSliderDown" type="string" /> <!-- 滚动条滑块 down 态 texture id --> </attr_def> <method_def> <ResetData file="GridView.xml.lua" func="ResetData" /> <InsertData file="GridView.xml.lua" func="InsertData" /> <RemoveData file="GridView.xml.lua" func="RemoveData" /> <SetData file="GridView.xml.lua" func="SetData" /> <GetData file="GridView.xml.lua" func="GetData" /> <AttachItemEvent file="GridView.xml.lua" func="AttachItemEvent" /> <!-- 监听子控件的事件 --> <DetachItemEvent file="GridView.xml.lua" func="DetachItemEvent" /> <!-- 取消监听子控件的事件 --> </method_def> <event_def> </event_def> <objtemplate> <children> <obj id="container" class="LayoutObject"> <attr> <left>0</left> <top>0</top> <width>father.width</width> <height>father.height</height> <limitchild>1</limitchild> </attr> <eventlist> <event name="OnPosChange" file="GridView.xml.lua" func="Container_OnPosChange" /> <event name="OnMouseWheel" file="GridView.xml.lua" func="Container_OnMouseWheel" /> </eventlist> </obj> <obj id="vscrollbar.bkg" class="TextureObject"> <attr> <left>father.width-12</left> <top>0</top> <width>12</width> <height>father.height</height> <zorder>100</zorder> </attr> <children> <obj id="vscrollbar.slider" class="TextureObject"> <attr> <left>2</left> <top>0</top> <width>8</width> <height>0</height> <zorder>1000</zorder> </attr> <eventlist> <event name="OnLButtonDown" file="GridView.xml.lua" func="VScrollBar_OnLButtonDown" /> <event name="OnLButtonUp" file="GridView.xml.lua" func="VScrollBar_OnLButtonUp" /> <event name="OnMouseMove" file="GridView.xml.lua" func="VScrollBar_OnMouseMove" /> <event name="OnMouseLeave" file="GridView.xml.lua" func="VScrollBar_OnMouseLeave" /> <event name="OnMouseWheel" file="GridView.xml.lua" func="Container_OnMouseWheel" redirect="father:container" /> </eventlist> </obj> </children> </obj> </children> <eventlist> <event name="OnInitControl" file="GridView.xml.lua" func="OnInitControl" /> <event name="OnDestroy" file="GridView.xml.lua" func="OnDestroy" /> </eventlist> </objtemplate> </control> </xlue>
gridview.xml.lua
------------------------------- 以下是外部函数 -------------------------------- -- 重置列表 function ResetData(ctrlObj, dataList) local attr = ctrlObj:GetAttribute() -- 列表置顶 attr.VirtualTop = 0 -- 先清空,再添加新数据 ctrlObj:RemoveData(1, #attr.IdexToDataMap) ctrlObj:InsertData(1, dataList) end -- 在列表指定位置插入数据 function InsertData(ctrlObj, insertIndex, dataList) local attr = ctrlObj:GetAttribute() if insertIndex == nil or dataList == nil then return end if type(dataList) ~= "table" or #dataList == 0 then return end if insertIndex < 1 or insertIndex > #attr.IdexToDataMap + 1 then return end -- 更新 IdexToDataMap 表 for i, data in ipairs(dataList) do local dataInfo = {} dataInfo.data = data -- data 可能重复,所以不能当作 key, 这里把 data 放到 dataInfo 里面,就可以用 dataInfo 作 key 了 table.insert(attr.IdexToDataMap, insertIndex+i-1, dataInfo) end -- 更新 ItemToIdexMap 表 -- 因为插入了新数据, insertIndex 后面的 dataIndex 增加了 #dataList for itemId, dataIndex in pairs(attr.ItemToIdexMap) do if dataIndex >= insertIndex then attr.ItemToIdexMap[itemId] = dataIndex + #dataList end end RefreshUI(ctrlObj) end -- 删除数据 function RemoveData(ctrlObj, beginIndex, endIndex) local attr = ctrlObj:GetAttribute() if beginIndex == nil or endIndex == nil then return end if beginIndex < 1 or beginIndex > #attr.IdexToDataMap then return end if endIndex < beginIndex or endIndex > #attr.IdexToDataMap then return end -- 更新 IdexToDataMap 表 local removedDataList = {} for i=beginIndex, endIndex do table.insert(removedDataList, attr.IdexToDataMap[beginIndex]) table.remove(attr.IdexToDataMap, beginIndex) end -- 更新 DataToItemMap 表 local removedItemList = {} for _, removedDataInfo in ipairs(removedDataList) do if attr.DataToItemMap[removedDataInfo] then table.insert(removedItemList, attr.DataToItemMap[removedDataInfo]) attr.DataToItemMap[removedDataInfo] = nil end end -- 更新 ItemToIdexMap 表 for _, removedItemId in ipairs(removedItemList) do if attr.ItemToIdexMap[removedItemId] then -- 删掉不用的项 attr.ItemToIdexMap[removedItemId] = nil RecycleItemObj(ctrlObj, removedItemId) end end for itemId, dataIndex in pairs(attr.ItemToIdexMap) do if dataIndex >= endIndex then -- 因为删除了旧数据, endIndex 后面的 dataIndex 减少了 endIndex-beginIndex+1 attr.ItemToIdexMap[itemId] = dataIndex - (endIndex-beginIndex+1) end end RefreshUI(ctrlObj) end -- 设置数据 function SetData(ctrlObj, dataIndex, data) local attr = ctrlObj:GetAttribute() -- 更新数据 if attr.IdexToDataMap[dataIndex] == nil then return end attr.IdexToDataMap[dataIndex].data = data -- 更新界面 local dataInfo = attr.IdexToDataMap[dataIndex] local itemId = attr.DataToItemMap[dataInfo] if itemId == nil then return end SetItemData(ctrlObj, itemId, data) end -- 获取数据 function GetData(ctrlObj, dataIndex) local attr = ctrlObj:GetAttribute() local dataInfo = attr.IdexToDataMap[dataIndex] if dataInfo == nil then return end return dataInfo.data end -- 监听 itemObj 发出的事件 function AttachItemEvent(ctrlObj, eventName, callback) local attr = ctrlObj:GetAttribute() if eventName == nil or callback == nil then return end if attr.EventCookieMap[eventName] == nil then attr.EventCookieMap[eventName] = {} end -- 分配 cookie local cookie = attr.EventCookieMap.CookieNum attr.EventCookieMap.CookieNum = cookie + 1 -- 记录到 EventCookieMap 中 attr.EventCookieMap[eventName][cookie] = callback if attr.ItemCookieMap[eventName] ~= nil then -- ItemCookieMap[eventName] 不为空,说明 itemObj 已经监听了这个事件 return cookie end -- 为每个 itemObj 设置 eventName 的监听器 attr.ItemCookieMap[eventName] = {} for _, itemId in pairs(attr.DataToItemMap) do local itemObj = ctrlObj:GetControlObject(itemId) local itemCookie = itemObj:AttachListener(eventName, true, function(...) OnItemEvent(eventName, select(1, ...)) end) -- 把 itemId 和 cookie 的对应关系记到表里面 attr.ItemCookieMap[eventName][itemId] = itemCookie end return cookie end -- 取消监听器 function DetachItemEvent(ctrlObj, eventName, cookie) local attr = ctrlObj:GetAttribute() if eventName == nil or cookie == nil then return end if attr.EventCookieMap[eventName] == nil then return end -- 去除 cookie 对应的监听器 attr.EventCookieMap[eventName][cookie] = nil local count = 0 for cookie, callback in pairs(attr.EventCookieMap[eventName]) do count = count + 1 end if count > 0 then return end -- 如果已经没有人监听这个事件了,那么 itemObj 也不用监听这个事件了 if attr.ItemCookieMap[eventName] == nil then return end for itemId, itemCookie in pairs(attr.ItemCookieMap[eventName]) do local itemObj = ctrlObj:GetControlObject(itemId) itemObj:RemoveListener(eventName, itemCookie) end attr.ItemCookieMap[eventName] = nil attr.EventCookieMap[eventName] = nil end ------------------------------- 以下是事件函数 -------------------------------- function OnInitControl(ctrlObj) local attr = ctrlObj:GetAttribute() -- 用三个 map 来记录 dataIndex, dataInfo, itemId 三者的关联,便于互相之间快速索引 attr.IdexToDataMap = {} attr.DataToItemMap = {} attr.ItemToIdexMap = {} -- 虚拟 Top ,指的是列表的第一个 dataInfo 在界面中应该处于的位置,依据这个来计算每个 dataInfo 的 top attr.VirtualTop = 0 -- 回收不用的 itemObj attr.RecycleItemList = {} attr.ItemIdCache = {} attr.ItemIdMax = 0 -- 记录外部调用 AttachItemEvent 时分配的 cookie 和 callback attr.EventCookieMap = {} attr.EventCookieMap.CookieNum = 0 -- cookie 的分配就简单地 +1 好了 -- 记录每个 itemObj 关注事件所返回的 cookie attr.ItemCookieMap = {} if attr.ScrollBarBkg ~= nil then local vsbarBkgObj = ctrlObj:GetControlObject("vscrollbar.bkg") vsbarBkgObj:SetTextureID(attr.ScrollBarBkg) end if attr.ScrollBarSliderNormal ~= nil then local vsbarObj = ctrlObj:GetControlObject("vscrollbar.slider") vsbarObj:SetTextureID(attr.ScrollBarSliderNormal) end end function OnDestroy(ctrlObj) end function Container_OnPosChange(containerObj, oldLeft, oldTop, oldRight, oldBottom, newLeft, newTop, newRight, newBottom) local ctrlObj = containerObj:GetOwnerControl() local oldWidth, oldHeight = oldRight-oldLeft, oldBottom-oldTop local newWidth, newHeight = newRight-newLeft, newBottom-newTop if newWidth == oldWidth and newHeight == oldHeight then -- 控件大小未改变,不需要需要刷新 return end RefreshUI(ctrlObj) end function Container_OnMouseWheel(containerObj, x, y, distance, flags) local ctrlObj = containerObj:GetOwnerControl() local attr = ctrlObj:GetAttribute() local moveDistance = distance / 5 attr.VirtualTop = attr.VirtualTop + moveDistance RefreshUI(ctrlObj) end function VScrollBar_OnLButtonDown(vsbarObj, x, y, flags) local ctrlObj = vsbarObj:GetOwnerControl() local attr = ctrlObj:GetAttribute() vsbarObj:SetCaptureMouse(true) if attr.ScrollBarSliderDown then vsbarObj:SetTextureID(attr.ScrollBarSliderDown) end attr.VScrollBar_LButtonDown_PosY = y end function VScrollBar_OnLButtonUp(vsbarObj, x, y, flags) local ctrlObj = vsbarObj:GetOwnerControl() local attr = ctrlObj:GetAttribute() vsbarObj:SetCaptureMouse(false) if attr.ScrollBarSliderNormal then vsbarObj:SetTextureID(attr.ScrollBarSliderNormal) end attr.VScrollBar_LButtonDown_PosY = 0 end function VScrollBar_OnMouseMove(vsbarObj, x, y, flags) local ctrlObj = vsbarObj:GetOwnerControl() local attr = ctrlObj:GetAttribute() if flags == 1 then -- 鼠标左键被按下 local moveDistance = y - attr.VScrollBar_LButtonDown_PosY moveDistance = - moveDistance attr.VirtualTop = attr.VirtualTop + moveDistance RefreshUI(ctrlObj) else if attr.ScrollBarSliderHover then vsbarObj:SetTextureID(attr.ScrollBarSliderHover) end end end function VScrollBar_OnMouseLeave(vsbarObj, x, y) local ctrlObj = vsbarObj:GetOwnerControl() local attr = ctrlObj:GetAttribute() if attr.ScrollBarSliderNormal then vsbarObj:SetTextureID(attr.ScrollBarSliderNormal) end end ------------------------------- 以下是私有函数 -------------------------------- function RefreshUI(ctrlObj) -- 调整 列数 AdjustColumnNum(ctrlObj) -- 调整 列宽度 AdjustColumnWidth(ctrlObj) -- 调整 VirtualTop AdjustVirtualTop(ctrlObj) -- 重新绑定 data 到 itemObj RebindDataToItem(ctrlObj) -- 调整 itemObj 的位置 AdjustItemPos(ctrlObj) -- 调整滚动条位置 AdjustVScrollBarPos(ctrlObj) end function AdjustColumnNum(ctrlObj) local attr = ctrlObj:GetAttribute() local containerObj = ctrlObj:GetControlObject("container") local l,t,r,b = containerObj:GetObjPos() local containerWidth, containerHeight = r-l, b-t if attr.AutoColumnCount == true then attr.ColumnNum = math.floor(containerWidth / (attr.ItemWidth+attr.ColumnSpace)) end end function AdjustColumnWidth(ctrlObj) local attr = ctrlObj:GetAttribute() local containerObj = ctrlObj:GetControlObject("container") local l,t,r,b = containerObj:GetObjPos() local containerWidth = r-l if attr.AutoColumnCount == false then if attr.AutoColumnWidth == true then attr.ItemWidth = math.floor((containerWidth+attr.ColumnSpace) / attr.ColumnNum) end end end function AdjustVirtualTop(ctrlObj) local attr = ctrlObj:GetAttribute() local containerObj = ctrlObj:GetControlObject("container") local virtualBottom = attr.VirtualTop + math.ceil(#attr.IdexToDataMap / attr.ColumnNum) * (attr.ItemHeight+attr.RowSpace) local l,t,r,b = containerObj:GetObjPos() local containerHeight = b-t -- 不要让列表底部有间隙 if virtualBottom < containerHeight then attr.VirtualTop = attr.VirtualTop + containerHeight - virtualBottom end -- 不要让列表顶部有间隙 if attr.VirtualTop > 0 then attr.VirtualTop = 0 end end function RebindDataToItem(ctrlObj) local attr = ctrlObj:GetAttribute() local containerObj = ctrlObj:GetControlObject("container") -- 1. 计算哪些 dataInfo 处于可视范围 local visibleDataMap = {} local l,t,r,b = containerObj:GetObjPos() local containerHeight = b-t local beginRow = math.floor( (0-attr.VirtualTop) / (attr.ItemHeight+attr.RowSpace) ) local endRow = math.ceil( (containerHeight-attr.VirtualTop) / (attr.ItemHeight+attr.RowSpace) ) + 1 local beginIndex = beginRow * attr.ColumnNum + 1 local endIndex = endRow * attr.ColumnNum beginIndex = math.min(beginIndex, #attr.IdexToDataMap) endIndex = math.min(endIndex, #attr.IdexToDataMap) if beginIndex > 0 and endIndex >= beginIndex then for dataIndex=beginIndex, endIndex do local dataInfo = attr.IdexToDataMap[dataIndex] visibleDataMap[dataInfo] = dataIndex end end -- 2. 回收不用的 itemObj local inVisibleDataMap = {} for dataInfo, itemId in pairs(attr.DataToItemMap) do if visibleDataMap[dataInfo] == nil then inVisibleDataMap[dataInfo] = itemId end end for dataInfo, itemId in pairs(inVisibleDataMap) do attr.DataToItemMap[dataInfo] = nil attr.ItemToIdexMap[itemId] = nil RecycleItemObj(ctrlObj, itemId) end -- 3. 为没有绑定 itemObj 的 dataInfo 进行绑定 for dataInfo, dataIndex in pairs(visibleDataMap) do if attr.DataToItemMap[dataInfo] == nil then local itemObj = CreateItemObj(ctrlObj) local itemId = itemObj:GetID() attr.DataToItemMap[dataInfo] = itemId attr.ItemToIdexMap[itemId] = dataIndex -- 初次绑定的 itemObj, 要调用 SetDataFunc SetItemData(ctrlObj, itemId, dataInfo.data) end end -- 4. 清空不用的 itemObj CleanItemObj(ctrlObj) end function AdjustItemPos(ctrlObj) local attr = ctrlObj:GetAttribute() local function CalculateItemPos(dataIndex) local containerObj = ctrlObj:GetControlObject("container") local left = math.floor((dataIndex-1) % attr.ColumnNum) * (attr.ItemWidth + attr.ColumnSpace) local top = math.floor((dataIndex-1) / attr.ColumnNum) * (attr.ItemHeight + attr.RowSpace) + attr.VirtualTop local right = left + attr.ItemWidth local bottom = top + attr.ItemHeight return left, top, right, bottom end for dataInfo, itemId in pairs(attr.DataToItemMap) do local itemObj = ctrlObj:GetControlObject(itemId) local dataIndex = attr.ItemToIdexMap[itemId] local left, top, right, bottom = CalculateItemPos(dataIndex) local l,t,r,b = itemObj:GetObjPos() if left ~= l or top ~= t or right ~= r or bottom ~= b then itemObj:SetObjPos(left, top, right, bottom) end end end function AdjustVScrollBarPos(ctrlObj) local attr = ctrlObj:GetAttribute() local vsbarObj = ctrlObj:GetControlObject("vscrollbar.slider") local vsbarBkgObj = ctrlObj:GetControlObject("vscrollbar.bkg") local containerObj = ctrlObj:GetControlObject("container") local listTop = attr.VirtualTop local listBottom = attr.VirtualTop + math.ceil(#attr.IdexToDataMap / attr.ColumnNum) * (attr.ItemHeight+attr.RowSpace) local listHeight = listBottom - listTop local l,t,r,b = containerObj:GetObjPos() local containerHeight = b-t local l,t,r,b = vsbarBkgObj:GetObjPos() local vsbarBkgHeight = b-t -- 大小 local vsbHeight = 0 if listHeight > containerHeight then vsbHeight = vsbarBkgHeight * containerHeight / listHeight end -- 位置 local vsbTop = 0 if listHeight > containerHeight then vsbTop = vsbarBkgHeight * (-listTop) / listHeight end local l,t,r,b = vsbarObj:GetObjPos() if t ~= vsbTop or b-t ~= vsbHeight then vsbarObj:SetObjPos(l, vsbTop, r, vsbTop + vsbHeight) end -- 滚动调消失时, 背景也要消失 if vsbHeight == 0 then vsbarBkgObj:SetVisible(false) else vsbarBkgObj:SetVisible(true) end end function CreateItemObj(ctrlObj) local attr = ctrlObj:GetAttribute() local containerObj = ctrlObj:GetControlObject("container") local factory = XLGetObject("Xunlei.UIEngine.ObjectFactory") -- 回收站里有 itemObj, 直接返回 if #attr.RecycleItemList > 0 then local itemObj = attr.RecycleItemList[#attr.RecycleItemList] table.remove(attr.RecycleItemList) return itemObj end -- 分配 itemId local itemId = nil if #attr.ItemIdCache > 0 then itemId = attr.ItemIdCache[#attr.ItemIdCache] table.remove(attr.ItemIdCache) else itemId = "item" .. attr.ItemIdMax attr.ItemIdMax = attr.ItemIdMax + 1 end -- 创建 itemObj local itemObj = factory:CreateUIObject(itemId, attr.ItemClass) containerObj:AddChild(itemObj) -- 设置监听器 for eventName, _ in pairs(attr.ItemCookieMap) do local itemCookie = itemObj:AttachListener(eventName, true, function(...) OnItemEvent(eventName, select(1, ...)) end) -- 把 itemObj 和 cookie 的关系记到表里面 attr.ItemCookieMap[eventName][itemObj] = itemCookie end return itemObj end function RecycleItemObj(ctrlObj, itemId) local attr = ctrlObj:GetAttribute() local itemObj = ctrlObj:GetControlObject(itemId) if itemObj == nil then return end table.insert(attr.RecycleItemList, itemObj) end function CleanItemObj(ctrlObj) local attr = ctrlObj:GetAttribute() local containerObj = ctrlObj:GetControlObject("container") for i,itemObj in ipairs(attr.RecycleItemList) do -- itemId 用不着了,回收到 ItemIdCache 里下次使用 local itemId = itemObj:GetID() table.insert(attr.ItemIdCache, itemId) if attr.ItemCookieMap[eventName] then attr.ItemCookieMap[eventName][itemObj] = nil end containerObj:RemoveChild(itemObj) end attr.RecycleItemList = {} end function SetItemData(ctrlObj, itemId, data) local attr = ctrlObj:GetAttribute() local itemObj = ctrlObj:GetControlObject(itemId) if itemObj == nil then return end itemObj[attr.ItemSetDataFunc](itemObj, data) end -- itemObj 发出事件时的回调函数 function OnItemEvent(eventName, ...) local itemObj = select(1, ...) local ctrlObj = itemObj:GetOwnerControl() local attr = ctrlObj:GetAttribute() local itemId = itemObj:GetID() local dataIndex = attr.ItemToIdexMap[itemId] if attr.EventCookieMap[eventName] == nil then return end for cookie, callback in pairs(attr.EventCookieMap[eventName]) do callback(ctrlObj, dataIndex, select(1, ...)) end end