JavaScript 滑动条效果

这个滑动条(拖动条)效果,一开始是参考了BlueDestiny的slider和Apple产品展示的样式,做了程序的原型。
在做了拖放效果之后,我想应该可以做一个更好的了,所以重做一遍,完善和扩展了一些功能。
碍于时间没有做得很强大,都是一些基本功能,希望各位多提意见!

完整实例下载

欢迎来我的blog交流

效果预览(代码太多只能贴个图了)

预览效果1:
这个是仿Apple滑动条产品展示效果。
除了原来的效果(包括点击缓动滑移、拖动滑移),我还加入了本程序特有的滚轮和键盘控制,滑动条两端鼠标放上去会自动滑动,滑动到两边还会自动换一个表示停止的图案。

JavaScript 滑动条效果_第1张图片
 
预览效果2:
这里是稍微模拟了一下滚动条,当然离真正的滚动条还差很远。
滑动条除了水平还支持垂直方向的滑动,在内容显示部分按上下方向(需要先点击选中一下)或使用鼠标滚轮也能控制滑动。

JavaScript 滑动条效果_第2张图片
 

预览效果3:
这里主要是滑动条关于值方面的应用,可以应用在度量计算等方面。

JavaScript 滑动条效果_第3张图片
 
程序说明:

首先需要一个容器,滑动范围就在这个容器里面,还有是容器里面的绝对定位的滑块,基本就是这两部分。
滑块拖放的部分请参考拖放效果,这里我把拖放程序扩展了一个设置滑块位置的SetPos方法方便程序使用。

【水平和垂直滑动】

程序支持水平和垂直滑动,设置Horizontal属性为true就是水平滑动(默认),为false就是垂直滑动。
这个属性只能在实例化时设置,初始化之后会就不要修改了。
程序初始化时就根据这个属性锁定拖放的方向:
this._drag[this._horizontal ? "LockY" : "LockX"] = true;

程序支持两个方向的滑动,如果每次都判断一下再分别设置参数会很麻烦,
所以程序中每次滑动都计算两个方向的位置参数,并把参数直接交给_drag来处理。
由于_drag在实例化时已经做了范围限制和方向锁定,已经带了位置参数修正,所以可以直接交给它处理。
这样虽然效率差点,但就能大大降低复杂度,我想还是值得的。

【自动滑移】

运行Run自动滑移程序后,就会自动滑移,参数可以设置滑移的方向(true为右/下,false为左/上)。
步长是根据百分比来设置的
var percent = this.GetPercent() + (bIncrease ? 1 : -1) * this.RunStep / 100;
this.SetPos((this.Container.clientWidth - this.Bar.offsetWidth) * percent, (this.Container.clientHeight - this.Bar.offsetHeight) * percent);

然后通过位置属性判断是否到了极限值,不是的话就用一个定时器继续滑动:
if(!(bIncrease ? this._IsMax : this._IsMin)){
       this._timer = setTimeout(Bind(this, this.Run, bIncrease), this.RunTime);
}

【缓动滑移】

除了SetPos还有一个EasePos缓动滑移程序可以设置滑块位置。
如果Ease属性是false时,EasePos跟SetPos一样直接设置位置,为true时就会缓动(减速)设置位置。
其中缓动的效果请参考图片切换展示效果(啊) ,程序中如果目标值超过极限值时不能直接判断是否到达目标值,不过可以用_IsMid属性(参考位置判断部分)来判断没有到极限值。

ps:程序只在鼠标点击控制和设置百分比位置中使用了EasePos,其它情况比较适合用SetPos。

【百分比和值】

这个是基本功能了,先看看GetPercent获取百分比程序,这个百分比就是滑块左边距离跟滑动区域的比例:
return this._horizontal ? this.Bar.offsetLeft / (this.Container.clientWidth - this.Bar.offsetWidth)
              : this.Bar.offsetTop / (this.Container.clientHeight - this.Bar.offsetHeight)
