2

我正在尝试调试移动浏览器上的一些 javascript 滑块代码的问题。它似乎是仅在移动设备上发生的舍入误差。以下代码在桌面(例如 Chrome)上可以正常工作,但在智能手机(例如 iPhone iOS 5/6、Samsung S2 ICS)上的 Webkit 中查看时,增加按钮无法在滑块上的较高值上工作。

试试这个http://jsfiddle.net/codecowboy/mLpfu/。单击“在移动设备上调试”按钮 - 它与左上方的“运行”按钮直接相邻(您需要登录才能看到此按钮)。将生成的 url 输入智能手机上的浏览器(最好是 iPhone / Android 上的 webkit 浏览器)。

拖动滑块说 265,点击增加。有些值会让你命中增加,有些则不会。值越高,问题越严重。

该代码使用 jQuery 和 noUISlider 插件。按钮点击代码为:

var btnIncrease= document.getElementById("increase");
        btnIncrease.addEventListener('click',function(e) {

            var slider = $("#noUiSlider");
            console.log(e);
            var value = slider.noUiSlider('value')[1]; //the 'value' method returns an array.
            console.log('value pre move '+value);
            value = value+1;
            slider.noUiSlider("move", { knob : 0, to:  parseInt(value,10) });

            console.log(slider.noUiSlider('value')[1]);


        });

谁能解释这是什么原因造成的?这可能是一个大/小端问题吗?还是 jQuery 中的错误?

上面的代码调用了 nouislider 插件,源码在这里:

