kornelski / slip

Slip.js — UI library for manipulating lists via swipe and drag gestures
BSD 2-Clause "Simplified" License
2.44k stars 213 forks source link

Scrolling is unfriendly #82

Open Snoturky opened 7 years ago

Snoturky commented 7 years ago

Scrolling seems to work by looking at how close the cursor is to the viewable part of a scrollable parent on each mousemove event. The closer it is to the edge, the more it scrolls for that single fire of the event. It is intuitive that the scroll speed is based upon proximity to the edge, but it is not intuitive that scrolling happens only after each pixel-by-pixel move of the cursor.

That means that holding an element steady at the edge of a scrollable parent does not scroll at all, but dragging the element back into the center of the scrollable parent (which usually indicates that you have found what you were looking for and want to stop scrolling) will cause you to scroll away.

Is there a way to make the scrolling smooth, so it will continuously scroll at some constant speed depending upon where the element is held, instead of only scrolling as the mouse continues to move?

This is especially troublesome when you pick up an element near the edge, because just the act of picking it up and moving it toward the middle of the scrollable area will cause it to scroll.

kornelski commented 7 years ago

I agree, it is problematic. It probably should keep updating continuously. Updating on mousemove makes scrolling dependent also on the rate browser sends updates, so it can't be relied upon.

Currently there's no solution for this proposed.

MarkHill89 commented 7 years ago

Not sure if this is anything related or not, but when using Slip for a Cordova application that I am putting together, the android build has had some terrible interactions. In can only move down the screen and not up, when recording list items. The selecting of the list item is very picky. If you scroll down have more list items than what is on the screen, it goes straight to the bottom of the list, and not smoothly in any fashion. These are just a few of the problems.

Sauvage9 commented 7 years ago

Here's a possible solution. It works by dividing the triggerzone into 10 parts and use the offset within the zone as a multiplier for scroll animation duration. Currently animation is done using requestAnimationFrame but can be polyfilled using setTimeout. It still needs some fine-tuning but it will perform as it should.

Add following to prototype: scrollEvt:{}, // object holding all required scroll params and scroll state

Replace following in prototype:

updateScrolling: function() {
    if(!this.target || this.scrollEvt.scrolling){return};

    var triggerOffset = 100,
        duration=100,
        offset = 0,
        that=this;

    var scrollable = this.target.scrollContainer,
        containerRect = scrollable.getBoundingClientRect(),
        targetRect = this.target.node.getBoundingClientRect(),
        bottomOffset = Math.min(containerRect.bottom, window.innerHeight) - targetRect.bottom,
        topOffset = targetRect.top - Math.max(containerRect.top, 0),
        maxScrollTop = this.target.origScrollHeight - Math.min(scrollable.clientHeight, window.innerHeight);

    if (bottomOffset < triggerOffset) {
      offset = Math.min(triggerOffset, triggerOffset - bottomOffset);
    }
    else if (topOffset < triggerOffset) {
      offset = Math.max(-triggerOffset, topOffset - triggerOffset);
    }

    if(offset){
        this.scrollEvt.scrolling=true;
        var speed=Math.abs(Math.ceil((offset*10)/triggerOffset)),speedarray=[5,5,5,4,4,3,3,2,2,1,1];
        this.animateScrolling(scrollable,this.target.node.offsetHeight,scrollable.scrollTop,duration*speedarray[speed],offset<0?-1:1);
    }
},

animateScrolling:function(scrollable,height,ypos,duration,direction,starttime,timestamp){
    var that=this,progress=0;
    if(timestamp){
        var starttime=starttime||timestamp;progress=(timestamp-starttime)/duration;if(progress>1){progress=1};scrollable.scrollTop=(ypos+height*progress*direction);
        this.state.onMove.call(this);
    }
    if(progress==1){this.cancelAnimateScrolling();this.updateScrolling();return};
    this.scrollEvt.raf=requestAnimationFrame(function(timestamp){that.animateScrolling(scrollable,height,ypos,duration,direction,starttime,timestamp)});

},

cancelAnimateScrolling:function(){
    this.scrollEvt.scrolling=false;
    cancelAnimationFrame(this.scrollEvt.raf);
},

updatePosition: function(e, pos) {
    if (this.target == null) {
        return;
    }
    this.latestPosition = pos;

    if (this.state.onMove && !this.scrollEvt.scrolling) {
        if (this.state.onMove.call(this) === false) {
            e&&e.preventDefault();
        }
    }

    //sample latestPosition 100ms for velocity
    if (this.latestPosition.time - this.previousPosition.time > 100) {
        this.previousPosition = this.latestPosition;
    }
},

onMouseUp: function(e) {
    if (this.usingTouch || e.button !== 0) return;
    if (this.state.onEnd && false === this.state.onEnd.call(this)) {
        e.preventDefault();
    }
    this.cancelAnimateScrolling();
},

onTouchEnd: function(e) {
    if (e.touches.length > 1) {
        this.cancel();
    } else if (this.state.onEnd && false === this.state.onEnd.call(this)) {
        e.preventDefault();
    }
    this.cancelAnimateScrolling();
},

When scrolling, scrollEvt.scrolling=true. This is useful to block other events and avoid duplicate calls. animateScrolling will call onmove since scrolling with a steady mouse will not, resulting in jagged movement.

TODO:

Sauvage9 commented 7 years ago

Update:

updateScrolling: function() {
    if(!this.target || this.scrollEvt.scrolling){return};

    var triggerOffset = 100,
        duration=100,
        offset = 0,
        that=this;

    var scrollable = this.target.scrollContainer,
        containerRect = scrollable.getBoundingClientRect(),
        targetRect = this.target.node.getBoundingClientRect(),
        bottomOffset = Math.min(containerRect.bottom, window.innerHeight) - targetRect.bottom,
        topOffset = targetRect.top - Math.max(containerRect.top, 0),
        maxScrollTop = this.target.origScrollHeight - Math.min(scrollable.clientHeight, window.innerHeight);

    if (bottomOffset < triggerOffset) {
      offset = Math.min(triggerOffset, triggerOffset - bottomOffset);
    }
    else if (topOffset < triggerOffset) {
      offset = Math.max(-triggerOffset, topOffset - triggerOffset);
    }

    if(!offset){return}
    if(offset<0 && this.getTotalMovement().y>=-(triggerOffset/10)){return}
    if(offset>0 && this.getTotalMovement().y<=(triggerOffset/10)){return}

    this.scrollEvt.scrolling=true;
    var speed=Math.abs(Math.ceil((offset*4)/triggerOffset));
    this.animateScrolling(scrollable,this.target.node.offsetHeight,scrollable.scrollTop,duration*[4,4,3,2,1][speed],offset<0?-1:1)
},

TODO:

Sauvage9 commented 7 years ago

Firefox autoscroll fix:

Add following after damnYouChrome declaration: var damnYouFirefox = /Firefox/.test(navigator.userAgent);

Add following after scrollContainer = scrollContainer || document.body;: if(damnYouFirefox && scrollContainer==document.body) {scrollContainer=document.documentElement};

Webifi commented 7 years ago

@Snoturky

Could you do a proper pull request for your changes?

carter-thaxton commented 7 years ago

Is it even necessary to check for Firefox, or can we just always use documentElement?