实现了一个环形关卡选择器,效果如下
相信很多同学都实现过了。这里分享一下我的做法。给刚入门的同学一个参考。
实现思路
主视图看过去,左边的节点在用户往左滑的时候,往右跑了,并且变小了。
右边的类似。
那么我们可以换个角度看,从俯视图看过去就是这样了:

从这个角度思考,问题就变得简单起来了
做法
- 建立圆形模型
- 布局节点
- 旋转圆形的时候更新节点
- 调用
建立圆形模型
{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在两边。

- 旋转圆形
监听父节点的触摸事件。因为要及时更新角度值,所以使用了代理。
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;
如果有不好的地方,欢迎指正讨论哈!
另:我尝试了一下用这个模块,做类似banner循环的效果,貌似不是很好实现。555

