分享一个简单的关卡选择器做法

实现了一个环形关卡选择器,效果如下

相信很多同学都实现过了。这里分享一下我的做法。给刚入门的同学一个参考。:slight_smile:

实现思路

主视图看过去,左边的节点在用户往左滑的时候,往右跑了,并且变小了。
右边的类似。
那么我们可以换个角度看,从俯视图看过去就是这样了:
image

从这个角度思考,问题就变得简单起来了

做法

  1. 建立圆形模型
  2. 布局节点
  3. 旋转圆形的时候更新节点
  4. 调用

建立圆形模型

    {angle:0}

一个对象,包含当前角度值。半径我存在了模块变量里。所以只需要有角度值这个属性就可以了。

布局节点

需要将所有节点创建后均匀布局到圆形边上。

    const _eachAngle = 360 / amount;
    const _arr = Array.from({ length: amount }).map(
        (v, i) => {
            const _item = i === 0 ? baseItem : cc.instantiate(baseItem);
            _item.parent = baseItem.parent;
            _item.y = 0;
            createHook && createHook(_item, i);
            return [_item, i, i * _eachAngle];
        }
    );

其实就是每个节点又变成了一个数组对象,包含【节点,序号,角度】的信息。

旋转圆形的时候更新节点

  • 更新节点信息
    我们将节点的数组对象中的角度信息加上圆形模型的当前角度
    求出节点相对于圆形模型的当前角度
    然后去算出节点应该具有的缩放或者透明度信息或者其它信息
        const _ratio = Math.abs(Math.cos(_curNodeAngle * Math.PI / 360));
        const _opacity = 255 * _ratio;
        indexNode.scale = _opacity / 2550 + 0.9;
        indexNode.opacity = _opacity;
        indexNode.x = RADIUS * Math.sin(_curNodeAngle * Math.PI / 180);
        indexNode.zIndex = _opacity;

这里用到了简单的三角函数
透明度和缩放是cos的绝对:0时最大,90是最小。(将函数周期放大了1倍)
坐标是sin:0是在中心,90在两边。
image

  • 旋转圆形
    监听父节点的触摸事件。因为要及时更新角度值,所以使用了代理。
const _curAngleObj = new Proxy({ angle: 0 }, {
        set(target, key, value) {
            key == 'angle' && _updateIndexNode(value);
            target[key] = value;
            return true;
        }
})
  • 触摸结束自动校正
    在停止触摸后,需要缓动回到最近的位置。因为用到了代理。所以可以直接用cocos的tween达到效果。
    _parentNode.on('touchend', e => {
        if (_isTouchLock) return;
        _isTouchLock = true;
        let _runAngle = Math.round(Data.curAngle.angle / _eachAngle) * _eachAngle
        cc.tween(Data.curAngle)
            .to(0.4, { angle: _runAngle }, { easing: 'sineOut' })
            .call(() => _isTouchLock = false).start();
    })

调用

导出模块后,在组件代码里引用传入参数就可以了。支持数量和半径的修改。

        const circleChoose = require('circle-choose');
        circleChoose({
            baseItem: someNode,
            amount: 4,
            radius: 120,
            createHook(node, index) {
                node.getChildByName('tip').getComponent(cc.Label).string = 'level\n' + (index + 1);
            }
        });

肯定需要对每一个节点进行区别对待。
所以可以传入创建节点的钩子回调(createHook),用来自定义节点内容。

最后贴下模块代码(javascript)

//模块数据
const Data = {
    opts: null,
    itemNodesArray: null,
    curIndex: 0,
    curAngle: null
}
//更新节点属性
function _updateIndexNode(curAngle = 0) {
    const RADIUS = Data.opts.radius;
    let _curIndex = -1;
    let _minAngle = 360;
    Data.itemNodesArray.forEach(([indexNode, index, angle]) => {
        let _curNodeAngle = angle + curAngle;
        if (Math.abs(_curNodeAngle) < _minAngle) {
            _minAngle = Math.abs(_curNodeAngle);
            _curIndex = index;
        }
        const _ratio = Math.abs(Math.cos(_curNodeAngle * Math.PI / 360));
        const _opacity = 255 * _ratio;
        indexNode.scale = _opacity / 2550 + 0.9;
        indexNode.opacity = _opacity;
        indexNode.x = RADIUS * Math.sin(_curNodeAngle * Math.PI / 180);
        indexNode.zIndex = _opacity;
    })
}
//初始化节点
function _initItems(createHook) {
    const { baseItem, amount } = Data.opts;
    const _eachAngle = 360 / amount;
    const _arr = Array.from({ length: amount }).map(
        (v, i) => {
            const _item = i === 0 ? baseItem : cc.instantiate(baseItem);
            _item.parent = baseItem.parent;
            _item.y = 0;
            createHook && createHook(_item, i);
            return [_item, i, i * _eachAngle];
        }
    );
    Data.itemNodesArray = _arr;
    _updateIndexNode();
}
/**
 * @param {object} opts
 * @param {cc.Node} opts.baseItem -基础的节点,用于创建其它节点。。
 * @param {number} opts.amount -要创建的节点数量。
 * @param {number} opts.radius -整个选择器的俯视半径
 * @param {Function} opts.createHook -创建每一个节点后的回调钩子
 */
function createCircleChoose(opts) {
    const { baseItem, amount } = opts;
    if (!(baseItem instanceof cc.Node)) return console.warn('should provide a cc.Node as the baseItem');
    if (!(baseItem.parent instanceof cc.Node)) return console.error('should provide a baseItem cc.Node which has a parent cc.Node');
    Data.opts = opts;
    _initItems(opts.createHook);
    const _parentNode = baseItem.parent;
    const _eachAngle = 360 / amount;
    const _curAngleObj = new Proxy({ angle: 0 }, {
        set(target, key, value) {
            key == 'angle' && _updateIndexNode(value);
            target[key] = value;
            return true;
        }
    })
    Data.curAngle = _curAngleObj;
    let _isTouchLock = false;
    _parentNode.on('touchmove', e => {
        if (_isTouchLock) return;
        Data.curAngle.angle += e.getDelta().x * 0.6;
    }, this)
    _parentNode.on('touchend', e => {
        if (_isTouchLock) return;
        _isTouchLock = true;
        let _runAngle = Math.round(Data.curAngle.angle / _eachAngle) * _eachAngle
        cc.tween(Data.curAngle)
            .to(0.4, { angle: _runAngle }, { easing: 'sineOut' })
            .call(() => {
                _isTouchLock = false;
            }).start();
    })
}
module.exports = createCircleChoose;

如果有不好的地方,欢迎指正讨论哈!:slight_smile:
另:我尝试了一下用这个模块,做类似banner循环的效果,貌似不是很好实现。555

4赞

大佬,有demo吗,膜拜下

大佬,你忘了监听parentNode的 touchcancel 事件