注意滑动区域是容器的clientWidth减去滑块的offsetWidth(关于这两个属性详细请看这里)。
由于返回的是很长的小数,如果要显示的话要处理一下。

对应的有SetPercent设置百分比位置程序,就是根据百分比参数设置滑块的位置:
this.EasePos((this.Container.clientWidth - this.Bar.offsetWidth) * value, (this.Container.clientHeight - this.Bar.offsetHeight) * value);

滑动条更多的应用是在于值的运用。程序中属性MinValue和MaxValue分别设置最小值和最大值。
ps:虽然说是最大值,但不一定就比较大的,不过这样写起来比较方便。
当设置了这两个属性(值)就能GetValue获取当前值了:
return this.MinValue + this.GetPercent() * (this.MaxValue - this.MinValue);

对应的SetValue设置值位置程序:
this.SetPercent((value- this.MinValue)/(this.MaxValue - this.MinValue));
这个很简单,懂点数学应该都明白了。

【位置状态】

程序中有位置程序onMin(最小值时)、onMax(最大值时)和onMid(中间值时)分别在各自位置时执行。
ps:onMid指的是除最小值最大值外的中间部分,不是中心值。
程序是在Move滑动程序中通过百分比来判断当前位置的(0时为最小值,1时为最大值,其他为中间值)。
由于Move程序并不会因为到了极限值就停止,如果仅仅根据百分比来判断那么到了极限值,值虽然不变但程序就会一直被触发。
而我需要的是当值不变的时候,对应位置程序仅仅触发一次。根据需求就衍生出三个位置状态属性_IsMin(最小值状态)、_IsMax(最大值状态)和_IsMid(中间值状态)。
用这几个状态属性和百分比就能实现需要的效果了:
var percent = this.GetPercent();
//最小值判断
if(percent > 0){
       this._IsMin = false;
}else{
       if(!this._IsMin){ this.onMin(); this._IsMin = true; }
}
//最大值判断
if(percent < 1){
       this._IsMax = false;
}else{
       if(!this._IsMax){ this.onMax(); this._IsMax = true; }
}
//中间值判断
if(percent > 0 && percent < 1){
       if(!this._IsMid){ this.onMid(); this._IsMid = true; }
}else{
       this._IsMid = false;
}

这三个位置状态属性在其他程序中也用来判断是否到了极限值。

【鼠标拖动控制】

鼠标拖动控制,就是通过拖动滑块来设置定位。
这个就跟滚动条意思差不多,主要是通过_drag本身的拖放效果来实现的(详细看这里拖放效果)。

【鼠标点击控制】

鼠标点击控制,就是当点击容器的时候能定位到点击的位置。
一般来说只要把ClickCtrl鼠标点击控制程序绑定容器的click事件中就可以了。
但这里有个问题,滑块的点击(拖动控制)跟容器的点击会发生冲突,具体表现是拖放结束后就“顺便”触发了容器的click。
这个本来在滑块的点击事件中取消冒泡就可以:
addEventHandler(this.Bar, "click", BindAsEventListener(this, function(e){ e.stopPropagation(); }));

但ie的click机制有点问题,测试下面的点击:




里面的div取消冒泡,点击它不会触发外面div的onclick,但如果在里面的div点,然拖动到外面的div放,就会触发了,而ff是不会的。
ps:从外面拖到里面也是一样的情况。
经过测试,我觉得是因为ie认为点击的点和放只要是发生在同一个元素的内部(包括内部的其他元素),那个这个点击就是有效的;而ff则认为点击的点和放必须在同一个元素内才有效。
这个导致的问题是,当拖放结束时如果放开鼠标的地方是容器上,那么就会发生冲突了。

那对于ie的这个现象,解决方法其实也很多,我用的方法很简单,设一个属性_ondrag来表示是否拖放中。
具体就是在DragStart开始拖放滑动程序中把_ondrag设为true,并在DragStop结束拖放滑动程序中把它设为false:
setTimeout(Bind(this, function(){ this._ondrag = false; }), 10)
这里用了setTimeout,因为拖放结束后才会触发容器的click,所以设一个延时,使这个值在容器的click触发后才修改。
这样就可以通过这个_ondrag来判断是否应该执行ClickCtrl了:
addEventHandler(this.Container, "click", BindAsEventListener(this, function(e){ this._ondrag || this.ClickCtrl(e);}));

