乐趣区

关于前端:当你按下方向键电视是如何寻找下一个焦点的

我工作的第一家公司次要做的是一个在智能电视下面运行的 APP,其实就是一个安卓 APP,也是混合开发的利用,外面很多页面是 H5 开发的。

电视咱们都晓得,是通过遥控器来操作的,没有鼠标也不能触屏,所以“点击”的操作变成了按遥控器的“上下左右确定”键,那么必然须要一个“焦点”来通知用户以后聚焦在哪里。

过后开发页面应用的是一个前人开发的焦点库,这个库会本人监听方向键并且主动计算下一个聚焦的元素。

为什么时隔多年会忽然想起这个呢,其实是因为最近在给我开源的思维导图增加方向键导航的性能时,想到其实和电视聚焦性能很相似,都是按方向键,来计算并且主动聚焦到下一个元素或节点:

那么如何寻找下一个焦点呢,联合我过后用的焦点库的原理,接下来实现一下。

1. 最简略的算法

第一种算法最简略,依据方向先找出以后节点该方向所有的其余节点,而后再找出直线间隔最近的一个,比方当按下了左方向键,上面这些节点都是符合要求的节点:

从中选出最近的一个即为下一个聚焦节点。

节点的地位信息示意如下:

focus(dir) {
    // 以后聚焦的节点
    let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
    // 以后聚焦节点的地位信息
    let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
    // 寻找的下一个聚焦节点
    let targetNode = null
    let targetDis = Infinity
    // 保留并保护间隔最近的节点
    let checkNodeDis = (rect, node) => {let dis = this.getDistance(currentActiveNodeRect, rect)
        if (dis < targetDis) {
            targetNode = node
            targetDis = dis
        }
    }
    // 1. 最简略的算法
    this.getFocusNodeBySimpleAlgorithm({
        currentActiveNode,
        currentActiveNodeRect,
        dir,
        checkNodeDis
    })
    // 找到了则让指标节点聚焦
    if (targetNode) {targetNode.active()
    }
}

无论哪种算法,都是先找出所有符合要求的节点,而后再从中找出和以后聚焦节点间隔最近的节点,所以保护最近间隔节点的函数是能够复用的,通过参数的模式传给具体的计算函数。

// 1. 最简略的算法
getFocusNodeBySimpleAlgorithm({
    currentActiveNode,
    currentActiveNodeRect,
    dir,
    checkNodeDis
}) {
    // 遍历思维导图节点树
    bfsWalk(this.mindMap.renderer.root, node => {
        // 跳过以后聚焦的节点
        if (node === currentActiveNode) return
        // 以后遍历到的节点的地位信息
        let rect = this.getNodeRect(node)
        let {left, top, right, bottom} = rect
        let match = false
        // 按下了左方向键
        if (dir === 'Left') {
            // 判断节点是否在以后节点的左侧
            match = right <= currentActiveNodeRect.left
            // 按下了右方向键
        } else if (dir === 'Right') {
            // 判断节点是否在以后节点的右侧
            match = left >= currentActiveNodeRect.right
            // 按下了上方向键
        } else if (dir === 'Up') {
            // 判断节点是否在以后节点的下面
            match = bottom <= currentActiveNodeRect.top
            // 按下了下方向键
        } else if (dir === 'Down') {
            // 判断节点是否在以后节点的上面
            match = top >= currentActiveNodeRect.bottom
        }
        // 符合要求,判断是否是最近的节点
        if (match) {checkNodeDis(rect, node)
        }
    })
}

成果如下:

根本能够工作,然而能够看到有个很大的毛病,比方按上键,咱们预期的应该是聚焦到下面的兄弟节点上,然而实际上聚焦到的是子节点:

因为这个子节点的确是在以后节点下面,且间隔最近的,那么怎么解决这个问题呢,接下来看看第二种算法。

2. 暗影算法

该算法也是别离解决四个方向,然而和后面的第一种算法相比,额定要求节点在指定方向上的延长须要存在穿插,延长处能够设想成是节点的暗影,也就是名字的由来:

找出所有存在穿插的节点后也是从中找出间隔最近的一个节点作为下一个聚焦节点,批改 focus 办法,改成应用暗影算法:

