自动生成按钮移动方向设计实现思路
本文由 小茗同学 发表于 2016-06-27 浏览(857)
最后修改 2016-07-08 标签:epg 按钮 自动 生成
[TOC]

本文最初写于:2016-03-09

前言

所谓自动生成按钮移动方向,就是找到所有按钮上下左右各个方向上最合适的目标按钮。首先我们能想到的基本思路当然是,获取页面所有按钮相对于屏幕左上角的left和top,还有width和height,然后再遍历所有按钮和所有方向,找到离它们最近的按钮即可。

那怎么样才叫最近呢?就以向右移动为例,是不是只要left最近就可以了呢?当然不是,还要考虑垂直距离问题,因为最近的按钮并不一定就是最合适的,所以我们要在水平方向和垂直方向之间做一个权衡,本文讨论的主要问题就是这个权衡如何来把握。

准备工作

假设已经拿到了页面所有按钮数组对象buttons,这个对象大致内容如下:

buttons =
[
    {id:'btn1', left:'', right:'', up:'', down:'', ...},
    {id:'btn2', left:'', right:'', up:'', down:'', ...},
    {id:'btn3', left:'', right:'', up:'', down:'', ...},
    ...
];

然后就是获取按钮的绝对坐标和宽高度信息,有一个原生方法可以直接用:Element.getBoundingClientRect(),除了返回left、top、width、height之外,还会返回bottom和right,其中,bottom=top+heightright=left+width

这个方法的兼容性其实非常好,IE9+,Android2.3+,即使是IE6-8也是部分支持(没有width和height,返回的对象不能被修改),如果你不放心兼容性(比如标清机顶盒),可以自己写一个:

function getAbsolutePosition(elem)
{
    if(elem == null) return {left: 0, top: 0, width: 0, height: 0};
    var left = elem.offsetLeft,
        top = elem.offsetTop,
        width = elem.offsetWidth,
        height = elem.offsetHeight;
    while(elem = elem.offsetParent)
    {
        left += elem.offsetLeft;
        top += elem.offsetTop;
    }
    return {left: left, top: top, width: width, height: height};
}

需要注意的是,getBoundingClientRect返回的是元素相对于屏幕顶部的距离,如果元素滚动到屏幕上方去了,top会返回负值,而后面我们自己定义的那个方法返回的是元素到文档顶部的距离,也就是不会考虑滚动的问题,当然,这两种结果对我们没有影响。

变量约定

为了说明方便,这里所有的例子除非特别说明外都是以按钮 向右移动 为例,其它方向依次类推即可,current表示当前按钮,next表示下一个目标按钮,dir表示方向,cxcy分别表示按钮的中点left和top,deg表示夹角。

方案一

一开始想的很简单:先把所有在当前按钮左边的按钮排除,然后把所有cy在当前按钮的top和bottom之间的按钮列出来,如果有,取出left最小的那个(如下图中,next1、next2、next3都符合要求,但是只取left最小的next1):

如果没有找到符合条件的按钮,再放开条件,在top和height各自往外延伸高度的一半,即:

(top-height/2) <=cy <= (bottom+height/2)

再要找不到,直接不限制top,直接找最近的。这种方法对于一般的情况还行,稍微特殊一点就应付不了了,比如下面这个,按照这个规则的话,current的right将会是next1而不是next2,因为next1的left更小:

方案二

方案一很显然有很多问题,初看起来好像主要是因为方案一的判断条件的梯度不够,只要增加梯度次数、以及减小梯度之间的间隙,然后依次判断就可以了,其实不止这个问题,还有一个大问题就是仅仅靠判断按钮的中点是远远不够的,比如下面这个例子:

可以看到,正确的目标按钮应该是next2,但是由于next1的cy比next2的cy小,left也比next2小,所以无论梯度多么小,生成的目标按钮都会是next1,所以仅仅通过按钮的中点判断是远远不够的,还要判断next按钮是否已经有部分处在current按钮的top和bottom之间。

另外,再看下面的例子:

正确按钮应该是next2,next1和next2的垂直方向都距离current比较远,但是由于next1的left更小,导致生成的目标按钮是next1。

这就说明,仅仅通过高度来设置梯度是不合理的,应当要兼顾水平距离和垂直距离,所以决定采用中心点连线与水平线的夹角来设置梯度。比如,我们设置0、30、45、60、90这几个梯度来依次查找合适的按钮。

方案三

重新梳理思路,首先排除left小于当前left的,然后筛选出大致与current在同一水平线的,找不到再按照夹角设置不同梯度依次查找。

