fighting touch events on mobile devices, highjacki

2019-09-18 15:44发布

问题:

I went through the code several times and I cannot find the reason it fails on touch based devices:

    /**
     * Initialize touch event listener.
     *
     * @returns {Plugin}
     */
    touch: function () {
        var self = this;

        this._$body.bind('touchstart', function (event) {
            var startEvent = event.originalEvent.touches[0];

            event.preventDefault();
            self._$body.bind('touchmove', function (event) {
                var moveEvent = event.originalEvent.touches[0];
                var diff = { x: startEvent.clientX - moveEvent.clientX, y: startEvent.clientY - moveEvent.clientY };
                var nextStep;
                event.preventDefault();
                if ((diff.y <= -100 || diff.y >= 100) && Math.abs(diff.y) > Math.abs(diff.x)) {
                    nextStep = diff.y < 0 ? self._currentStep - 1 : self._currentStep + 1;
                    self.customScrollTo(nextStep);
                }
                return false;
            });

            return false;
        });

        return this;
    },

demo (self signed ssl, don't worry!): https://sandbox.idev.ge/roomshotel/html5_v3/

Problem: Scroll jumps straight to bottom when touch is activated.

Expected result: One touch interaction equals 1 section scrolled.

Any thoughts?

回答1:

I too think the touchmove event's callback is being fired on every touch move. By returning false from that function you only cancel that single touch move event and not all following touch move events.

You cannot use a touchend event since you want to call self.customScrollTo(nextStep); as soon as the pointer has travelled 100px.

You want to prevent your touchmove callback from being executed after the pointer has travelled 100px, this can be done in many ways, ie.

  1. Using a flag variable like var trackPointer = true;, check this flag each time touchmove is being triggered and set this flag to false when the pointer has travelled 100px.
  2. When the pointer has travelled 100px, set the startEvent to null and check this variable on touchmove.
  3. Unbind the touchmove event when the pointer has travelled 100px.

NB: The touchmove event is being bound each time touchstart is triggered on this element, these events do not overwrite each other but get stacked! So you might want to consider binding the event only once (ie. on DOM ready) or unbind the event when it's no longer necessary.

The latter is probably the easiest and could be done ie. on touchend (use namespaces just to be sure to not unbind the same events bound by other scripts):

// Unbind all touchmove.myNameSpace events on touchend.myNameSpace.
self._$body.bind('touchend.myNameSpace').function (event) {
  self._$body.unbind('touchmove.myNameSpace');
});

and when the pointer has travelled 100px:

self.customScrollTo(nextStep);
// Unbind all touchmove.myNameSpace events.
self._$body.unbind('touchmove.myNameSpace');

Since 'touchend' is not triggered when the pointer is outside the element (I am not sure about touchmove), you might also want to unbind right before binding:

event.preventDefault();
// Unbind all touchmove.myNameSpace events and (re)bind touchmove.myNameSpace event.
self._$body.unbind('touchmove.myNameSpace').bind('touchmove.myNameSpace', function (event) {
  var moveEvent = event.originalEvent.touches[0];

So you could try (I have not tested it):

/**
 * Initialize touch event listener.
 *
 * @returns {Plugin}
 */
touch: function () {
    var self = this;

    this._$body.bind('touchstart', function (event) {
        var startEvent = event.originalEvent.touches[0];

        event.preventDefault();
        self._$body.unbind('touchmove.myNameSpace').bind('touchmove.myNameSpace', function (event) {
            var moveEvent = event.originalEvent.touches[0];
            var diff = { x: startEvent.clientX - moveEvent.clientX, y: startEvent.clientY - moveEvent.clientY };
            var nextStep;
            event.preventDefault(); // <- Not necessary since you completely cancel the event by returning false.
            if ((diff.y <= -100 || diff.y >= 100) && Math.abs(diff.y) > Math.abs(diff.x)) {
                nextStep = diff.y < 0 ? self._currentStep - 1 : self._currentStep + 1;
                self.customScrollTo(nextStep);

                // Unbind all touchmove.myNameSpace events.
                self._$body.unbind('touchmove.myNameSpace');
            }
            return false;
        });

        return false;
    });

    // Unbind all touchmove.myNameSpace events on touchend.myNameSpace.
    self._$body.bind('touchend.myNameSpace').function (event) {
        self._$body.unbind('touchmove.myNameSpace');
    });

    return this;
},

PS: You might want to use a library like HammerJS (https://github.com/hammerjs/hammer.js) to make gestures work cross browser and also on non-touch devices.



回答2:

First I will suggest you to use Modernizr to detect touch

if (Modernizr.touch) {
    $("body").swipe({
        //Generic swipe handler for all directions
        swipe: function (event, direction, distance, duration, fingerCount, fingerData) {
         if(distance >= 100) //Check the units in px or unit
         {
            if (direction === "up") {
                   self.customScrollTo(nextStep);
            } else if (direction === "down") {
                   self.customScrollTo(prevStep);
            }
         }
       }
    });
}

I am just putting logic above. "self" is your object only.

for swipe event to work you have to include a jquery plugin https://github.com/mattbryson/TouchSwipe-Jquery-Plugin



回答3:

I think the event touchmove is firing multiple times, try to use touchend instead.

Regarding to: http://css-tricks.com/the-javascript-behind-touch-friendly-sliders/ I have tested your code with google chrome live javascript edit. and it worked with the following modification in touchmove event.

setTimeout(function(){ self.customScrollTo(nextStep); }, 250);


回答4:

/**
 * Initialize touch event listener.
 *
 * @returns {Plugin}
 */

touch: function () {
    var self = this;
    var flag = false;
    this._$body.bind('touchstart', function (event) {
        var startEvent = event.originalEvent.touches[0];


        self._$body.bind('touchmove', function (event) {
            var moveEvent = event.originalEvent.touches[0];
            var diff = { x: startEvent.clientX - moveEvent.clientX, y: startEvent.clientY - moveEvent.clientY };
            var nextStep;

            if (((diff.y <= -100 || diff.y >= 100) && Math.abs(diff.y) > Math.abs(diff.x)) && flag == true) {

                nextStep = diff.y < 0 ? self._currentStep - 1 : self._currentStep + 1;
                self.customScrollTo(nextStep);
                startEvent = event.originalEvent.touches[0];  // Just added this line here.
            }
            event.preventDefault();

            return false;
        });

        return false;
    });

    return this;
},

I guess there is a simple edit required. After you have exceeded the "diff" by 100 or -100 then just update the value of "startEvent" variable. Hence I have added the statement

  startEvent = event.originalEvent.touches[0];

So when the event for "self.customScrollTo(nextStep)" is triggered, the "startEvent" value is new value which is the current position of the touched point. And hence when again the touchmove event is called. It will again get a new set of values to calculate. Please check this code and tell me if this works or not. Even if it is not right.