如何在循环中为形状添加点击功能?

3
我正在将文本数组传递给我的 circleCreate 函数,该函数为每个文本创建一个楔形。我想要做的是为每个楔形添加一个点击事件,所以当用户单击一个楔形时,它会弹出一个带有每个楔形文本的警报。

但是它没有起作用。只有外部圆圈会警报文本。而且它总是显示相同的文本。两个内部圆圈都会警报未定义。

http://jsfiddle.net/Yushell/9f7JN/

var layer = new Kinetic.Layer();

function circleCreate(vangle, vradius, vcolor, vtext) {
    startAngle = 0;
    endAngle = 0;
    for (var i = 0; i < vangle.length; i++) {
        // WEDGE
        startAngle = endAngle;
        endAngle = startAngle + vangle[i];
        var wedge = new Kinetic.Wedge({
            x: stage.getWidth() / 2,
            y: stage.getHeight() / 2,
            radius: vradius,
            angleDeg: vangle[i],
            fill: vcolor,
            stroke: 'black',
            strokeWidth: 1,
            rotationDeg: startAngle
        });
        /* CLICK NOT WORKING
        wedge.on('click', function() {
            alert(vtext[i]);
        });*/
        layer.add(wedge);
    }
    stage.add(layer);
}
3个回答

2
这是在处理异步JavaScript代码(如事件处理程序)时会遇到的典型问题。在您的circleCreate()函数中,for循环使用一个变量i,它为每个楔形增加。这在使用i创建楔形时是可以的:
            angleDeg: vangle[i],

但是在您将其用于click事件处理程序内部时,它会失败:

            alert(vtext[i]);

为什么会出现这种情况?

当你使用new Kinetic.Wedge()创建楔形图时,这是直接在循环内部完成的。这段代码是同步运行的;它使用i的值,正是在该次循环迭代运行时存在的那个值。

但是click事件处理程序并不会在那个时候运行。如果你从未点击,它可能根本不会运行。当你点击一个楔形图时,它的事件处理程序会在此时被调用,而原始循环已经运行完毕很久了。

那么,当事件处理程序确实运行时,i的值是多少呢?它是代码最初运行时留下的任何值。这个for循环在i等于vangle.length时退出——换句话说,i已经超过了数组的末尾,因此vangle[i]undefined

你可以通过闭包轻松解决这个问题,只需为每个循环迭代调用一个函数即可:

var layer = new Kinetic.Layer();

function circleCreate(vangle, vradius, vcolor, vtext) {
    startAngle = 0;
    endAngle = 0;
    for (var i = 0; i < vangle.length; i++) {
        addWedge( i );
    }
    stage.add(layer);

    function addWedge( i ) {
        startAngle = endAngle;
        endAngle = startAngle + vangle[i];
        var wedge = new Kinetic.Wedge({
            x: stage.getWidth() / 2,
            y: stage.getHeight() / 2,
            radius: vradius,
            angleDeg: vangle[i],
            fill: vcolor,
            stroke: 'black',
            strokeWidth: 1,
            rotationDeg: startAngle
        });
        wedge.on('click', function() {
            alert(vtext[i]);
        });
        layer.add(wedge);
    }
}

现在发生的情况是调用 addWedge()函数会为每个循环迭代单独捕获i的值。如您所知,每个函数都可以有自己的本地变量/参数,而addWedge()中的i是该函数的局部变量-特别是每个单独的函数调用的局部变量。(请注意,因为addWedge()是自己的函数,所以该函数内部的i与外部的circleCreate()函数中的i不同。如果这让人困惑,将其命名为其他名称即可。) 更新后的代码片段

更好的方法

我建议采用不同的方法来构建您的数据结构。当我阅读您的代码时,注意到了角度和文本数组:
var anglesParents = [120, 120, 120];
var parentTextArray = ['Parent1', 'Parent2', 'Parent3'];

对于子代和孙代,有类似但更长的数组对。

您可以使用这些数组中的值与circleCreate()中的vtext[i]vangle[i]引用一起使用。

通常情况下,除非有特定原因要使用这种并行数组,否则如果将它们合并为单个对象数组,则代码将变得更加清晰:

[
    { angle: 120, text: 'Parent1' },
    { angle: 120, text: 'Parent2' },
    { angle: 120, text: 'Parent3' }
]

对于您的嵌套圆形,我们可以进一步将所有三个环组合成一个大型对象数组,描述整个嵌套环集。在这里,您有这些数组:

var anglesParents = [120, 120, 120];
var anglesChildren = [120, 60, 60, 60, 60];
var anglesGrandchildren = [
    33.33, 20, 23.33, 43.33, 22.10, 25.26,
    12.63, 28, 32, 33, 27, 36, 14.4, 9.6
];
var grandchildrenTextArray = [
    'GrandCHild1', 'GrandCHild2', 'GrandCHild3', 'GrandCHild4',
    'GrandCHild5', 'GrandCHild6', 'GrandCHild7', 'GrandCHild8',
    'GrandCHild9', 'GrandCHild10', 'GrandCHild11', 'GrandCHild12',
    'GrandCHild13', 'GrandCHild14', 'GrandCHild15', 'GrandCHild16'
];
var childrenTextArray = [
    'Child1', 'Child2', 'Child3', 'Child4', 'Child5'
];
var parentTextArray = ['Parent1', 'Parent2', 'Parent3'];

这需要:

