• ‎Cocos2d-x 学习笔记(21) ScrollView (CCScrollView)


    1. 简介

    CCScrollView.cpp文件内的滚动视图ScrollView直接继承了Layer+ActionTweenDelegate。

    滚动视图能在屏幕区域内,用户通过触摸拖动屏幕,实现大于屏幕尺寸的图片的滚动效果。

    滚动视图尺寸是我们的可视尺寸,滚动视图包含的成员container(layer)是被拖动的大图所在的层。

    实现滚动视图效果,需要以下几个方面的工作:

    · 获取屏幕尺寸。

    · 一个layer。

    · layer中addChild一张/多张大图。

    · layer尺寸设置成包含所有图片的尺寸。

    · 创建滚动视图,需要可视区域size和layer。

    · this->addChild(滚动视图)。

    2. 一些变量的作用

    - ScrollViewDelegate* _delegate

    ScrollView使用了Delegate机制,ScrollView关联了一个delegate。ScrollViewDelegate类有两个虚函数,我们可以用子类继承该类,重写两方法。

    virtual void scrollViewDidScroll(ScrollView* view) {}; //滚动时触发的回调函数
    virtual void scrollViewDidZoom(ScrollView* view) {}; //缩放时触发的回调函数

    - Direction _direction

    用枚举表示限制的滚动方向。

        enum class Direction
        {
            NONE = -1,
            HORIZONTAL = 0,
            VERTICAL,
            BOTH
        }

    - bool _dragging

    用户是否开始拖动的标志。仅在onTouchBegan置true,表明拖动开始;构造函数、onTouchEnded、onTouchCancelled中置false,表明没有拖动。

    - bool _touchMoved

    用户是否已经拖动的标志。和_dragging类似,但仅在onTouchMoved的第一次执行中置true,在onTouchEnded、onTouchCancelled中置false。

    - bool _bounceable

    回弹功能的标志,在拖动中和结束时,即onTouchMoved、onTouchEnd方法,通过该标志决定了是否有回弹效果。

    create方法置true,即默认开启回弹。

    - Vec2 _maxInset

    - Vec2 _minInset

    默认均为0,是container偏移量范围的两个临界点,仅通过setContentSize设置,仅对有回弹的ScrollView起作用。

    3. 一些方法的使用

    - create

    两种重载:

    ScrollView* create(Size size, Node* container = NULL)
    ScrollView* create()

    create方法调用了initWithViewSize方法。

    container位置设为(0,0)。

    Layer在构造函数中,忽略锚点对位置的影响,位置基准点始终为左下角,位置设为(0,0),锚点(0.5,0.5)。ScrollView继承了Layer,container一般是Layer类型。

    - setContainer

    设置成员container:

    void ScrollView::setContainer(Node * pContainer)
    {
        if (nullptr == pContainer)
            return;
    
        this->removeAllChildrenWithCleanup(true); //删除原先的container
        this->_container = pContainer;
    
        this->_container->setIgnoreAnchorPointForPosition(false); //container的位置需要锚点
        this->_container->setAnchorPoint(Vec2(0.0f, 0.0f));
    
        this->addChild(this->_container); //不是Node的addChild
    
        this->setViewSize(this->_viewSize);
    }

    值得注意的是:

    如果layer是作为create的参数而设为container的话,layer锚点(0.5,0.5),忽略锚点对于位置的影响。

    如果layer是通过setContainer设为container的话,layer锚点(0,0),不忽略锚点对位置的影响。

    虽然上面两种情况锚点不同,但是最终都是以左下角为基准点设置位置的。

    这里的addChild方法不是父类Node的方法,而是ScrollView重写的addChild。

    - addChild

    ScrollView重写了Node的addChild方法。

    void addChild(Node * child, int zOrder, int tag)
    void addChild(Node * child, int zOrder, const std::string &name)

    滚动视图进行addChild,实际上是把参数node添加到滚动视图的成员container里。滚动视图的removeChild同理。

    如果node是container,则把container作为ScrollView的子节点。

    void ScrollView::addChild(Node * child, int zOrder, int tag)
    {
        if (_container != child) {
            _container->addChild(child, zOrder, tag);
        } else {
            Layer::addChild(child, zOrder, tag);
        }
    }

    - setViewSize

    设置可视区域尺寸。先把参数作为viewSize值。接着执行Layer::setContentSize(size),设置ScrollView的尺寸。

    - setContentSize

    重写了Node的该方法,设置的是container的尺寸。

    调用了updateInset方法,执行:

            _maxInset = this->maxContainerOffset();
            _maxInset.set(_maxInset.x + _viewSize.width * INSET_RATIO, _maxInset.y + _viewSize.height * INSET_RATIO);
            _minInset = this->minContainerOffset();
            _minInset.set(_minInset.x - _viewSize.width * INSET_RATIO, _minInset.y - _viewSize.height * INSET_RATIO);

    设置了_maxInset:maxContainerOffset的值加上部分可视范围,_minInset:minContainerOffset减去上部分可视范围。

    _maxInset和_minInset在有回弹时才有用处。

    - setTouchEnabled(bool enabled)

    初始化监听器或删除监听器。

    void ScrollView::setTouchEnabled(bool enabled)
    {
        //先删除原监听器
        _eventDispatcher->removeEventListener(_touchListener);
        _touchListener = nullptr;
    
        if (enabled) //如果启用触摸监听
        {
            _touchListener = EventListenerTouchOneByOne::create();
            _touchListener->setSwallowTouches(true);
    
           //... ScrollView的4个回调函数作为监听器4种触摸的回调函数
            
            _eventDispatcher->addEventListenerWithSceneGraphPriority(_touchListener, this);
        }
        else // 如果停用触摸监听
        {
            _dragging = false;
            _touchMoved = false;
            _touches.clear();
        }
    }

    - maxContainerOffset 

    - minContainerOffset

    计算container允许的偏移量范围。

    值为正(负),即container在正(负)方向被拖动的最大值。

    计算时,需要“anchorPoint”,此点计算过程:

    Point anchorPoint = _container->isIgnoreAnchorPointForPosition()?Point::ZERO:_container->getAnchorPoint();

    我们知道,container是可视区域ScrollView的子节点,container位置的基准点都是自己的左下角,从而使得container的左下边与可视区域的左下边对齐,container才能在拖动过程中被完整展示。

    可以看出,在不人为修改container锚点的情况下,无论是create方法设置container,还是setContainer方法,anchorPoint将始终为(0,0)。

    因此,maxContainerOffset的返回值永远为(0,0)。minContainerOffset的返回值的X永远是container宽减去可视区域宽,Y永远是container长减去可视区域长。

    4. Touch事件的4个回调函数

    ScrollView继承了Layer,因此可以重写Layer的触摸事件的4个回调函数,实现对触摸拖动的处理。

    - onTouchBegan

    ScrollView的触摸支持单点/多点触摸 

    bool ScrollView::onTouchBegan(Touch* touch, Event* /*event*/)
    {
        if (!this->isVisible() || !this->hasVisibleParents())
        {
            return false;
        }
        
        Rect frame = getViewRect(); //当前可视范围的Rect
    
        //要求:触摸发生在单点或两点,触摸不是移动状态,触摸点在可视范围内
        if (_touches.size() > 2 ||
            _touchMoved          ||
            !frame.containsPoint(touch->getLocation()))
        {
            return false;
        }
    
        if (std::find(_touches.begin(), _touches.end(), touch) == _touches.end())
        {
            _touches.push_back(touch); //容器内的Touch数量1或2
        }
    
        if (_touches.size() == 1) //单点触摸,用于拖动
        { 
            _touchPoint     = this->convertTouchToNodeSpace(touch); //相对于ScrollView的坐标
            _touchMoved     = false; //正在移动的标志
            _dragging     = true; //拖动专用标志
            _scrollDistance.setZero();
            _touchLength    = 0.0f;
        }
        else if (_touches.size() == 2) //两点触摸,用于缩放
        {
            _touchPoint = (this->convertTouchToNodeSpace(_touches[0]).getMidpoint(
                            this->convertTouchToNodeSpace(_touches[1]))); // 两点的中点,作为缩放的“中心”
            
            _touchLength = _container->convertTouchToNodeSpace(_touches[0]).getDistance(
                           _container->convertTouchToNodeSpace(_touches[1])); //两点的距离,用于计算Moved时缩放大小
            
            _dragging  = false; //拖动专用标志
        } 
        return true;
    }

    - onTouchMoved

    在该方法内部,触摸分为单点拖动和两点缩放分别进行处理。

    -- 拖动

    当前触摸是拖动情况的条件是:

    _touches.size() == 1 && _dragging

    单点拖动时的基本逻辑是:

    根据限制方向的不同,计算container移动的向量,设置container的新位置。当有回弹功能时,且新位置超出偏移范围,移动向量需要缩小,这是回弹效果之一。当没有回弹功能时,且新位置超出偏移范围,则位置被置于边界。

    流程大致是:

    1. 当前坐标减去Began坐标或上次Moved的坐标(_touchPoint),得出坐标移动的向量moveDistance。

    2. 根据限制的拖动方向(_direction)的不同分3种情况。计算该情况下的移动长度(float dis)。

    3. 判断此时container位置是否超出偏移范围。如果超出,则对向量moveDistance缩小(乘回弹系数BOUNCE_BACK_FACTOR)。

    4. 如果当前是Began触摸后的第一次Moved(bool _touchMoved),且本次移动长度dis小于最小移动距离时MOVE_INCH,回调函数结束,即忽略本次拖动。

    5. 如果当前是Began触摸后的第一次Moved(bool _touchMoved),本次移动向量moveDistance改为0。此处不知为何?

    6. 本次Moved坐标赋给_touchPoint,供下次Moved使用。

    7. _touchMoved置true。

    8. 根据限制的拖动方向(_direction)的不同分3种情况。计算该情况下的移动向量moveDistance。

    9. 计算container新坐标。当前坐标加移动向量moveDistance。

    10. 根据是否回弹分两种情况,设置container新坐标(setContentOffset方法)。

    -- 缩放

    当前触摸是缩放情况的条件是:

    _touches.size() == 2 && !_dragging

    两点缩放时的基本逻辑是:

    根据当前两点距离和开始距离的比值,计算出当前的缩放值。两点中心在缩放前后的位置变化作为偏移量,设置container新的位置。实现了缩放是围绕两点中心进行,而不是锚点的效果。

    流程大致是:

    1. 获取当前两个touch的距离长度。

    2. 执行setZoomScale方法,参数为:当前缩放值=开始时缩放值scale*(当前两点距离/开始时两点距离)

    this->setZoomScale(this->getZoomScale()*len/_touchLength);

    接下来看setZoomScale方法的分析:

    1. 判断开始时两点距离是否为0。为0时,可视区域的中心点作为点center。不为0时,_touchPoint即开始时两点连线的中点作为点center。

    2. 点center坐标转为相对container的坐标。

    oldCenter = _container->convertToNodeSpace(center);

    3. container执行setScale方法,参数为我们计算的当前缩放值。

    缩放值范围会被调整为_minScale和_maxScale之间,但这两个范围值在init方法中默认都是1,故需要我们自行更改范围才能实现缩放。两者都有set方法。

    4. setScale之后,container的位置坐标会更改。把缩放后的点center坐标转为相对container的世界坐标。

    newCenter = _container->convertToWorldSpace(oldCenter);

    5. 计算缩放前后点center坐标的差值,作为偏移量offset。

    6. 执行缩放触发的scrollViewDidZoom方法。

    7. 执行setContentOffset方法,参数为container当前坐标加上偏移量offset。设置了本次缩放后的位置。

    - onTouchEnded

    onTouchEnded基本逻辑是:

    当结束触摸时:如果container位置在偏移范围内,直接设置container位置为当前结束点的位置;如果container位置超出偏移范围,根据有无回弹效果分为下面两种情况。

    有回弹效果时:创建动作序列,对container执行runAction。第一个动作是MoveTo,设置了时间、位置是边界坐标。第二个动作是回调执行scrollViewDidScroll。

    无回弹效果时:Moved和Ended时container位置被限制在偏移范围内。

    该方法中把拖动标志_dragging和_touchMoved置false。

    5. scrollViewDidScroll scrollViewDidZoom 相关

    ScrollView有成员ScrollViewDelegate* _delegate,我们可以用子类重写ScrollViewDelegate的拖动和缩放触发的函数。

    - scrollViewDidScroll

    该方法在3个函数里被调用,setContentOffset、performedAnimatedScroll、stoppedAnimatedScroll。

    setContentOffset:用于设置container的位置。当立即设置位置,将会触发scrollViewDidScroll;当有回弹故用动作设置位置时,不会在本函数中触发函数scrollViewDidScroll。

    performedAnimatedScroll:是一个Timer的回调函数。当有回弹效果,在动作序列中MoveTo执行时,每帧触发该回调函数,回调函数中触发的是scrollViewDidScroll。

    stoppedAnimatedScroll:回弹效果的动作序列MoveTo结束后,执行该函数,销毁包含performedAnimatedScroll的Timer,并执行performedAnimatedScroll。

    所以,对于拖动的情况,当触摸在移动,每次调用onTouchMoved都会触发scrollViewDidScroll。回弹时每帧触发scrollViewDidScroll。

    - scrollViewDidZoom

    在每次缩放触发onTouchMoved函数时,其中的setZoomScale方法都会触发scrollViewDidZoom。setZoomScale中还需要根据偏移量设置container新位置,调用的setContentOffset会调用scrollViewDidScroll。

    所以,对于缩放的情况,当触摸在移动,每次调用onTouchMoved会触发scrollViewDidZoom和scrollViewDidScroll。

  • 相关阅读:
    能帮你找到网页设计灵感的16个网站
    Alpha和索引色透明
    CSS2.0中最常用的18条技巧
    汇编指令CPUID
    ewebeditor漏洞解決方法
    关于SQL SERVER建立索引需要注意的问题
    Apple QuickTime
    免杀修改特征码需要掌握的汇编知识
    【我翻译的文章】理解和应用F#中的“use”语法
    【我翻译的文章】Promesh.NET:一个.NET的MVC Web框架
  • 原文地址:https://www.cnblogs.com/deepcho/p/cocos2dx-ccscrollview.html
Copyright © 2020-2023  润新知