【cocos2d-x 源码解析】可滚动容器

继承关系

|-UILayout
    |-UIScrollView
        |-UIListViewEx
    |-UIListView
        |-UIListViewSmartAlign
    |-UIPageView

UILayout

UILayout 是一个继承自 UIWidget 的容器,添加了布局选项,通过 setLayoutType 函数可以设置容器的布局,布局选项有

typedef enum
{
    LAYOUT_ABSOLUTE,
    LAYOUT_LINEAR_VERTICAL,
    LAYOUT_LINEAR_HORIZONTAL,
    LAYOUT_RELATIVE
}LayoutType;
  • 默认是 LAYOTU_ABSOLUTE,也就是无布局
  • LAYOUT_LINEAR_VERTICAL 垂直线性布局,所有控件会按照添加顺序从容器顶部往底部排列
  • LAYOUT_LINEAR_HORIZONTAL 水平线性布局,所有控件会按照添加顺序从容器左端往右端排列
  • LAYOUT_RELATIVE 相对布局,控件的位置会相对于容器或者另一个控件来设置

除了 UILayout 的布局选项之外,还要给每个控件设置一个布局参数,在 UIWidget 中有一个 setLayoutParameter 方法,用于设置控件的布局参数,布局参数的基类是 UILayoutParameter,它有两个子类,UILinearLayoutParameter 对应线性布局选项,UIRelativeLayoutParameter 对应相对布局选项

线性布局和相对布局共有的参数是 UIMargin,设置控件之间的间隔,包括左边间隔、顶边间隔、右边间隔和底边间隔

UIMargin(float l, float t, float r, float b);

线性布局独有的参数是 Gravity,用于设置控件的对齐方式

typedef enum
{
    LINEAR_GRAVITY_NONE,
    LINEAR_GRAVITY_LEFT,
    LINEAR_GRAVITY_TOP,
    LINEAR_GRAVITY_RIGHT,
    LINEAR_GRAVITY_BOTTOM,
    LINEAR_GRAVITY_CENTER_VERTICAL,
    LINEAR_GRAVITY_CENTER_HORIZONTAL
}UILinearGravity;

如果布局选项是 LAYOUT_LINEAR_VERTICAL,则可用的对齐参数有 LINEAR_GRAVITY_LEFT、LINEAR_GRAVITY_RIGHT 和 LINEAR_GRAVITY_CENTER_HORIZONTAL,因为竖直方向已经自动布局了,所以只需要设置水平方向的对齐方式

如果布局选项是 LAYOUT_LINEAR_HORIZONTAL,则可用的对齐参数有 LINEAR_GRAVITY_TOP、LINEAR_GRAVITY_BOTTOM 和 LINEAR_GRAVITY_CENTER_VERTICAL,因为水平方向已经自动布局了,所以只需要设置竖直方向的对齐方式

相对布局要设置相对的结点和对齐方式,对齐方式包括

typedef enum
{
    RELATIVE_ALIGN_NONE,
    RELATIVE_ALIGN_PARENT_TOP_LEFT,
    RELATIVE_ALIGN_PARENT_TOP_CENTER_HORIZONTAL,
    RELATIVE_ALIGN_PARENT_TOP_RIGHT,
    RELATIVE_ALIGN_PARENT_LEFT_CENTER_VERTICAL,
    RELATIVE_CENTER_IN_PARENT,
    RELATIVE_ALIGN_PARENT_RIGHT_CENTER_VERTICAL,
    RELATIVE_ALIGN_PARENT_LEFT_BOTTOM,
    RELATIVE_ALIGN_PARENT_BOTTOM_CENTER_HORIZONTAL,
    RELATIVE_ALIGN_PARENT_RIGHT_BOTTOM,

    RELATIVE_LOCATION_ABOVE_LEFTALIGN,
    RELATIVE_LOCATION_ABOVE_CENTER,
    RELATIVE_LOCATION_ABOVE_RIGHTALIGN,
    RELATIVE_LOCATION_LEFT_OF_TOPALIGN,
    RELATIVE_LOCATION_LEFT_OF_CENTER,
    RELATIVE_LOCATION_LEFT_OF_BOTTOMALIGN,
    RELATIVE_LOCATION_RIGHT_OF_TOPALIGN,
    RELATIVE_LOCATION_RIGHT_OF_CENTER,
    RELATIVE_LOCATION_RIGHT_OF_BOTTOMALIGN,
    RELATIVE_LOCATION_BELOW_LEFTALIGN,
    RELATIVE_LOCATION_BELOW_CENTER,
    RELATIVE_LOCATION_BELOW_RIGHTALIGN
}UIRelativeAlign;

