27

There are many "pull to refresh" plugins. I have already tested 5 of them. But none of them running fast (especially on old smartphones).

What is the best (buttery UX performance and responsiveness) way to check for pull to refresh?

PS: I don't need any animation. I just want to recognize if a user "pull to refresh"

Mr. Pyramid
  • 3,855
  • 5
  • 32
  • 56
Peter
  • 11,413
  • 31
  • 100
  • 152

6 Answers6

41

Performance requires minimal code

Plugins and libraries have to be written to be as flexible and general as possible, in order to solve many related problems. This means they'll always be bulkier than you need, impacting performance. It also means you'll never have to maintain that code. That's the trade off.

If performance is your goal, build it yourself.

Since ALL you need is a pull-down detection, build a simple swipe detector. Of course, you'll have to adapt this to your needs, and the event-properties, event-triggers of the OS and browser you're targeting.

Simplified from my old js-minimal-swipe-detect

var pStart = { x: 0, y: 0 };
var pStop = { x: 0, y: 0 };

function swipeStart(e) {
  if (typeof e["targetTouches"] !== "undefined") {
    var touch = e.targetTouches[0];
    pStart.x = touch.screenX;
    pStart.y = touch.screenY;
  } else {
    pStart.x = e.screenX;
    pStart.y = e.screenY;
  }
}

function swipeEnd(e) {
  if (typeof e["changedTouches"] !== "undefined") {
    var touch = e.changedTouches[0];
    pStop.x = touch.screenX;
    pStop.y = touch.screenY;
  } else {
    pStop.x = e.screenX;
    pStop.y = e.screenY;
  }

  swipeCheck();
}

function swipeCheck() {
  var changeY = pStart.y - pStop.y;
  var changeX = pStart.x - pStop.x;
  if (isPullDown(changeY, changeX)) {
    alert("Swipe Down!");
  }
}

function isPullDown(dY, dX) {
  // methods of checking slope, length, direction of line created by swipe action
  return (
    dY < 0 &&
    ((Math.abs(dX) <= 100 && Math.abs(dY) >= 300) ||
      (Math.abs(dX) / Math.abs(dY) <= 0.3 && dY >= 60))
  );
}

document.addEventListener(
  "touchstart",
  function (e) {
    swipeStart(e);
  },
  false
);
document.addEventListener(
  "touchend",
  function (e) {
    swipeEnd(e);
  },
  false
);
Dorian
  • 7,749
  • 4
  • 38
  • 57
Tony Chiboucas
  • 5,505
  • 1
  • 29
  • 37
  • i think this is exactly what im looking for. But i get an Error "abs is not defined" :/ – Peter Sep 16 '17 at 02:44
  • 3
    @Peter change `abs()` to `Math.abs()` – K Scandrett Sep 17 '17 at 04:23
  • @KScandrett, Thanks for that. Poor oversight on my part. – Tony Chiboucas Sep 18 '17 at 15:17
  • @Peter: add `var abs = Math.abs;` – deblocker Sep 21 '17 at 16:59
  • How can we do this for desktop too? – Oliver Dixon Oct 28 '20 at 07:22
  • @OliverDixon add the relevant event-listeners at the end: `mousedown` and `mouseup` – Tony Chiboucas Apr 02 '21 at 05:11
  • Good but it requires to pull too long, almost all my screen, how to reduce the length of 50%? i tried to modify isPullDown() but I don't undersand abs(). Which values to put to decrease? @TonyChiboucas – Coudrak Sep 16 '22 at 06:52
  • @Coudrak, `Math.abs()` is absolute value (remove negatives). `dX` is the distance moved in the x axis, `dY` is the distance moved in the y axis. `dX/dY` gives you the slope. Existing formula essentially says, "IF Y is down more than 300px and X left/right is less than 100, OR the slope is steeper than 1/3 and Y is down more than 60px" Maybe get some help from someone who knows Geometry and Trig? – Tony Chiboucas Oct 10 '22 at 17:58
1

have you tired these solutions??

You need to check this fiddle

var mouseY = 0
var startMouseY = 0