怎么才算叫大致处于同一水平线呢?我们姑且考虑这么几种情况:

  1. next 的 top 和 bottom 完全在 current 的 top 和 bottom 之间的,下图中的n1、n2、n3都属于这种情况;
  2. next 的 top 在 current 的 中间线cy 之上、并且bottom 在 current 的 bottom 之下,如下图中的n4;
  3. next 的 bottom 在 current 的 中间线cy 之下、并且 top 在 current 的 top 之上,如下图中的n5;
  4. next 的 top 在 current 的 top 之上、bottom 在 current 的 bottom 之下的,如下图中的n6;
  5. next 的高度有一半进入了双红线之间的,也就是 next 的 中点cy 在 current 的 top 和 bottom 之间的,如下图中的n7;

为了统一,我们把大致处于同一水平线的情况看做是deg=0的特殊情况,现假设梯度为[0,30,45,60,90],如果deg=0下找到了几个符合要求的按钮,那么取出left最小的,如果没有找到,再去找夹角小于等于30的,30找不到再找45,以此类推。

夹角我们前面说了,采用2个按钮中点连线与水平线的夹角:

如果知道90还是找不到,那么返回空字符串表示指定方向上没有合适的目标按钮。

方案四(细节优化)

用方案三来测试可以发现,大部分情况确实没啥问题了,但是还是有一些特殊情况没有考虑到。

首先

在排除不在一个方向的按钮时方法错了,前面我们是把next.left<=current.left的按钮认为在当前按钮的左边,其实应该用right来判断:next.right<=current.right,如以下特殊情况,虽然left在current的left的左边,但是我们仍然认为这是一个符合条件的按钮,不能排除:

然后

仔细分析方案三中针对大致处于同一水平线列出的5种情况可以发现,情况5包含了所有的情况1,部分包含了情况2、3、4,而情况2、3又都包含了情况4,故只需要考虑2、3、5这三种情况即可,删除1和4的多余判断。

其次

最大角度设置为90显然不合理,它会把很多不合适的按钮给考虑进来(如下图,为了画图方便,这里的角度并没有非常大,显然next不合要求):

但是又不能设置太低,否则又有可能我们想让它符合条件它却不符合,如下图中的收藏到个人中心,如果角度设置太低的话会导致up方向无法匹配个人中心:

经过测试一般设定为80-85为宜,我一般设置成85。

但是

但是,仅仅是降低最大角度就没问题了么?再看一个案例:

虽然角度不是很大,只有45度的样子,但是dir=right的话会成功匹配,很显然我们并不想让它匹配。

由此发现,除了考虑角度之外,还应考虑next的right与current的right的距离,二者不能太近,那么具体如何考虑呢?一开始想到的方法是计算2个right之间的距离,必须大于next的right的一定百分比,比如10%,即:

(next.right-current.right) >= next.right*0.1

但这样似乎增加了计算量,每次匹配角度时还要单独计算right距离,最后干脆决定改成直接计算2个right的中点的夹角(其它方向以此类推,比如dir==down的话,就计算bottom中点的夹角):

这样就把上面那种情况排除了。仔细想想完全只考虑right不考虑中点似乎某些情况会有些不妥?暂时先放这里。

最后

还有一些特殊情况必须处理,比如同时找到了3个合适的按钮并且它们的left完全一致,不作处理的话可能随机取一个,此时最好做一些人为处理,一般可能有2种需求,一种是取中点离current的中间线最近的(如下图中的next2),还有一种是取top距离current的top最近的(如下图中的next1),我一般喜欢取后者:

最后的最后

稍微优化一下计算量,比如,如果已经找到了1个deg为0的按钮,其它按钮只要deg不为0就不要计算了,减少不必要的三角函数计算,角度可以一次性全部计算出来,而没必要遍历每个梯度时重新计算。

结语

再牛逼的代码也挡不住千奇百怪的需求,所以应当增加一些可配置项能够让用户自己选择哪些按钮来参与自动生成。

比如实际使用中经常出现必须某些按钮我可能不想参与自动生成,或者某些按钮的某些方向我想自己设置目标按钮,再或者可能需要针对不同类型的按钮进行分组,不同组别的按钮之间是不能互相移动的,这个只要在代码里面增加可选的配置项即可。

然后就是兼容性,标清机顶盒目前还没试过,最主要是懒得尝试,标清不建议使用这种方式,不然踩不完的坑。

再然后就是性能,PC上一般20个按钮以内一般几毫秒搞定,计算量随着按钮的个数增加呈指数增长,大部分情况下还是能应付的。