定義
可視區(qū)域加載,顧名思義,即listview只加載數(shù)據(jù)源中的可視部分,而不可視部分則不需要加載。其實質(zhì)也是懶加載技術(簡稱lazyload)的一種,已經(jīng)不是什么新鮮的技術了,js程序員對網(wǎng)頁性能優(yōu)化經(jīng)常會采用這種方案,大型網(wǎng)站中都有l(wèi)azyload的身影,如谷歌的圖片搜索頁,淘寶網(wǎng)等。lazyload的核心是按需加載。
使用場合
在游戲中,還有哪些地方需要用到這種可視區(qū)域的listview呢?沒錯,聊天窗口。現(xiàn)在手游的聊天內(nèi)容展示越來越豐富,再不是一兩年前的手游純文字聊天能比擬的。玩家頭像、富文本、超鏈接、動態(tài)表情,在加上數(shù)量龐大的聊天記錄,不做任何優(yōu)化的話,僅打開一個聊天窗口一段時間,也會消耗不少的cpu和內(nèi)存。如果項目有這種聊天需求,也是時候考慮優(yōu)化方案了。
方案設計
在設計優(yōu)化方案之前,參考了下夢幻手游的世界聊天,看看這個同樣用cocos2dx-lua的scrollview控件,添加了怎樣的特性。偶然發(fā)現(xiàn),把加載了一定數(shù)量的消息滾動到中央,手指在聊天框中間做上下幅度4cm的拖動,無論是向上還是向下滾動,都會發(fā)生一瞬間的卡頓。到這里其實已經(jīng)印證了我猜想中的方案了,下面來說一下這個方案。
在我的方案中,數(shù)據(jù)源(青色背景),加載區(qū)域(藍色),可視區(qū)域(灰色)可以用下面的圖來描述(有點丑,將就下)

有點像滑動的窗口故取名SlideWindow。
當屏幕可視區(qū)域滾動到已加載區(qū)域的邊緣時候,加載區(qū)域往滾動方向加載更多數(shù)據(jù),并且加載區(qū)域為保持一定數(shù)量的item,刪除掉對應反方向的數(shù)據(jù)。這種可視區(qū)域維持一定數(shù)量的item,應該就是導致夢幻聊天在某個臨界區(qū)上下滾動一瞬間卡頓的原因了。使用這種方案,無論夢幻的聊天累積了多少條聊天記錄,每次都只是加載一定數(shù)量的item,在拖動時按需加載,調(diào)和了數(shù)量大和首次打開界面流暢的矛盾。夢幻的聊天還有一個非常人性的設計,當可視區(qū)域在頂部的時候,設置為鎖定狀態(tài)(新消息置頂),而當可視區(qū)在中間時候,為非鎖定狀態(tài)(新消息顯示數(shù)目提示,而不會讓玩家瀏覽歷史記錄時候受到新消息置頂?shù)睦_)。
實現(xiàn)
這里我用的cocos2dx-lua實現(xiàn)了一個原型,暫時還沒有加入到項目中,實現(xiàn)思想跟之前實現(xiàn)的lazylistview差不多。
這是封裝的成員屬性,維護已加載區(qū)域的頭指針和尾指針。
function SlideWindow:ctor(list_view)
self.list_view_ = list_view
self.max_size_ = 40 -- 列表項的最大個數(shù)
self.load_step_ = 20 -- 每次滑動加載的步長
self.head_ = 1 -- 當前頭的index
self.tail_ = 0 -- 當前尾的index
self.on_item_callback_ = nil
end
當檢測到需要加載數(shù)據(jù)時,調(diào)用need_load
-- 觸發(fā)加載,-1為向上加載,1為向下加載
-- @return 返回本次加載的個數(shù)
function SlideWindow:need_load(direction)
local ret = 0
local start = self.tail_ + 1
if direction < 0 then
start = self.head_ - 1
end
-- 向上沒有數(shù)據(jù),返回
if start <= 0 then
return 0
end
-- 嘗試加載load_step個列表
local item_tpl = 1
while item_tpl and ret < self.load_step_ do
item_tpl = self.on_item_callback_(start + ret*direction)
if item_tpl then
if direction < 0 then
if self.head_ == 1 then
break
end
self.head_ = self.head_ - 1
self.list_view_:insertCustomItem(item_tpl,0)
else
self.tail_ = self.tail_ + 1
self.list_view_:pushBackCustomItem(item_tpl)
end
ret = ret + 1
end
end
if ret == 0 then
return ret
end
-- 判斷是否達到最大數(shù)量
if self.tail_ - self.head_ + 1 > self.max_size_ then
for i=1,self.tail_ - self.head_ - self.max_size_ do
if direction < 0 then
self.tail_ = self.tail_ - 1
self.list_view_:removeLastItem()
else
self.head_ = self.head_ + 1
self.list_view_:removeItem(0)
end
end
end
return ret
end
監(jiān)聽函數(shù),控制觸發(fā)在加載的時機
-- 監(jiān)聽SlideWindow滾動,動態(tài)加載列表項
function SlideWindow:_init_listener()
local extend = false
local last_pos = 0
local distance = 60
local inner = self.list_view_:getInnerContainer()
local outer_size = self.list_view_:getContentSize()
function scroll_callback(sender, eventType)
if eventType == SCROLLVIEW_EVENT_SCROLLING then
--print("正在滾動")
local inner_size = self.list_view_:getInnerContainerSize()
local pos = inner:getPositionY()
if not extend then
if pos > -distance and last_pos - pos < 0 then -- 接近底部
extend = true
self:need_load(1)
elseif inner_size.height - (outer_size.height-pos) < distance and last_pos - pos > 0 then -- 接近頂部
extend = true
self:need_load(-1)
end
extend = false
end
last_pos = pos
end
end
self.list_view_:addEventListenerScrollView(scroll_callback)
end
到這里已經(jīng)基本寫完了,大概一百多行代碼,還有更多流暢性的問題就要留待使用中逐步優(yōu)化了。