var rings = [
    {
        radius: 200,
        color: 'grey',
        slices: [
            { angle: 33.33, text: 'GrandChild1' },
            { angle: 20, text: 'GrandChild2' },
            { angle: 23.33, text: 'GrandChild3' },
            { angle: 43.33, text: 'GrandChild4' },
            { angle: 22.10, text: 'GrandChild5' },
            { angle: 25.26, text: 'GrandChild6' },
            { angle: 12.63, text: 'GrandChild7' },
            { angle: 28, text: 'GrandChild8' },
            { angle: 32, text: 'GrandChild9' },
            { angle: 33, text: 'GrandChild10' },
            { angle: 27, text: 'GrandChild10' },
            { angle: 36, text: 'GrandChild12' },
            { angle: 14.4, text: 'GrandChild13' },
            { angle: 9.6, text: 'GrandChild14' }
        ]
    },
    {
        radius: 150,
        color: 'darkgrey',
        slices: [
            { angle: 120, text: 'Child1' },
            { angle: 60, text: 'Child2' },
            { angle: 60, text: 'Child3' },
            { angle: 60, text: 'Child4' },
            { angle: 60, text: 'Child5' }
        ]
    },
    {
        radius: 100,
        color: 'lightgrey',
        slices: [
            { angle: 120, text: 'Parent1' },
            { angle: 120, text: 'Parent2' },
            { angle: 120, text: 'Parent3' }
        ]
    }
];

现在这段文字比原文更长,因为其中包含了angle:text:属性名,但是服务器和浏览器使用的gzip压缩可以很好地压缩这些内容。

更重要的是,它有助于简化和澄清代码,避免错误。你是否注意到你的anglesGrandchildrengrandchildrenTextArray长度不同? :-)

使用一个对象数组而不是平行数组可以避免出现此类错误。

为了使用这些数据,请删除circleCreate()函数及其调用:

circleCreate(anglesGrandchildren, 200, "grey", grandchildrenTextArray);
circleCreate(anglesChildren, 150, "darkgrey", childrenTextArray);
circleCreate(anglesParents, 100, "lightgrey", parentTextArray);

并将它们替换为:

function createRings( rings ) {
    var startAngle = 0, endAngle = 0,
        x = stage.getWidth() / 2,
        y = stage.getHeight() / 2;

    rings.forEach( function( ring ) {
        ring.slices.forEach( function( slice ) {
            startAngle = endAngle;
            endAngle = startAngle + slice.angle;
            var wedge = new Kinetic.Wedge({
                x: x,
                y: y,
                radius: ring.radius,
                angleDeg: slice.angle,
                fill: ring.color,
                stroke: 'black',
                strokeWidth: 1,
                rotationDeg: startAngle
            });
            wedge.on('click', function() {
                alert(slice.text);
            });
            layer.add(wedge);
        });
    });

    stage.add(layer);
}

createRings( rings );

现在这段代码并没有比原来的更短,但一些细节更加清晰:`slice.angle` 和 `slice.text` 明确表明角度和文本属于同一个扇区对象,在原始代码中,我们只能希望 `vangle` 和 `vtext` 数组是正确匹配且彼此正确对齐的。
我还使用了 `forEach()` 而不是 `for` 循环;由于您正在使用 Canvas,因此我们知道您正在使用现代浏览器。一个好处是 `forEach()` 使用函数调用,因此它自动为您提供闭包。
此外,我将 `x` 和 `y` 的计算移到了循环外部,因为它们对于每个楔形图都是相同的。
这是带有更新后的代码和数据的最新 fiddle

这也完美地奏效了。顺便问一下,我能不能在每个楔形图的可见部分添加一个文本路径?我试图做一个圆形菜单。但是我不知道应该将什么作为textpath的数据属性传递:http://jsfiddle.net/Yushell/d85fc/ - Yushell
嗯...对此我不太确定,抱歉! - Michael Geary
感谢Michael提供如此详细清晰的解释。我理解了每一行代码。考虑到我对这方面还很新,我怀疑我自己不可能想出这样高水平的代码。我仍然卡在“每个楔形图上的文本”部分。但是这篇文章对我非常有帮助。来自墨西哥的问候。 - Yushell

2

由于每次循环迭代时定义的匿名函数作为事件处理程序都将共享相同的作用域,因此每个函数都会引用与您尝试显示文本的数组地址相同的变量(i)。由于您每次循环都重新定义变量 i,所以您始终会在每个点击事件中看到消息数组中的最后一条文本消息显示,因为分配给 i 的最后一个值将是您的数组长度。

这里是解决方案:

var layer = new Kinetic.Layer();

function circleCreate(vangle, vradius, vcolor, vtext) {
    startAngle = 0;
    endAngle = 0;
    for (var i = 0; i < vangle.length; i++) {
        // WEDGE
        startAngle = endAngle;
        endAngle = startAngle + vangle[i];
        var wedge = new Kinetic.Wedge({
            x: stage.getWidth() / 2,
            y: stage.getHeight() / 2,
            radius: vradius,
            angleDeg: vangle[i],
            fill: vcolor,
            stroke: 'black',
            strokeWidth: 1,
            rotationDeg: startAngle
        });
        (function(index) {
            wedge.on('click', function() {
                alert(vtext[i]);
            });
        })(i)
        layer.add(wedge);
    }
    stage.add(layer);
}

1
谢谢。这个有效了,只需要将alert(vtext[i]);更改为alert(vtext[index]);即可。 - Yushell

1
你的问题在于循环索引。尝试这样做:

    (function(j) {
        wedge.on('click', function() {
            alert(vtext[j]);
        });
    })(i);

点击这里查看。

问题在于当你的click处理程序被调用时,i的值为循环结束时的值,因此vtext[i]显然是未定义的。通过将其包装在闭包中,您可以保存循环索引的值在循环运行时,以供您的click处理程序使用。


有没有办法可以在每个楔形图的可见部分添加文本路径?我正在尝试创建一个圆形菜单,但我不知道应该将什么作为textpath的数据属性:jsfiddle.net/Yushell/d85fc - Yushell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接