Search code examples
javascriptjqueryiosweb-applicationswebkit

Controlling vertical scrolling and horizontal swiping on a list in an iOS web app with JavaScript


I'm creating a web application specifically targeted for phones (primarily iPhone, but Android & WP are on the horizon...).

One of the screens contains a scrolling list of items. I would like the list to behave similarly to the built-in iOS Mail app.

In other words...

  1. If the user touches the list and moves up or down, the list scrolls vertically.
  2. If the user flicks up or down, the list scrolls vertically with natural momentum
  3. If the user touches the list and moves ONLY left - the particular item slides to the left revealing a delete button.
  4. IMPORTANTLY - the list should scroll OR the item should slide, BUT NEVER BOTH.

So - it's important to figure out what the user's intention is, which means I probably need to prevent ANY response until I figure out whether the user is moving her finger vertically or horizontally.

By simply setting these CSS styles on the list container...

overflow-y: auto;
-webkit-overflow-scrolling: touch;

... I get #1 & #2 above. So, I need to figure out how implement #3.

My first thought was to implement something like this (pseudocode)...

  1. Create a touchstart event listener on the list container. In the callback, store the x- and y-coordinates of the user's starting touch position.
  2. Create a touchmove event listener on the list container. In the callback, figure out how far the user's finger has moved (e.g., delta_x and delta_y)
  3. If delta_x AND delta_y are both less than 10 pixels - don't do anything (don't scroll the list or slide the item) - since we haven't yet figured out whether the user plans to move up/down or left/right.
  4. If EITHER delta_x OR delta_y are more than 10 pixels - we can assume the user has moved far enough to express her intention. If delta_y > delta_x, assume she's moving up/down, and allow the list to scroll, but don't slide the item. If delta_x > delta_y, assume she's moving left/right, so we should allow the item to slide, but not allow the list to scroll.

I expected that I would use event.preventDefault() in either the touchstart or touchmove to control when scrolling should begin. E.g.,

div.addEventListener("touchstart", function(e) {
    touchStart = {
        x: e.touches[0].pageX,
        y: e.touches[0].pageY
    }
}, false);
div.addEventListener("touchmove", function(e) {
    touchNow = {
        x: e.touches[0].pageX,
        y: e.touches[0].pageY
    }
    var
        dx = touchStart.x - touchNow.x,
        dy = touchStart.y - touchNow.y;
    if ((Math.abs(dx) < 10) && (Math.abs(dy) < 10)) {
        // prevent scrolling
        e.preventDefault();
    } else if (Math.abs(dx) > Math.abs(dy) < 10) {
        // moving right/left - slide item
    } else {
        // moving up/down - allow scrolling
    }
}, false);

However - this doesn't work. Regardless of how far you move, the list NEVER scrolls.

Obviously - I'm misunderstanding what triggers the scrolling, and what event.preventDefault() is supposed to do in this context.

So - is there a way to accomplish what I'm after?

I'm hoping for a pure JavaScript solution (so I understand it better), but a jQuery approach would be fine to. I'm definitely hoping to avoid a jQuery plugin/library/framework if at all possible...

Thanks in advance!


Solution

  • I don't suggest reinventing the wheel. There are a number of libraries out there that supports gesture detection. Out of which, I suggest using Hammer.js to detect the touch events.

    It doesn't have any dependencies, and it's small, only 3.96 kB minified + gzipped!

    And it is all about handling touch events, nothing else.

    In your case, Hammer has inbuilt swipe detection.

    You can customize the default swipe gesture by specifying :

    • direction: direction in which you want to detect the swipe gesture (more info)
    • threshold: Minimal distance required before recognizing
    • velocity : Minimal velocity required before recognizing (unit is in px per ms)

      and more.

    Following is a simple example (Stack Snippet seems to have some issue emulating touch events, fiddle works fine):

    var myElement = document.getElementById("container");
    var hammertime = new Hammer(myElement, {});
    hammertime.get('swipe').set({
      direction: 2 // 2 stands for left
    })
    hammertime.on('swipe', function(event) {
      event.target.querySelector(".delete").classList.add("show");
    });
    * {
      margin: 0;
      padding: 0;
    }
    #container {
      width: 250px;
      height: 300px;
      overflow-y: auto;
      -webkit-overflow-scrolling: touch;
      background: dodgerblue;
    }
    #list {
      height: 100%;
      list-style: none;
    }
    #list li {
      position: relative;
      box-sizing: border-box;
      width: 100%;
      height: 50px;
      border: 2px solid #fff;
    }
    #list li span.delete {
      display: inline-block;
      position: absolute;
      left: 250px;
      width: 50px;
      height: 40px;
      line-height: 40px;
      margin: 3px;
      text-align: center;
      background: #fff;
      transition: left 0.5s ease-in;
    }
    #list li span.delete.show {
      left: 170px;
    }
    <script src="http://cdn.jsdelivr.net/hammerjs/2.0.4/hammer.min.js"></script>
    <div id="container">
      <ul id="list">
        <li class="item"><span class="delete">&#215;</span>
        </li>
        <li class="item"><span class="delete">&#215;</span>
        </li>
        <li class="item"><span class="delete">&#215;</span>
        </li>
        <li class="item"><span class="delete">&#215;</span>
        </li>
        <li class="item"><span class="delete">&#215;</span>
        </li>
        <li class="item"><span class="delete">&#215;</span>
        </li>
        <li class="item"><span class="delete">&#215;</span>
        </li>
        <li class="item"><span class="delete">&#215;</span>
        </li>
        <li class="item"><span class="delete">&#215;</span>
        </li>
        <li class="item"><span class="delete">&#215;</span>
        </li>
      </ul>
    </div>


    If you are more interested in learning how the touch event works rather than getting the job done , then I suggest looking under the hood of Hammer.


    There is a little Hammer.js jQuery plugin as well, for those who can't part with jQuery.