(function( $ ){

$.fn.noUiSlider = function( method, options ) {

    function neg(a){ return a<0; }
    function abs(a){ return Math.abs(a); }
    function roundTo(a,b) { return Math.round(a / b) * b; }
    function dup(a){ return jQuery.extend(true, {}, a); }

    var defaults, methods, helpers, options = options||[], functions, touch = ('ontouchstart' in document.documentElement);

    defaults = {

    /*
     * {handles}            Specifies the number of handles. (init)
     * [INT]                1, 2
     */
        'handles'   :       2,
    /*
     * {connect}            Whether to connect the middle bar to the handles. (init)
     * [MIXED]              "upper", "lower", false, true
     */
        'connect'   :       true,
    /*
     * {scale};             The values represented by the slider handles. (init,move,value)
     * [ARRAY]              [-+x,>x]
     */
        'scale'     :       [0,100],
    /*
     * {start}              The starting positions for the handles, mapped to {scale}. (init)
     * [ARRAY][INT]         [y>={scale[0]}, y=<{scale[1]}], integer in range.
     */
        'start'     :       [25,75],
    /*
     * {to}                 The position to move a handle to. (move)
     * [INT]                Any, but will be corrected to match z > {scale[0]} || _l, z < {scale[1]} || _u
     */
        'to'        :       0,
    /*
     * {handle}             The handle to move. (move)
     * [MIXED]              0,1,"lower","upper"
     */
        'handle'    :       0,
    /*
     * {change}             The function to be called on every change. (init)
     * [FUNCTION]           param [STRING]'move type'
     */
        'change'    :       '',
    /*
     * {end}                The function when a handle is no longer being changed. (init)
     * [FUNCTION]           param [STRING]'move type'
     */
        'end'       :       '',
    /*
     * {step}               Whether, and at what intervals, the slider should snap to a new position. Adheres to {scale} (init)
     * [MIXED]              <x, FALSE
     */
        'step'      :       false,
    /*
     * {save}               Whether a scale give to a function should become the default for the slider it is called on. (move,value)
     * [BOOLEAN]            true, false
     */
        'save'      :       false,
    /*
     * {click}              Whether the slider moves by clicking the bar
     * [BOOLEAN]            true, false
     */
        'click'     :       true

    };

    helpers = {

        scale:              function( a, b, c ){    
            var d = b[0],e = b[1];
            if(neg(d)){
                a=a+abs(d);
                e=e+abs(d);
            } else {
                a=a-d;
                e=e-d;
            }
            return (a*c)/e;
        },
        deScale:            function( a, b, c ){
            var d = b[0],e = b[1];
            e = neg(d) ? e + abs(d) : e - d;
            return ((a*e)/c) + d;                   
        },
        connect:            function( api ){

            if(api.connect){

                if(api.handles.length>1){
                    api.connect.css({'left':api.low.left(),'right':(api.slider.innerWidth()-api.up.left())});
                } else {
                    api.low ? api.connect.css({'left':api.low.left(),'right':0}) : api.connect.css({'left':0,'right':(api.slider.innerWidth()-api.up.left())});
                }

            }

        },
        left:               function(){
            return parseFloat($(this).css('left'));
        },
        call:               function( f, t, n ){
            if ( typeof(f) == "function" ){ f.call(t, n) }
        },
        bounce:             function( api, n, c, handle ){

            var go = false;

            if( handle.is( api.up ) ){

                if( api.low && n < api.low.left() ){

                    n = api.low.left();
                    go=true;

                }

            } else {

                if( api.up && n > api.up.left() ){

                    n = api.up.left();
                    go=true;

                }

            }

            if ( n > api.slider.innerWidth() ){

                n = api.slider.innerWidth()

                go=true;

            } else if( n < 0 ){

                n = 0;
                go=true;

            }

            return [n,go];

        }

    };

    methods = {

        init:               function(){

            return this.each( function(){

                /* variables */

                var s, slider, api;

                /* fill them */

                slider      = $(this).css('position','relative');
                api         = new Object();

                api.options = $.extend( defaults, options );
                s           = api.options;

                typeof s.start == 'object' ? 1 : s.start=[s.start];

                /* Available elements */

                api.slider  = slider;
                api.low     = $('<div class="noUi-handle noUi-lowerHandle"><div></div></div>');
                api.up      = $('<div class="noUi-handle noUi-upperHandle"><div></div></div>');
                api.connect = $('<div class="noUi-midBar"></div>');

                /* Append the middle bar */

                s.connect ? api.connect.appendTo(api.slider) : api.connect = false;

                /* Append the handles */

                // legacy rename
                if(s.knobs){
                    s.handles=s.knobs;
                }

                if ( s.handles === 1 ){

                    /*
                        This always looks weird:
                        Connect=lower, means activate upper, because the bar connects to 0.
                    */

                    if ( s.connect === true || s.connect === 'lower' ){

                        api.low     = false;
                        api.up      = api.up.appendTo(api.slider);
                        api.handles = [api.up];

                    } else if ( s.connect === 'upper' || !s.connect ) {

                        api.low     = api.low.prependTo(api.slider);
                        api.up      = false;
                        api.handles = [api.low];

                    }

                } else {

                    api.low     = api.low.prependTo(api.slider);
                    api.up      = api.up.appendTo(api.slider);
                    api.handles = [api.low, api.up];

                }

                if(api.low){ api.low.left = helpers.left; }
                if(api.up){ api.up.left = helpers.left; }

                api.slider.children().css('position','absolute');

                $.each( api.handles, function( index ){

                    $(this).css({
                        'left' : helpers.scale(s.start[index],api.options.scale,api.slider.innerWidth()),
                        'zIndex' : index + 1
                    }).children().bind(touch?'touchstart.noUi':'mousedown.noUi',functions.start);

                });

                if(s.click){
                    api.slider.click(functions.click).find('*:not(.noUi-midBar)').click(functions.flse);
                }

                helpers.connect(api);

                /* expose */
                api.options=s;
                api.slider.data('api',api);

            });

        },
        move:               function(){

            var api, bounce, to, handle, scale;

            api = dup($(this).data('api'));
            api.options = $.extend( api.options, options );

            // rename legacy 'knob'
            if(api.options.knob){
                api.options.handle = api.options.knob;
            }

            // flatten out the legacy 'lower/upper' options
            handle  = api.options.handle;
            handle  = api.handles[handle == 'lower' || handle == 0 || typeof handle == 'undefined' ? 0 : 1];
            bounce  = helpers.bounce(api, helpers.scale(api.options.to, api.options.scale, api.slider.innerWidth()), handle.left(), handle);

            handle.css('left',bounce[0]);

            if( (handle.is(api.up) && handle.left() == 0) || (handle.is(api.low) && handle.left() == api.slider.innerWidth()) ){
                handle.css('zIndex',parseInt(handle.css('zIndex'))+2);
            }

            if(options.save===true){
                api.options.scale = options.scale;
                $(this).data('api',api);
            }

            helpers.connect(api);
            helpers.call(api.options.change, api.slider, 'move');
            helpers.call(api.options.end, api.slider, 'move');

        },
        value:              function(){

            var val1, val2, api;

            api = dup($(this).data('api'));
            api.options = $.extend( api.options, options );

            val1    = api.low ? Math.round(helpers.deScale(api.low.left(), api.options.scale, api.slider.innerWidth()))  : false;
            val2    = api.up ? Math.round(helpers.deScale(api.up.left(), api.options.scale, api.slider.innerWidth()))  : false;

            if(options.save){
                api.options.scale = options.scale;
                $(this).data('api',api);
            }

            return [val1,val2];

        },
        api:                function(){
            return $(this).data('api');
        },
        disable:            function(){
            return this.each( function(){
                $(this).addClass('disabled');
            });
        },
        enable:             function(){
            return this.each( function(){
                $(this).removeClass('disabled');
            });
        }

    },

    functions = {

        start:              function( e ){

            if(! $(this).parent().parent().hasClass('disabled') ){

                e.preventDefault();
                $('body').bind( 'selectstart.noUi' , functions.flse);
                $(this).addClass('noUi-activeHandle');

                $(document).bind(touch?'touchmove.noUi':'mousemove.noUi', functions.move);

                touch?$(this).bind('touchend.noUi',functions.end):$(document).bind('mouseup.noUi', functions.end);

            }

        },
        move:               function( e ){

            var a,b,h,api,go = false,handle,bounce;

            h       = $('.noUi-activeHandle');
            api     = h.parent().parent().data('api');
            handle  = h.parent().is(api.low) ? api.low : api.up;
            a       = e.pageX - Math.round( api.slider.offset().left );

            // if there is no pageX on the event, it is probably touch, so get it there.
            if(isNaN(a)){
                a = e.originalEvent.touches[0].pageX - Math.round( api.slider.offset().left );
            }

            // a = p.nw  == New position 
            // b = p.cur == Old position

            b       = handle.left();
            bounce  = helpers.bounce(api, a, b, handle);
            a       = bounce[0];
            go      = bounce[1];

            if ( api.options.step && !go){

                // get values from options
                var v1 = api.options.scale[0], v2 = api.options.scale[1];

                // convert values to [0-X>0] range
                // edge case: both values negative;
                if( neg(v2) ){ 
                    v2 = abs( v1 - v2 );
                    v1 = 0;
                }
                    // handle all values
                    v2 = ( v2 + ( -1 * v1 ) );

                // converts step to the new range
                var con = helpers.scale( api.options.step, [0,v2], api.slider.innerWidth() );

                // if the current movement is bigger than step, set to step.
                if ( Math.abs( b - a ) >= con ){
                    a = a < b ? b-con : b+con;
                    go = true;
                }

            } else {
                go = true;
            }

            if(a===b){
                go=false;
            }

            if(go){

                handle.css('left',a);
                if( (handle.is(api.up) && handle.left() == 0) || (handle.is(api.low) && handle.left() == api.slider.innerWidth()) ){
                    handle.css('zIndex',parseInt(handle.css('zIndex'))+2);
                }
                helpers.connect(api);
                helpers.call(api.options.change, api.slider, 'slide');

            }

        },
        end:                function(){

            var handle, api;

            handle  = $('.noUi-activeHandle');
            api     = handle.parent().parent().data('api');

            $(document).add('body').add(handle.removeClass('noUi-activeHandle').parent()).unbind('.noUi');

            helpers.call(api.options.end, api.slider, 'slide');

        },
        click:              function( e ){

            if(! $(this).hasClass('disabled') ){

                var api = $(this).data('api');
                var s   = api.options;
                var c   = e.pageX - api.slider.offset().left;

                c = s.step ? roundTo(c,helpers.scale( s.step, s.scale, api.slider.innerWidth() )) : c;

                if( api.low && api.up ){
                    c < ((api.low.left()+api.up.left())/2) ? api.low.css("left", c) : api.up.css("left", c);
                } else {
                    api.handles[0].css('left',c);
                }

                helpers.connect(api);
                helpers.call(s.change, api.slider, 'click');
                helpers.call(s.end, api.slider, 'click');

            }

        },
        flse:               function(){
            return false;
        }

    }

    if ( methods[method] ) {
        return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
    } else if ( typeof method === 'object' || ! method ) {
        return methods.init.apply( this, arguments );
    } else {
        $.error( 'No such method: ' +  method );
    }

};

})( jQuery );
4

