首页 > 代码库 > cocos2d-x CCScrollView 源码分析

cocos2d-x CCScrollView 源码分析

版本源码来自2.x,转载请注明

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决定的。