接着看ClickCtrl鼠标点击控制程序,首先获取容器的相对文档的位置:
var o = this.Container, iLeft = o.offsetLeft, iTop = o.offsetTop;
while (o.offsetParent) { o = o.offsetParent; iLeft += o.offsetLeft; iTop += o.offsetTop; }
注意,要逐级向上获取才能取得相对相对文档的位置。
然后通过pageX(pageY)和滑块(这里是要设置到滑块的中间位置所以取一半)得到要设置的位置:
this.EasePos(e.pageX - iLeft - this.Bar.offsetWidth / 2, e.pageY - iTop - this.Bar.offsetHeight / 2);
这里要用pageX(pageY)来取值,而不是clientX(clientY),因为后者是没有计算滚动条的。
ps:ie没有pageX(pageY),不过在Event程序中已经给window.event添加了这个属性:
oEvent.pageX = oEvent.clientX + document.documentElement.scrollLeft;
oEvent.pageY = oEvent.clientY + document.documentElement.scrollTop;

【鼠标滚轮控制】

鼠标滚轮控制,就是通过鼠标滚轮滚动来控制滑块的滑动。
首先ie绑定滚轮事件用的是mousewheel,ff用的是DOMMouseScroll,所以在WheelBind绑定鼠标滚轮程序中是这样设置的:
addEventHandler(o, isIE ? "mousewheel" : "DOMMouseScroll", BindAsEventListener(this, this.WheelCtrl));

接着看WheelCtrl鼠标滚轮控制程序,通过event的detail属性可以获取鼠标滚动的距离(值大小)和方向(正负)。
利用它来设置要滑动的位置:
var i = this.WheelSpeed * e.detail;
this.SetPos(this.Bar.offsetLeft + i, this.Bar.offsetTop + i);
但ie没有detail,对应的有wheelDelta,wheelDelta的数值刚好是detail的40倍,而且方向相反(正负相反),所以Event程序中是这样给window.event添加detail的:
oEvent.detail = oEvent.wheelDelta / (-40);

为了防止触发其他滚动条,这里用了preventDefault取消默认动作。
注意不是用取消冒泡(貌似滚屏是事件的默认动作)。

【方向键控制】

方向键控制,就是通过键盘的左右(上下)方向键来控制滑块的滑动。
首先用KeyBind方向键绑定程序把KeyCtrl方向键控制程序绑定到对象的keydown事件中:
addEventHandler(o, "keydown", BindAsEventListener(this, this.KeyCtrl));

在KeyCtrl中,通过event的keyCode属性获取键盘的按键(左37、上38、右39、下40)并进行相应的操作:
switch (e.keyCode) {
       case 37 ://左
              iLeft -= iWidth; break;
       case 38 ://上
              iTop -= iHeight; break;
       case 39 ://右
              iLeft += iWidth; break;
       case 40 ://下
              iTop += iHeight; break;
       default :
              return;//不是方向按键返回
}

同样为了防止触发其他滚动条,也用了preventDefault取消默认动作。

【focus和tabIndex】

在KeyBind程序中,除了绑定对象的keydown事件,还不够的,可以在ff测试下面的代码:

无论怎样都触发不了onkeydown事件(ie可以触发),那就奇怪了,按照一般的思路应该是可以的啊。
这个可以从w3c关于KeyboardEvent的部分中找到原因:
Keyboard events are commonly directed at the element that has the focus.
大概就是说键盘按键事件一般指向能获取焦点的元素,就是不能获取焦点的元素就不能触发键盘按键事件了。

难道div就不能获取焦点?用下面的代码测试(ff):


还真的不行,那问题就在于怎么使div能获取焦点了(当然这个是转了不少弯才想出来的)。

