‎Cocos2d-x 學習筆記(21) ScrollView (CCScrollView)

  • 2019 年 10 月 3 日
  • 筆記

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。