focus(dir) {
    // 以后聚焦的节点
    let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
    // 以后聚焦节点的地位信息
    let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
    // 寻找的下一个聚焦节点
    // ...
    // 保留并保护间隔最近的节点
    // ...

    // 2. 暗影算法
    this.getFocusNodeByShadowAlgorithm({
        currentActiveNode,
        currentActiveNodeRect,
        dir,
        checkNodeDis
    })

    // 找到了则让指标节点聚焦
    if (targetNode) {targetNode.active()
    }
}
// 2. 暗影算法
getFocusNodeByShadowAlgorithm({
    currentActiveNode,
    currentActiveNodeRect,
    dir,
    checkNodeDis
}) {
    bfsWalk(this.mindMap.renderer.root, node => {if (node === currentActiveNode) return
        let rect = this.getNodeRect(node)
        let {left, top, right, bottom} = rect
        let match = false
        if (dir === 'Left') {
            match =
                left < currentActiveNodeRect.left &&
                top < currentActiveNodeRect.bottom &&
                bottom > currentActiveNodeRect.top
        } else if (dir === 'Right') {
            match =
                right > currentActiveNodeRect.right &&
                top < currentActiveNodeRect.bottom &&
                bottom > currentActiveNodeRect.top
        } else if (dir === 'Up') {
            match =
                top < currentActiveNodeRect.top &&
                left < currentActiveNodeRect.right &&
                right > currentActiveNodeRect.left
        } else if (dir === 'Down') {
            match =
                bottom > currentActiveNodeRect.bottom &&
                left < currentActiveNodeRect.right &&
                right > currentActiveNodeRect.left
        }
        if (match) {checkNodeDis(rect, node)
        }
    })
}

就是判断条件减少了是否穿插的比拟,成果如下:

能够看到暗影算法胜利解决了后面的跳转问题,然而它也并不完满,比方上面这种状况按左方向键找不到可聚焦节点了:

因为左侧没有存在穿插的节点,然而其实能够聚焦到父节点上,怎么办呢,咱们先看一下下一种算法。

3. 区域算法

所谓区域算法也很简略,把以后聚焦节点的周围平分成四个区域,对应四个方向,寻找哪个方向的下一个节点就先找出中心点在这个区域的所有节点,再从中抉择间隔最近的一个即可:

focus(dir) {
    // 以后聚焦的节点
    let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
    // 以后聚焦节点的地位信息
    let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
    // 寻找的下一个聚焦节点
    // ...
    // 保留并保护间隔最近的节点
    // ...

    // 3. 区域算法
    this.getFocusNodeByAreaAlgorithm({
        currentActiveNode,
        currentActiveNodeRect,
        dir,
        checkNodeDis
    })

    // 找到了则让指标节点聚焦
    if (targetNode) {targetNode.active()
    }
}
// 3. 区域算法
getFocusNodeByAreaAlgorithm({
    currentActiveNode,
    currentActiveNodeRect,
    dir,
    checkNodeDis
}) {
    // 以后聚焦节点的中心点
    let cX = (currentActiveNodeRect.right + currentActiveNodeRect.left) / 2
    let cY = (currentActiveNodeRect.bottom + currentActiveNodeRect.top) / 2
    bfsWalk(this.mindMap.renderer.root, node => {if (node === currentActiveNode) return
        let rect = this.getNodeRect(node)
        let {left, top, right, bottom} = rect
        // 遍历到的节点的中心点
        let ccX = (right + left) / 2
        let ccY = (bottom + top) / 2
        // 节点的中心点坐标和以后聚焦节点的中心点坐标的差值
        let offsetX = ccX - cX
        let offsetY = ccY - cY
        if (offsetX === 0 && offsetY === 0) return
        let match = false
        if (dir === 'Left') {match = offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY} else if (dir === 'Right') {match = offsetX > 0 && offsetX >= -offsetY && offsetX >= offsetY} else if (dir === 'Up') {match = offsetY <= 0 && offsetY < offsetX && offsetY < -offsetX} else if (dir === 'Down') {match = offsetY > 0 && -offsetY < offsetX && offsetY > offsetX}
        if (match) {checkNodeDis(rect, node)
        }
    })
}

比拟的逻辑能够参考下图:

联合暗影算法和区域算法

后面介绍暗影算法时说了它有肯定局限性,区域算法计算出的后果则能够对它进行补充,然而现实状况下暗影算法的后果是最合乎咱们的预期的,那么很简略,咱们能够把它们两个联合起来,调整一下程序,先应用暗影算法计算节点,如果暗影算法没找到,那么再应用区域算法寻找节点,简略算法也能够加在最初:

focus(dir) {
    // 以后聚焦的节点
    let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
    // 以后聚焦节点的地位信息
    let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
    // 寻找的下一个聚焦节点
    // ...
    // 保留并保护间隔最近的节点
    // ...

    // 第一优先级:暗影算法
    this.getFocusNodeByShadowAlgorithm({
        currentActiveNode,
        currentActiveNodeRect,
        dir,
        checkNodeDis
    })

    // 第二优先级:区域算法
    if (!targetNode) {
        this.getFocusNodeByAreaAlgorithm({
            currentActiveNode,
            currentActiveNodeRect,
            dir,
            checkNodeDis
        })
    }

    // 第三优先级:简略算法
    if (!targetNode) {
        this.getFocusNodeBySimpleAlgorithm({
            currentActiveNode,
            currentActiveNodeRect,
            dir,
            checkNodeDis
        })
    }

    // 找到了则让指标节点聚焦
    if (targetNode) {targetNode.active()
    }
}

成果如下:

是不是很简略呢,具体体验能够点击思维导图。

退出移动版