$("body").on("mousedown", function (ev) {
  mouseY = ev.pageY
  startMouseY = mouseY

  $(document).mousemove(function (e) {
    if (e.pageY > mouseY) {
      var d = e.pageY - startMouseY
      console.log("d: " + d)
      if (d >= 200) location.reload()
      $("body").css("margin-top", d / 4 + "px")
    } else $(document).unbind("mousemove")
  })
})

$("body").on("mouseup", function () {
  $("body").css("margin-top", "0px")
  $(document).unbind("mousemove")
})

$("body").on("mouseleave", function () {
  $("body").css("margin-top", "0px")
  $(document).unbind("mousemove")
})

and if you are looking for some plugin this plugin might help you

Dorian
  • 7,749
  • 4
  • 38
  • 57
Mr. Pyramid
  • 3,855
  • 5
  • 32
  • 56
  • 1
    @mr-pyramid, most mobile devices do not record any `mouse**` events for touch. This would work fine for desktops and laptops, with or without touch, but will not work for Android, iOS, or WinPhone devices. – Tony Chiboucas Sep 19 '17 at 15:46
  • does your answer solves the issues for Android and mobile devices? – Mr. Pyramid Sep 22 '17 at 07:05
  • 2
    It _should_ work on most mobile devices, but the original (more bulky) code from which I created this solution, has only been tested on 2 browers in android, 5 on PC, and 2 on Mac. Your solution _will not work with ANY touch actions_ which was the problem OP was asking to solve. – Tony Chiboucas Sep 22 '17 at 14:48
1

What about this?

var lastScrollPosition = 0;
window.onscroll = function(event)
{
  if((document.body.scrollTop >= 0) && (lastScrollPosition < 0))
  {
    alert("refresh");
  }
  lastScrollPosition = document.body.scrollTop;
}

If your browser doesn't scroll negative, then you could replace line 4 with something like this: if((document.body.scrollTop == 0) && (lastScrollPosition > 0))

Alternatively for android devices, you could swap out lastScrollPosition for "ontouchmove" or other gesture events.

TheMindVirus
  • 111
  • 2
  • Hi, seemed to me that your comment is part of your answer. Try to put them together so that people would better understand it. – Fei Jun 08 '19 at 00:37
1

I know this answer has been answered by a lot many people but it may help someone.

It's based on Mr. Pryamid's answer. but his answer does not work touch screen. that only works with mouse.

this answer works well on touch screen and desktop. i have tested in chrome in desktop and chrome in android

<!DOCTYPE html>
<html>
  <head>
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      html,
      body {
        width: 100%;
        height: 100%;
      }

      .pull-to-refresh-container {
        width: 100%;
        height: 100%;
        background-color: yellow;
        position: relative;
      }

      .pull-to-refresh-content-container {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: white;
        margin: 0px;
        text-align: center;
      }
    </style>
  </head>
  <body>
    <div class="pull-to-refresh-container">
      <div class="pull-to-refresh-loader-container">
        loading ...
      </div>
      <div class="pull-to-refresh-content-container">
        here lies the content
      </div>
    </div>

    <script>
      var mouseY = 0
      var startMouseY = 0
      var container = document.querySelector(
        ".pull-to-refresh-content-container"
      )

      container.onmousedown = (ev) => {
        mouseY = ev.pageY
        startMouseY = mouseY

        container.onmousemove = (e) => {
          if (e.pageY > mouseY) {
            var d = e.pageY - startMouseY

            console.log("d: " + d)
            container.style.marginTop = d / 4 + "px"
            if (d >= 300) {
              // alert("load more content");
            }
          } else {
            container.onmousemove = null
          }
        }
      }

      container.onmouseup = (ev) => {
        container.style.marginTop = "0px"
        container.onmousemove = null
      }

      container.onmouseleave = (ev) => {
        container.style.marginTop = "0px"
        container.onmousemove = null
      }
      container.ontouchstart = (ev) => {
        mouseY = ev.touches[0].pageY
        startMouseY = mouseY

        container.ontouchmove = (e) => {
          if (e.touches[0].pageY > mouseY) {
            var d = e.touches[0].pageY - startMouseY

            console.log("d: " + d)
            container.style.marginTop = d / 4 + "px"
            if (d >= 300) {
              // alert("load more content");
            }
          } else {
            container.onmousemove = null
          }
        }
      }

      container.ontouchcancel = (ev) => {
        container.style.marginTop = "0px"
        container.onmousemove = null
      }

      container.ontouchend = (ev) => {
        container.style.marginTop = "0px"
        container.onmousemove = null
      }
    </script>
  </body>