设置完 UILayout 的布局选项和每个控件的布局参数之后,UILayout 会在函数 doLayout 里自动计算每个控件的位置

常用接口:

//设置布局
void setLayoutType(LayoutType type);
//添加控件
bool addChild(UIWidget* child);
//设置是否裁剪出界区域
void setClippingEnabled(bool able);
//设置背景颜色类型
void setBackGroundColorType(LayoutBackGroundColorType type);
//设置背景颜色
void setBackGroundColor(const ccColor3B &color);
void setBackGroundColor(const ccColor3B &startColor, const ccColor3B &endColor);
//设置背景颜色透明度
void setBackGroundColorOpacity(int opacity);
//设置背景图片
void setBackGroundImage(const char* fileName,TextureResType texType = UI_TEX_TYPE_LOCAL);

UIScrollView

UIScrollView 继承自 UILayout,其内部添加了一个容器 m_pInnerContainer,这也是一个 UILayout,所有添加到 UIScrollView 的控件都是添加在这个子容器 m_pInnerContainer 上。UIScrollView 默认注册了触摸事件,拖动 UIScrollView 时移动的是 m_pInnerContainer 容器,这样添加到滚动面板上的所有控件都会同步进行滚动。

适用场合: UIScrollView 其实只是一个普通的容器,只不过添加了滚动事件,自动处理各种滚动逻辑和边界控制;添加到 UIScrollView 上的控件并不会自动进行布局,就和添加在普通 UILayout 上一样,所以添加之后要手动设置每个控件的位置或者手动设置布局。UIScrollView 适用于子控件需要滚动同时子控件位置比较随意的场合。

常用接口:

//设置滚动方向
void setDirection(SCROLLVIEW_DIR dir);
//滚动到某个位置
void scrollToBottom(float time, bool attenuated);
//...
//移动到某个位置
void jumpToBottom();
//...
//设置是否启用回弹效果
void setBounceEnabled(bool enabled);

UIListViewEx

UIListViewEx 继承自 UIScrollView,保留了滚动的特性,添加了自动布局的特性。UIListViewEx 内部设置了布局方式,所以添加控件的时候不需要关心控件的位置。UIListViewEx 的布局方式有两种,水平线性布局和垂直线性布局,在初始化函数 init 中设置默认布局为垂直布局

setLayoutType(LAYOUT_LINEAR_VERTICAL);

可以通过 setDirection 方法来修改布局方式,同时修改滚动方向

void UIListViewEx::setDirection(SCROLLVIEW_DIR dir)
{
    switch (dir)
    {
        case SCROLLVIEW_DIR_VERTICAL:
            setLayoutType(LAYOUT_LINEAR_VERTICAL);
            break;
        case SCROLLVIEW_DIR_HORIZONTAL:
            setLayoutType(LAYOUT_LINEAR_HORIZONTAL);
            break;
        case SCROLLVIEW_DIR_BOTH:
            return;
        default:
            return;
            break;
    }
    UIScrollView::setDirection(dir);
}

创建 UIListViewEx 之后,设置其项间隔和对齐方式,往 UIListViewEx 添加 Item 时会先调用 remedyLayoutParameter 设置 Item 的布局参数,然后再把 Item 添加到 m_pInnerContainer 上

设置完 UIListViewEx 的布局选项和每个控件的布局参数之后,doLayout 函数会自动计算每个控件的位置,这是 UILayout 的功能。计算的时候是使用 getChildren 得到所有控件,然后逐个设置位置,后一个控件的位置依赖于前一个控件的位置,所以最顶部的控件最先添加,最底部的最后添加(对于垂直布局而言)。使用 pushBack*() 的方式添加控件能保证最后添加的控件在最底部,但使用 insert*() 或 remove*() 呢?这个问题让我疑惑了很久,后来才知道是通过设置控件的 ZOrder 解决的;原来 Node 添加子结点时会根据 ZOrder 来排列子结点

 void CCNode::addChild(CCNode *child, int zOrder, int tag)
{    
    CCAssert( child != NULL, "Argument must be non-nil");
    CCAssert( child->m_pParent == NULL, "child already added. It can't be added again");

    if( ! m_pChildren )
    {
        this->childrenAlloc();
    }

    this->insertChild(child, zOrder);

    child->m_nTag = tag;

    child->setParent(this);
    child->setOrderOfArrival(s_globalOrderOfArrival++);

    if( m_bRunning )
    {
        child->onEnter();
        child->onEnterTransitionDidFinish();
    }
}

