需求描述

Cocos原生tableview可以非常方便的实现横向/竖向列表

有一天需求突然变成倾斜的横向列表

需求效果如图所示

 

原理分析

CCTableView.h

std::vector<float> _vCellsPositions;

列表中每个元素的相对位置是确定的

保存在以上数据结构中,仅存了一个数字

因为默认HORIZONTAL列表元素的y值始终为0

VERTICAL列表元素的x值始终为0

 

这样就可以把列表的全部元素想象成一条竖着或者横着的长龙

显示区域只能显示龙的一小部分

用户拖动决定了龙露出来的位置

因此为了实现斜向滑动需要做以下两点修改

1.把元素位置由横向排列,改为斜着排列

2.把用户拖动有横向拖动,改为斜着拖动

 

代码实现

首先我们在ScrollView类中定义一个变量(TableView继承了ScrollView)

_skewTangent

表示横向列表的倾斜角正弦值

默认不倾斜正弦值为0

private:
    float _skewTangent;
public:
    void setSkewTangent(float val){ _skewTangent = val; }

 

 

然后我们设置元素们的位置

源代码中横向列表的y值是0

注释掉它,替换为由正弦计算得出的y值

Vec2 TableView::__offsetFromIndex(ssize_t index)
{
    Vec2 offset;
    Size  cellSize;

    switch (this->getDirection())
    {
        case Direction::HORIZONTAL:
            //offset = Vec2(_vCellsPositions[index], 0.0f);
            offset = Vec2(_vCellsPositions[index], -1 * _skewTangent * _vCellsPositions[index]);
            break;
        default:
            offset = Vec2(0.0f, _vCellsPositions[index]);
            break;
    }

    return offset;
}

 

再解决拖动问题

在用户横向拖动的时候

增加Y方向上的位移即可

/* change moveDistance by _skewTangent*/

void ScrollView::onTouchMoved(Touch* touch, Event* event)
{
    if (!this->isVisible())
    {
        return;
    }

    if (std::find(_touches.begin(), _touches.end(), touch) != _touches.end())
    {
        if (_touches.size() == 1 && _dragging)
        { // scrolling
            Vec2 moveDistance, newPoint;
            Rect  frame;
            float newX, newY;
            
            frame = getViewRect();

            newPoint     = this->convertTouchToNodeSpace(_touches[0]);
            moveDistance = newPoint - _touchPoint;
            
            float dis = 0.0f;
            if (_direction == Direction::VERTICAL)
            {
                dis = moveDistance.y;
                float pos = _container->getPosition().y;
                if (!(minContainerOffset().y <= pos && pos <= maxContainerOffset().y)) {
                    moveDistance.y *= BOUNCE_BACK_FACTOR;
                }
            }
            else if (_direction == Direction::HORIZONTAL)
            {
                dis = moveDistance.x;
                float pos = _container->getPosition().x;
                if (!(minContainerOffset().x <= pos && pos <= maxContainerOffset().x)) {
                    moveDistance.x *= BOUNCE_BACK_FACTOR;
                }
            }
            else
            {
                dis = sqrtf(moveDistance.x*moveDistance.x + moveDistance.y*moveDistance.y);
                
                float pos = _container->getPosition().y;
                if (!(minContainerOffset().y <= pos && pos <= maxContainerOffset().y)) {
                    moveDistance.y *= BOUNCE_BACK_FACTOR;
                }
                
                pos = _container->getPosition().x;
                if (!(minContainerOffset().x <= pos && pos <= maxContainerOffset().x)) {
                    moveDistance.x *= BOUNCE_BACK_FACTOR;
                }
            }

            if (!_touchMoved && fabs(convertDistanceFromPointToInch(dis)) < MOVE_INCH )
            {
                //CCLOG("Invalid movement, distance = [%f, %f], disInch = %f", moveDistance.x, moveDistance.y);
                return;
            }
            
            if (!_touchMoved)
            {
                moveDistance = Vec2::ZERO;
            }
            
            _touchPoint = newPoint;
            _touchMoved = true;
            
            if (_dragging)
            {
                switch (_direction)
                {
                    case Direction::VERTICAL:
                        moveDistance = Vec2(0.0f, moveDistance.y);
                        break;
                    case Direction::HORIZONTAL:
                        /* change moveDistance by _skewTangent*/
                        //moveDistance = Vec2(moveDistance.x, 0.0f);
                        moveDistance = Vec2(moveDistance.x, _skewTangent * moveDistance.x);
                        break;
                    default:
                        break;
                }

                newX     = _container->getPosition().x + moveDistance.x;
                newY     = _container->getPosition().y + moveDistance.y;

                _scrollDistance = moveDistance;
                this->setContentOffset(Vec2(newX, newY));
            }
        }
        else if (_touches.size() == 2 && !_dragging)
        {
            const float len = _container->convertTouchToNodeSpace(_touches[0]).getDistance(
                                            _container->convertTouchToNodeSpace(_touches[1]));
            this->setZoomScale(this->getZoomScale()*len/_touchLength);
        }
    }
}

 

这样子基本满足了需求

但会产生一些问题

在滑动到左端和右端时

Y方向上位置会出错

还要加上以下代码处理

void ScrollView::relocateContainer(bool animated)
{
    Vec2 oldPoint, min, max;
    float newX, newY;
    
    min = this->minContainerOffset();
    max = this->maxContainerOffset();
    
    oldPoint = _container->getPosition();

    newX     = oldPoint.x;
    newY     = oldPoint.y;
    if (_direction == Direction::BOTH || _direction == Direction::HORIZONTAL)
    {
        newX     = MAX(newX, min.x);
        newX     = MIN(newX, max.x);
        /* code goes here*/
        if (_skewTangent != 0.0f && (newX == min.x || newX == max.x))
        {
            //斜向滑动边界处理
            newY = MIN(newY, max.y + _skewTangent * max.x);
            newY = MAX(newY, min.y + _skewTangent * min.x);
        }
    }

    if (_direction == Direction::BOTH || _direction == Direction::VERTICAL)
    {
        newY     = MIN(newY, max.y);
        newY     = MAX(newY, min.y);
    }

    if (newY != oldPoint.y || newX != oldPoint.x)
    {
        this->setContentOffset(Vec2(newX, newY), animated);
    }
}

 

也可以考虑通过修改以下方法

minContainerOffset()

maxContainerOffset()

但这次我没有这么做

 

以上遍实现了该功能