最后发现给元素设置tabIndex就能让元素能获取焦点了,如果不需要详细了解的话下面可以略过。
首先看看w3c关于onfocus的部分:
The onfocus event occurs when an element receives focus either by the pointing device or by tabbing navigation.
This attribute may be used with the following elements: A, AREA, LABEL, INPUT, SELECT, TEXTAREA, and BUTTON.
当元素通过指定(点击)或tab导航(Tabbing navigation)获得焦点,onfocus事件就会触发。
该属性会使用在以下元素(就是说默认可以获取焦点的元素):A, AREA, LABEL, INPUT, SELECT, TEXTAREA, and BUTTON.
测试下面的代码:
focus
果然两个事件都可以执行。

接着看Tabbing navigation的部分:
Those elements that do not support the tabindex attribute or support it and assign it a value of "0" are navigated next. These elements are navigated in the order they appear in the character stream.
这里看得不太明白,关键的意思是给元素设置tabindex为0就可以被导航到了(能获取焦点了)。
测试下面的代码(ff):

果然两个事件都能触发了。

不过w3c说得很模糊,msdn上倒是很清楚:
An element can have focus if the tabIndex property is set to any valid negative or positive integer.
Elements that receive focus can fire the onblur and onfocus events as of Internet Explorer 4.0, and the onkeydown, onkeypress, and onkeyup events as of Internet Explorer 5.
只要元素的tabIndex属性设置成任何有效的整数那么该元素就能取得焦点。元素在取得焦点后就能触发onblur,onfocus,onkeydown, onkeypress和onkeyup事件。

不同tabIndex值在tab order(Tabbing navigation)中的情况:
Objects with a positive tabIndex are selected in increasing iIndex order and in source order to resolve duplicates.
Objects with an tabIndex of zero are selected in source order.
Objects with a negative tabIndex are omitted from the tabbing order.
tabIndex值是正数的对象根据递增的值顺序和代码中的位置顺序来被选择
tabIndex值是0的对象根据在代码中的位置顺序被选择
tabIndex值是负数的对象会被忽略

这个不知道是否符合标准,但貌似ff跟ie是一样的(不同的地方后面会说)。
那么设置一个负的tabIndex值是最理想的了。

ps:关于ff的tabindex,在Test cases for tabindex bugs in Firefox有更详细更专业的分析,有兴趣的推荐看看。