因此,只要保证顶部的 ZOrder 较小,底部的 ZOrder 较大,就可以保证顶部的控件先添加,底部的控件后添加,控件的位置计算就不会有问题了。然而,在 insert*() 方法中却没有找到设置 ZOrder 的代码

void UIListViewEx::insertCustomItem(UIWidget* item, int index)
{
    m_pItems->insertObject(item, index);
    remedyLayoutParameter(item);
    addChild(item);
}

但是,在刷新界面方法 refreshView 发现了

void UIListViewEx::refreshView()
{
    if (!m_pItems)
    {
        return;
    }
    ccArray* arrayItems = m_pItems->data;
    int length = arrayItems->num;
    for (int i=0; i<length; i++)
    {
        UIWidget* item = (UIWidget*)(arrayItems->arr[i]);
        item->setZOrder(i);
        remedyLayoutParameter(item);
    }
    updateInnerContainerSize();
}

有几个地方会调用 refreshView 方法,其中有一处是 UIListViewEx 大小改变时,而无论是添加控件还是删除控件,UIListViewEx 的大小都会改变,所以添加控件或删除控件都会重新设置每个控件的 ZOrder

void UIListViewEx::onSizeChanged()
{
    UIScrollView::onSizeChanged();
    refreshView();
}

适用场合: UIListViewEx 在 UIScrollView 的基础上增加了自动布局,适用于控件水平排列或垂直排列同时又需要滚动的场合;但因为它需要把所有的列表项都创建出来,所以对于列表项太多的情况容易出现卡顿

常用接口:

//设置控件排列方向及滚动方向
void setDirection(SCROLLVIEW_DIR dir);
//设置默认控件模型
void setItemModel(UIWidget* model);
//添加默认控件
void pushBackDefaultItem();
//插入默认控件
void insertDefaultItem(int index);
//添加控件
void pushBackCustomItem(UIWidget* item);
//插入控件
void insertCustomItem(UIWidget* item, int index);
//移除控件
void removeItem(int index);
//获取控件
UIWidget* getItem(unsigned int index);
//获取控件下标
unsigned int getIndex(UIWidget* item) const;
//设置控件对齐方式
void setGravity(ListViewGravity gravity);
//设置控件间隔
void setItemsMargin(float margin);

### UIListView
UIListView 继承自 UILayout,既没有使用 UIScrollView 的滚动功能,也没有使用 UILayout 的布局,而是自己注册了触摸事件和处理控件滚动。因为没有使用布局,所以添加控件之后要手动设置控件的初始位置。UIListView 的特别之处在于它只添加少数的控件,然后在滚动过程中刷新控件的内容,比如实际应用中需要 100 个控件,但实际上只创建了 5 个控件,只不过在滚动的时候改变的控件的数据,看起来就像创建了 100 个控件一样。UIListView 使用两个事件来处理控件的初始化和刷新

LISTVIEW_EVENT_INIT_CHILD 事件用于控件数据的初始化,这个事件的触发过程如下
* 首先,创建完 UIListView 之后注册事件 listView:addEventListener(onInitChild, LISTVIEW_EVENT_INIT_CHILD)
* 然后,调用 listView:initChildWithDataLength(len) 初始化控件数据,之后就是 UIListView 的内部调用了
* 在 initChildWithDataLength 会为每一个控件执行一次 initChildEvent 方法
* 在 initChildEvent 方法中会调用 executeEvent 方法,然后事件触发就进入到前面注册的 onInitChild 方法
* 一般创建完列表之后只添加几个空的控件,然后在 onInitChild 方法中进行数据填充

