版本源碼來自2.x,轉載請註明
另我實現了可以循環的版本http://blog.csdn.net/u011225840/article/details/31354703
1.繼承樹結構
可以看出,CCScrollView本質是CCLayer的一種,具備層的一切屬性和方法。關於CCLayer的源碼分析,後續會有。
2.重要的成員
1. CCScrollViewDelegate* m_pDelegate;
cocos2d-x中,運用了很多delegate這種模式。下面簡單的說明下delegate這種模式。(至於delegate與proxy的區別,請先參考下headfirst中的proxy三種情況,然後可以google區別,這裏不再贅述。)
XXXDelegate中封裝了接口(c++中的實現就是虛函數與必須實現的純虛函數),類A中存在某些方法,比如說View中的getDataNum,View會根據數據的多少來確定界面的顯示方式,但是A與數據並沒有直接的關聯。於是乎,在View A中的getDataNum會調用A內部的DataDelegate的方法來獲取數據的多少。至於這個數據具體是什麼,只需要實現一個DataDelegate的具體類即可。這樣,View與數據的耦合度就非常低。View只依賴抽象的DataDelegate。
在後續的源碼分析中,可以看出delegate的妙處。
3.源碼解析
3.1 ccTouchBegan
對於ccTouchBegan中的重要部分,我添加了註釋。可以通過代碼看出,CCscrollView支持單點和雙點觸摸。
bool CCScrollView::ccTouchBegan(CCTouch* touch, CCEvent* event)
{
if (!this->isVisible() || !m_bCanTouch)
{
return false;
}
CCRect frame = getViewRect();
//dispatcher does not know about clipping. reject touches outside visible bounds.
/*
1. ccScrollView只允許至多兩個觸摸點,多於兩個後將不會認爲發成了觸摸。
2. 當CCScrollView處於移動狀態時,在此狀態下新發生觸摸將不會被認爲發生。
3.注意frame不是當前的尺寸,而是當前ViewSize的frame,也就是觸摸點必須在顯示的Rect內纔會認定爲觸摸(可以通過setViewSize來設置大小)
*/
if (m_pTouches->count() > 2 ||
m_bTouchMoved ||
!frame.containsPoint(m_pContainer->convertToWorldSpace(m_pContainer->convertTouchToNodeSpace(touch))))
{
m_pTouches->removeAllObjects();
return false;
}
if (!m_pTouches->containsObject(touch))
{
m_pTouches->addObject(touch);
}
//CCLOG("CCScrollView::ccTouchBegan %d", m_pTouches->count());
/*
當觸摸點爲1的時候,設置單點觸摸的屬性。尤其是m_bDragging屬性表示觸摸行爲是拖動
*/
if (m_pTouches->count() == 1)
{ // scrolling
m_tTouchPoint = this->convertTouchToNodeSpace(touch);
m_bTouchMoved = false;
m_bDragging = true; //dragging started
m_tScrollDistance = ccp(0.0f, 0.0f);
m_fTouchLength = 0.0f;
}
/*
當觸摸點個數爲2時,設置雙點觸摸的屬性
*/
else if (m_pTouches->count() == 2)
{
m_tTouchPoint = ccpMidpoint(this->convertTouchToNodeSpace((CCTouch*)m_pTouches->objectAtIndex(0)),
this->convertTouchToNodeSpace((CCTouch*)m_pTouches->objectAtIndex(1)));
m_fTouchLength = ccpDistance(m_pContainer->convertTouchToNodeSpace((CCTouch*)m_pTouches->objectAtIndex(0)),
m_pContainer->convertTouchToNodeSpace((CCTouch*)m_pTouches->objectAtIndex(1)));
m_bDragging = false;
}
return true;
}
3.2 ccTouchMoved
同上,代碼裏面加了註釋
void CCScrollView::ccTouchMoved(CCTouch* touch, CCEvent* event)
{
if (!this->isVisible())
{
return;
}
/*
如果此時不允許滾動,則退出。這個可以通過set函數設置。默認爲false
*/
if(this->m_bScrollLock)
{
return;
}
if (m_pTouches->containsObject(touch))
{
/*
啊哦,好玩的來咯。
滾動狀態時
*/
if (m_pTouches->count() == 1 && m_bDragging)
{ // scrolling
CCPoint moveDistance, newPoint, maxInset, minInset;
CCRect frame;
float newX, newY;
frame = getViewRect();
//獲得當前點的座標,並且獲得當前點與上一次觸碰點的距離(moveDistance也是CCPoint,x與y是當前點與上一點的x距離,y距離)
newPoint = this->convertTouchToNodeSpace((CCTouch*)m_pTouches->objectAtIndex(0));
moveDistance = ccpSub(newPoint, m_tTouchPoint);
float dis = 0.0f;
//如果有方向的限定,根據方向限定獲取對應的距離
if (m_eDirection == kCCScrollViewDirectionVertical)
{
dis = moveDistance.y;
}
else if (m_eDirection == kCCScrollViewDirectionHorizontal)
{
dis = moveDistance.x;
}
else
{
dis = sqrtf(moveDistance.x*moveDistance.x + moveDistance.y*moveDistance.y);
}
//如果移動距離過短,則不判斷髮生了移動
if (!m_bTouchMoved && fabs(convertDistanceFromPointToInch(dis)) < MOVE_INCH )
{
//CCLOG("Invalid movement, distance = [%f, %f], disInch = %f", moveDistance.x, moveDistance.y);
return;
}
//第一次移動,則將moveDistance置0
if (!m_bTouchMoved)
{
moveDistance = CCPointZero;
}
m_tTouchPoint = newPoint;
m_bTouchMoved = true;
//點必須在viewRect內部
if (frame.containsPoint(this->convertToWorldSpace(newPoint)))
{
//根據可以移動的direction來設置moveDistance
switch (m_eDirection)
{
case kCCScrollViewDirectionVertical:
moveDistance = ccp(0.0f, moveDistance.y);
break;
case kCCScrollViewDirectionHorizontal:
moveDistance = ccp(moveDistance.x, 0.0f);
break;
default:
break;
}
//這個版本無用啊。。。。
maxInset = m_fMaxInset;
minInset = m_fMinInset;
//獲取容器的新座標,注意是容器哦
newX = m_pContainer->getPosition().x + moveDistance.x;
newY = m_pContainer->getPosition().y + moveDistance.y;
//滾動的CCPoint矢量設置
m_tScrollDistance = moveDistance;
this->setContentOffset(ccp(newX, newY));
}
}
//雙點觸摸時,效果是縮放,len是雙點觸摸每次移動時的距離,
//而m_fTouchLength是雙點開始時的距離,會根據move過程中距離與初始距離的比例進行縮放
else if (m_pTouches->count() == 2 && !m_bDragging)
{
const float len = ccpDistance(m_pContainer->convertTouchToNodeSpace((CCTouch*)m_pTouches->objectAtIndex(0)),
m_pContainer->convertTouchToNodeSpace((CCTouch*)m_pTouches->objectAtIndex(1)));
this->setZoomScale(this->getZoomScale()*len/m_fTouchLength);
}
}
}
單點觸摸時,調用了一個函數叫setContentOffset。下面繼續分析contentOffset。setContentOffset
void CCScrollView::setContentOffset(CCPoint offset, bool animated/* = false*/)
{
//默認情況,不做處理
if (animated)
{ //animate scrolling
this->setContentOffsetInDuration(offset, BOUNCE_DURATION);
}
//好玩的東西哦
else
{ //set the container position directly
//是否做越界處理,什麼是越界,就是當你拖動整個容器時,如果已經到了容器的邊界,還能不能再拖動,可以通過set函數進行設置
if (!m_bBounceable)
{
const CCPoint minOffset = this->minContainerOffset();
const CCPoint maxOffset = this->maxContainerOffset();
offset.x = MAX(minOffset.x, MIN(maxOffset.x, offset.x));
offset.y = MAX(minOffset.y, MIN(maxOffset.y, offset.y));
}
//CCLOG("The offset x is %f , y is %f",offset.x,offset.y);
m_pContainer->setPosition(offset);
//偉大的delegate來了,當你在滾動過程中想做除了基本界面滾動的額外操作時,請根據自己的不同情況,實現該delegate~完美的依賴抽象的設計,nice
if (m_pDelegate != NULL)
{
m_pDelegate->scrollViewDidScroll(this);
}
}
}
雙點觸摸時,調用了一個重要的函數setZoomScale。
setZoomScale
void CCScrollView::setZoomScale(float s)
{
if (m_pContainer->getScale() != s)
{
CCPoint oldCenter, newCenter;
CCPoint center;
//設置縮放中心
if (m_fTouchLength == 0.0f)
{
center = ccp(m_tViewSize.width*0.5f, m_tViewSize.height*0.5f);
center = this->convertToWorldSpace(center);
}
else
{
center = m_tTouchPoint;
}
//縮放後中心的位置相對於world座標系會產生offset,這裏將offset進行計算
oldCenter = m_pContainer->convertToNodeSpace(center);
m_pContainer->setScale(MAX(m_fMinScale, MIN(m_fMaxScale, s)));
newCenter = m_pContainer->convertToWorldSpace(oldCenter);
const CCPoint offset = ccpSub(center, newCenter);
//delegate的又一次出現
if (m_pDelegate != NULL)
{
m_pDelegate->scrollViewDidZoom(this);
}
//將產生的offset進行處理
this->setContentOffset(ccpAdd(m_pContainer->getPosition(),offset));
}
}
3.3 ccTouchEnded
能堅持看到這裏,已經可以看見勝利的曙光了,曙光。。。
void CCScrollView::ccTouchEnded(CCTouch* touch, CCEvent* event)
{
if (!this->isVisible())
{
return;
}
//將touch從pTouches中移除
if (m_pTouches->containsObject(touch))
{
//當剩下一個touch時,需要在每一幀調用方法deaccelerateScrolling
if (m_pTouches->count() == 1 && m_bTouchMoved)
{
this->schedule(schedule_selector(CCScrollView::deaccelerateScrolling));
}
m_pTouches->removeObject(touch);
//CCLOG("CCScrollView::ccTouchEnded %d", m_pTouches->count());
//m_pDelegate->scrollViewDidStop(this);
}
//沒有touch時,需要設置狀態
if (m_pTouches->count() == 0)
{
m_bDragging = false;
m_bTouchMoved = false;
}
}
deaccelerateScrolling
在這個函數中,有一個地方沒有理解,求大神指點
void CCScrollView::deaccelerateScrolling(float dt)
{
//如果剛好在幀開始前 又有一個觸摸點發生了began,造成了滾動狀態,則取消並返回
if (m_bDragging)
{
this->unschedule(schedule_selector(CCScrollView::deaccelerateScrolling));
return;
}
//好玩的東西來咯
float newX, newY;
CCPoint maxInset, minInset;
CCLOG("The end distance is %f",m_tScrollDistance.x);
//這裏我不清楚爲啥要出來,我用輸出發現在move中,已經將此offset設置過了,不知爲何還要設置,求大神解答。
m_pContainer->setPosition(ccpAdd(m_pContainer->getPosition(), m_tScrollDistance));
//是否允許越界,獲得的inset信息
if (m_bBounceable)
{
maxInset = m_fMaxInset;
minInset = m_fMinInset;
}
else
{
maxInset = this->maxContainerOffset();
minInset = this->minContainerOffset();
}
//check to see if offset lies within the inset bounds
newX = MIN(m_pContainer->getPosition().x, maxInset.x);
newX = MAX(newX, minInset.x);
newY = MIN(m_pContainer->getPosition().y, maxInset.y);
newY = MAX(newY, minInset.y);
newX = m_pContainer->getPosition().x;
newY = m_pContainer->getPosition().y;
m_tScrollDistance = ccpSub(m_tScrollDistance, ccp(newX - m_pContainer->getPosition().x, newY - m_pContainer->getPosition().y));
m_tScrollDistance = ccpMult(m_tScrollDistance, SCROLL_DEACCEL_RATE);
this->setContentOffset(ccp(newX,newY));
if ((fabsf(m_tScrollDistance.x) <= SCROLL_DEACCEL_DIST &&
fabsf(m_tScrollDistance.y) <= SCROLL_DEACCEL_DIST) ||
newY > maxInset.y || newY < minInset.y ||
newX > maxInset.x || newX < minInset.x ||
newX == maxInset.x || newX == minInset.x ||
newY == maxInset.y || newY == minInset.y)
{
this->unschedule(schedule_selector(CCScrollView::deaccelerateScrolling));
//越界動畫,從越界部分慢慢移動到不越界狀態的函數。
this->relocateContainer(true);
//偉大的delegate。。。
m_pDelegate->scrollViewDidStop(this);
}
}
relocateContainer
void CCScrollView::relocateContainer(bool animated)
{
//這個函數將容器從當前地方通過動畫移動到玩家自己設置的允許偏移的地方
CCPoint oldPoint, min, max;
float newX, newY;
//偏移值自己可以設置
min = this->minContainerOffset();
max = this->maxContainerOffset();
oldPoint = m_pContainer->getPosition();
newX = oldPoint.x;
newY = oldPoint.y;
if (m_eDirection == kCCScrollViewDirectionBoth || m_eDirection == kCCScrollViewDirectionHorizontal)
{
newX = MAX(newX, min.x);
newX = MIN(newX, max.x);
}
if (m_eDirection == kCCScrollViewDirectionBoth || m_eDirection == kCCScrollViewDirectionVertical)
{
newY = MIN(newY, max.y);
newY = MAX(newY, min.y);
}
//還是調用setContentOffset,但是需要動畫
if (newY != oldPoint.y || newX != oldPoint.x)
{
this->setContentOffset(ccp(newX, newY), animated);
}
}
setContentOffsetInDuration
void CCScrollView::setContentOffsetInDuration(CCPoint offset, float dt)
{
CCFiniteTimeAction *scroll, *expire;
//滾動的偏移動畫
scroll = CCMoveTo::create(dt, offset);
//滾動完成後的動畫(負責停止performedAnimatedScroll,並且調用delegate)
expire = CCCallFuncN::create(this, callfuncN_selector(CCScrollView::stoppedAnimatedScroll));
m_pContainer->runAction(CCSequence::create(scroll, expire, NULL));
//負責不停調用delegate
this->schedule(schedule_selector(CCScrollView::performedAnimatedScroll));
}
4.小結
看到這裏,CCScrollView裏面屬於自己獨有部分的東西基本已經看完。可以看出:
1.CCScrollView支持兩種操作,滾動和縮放。
2.CCScrollView通過delegate將數據與界面解耦。
3.CCScrollView本質是一個CClayer,他展示的是自己內部的container,並且CCScrollView的觸摸以及展示是根據ViewSize 還不是本身的SIze決定的。