</html>
Dorian
  • 7,749
  • 4
  • 38
  • 57
Harkal
  • 1,770
  • 12
  • 28
  • 1
    This could be much cleaner javascript... and it's not a great idea to chain event listeners like that (onmousedown, add a listener to onmousemove). It will definately work, and I'll give you an upvote if you can clean up all of the code duplication:` `document.querySele...` – Tony Chiboucas Apr 02 '21 at 05:16
  • 1
    @TonyChiboucas thanx for the suggestion mate. i didnt write the code for using in production but just for fun so i posted it without optimization and stuff. good day anyways ;) – Harkal Apr 02 '21 at 19:36
  • clean code isn't about production, it makes it easier to troubleshoot, adapt, understand, reuse. Clean code, is reusable and maintainable code. – Tony Chiboucas Apr 06 '21 at 04:29
0

Here is how I did it with Stimulus and Turbolinks:

See video in action: https://i.stack.imgur.com/0inOc.jpg

import { Controller } from "stimulus"
import Turbolinks from "turbolinks"

export default class extends Controller {
  static targets = ["logo", "refresh"]

  touchstart(event) {
    this.startPageY = event.changedTouches[0].pageY
  }

  touchmove(event) {
    if (this.willRefresh) {
      return
    }

    const scrollTop = document.documentElement.scrollTop
    const dy = event.changedTouches[0].pageY - this.startPageY

    if (scrollTop === 0 && dy > 0) {
      this.logoTarget.classList.add("hidden")
      this.refreshTarget.classList.remove("hidden")

      if (dy > 360) {
        this.willRefresh = true
        this.refreshTarget.classList.add("animate-spin")
        this.refreshTarget.style.transform = ""
      } else {
        this.refreshTarget.classList.remove("animate-spin")
        this.refreshTarget.style.transform = `rotate(${dy}deg)`
      }
    } else {
      this.logoTarget.classList.remove("hidden")
      this.refreshTarget.classList.add("hidden")
      this.refreshTarget.classList.remove("animate-spin")
      this.refreshTarget.style.transform = ""
    }
  }

  touchend(event) {
    if (this.willRefresh) {
      Turbolinks.visit(window.location.toString(), { action: 'replace' })
    } else {
      this.logoTarget.classList.remove("hidden")
      this.refreshTarget.classList.add("hidden")
      this.refreshTarget.classList.remove("animate-spin")
      this.refreshTarget.style.transform = ""
    }
  }
}
  body(class="max-w-md mx-auto" data-controller="pull-to-refresh" data-action="touchstart@window->pull-to-refresh#touchstart touchmove@window->pull-to-refresh#touchmove touchend@window->pull-to-refresh#touchend")
        = image_tag "logo.png", class: "w-8 h-8 mr-2", "data-pull-to-refresh-target": "logo"
        = image_tag "refresh.png", class: "w-8 h-8 mr-2 hidden", "data-pull-to-refresh-target": "refresh"
Dorian
  • 7,749
  • 4
  • 38
  • 57
-7

One simple way:

$(document.body).pullToRefresh(function() {
         setTimeout(function() {
            $(document.body).pullToRefreshDone();
         }, 2000);
      });
jack.lin
  • 15
  • 5
  • 1
    Okay, what are you trying to accomplish here? `jQuery.pullToRefresh` is not a function (nor a plugin I'm aware of), and your answer is lacking in any description or context. I suspect you're referencing a jQuery extension, which may be a cleaner solution than my older one, so please expand on your answer a bit. – Tony Chiboucas May 30 '18 at 23:39
  • 1
    this function came from framework7.io, not from jQuery. – GIA Mar 26 '21 at 16:19