1.做了什麼
問題:
在使用ListView的時候,有多少個數據就會創建多少個item,並不會重複利用或回收釋放。
隨着數據量的增加,會對性能造成很大的影響。
解決方案:
clone 改成 create[據說是這樣,我沒有測試過 = =…]
我們在使用ListView的時候,創建一個item,是通過lua重寫的pushBackCustomItemView,
它會先調用ListView的pushBackDefaultItem,通過clone創建一個csb,我們再把數據賦過去。
所以,我們完全可以create一個csb,相對於clone會快一些。缺點: 這個應該會有點效果,嗯,有點效果而已。
分幀加載(逐幀加載)
並不是在一幀全加載完,而是選擇每幀加載一定個數,直到加載完成缺點: 通過lua現有的協程來實現,但是流暢度不是很好,剛進入界面的時候可能看到item是逐漸加載進來的。
異步加載
這個主要對一些圖片多的item,我們如果需要切換圖,可以通過異步加載,等圖片加載完再換圖,這樣不影響之後item的加載。缺點: 會看到 默認圖(csb創建的樣子) -> 真正效果的轉換過程。
滑動到底加載
就是先加載一定數量,監聽到底部了,再拉取後面的部分,直到全部加載完。缺點: 做一系列監聽滑動等,沒有根本解決問題。
重用item[本次實現的方法]
其實,上面的那些方法,都是優化的技巧,並沒有從根本上解決問題。
我們要根本的解決問題,就是創建可視區域可容納數量+1的item,然後不斷重用這些item。
在ListView同一時刻,只能見到5個item,那我就創建6個item,然後不斷重用這些item。
2.怎麼做的
機制
首先明確view與inner,
view像一個窗口,它的大小就是我們可以見到的大小(當然要設置裁切),
inner是我們創建的所有item添加的地方(item並不是加載ScrollView上,而是加在了inner上)
ScrollView/ListView會監聽滑動,同時相應的移動inner的位置,從而讓我們看到item位置的變化。
簡而言之,item加載inner上,是inner動,不是view動。
想法
在ScrollView或者ListView中,正常情況是這樣的:
(前面數字代表item位置,後面數字代表item, ~~~~代表可視區域)
1 1 1
~~~~
2 2 2
3 3 3
~~~~
4 4 4
~~~~
5 5 5
6 6 6
~~~~
7 7 7
8 8 8
可以發現,
前面的例子中, 只能看見2, 3, 4; 但是看不見的1, 5, 6, 7, 8 依舊存在
後面的例子中, 只能看見4, 5, 6; 但是看不見的1, 2, 3, 7, 8 依舊存在
所以,我們改成下面的樣子:
1
~~~~
2 2
3 3
~~~~
4 4 4
~~~~
5 5
6 6
~~~~
7
8
因爲可視區域只有3個item,我們就創建3個item,然後不斷重用它們。(當然實際操作中,需要多創建一個,否則有穿幫風險)
但是,位置,我們依舊留着(劃重點,* inner大小不變 *,否則無法滑動),
在往下滑的時候,最上面的跑到下面去頂替下面的item;
往上滑的時候,最下面的跑到上面去頂替上面的item;
做法
實現方法,
可以通過監聽ScrollView滑動,每當ScrollView滾動,我們可以知道當前inner位置,
然後知道item的位置,從而判斷item需不需要移動位置。
這裏,用的是編輯一個繪製方法,每隔一段時間,都看一下各個item位置,然後根據需求移動位置。
我們在加載csb的時候將ScrollView記錄下來,在view的update中調用它。
(本來想重寫update,但是遇到了一些問題,所以妥協用了它,具體可以看後面 遇到的問題)
init:
--[[
name : item類名
totalItemNum : item總數
... : 創建item時需要的參數
--]]
function ScrollView:setItemViewModel(name, totalItemNum, ...)
主要代碼:
-- 得到所需繪製item個數
local count = math.ceil(self:getContentSize().height / self.tItemContentSize.height) + 1
for i = 1, count do
-- 創建item
local view = CSBReaderLoad(name)
view:init(...)
if view.setIndex then
view:setIndex(i)
end
-- 將父節點轉爲widget類型(原因可見 遇到的問題)
local widget = quick.packageNodeToWidget(view.pLayer:getChildByName("LayerTouch"))
view.pLayer = widget
-- widget隨父節點透明度變化,默認是false
widget:setCascadeOpacityEnabled(true)
self:addChild(view.pLayer)
-- 總數小於所需繪製item個數,需要隱藏多創建的
view.pLayer:setVisible(i > 0 and i <= self.totalItemNum)
-- 設置位置,注意我們加item是從下往上加的
view.pLayer:setPositionY(self.tItemContentSize.height * (self.totalItemNum - i))
table.insert(self.tItemView, view)
end
update:
function ScrollView:updateView(dt)
主要代碼:
-- 控制刷新時間
self.updateTimer = self.updateTimer + dt
if (self.updateTimer < self.updateInterval) then
return
end
self.updateTimer = 0;
...
-- 遍歷所有創建的item,如果它們需要移動位置,就移動它們的位置,並讓它們重繪自己
for i = 1, length do
local item = self:getItemView(i)
local viewPos = self:getPositionInView(item.pLayer);
if increaseVal < 0 then
if viewPos.y < minBoundary then
if item.index and item.setIndex then
item:setIndex(item.index - length)
item.pLayer:setVisible(item.index > 0 and item.index <= self.totalItemNum)
end
item.pLayer:setPositionY(item.pLayer:getPositionY() + offset)
end
elseif increaseVal > 0 then
if (viewPos.y > maxBoundary and item.pLayer:getPositionY() - offset > -self.tContentSize.height) then
if item.index and item.setIndex then
item:setIndex(item.index + length)
item.pLayer:setVisible(item.index > 0 and item.index <= self.totalItemNum)
end
item.pLayer:setPositionY(item.pLayer:getPositionY() - offset)
end
end
end
3. 遇到的問題
關於update
在3.x中lua啓用定時器有兩種方法:
第一種方法 scheduleUpdateWithPriorityLua(update, priority)
update - 刷新函數,
priority - 優先級,此方法在Node類中實現,所以它的子類都可以使用。
此方法默認爲每幀都刷新因此,無法自定義刷新時間。
這裏,沒有用這個方法,是因爲ScrollView自己已經實現了update方法。
所以,當我們重新註冊給ScrollView一個update的時候,發現無法替換。
這裏涉及到計時器存儲刷新方法:
刷新方法通過哈希表存儲,在主循環期間,不移除已有方法,而是將它暫停,且恢復時不加載新方法,而是將原有方法恢復。
啓用定時器的源碼如下:
void Node::scheduleUpdateWithPriorityLua(int nHandler, int priority)
{
unscheduleUpdate();
#if CC_ENABLE_SCRIPT_BINDING
_updateScriptHandler = nHandler;
#endif
_scheduler->scheduleUpdate(this, priority, !_running);
}
執行: unscheduleUpdate();
會先判斷節點是否有update方法,在哈希表中查找,並執行移除方法:
tHashUpdateEntry *element = nullptr;
HASH_FIND_PTR(_hashForUpdates, &target, element);
if (element)
{
if (_updateHashLocked)
{
element->entry->markedForDeletion = true;
}
else
{
this->removeUpdateFromHash(element->entry);
}
}
上面移除方法,會根據_updateHashLocked值來執行,
它爲真時,
如果節點原來有update,就先廢棄它,廢棄的方法是,將它標記爲已刪除,並讓它暫停。注意!這裏並沒有真正的刪除,而是將他表示是否刪除的字段改值。
它爲假時,
直接從哈希表中移除update方法。
執行:_scheduler->scheduleUpdate(this, priority, !_running);
加入update,也會先從哈希表中查找update,再執行添加方法。
tHashUpdateEntry *hashElement = nullptr;
HASH_FIND_PTR(_hashForUpdates, &target, hashElement);
if (hashElement)
{
// check if priority has changed
if ((*hashElement->list)->priority != priority)
{
if (_updateHashLocked)
{
CCLOG("warning: you CANNOT change update priority in scheduled function");
hashElement->entry->markedForDeletion = false;
hashElement->entry->paused = paused;
return;
}
else
{
// will be added again outside if (hashElement).
unscheduleUpdate(target);
}
}
else
{
hashElement->entry->markedForDeletion = false;
hashElement->entry->paused = paused;
return;
}
}
添加方法,會先判斷優先級,如果優先級相同,那麼就恢復原來的update。
否則,根據 _updateHashLocked 值執行接下來操作。
從移除和添加可以發現,關鍵值在於 _updateHashLocked的值,
這個值在Scheduler::update中設置,開始的時候設置爲true,最後結束設置爲false。
所以,如果要修改,就很麻煩,就放棄用這個方法了。
道理同樣適用於所有自己已經重寫了update,想要更換update情形。
- 第二種方法,通過定時管理器調用
就是上面指的Scheduler,不過我們不調ScrollView的,而是創建一個新的。
scheduler:scheduleScriptFunc(update, inteval, isOnce)
scheduler - cc.Director:getInstance():getScheduler()
update - 更新方法
inteval - 刷新時間間隔
isOnce - 是否只執行一次
注意,如果用這個方法,需要負責創建,也要負責移除。
上面方法會返回一個id,之後可以通過這個id來刪除它。
cc.Director:getInstance():getScheduler():unscheduleScriptEntry(id)
爲什麼要把item包裝成Widget
在剛開始往ScrollView加child時,方法是將item的Node直接往ScrollView addChild(ScrollView封裝了它,其實就是往inner addChild)
但是當直接addChild時,會產生很多問題:比如按鈕吞噬觸摸,無法滑動等等。
那就要問一下了,爲什麼ListView沒事呢?
這其實是Cocos對繼承自ccui.Widget的事件的處理。
所有的控件事件監聽都是單點觸摸,並且會吞噬事件。
_touchListener = EventListenerTouchOneByOne::create();
CC_SAFE_RETAIN(_touchListener);
_touchListener->setSwallowTouches(true);
_touchListener->onTouchBegan = CC_CALLBACK_2(Widget::onTouchBegan, this);
_touchListener->onTouchMoved = CC_CALLBACK_2(Widget::onTouchMoved, this);
_touchListener->onTouchEnded = CC_CALLBACK_2(Widget::onTouchEnded, this);
_touchListener->onTouchCancelled = CC_CALLBACK_2(Widget::onTouchCancelled, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(_touchListener, this);
在widget的onTouchBegan, onTouchMove, onTouchEnd中,都會調用 propagateTouchEvent,
這個方法是傳播事件,每個子節點會吞噬事件,自己處理完,再向父節點傳遞,一般ScrollView、ListView、PageView會處理這些事件。
Widget* widgetParent = getWidgetParent();
if (widgetParent)
{
widgetParent->_hittedByCamera = _hittedByCamera;
widgetParent->interceptTouchEvent(event, sender, touch);
widgetParent->_hittedByCamera = nullptr;
}
可以看出,只有繼承自Widget類的,纔會接收到interceptTouchEvent,並進行處理。
而且,ScrollView的interceptTouchEvent 已經處理好了按鈕的點擊,取消等效果。
void ScrollView::interceptTouchEvent(Widget::TouchEventType event, Widget *sender,Touch* touch)
{
if(!_touchEnabled)
{
Layout::interceptTouchEvent(event, sender, touch);
return;
}
if(_direction == Direction::NONE)
return;
Vec2 touchPoint = touch->getLocation();
switch (event)
{
case TouchEventType::BEGAN:
{
_isInterceptTouch = true;
_touchBeganPosition = touch->getLocation();
handlePressLogic(touch);
}
break;
case TouchEventType::MOVED:
{
_touchMovePosition = touch->getLocation();
// calculates move offset in points
float offsetInInch = 0;
switch (_direction)
{
case Direction::HORIZONTAL:
offsetInInch = convertDistanceFromPointToInch(Vec2(std::abs(sender->getTouchBeganPosition().x - touchPoint.x), 0));
break;
case Direction::VERTICAL:
offsetInInch = convertDistanceFromPointToInch(Vec2(0, std::abs(sender->getTouchBeganPosition().y - touchPoint.y)));
break;
case Direction::BOTH:
offsetInInch = convertDistanceFromPointToInch(sender->getTouchBeganPosition() - touchPoint);
break;
default:
break;
}
if (offsetInInch > _childFocusCancelOffsetInInch)
{
sender->setHighlighted(false);
handleMoveLogic(touch);
}
}
break;
case TouchEventType::CANCELED:
case TouchEventType::ENDED:
{
_touchEndPosition = touch->getLocation();
handleReleaseLogic(touch);
if (sender->isSwallowTouches())
{
_isInterceptTouch = false;
}
}
break;
}
}
之前的方法有問題,就是因爲直接將Node addChild到ScrollView,當觸摸傳遞到Node,發現無法轉成Widget對象,就放棄了向上傳播事件。
所以,需要將item包裝成Widget來讓它將事件傳遞給ScrollView。
4. 總結
怎麼用這個呢?
- 調用 ScrollView:setItemViewModel(item, item總數, 創建item所需的額外參數)
- 所有的item要有方法 item:setIndex(index), 並且以 self.index 作爲自己的index[這裏可以寫一個類來封裝,讓所有item都繼承它]
- 在刪除的時候,要將ScrollView的每幀更新方法移除
現在,ScrollView已經可以重用item了。
但是,還是比較粗糙;做爲一個控件,僅僅是這樣可不行。
之後,會對這個控件慢慢優化,讓它支持更多的功能,更加得心應手。