Phonegap mobile app tapping while scrolling selects incorrect item
I have a hybrid mobile app developed with phone gap and targeted for iOS only devices. I use Backbone.js as my MVC framework, jQuery, FastClick.js and Hammer.js for events. I have a list of items which is vertically scrollable. If I tap on an item, it should open the details view. This works fine if I tap on the item when the list is not scrolling. But if I tap on an item while the list is scrolling or decelerating, it selects the wrong item and shows its details. I looked at Tapping on scrolled list generates tap event for wrong element, javascript scroll event for iPhone/iPad? and other sites which suggest that I listen to the onscroll
event of my scrolling list. This event is fired whenever the user scrolls the list. I disable the tap event in the callback for onscroll. I set a timer in the callback with timeout of 300ms and then enable the tap event in that callback which executes after 300 ms. If I get another scroll event before the timer fires, I cancel the earlier timer and set it again to fire after 300ms. There is no other event that gets fired when the scrolling stops completely. So, I have to rely on this event only.
The issue is the event fires even when the scrolling is decelerating and not completely stopped. Due to this, the timer gets fired even while the list is decelerating and not stopped and I run into the wrong details selection issue again. The event fires again when the scroll stops completely as well. If I increase my timer to be >300ms, then in case of non-momentum scroll, it takes longer for the tap to be enabled and the user will keep tapping multiple times.
Below are the code snippets:
When view loads, bind the tap
event and the onscroll
event:
that.$('.scrollListItem').hammer().bind('tap',$.proxy(that.showDetail,that));
this.$('#scrollList').bind('scroll',$.proxy(this.checkscroll,this));
checkscroll
function
checkScroll: function(e){
this.$('.scrollListItem').hammer().unbind('tap');
clearTimeout(myGlobalScrollTimer);
var that = this;
myGlobalScrollTimer = setTimeout(function(){
that.$('.scrollListItem').hammer().bind('tap',$.proxy(that.showDetail,that));
},300);
}
The checkScroll function is firing currently even while the scroll list is decelerating and it hasn't stopped completely. How do I detect that the scrolling is completely stopped and the UI is no longer decelerating and only then enable the tap event? Is there any other way to solution this? Please advice.
The problem is caused by PhoneGap still using UIWebView by default, instead of WKWebView (that was introduced in ios 8). If you can, switch to using the new WKWebView. I think there's plugins, like https://github.com/Telerik-Verified-Plugins/WKWebView, that let you do this.
One of the benefits of WKWebView is that it has significantly better scroll event fidelity. In fact, you'll probably need to debounce since your app will receive hundreds where before UIWebView only sends somewhere between 1 and 3 scroll events.
In case you are interested, the reason you get the incorrect coordinate during scrolling is that UIWebView uses the GPU to scroll a bitmap of your scrollable area so it doesn't know the accurate coordinates.
If you have to use UIWebView in PhoneGap, consider using the "click" event to avoid lots of nasty code to determine if whether scrolling is actually happening. If you really need fastclick, then here's a couple of tricks to determine if scrolling is still happening (this is from memory so numbers might be off a bit)
touchmove
and the touchend
or touchcancel
(and use the events' timestamps to be really fancy). If the user flicked to cause a high-velocity, longer scroll use a timeout of 2.5 seconds (tweak as you see fit). If it was a low-velocity scroll, use 300ms. If the user was just dragging so it was really low velocity use around 50ms. scroll
event handler, if the previous scroll event's timestamp was more than 1.25 second this is probably the last scroll event so use a timeout of 100ms. If this is the first scroll event after touchend, then use the velocity logic above to determine timeout. touchend
/ touchcancel
, check the distance to the edge of the scrollable area. If it is close, compensate since scrolling will end once it hits the edge and does the elastic visual effect.