LISTVIEW_EVENT_UPDATE_CHILD 事件用于滚动控件时的数据更新,这个事件的触发过程如下
* 首先,创建完 UIListView 之后注册事件 listView:addEventListener(onUpdateChild, LISTVIEW_EVENT_UPDATE_CHILD )
* 拖动 ListView 或其它操作使 ListView 发生滚动时会进入到 scrollChildren 方法
* 在 scrollChildren 中调用 moveChildren 设置每个控件的位置,判断是否发生控件交替,即一端控件出界,另一端有新控件进入,如果发生控件交替则调用 updateChild 方法
* 在 updateChildren 中调用 getAndCallback 方法
* 在 getAndCallBack 方法中,调用 setUpdateChild(child);setUpdateDataIndex(m_nEnd); 设置当前要更新的控件及其下标,然后调用 updateChildEvent 方法
* 在 updateChildEvent 中调用 executeEvent 方法,然后事件触发就进入到前面注册的 onUpdateChild 方法
* 在 onUpdateChild 方法调用 getUpdateDataIndex 获取要更新的控件下标,调用 getUpdateChild 获取要更新的控件,然后给这个控件填充数据

有一点要注意的就是 initChildWithDataLength 传入的长度是所有项的数量,而不是创建的控件数,比如一个列表需要 100 项,然后只添加了 5 个控件,则要传入的长度是 100。只有这个长度比控件数大时才会发生控件交替,即 onUpdateChild 函数才会调用。另外,至少要为 ListView 添加 5 个控件,否则 ListView 无法滚动,更不会发生控件交替。

适用场合: UIListView 使用自己的一套滚动机制和控件管理管理机制,不使用 UILayout 的布局,因此需要手动设置控件位置;它的优点是对于列表项很多的情况只需要创建少数的控件,但需要注册相应的事件,在滚动过程中自动刷新数据,是一种时间换空间的做法;UIListView 使用起来会比 UIListViewEx 麻烦一些,适用于列表项比较多的情况

常用接口:

//设置滚动方向
void setDirection(ListViewDirection dir);
//初始化控件数据
void initChildWithDataLength(int length);
//获取更新控件的下标
int getUpdateDataIndex();
//获取更新控件
UIWidget* getUpdateChild();
//注册初始化控件事件
void addInitChildEvent(cocos2d::CCObject* target, SEL_ListViewInitChildEvent seletor);
//注册刷新控件事件
void addUpdateChildEvent(cocos2d::CCObject* target, SEL_ListViewUpdateChildEvent selector);
//设置是否启动滚动效果
void setElasticity(bool bElasticity);

UIListViewSmartAlign

UIListViewSmartAlign 继承自 UIListView,重写了 setLoopPosition 等几个方法,看了一下代码就是在滚动控件时设置控件位置的代码改了一点;测试之后发现和 UIListView 没什么区别,而且 UIListViewSmartAlign 并没有引入什么新功能,使用的接口和 UIListView 完全一样;网上也查不到这个类的相关信息,在项目中也没出现过,感觉就是和 UIListView 一样的东西。

UIPageView

UIPageView 继承自 UILayout,是一种可以翻页的容器。UIPageView 重写了 UILayout 的 setLayoutType 和 getLayoutType 方法,使其无法使用布局;这点与前面几个容器很不一样,UIListViewEx 内置了线性布局,UIScrollView 和 UIListView 虽然没有内置的布局,但也可以通过 setLayoutType 接口来设置布局,而 UIPageView 则无法设置布局,这是因为其内部有自己的一套排列页面和翻页的方法,不能因为设置了布局产生影响。

UIPageView 使用一个数组 m_pages 保存所有添加的页,每添加一页都根据之前的页面来计算当前页的位置;翻页的实现和 UIListView 有点相似,当拖动面板时调用 scrollPages,在 scrollPages 中判断是否需要翻页,需要的话再调用 movePages 设置页面的位置。

适用场合: UIPageView 和前面几个区别挺大,适用于有多个页面切换的场合

常用接口

//添加页面
void addPage(UILayout* page);
//插入页面
void insertPage(UILayout* page, int idx);
//移除页面
void removePage(UILayout* page);
void removePageAtIndex(int index);
void removeAllPages();
//获取页面
UILayout* getPage(int index);
//获取页面下标
int getCurPageIndex() const;
//往页面添加控件
void addWidgetToPage(UIWidget* widget, int pageIdx, bool forceCreate);

猜你喜欢

转载自blog.csdn.net/xingxinmanong/article/details/78225686
今日推荐