2 回答 2

1

可能的罪魁祸首(离开该源代码)在move: function( e )

a = e.pageX - Math.round( api.slider.offset().left );
// if there is no pageX on the event, it is probably touch, so get it there.
if(isNaN(a)){
  a = e.originalEvent.touches[0].pageX - Math.round( api.slider.offset().left );
}
// a = p.nw  == New position

假设所Math涉及的库同样准确,并且 api.slider.offset().left 在平台之间没有变化,我会从源代码中进行一些日志记录以确定e.pageX/的用途和值e.originalEvent.touches[0].pageX

另一种可能性是滑块的左偏移量,它刚刚从 DOM ( $('.noUi-activeHandle').parent().parent().data('api').slider.offset().left) 中拉出,在不同平台之间的精度会有所不同。也记录一下,我猜。

不过,两者都没有表明不准确性呈指数增长,因此可能还有其他问题。

于 2013-01-16T22:06:13.367 回答
-4

javascript 确实存在舍入问题。所有数字都是内部浮点数,因此是由于浮点舍入问题。

尝试使用http://silentmatt.com/biginteger/看看它是否能解决您的问题。

请查看http://blog.greweb.fr/2013/01/be-careful-with-js-numbers/了解更多详细信息。

于 2013-01-14T23:18:30.973 回答