那ie通过一开始的测试,是不是就说明不需要了呢?我们换一个元素测试:

    换成ul就又不能触发事件了,怎么搞的。

    再看看msdn,里面有一段:
    The following elements can have focus by default but are not tab stops. .略. applet, div, frameSet, span, table, td.
    下面的元素默认能获取焦点但不能tab导航:applet, div, frameSet, span, table, td.
    看来ie真是“为程序员着想”,但其他元素总不能漏了啊,还是全部都设置tabIndex好了。

    终于回到程序上来,首先设置tabIndex:
    o.tabIndex = -1;
    ff元素获得焦点后会出现一个虚线框,去掉会美观一点:
    isIE || (o.style.outline = "none");
    ps:如果tabIndex设为0或以上的话也会出现虚线框。

    绑定了keydown之后,点击一下容器(获取焦点)后就能用方向键控制方向了,但如果(没有获得焦点时)点击滑块,还是触发不了事件。
    因为滑块在拖动效果中ie的鼠标捕获和ff的取消默认动作导致容器不能获得焦点,那手动设置可以吗?
    是可以的,ff中就是直接在滑块的mousedown事件中执行容器的focus方法获得焦点。
    ie本来也是可以的,但ie中当对象执行focus方法时,如果该对象有部分在滚动条外就会自动滚动到适当的位置(还好点击不会这样)。
    为了降低影响,程序中把滑块也绑定了键盘控制,这样点击滑块时只要执行滑块的focus方法获得焦点就可以了:
    var oFocus = isIE ? (this.KeyBind(this.Bar), this.Bar) : this.Container;
    addEventHandler(this.Bar, "mousedown", function(){ oFocus.focus(); });

    ps:由于focus并不会冒泡(w3c标准),所以不用担心滑块的focus导致容器获得焦点。
    ps2:w3c的文档还真是难读,还是msdn的易懂。

    【样式设置】

    程序没有对margin之类的样式进行处理,所以尽量使用“干净”的元素,如果用ul那些,请先“清理”好。

    在仿Apple滑动条产品展示效果中,像那样横排的展示,最好是放在table里才能保证不换行,否则就要放一个很长很长的副容器来放内容。
    里面还有一个样式比较特别的,细心的话可以看到滑块是突出了一个半圆,而且是刚好能嵌在两端的。
    这里主要是滑块两边的两个层(两个半圆)使用了绝对定位,设置了负的位置值(左边是负的left,右边是负的right)。
    还有就是到达两端时对应的层会自动换一个背景图,但其实是同一张图:

    这里是用了变换背景图位置的方法,这个方法也不新鲜了,这里要说说的是,虽然只是换垂直方向的坐标,但backgroundPositionY只是ie的方法,所以还是要用backgroundPosition。

    【应用技巧】

    在仿Apple滑动条产品展示效果中,可以看到MaxValue设成了内容容器的scrollWidth和clientWidth之差:
    MaxValue: $("idContent").scrollWidth - $("idContent").clientWidth,
    其实这个值就是内容容器scrollLeft的最大值,这样在滑动时要设置的内容容器的scrollLeft刚好就是GetValue方法的值了(预览效果2也一样):
    onMove: function(){ $("idContent").scrollLeft = this.GetValue(); }

    预览效果2中,滑块的高度也特别设置过:
    $("idBar2").style.height = $("idSlider2").clientHeight * Math.min($("idContent2").clientHeight / $("idContent2").scrollHeight, 1) - 4 + "px";
    其实就是使内容跟内容容器的高度之比等于滑块跟滑动容器之比,当然这个比不能大于1,否则就滑块高度就超过容器高度了,里面的4是边框宽度。
    这样的好处是滑块会根据实际内容自动设置大小,就像一般的滚动条,内容越多滚动条就越小,反之就越大,这利于用户体验。
    ps:仿Apple那个为了突出效果所以没有设置,实际应用中也应该这样设置一下。

    预览效果3中,从GetValue和GetPercent取得的数有可能是很长的小数,所以显示时必须处理一下。
    这里看到程序中parseInt使用了两个参数,而且第二个参数是10,是不是多余的呢?
    不是的,因为手册上说了:
    如果没有提供,则前缀为 '0x' 的字符串被当作十六进制,前缀为 '0' 的字符串被当作八进制。所有其它字符串都被当作是十进制的。
    而对于手动输入的数字,前面加了个0也是很普通的情况,这样无意间就会变成八进制了。

    使用说明

    首先实例化一个滑动条对象,需要两个参数,分别是滑动容器和滑块(滑块要在容器里面哦):
    var sld = new Slider("idSlider", "idBar")

    有以下这些可选参数和属性:
    属性:默认值//说明
    MinValue:       0,//最小值
    MaxValue:       100,//最大值
    WheelSpeed: 5,//鼠标滚轮速度,越大越快(0则取消鼠标滚轮控制)
    KeySpeed:        50,//方向键滚动速度,越大越慢(0则取消方向键控制)
    Horizontal:       true,//是否水平滑动
    RunTime:       20,//自动滑移的延时时间,越大越慢
    RunStep:       2,//自动滑移每次滑动的百分比
    Ease:              false,//是否缓动
    EaseStep:       5,//缓动等级,越大越慢
    onMin:              function(){},//最小值时执行
    onMax:              function(){},//最大值时执行
    onMid:              function(){},//中间值时执行
    onDragStart:function(){},//拖动开始时执行
    onDragStop:       function(){},//拖动结束时执行
    onMove:              function(){}//滑动时执行

     

    你可能感兴趣的:(JavaScript)