-
Notifications
You must be signed in to change notification settings - Fork 13.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(refresher): Allow refrsher to work with native scrolling
This update allows `<ion-refresher>` to work with native scrolling. Native scrolling can be enabled in the state deffinition, through the `$ionicConfigProvider` like `$ionicConfig.scrolling.jsScrolling(false);` or in the controller directly. It should function exactly the same as with JS scrolling enabled. This is a merge of the wip-scrolling branch.
- Loading branch information
1 parent
e90477c
commit 7134114
Showing
9 changed files
with
565 additions
and
120 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
IonicModule | ||
.controller('$ionicRefresher', [ | ||
'$scope', | ||
'$attrs', | ||
'$element', | ||
'$ionicBind', | ||
'$timeout', | ||
function($scope, $attrs, $element, $ionicBind, $timeout) { | ||
var self = this, | ||
isDragging = false, | ||
isOverscrolling = false, | ||
dragOffset = 0, | ||
lastOverscroll = 0, | ||
ptrThreshold = 60, | ||
activated = false, | ||
scrollTime = 500, | ||
startY = null, | ||
deltaY = null, | ||
canOverscroll = true, | ||
scrollParent, | ||
scrollChild; | ||
|
||
if (!isDefined($attrs.pullingIcon)) { | ||
$attrs.$set('pullingIcon', 'ion-android-arrow-down'); | ||
} | ||
|
||
$scope.showSpinner = !isDefined($attrs.refreshingIcon); | ||
|
||
$ionicBind($scope, $attrs, { | ||
pullingIcon: '@', | ||
pullingText: '@', | ||
refreshingIcon: '@', | ||
refreshingText: '@', | ||
spinner: '@', | ||
disablePullingRotation: '@', | ||
$onRefresh: '&onRefresh', | ||
$onPulling: '&onPulling' | ||
}); | ||
|
||
function handleTouchend() { | ||
// if this wasn't an overscroll, get out immediately | ||
if (!canOverscroll && !isDragging) { | ||
return; | ||
} | ||
// reset Y | ||
startY = null; | ||
// the user has overscrolled but went back to native scrolling | ||
if (!isDragging) { | ||
dragOffset = 0; | ||
isOverscrolling = false; | ||
setScrollLock(false); | ||
return true; | ||
} | ||
isDragging = false; | ||
dragOffset = 0; | ||
|
||
// the user has scroll far enough to trigger a refresh | ||
if (lastOverscroll > ptrThreshold) { | ||
start(); | ||
scrollTo(ptrThreshold, scrollTime); | ||
|
||
// the user has overscrolled but not far enough to trigger a refresh | ||
} else { | ||
scrollTo(0, scrollTime, deactivate); | ||
isOverscrolling = false; | ||
} | ||
return true; | ||
} | ||
|
||
function handleTouchmove(e) { | ||
// if multitouch or regular scroll event, get out immediately | ||
if (!canOverscroll || e.touches.length > 1) { | ||
return; | ||
} | ||
//if this is a new drag, keep track of where we start | ||
if (startY === null) { | ||
startY = parseInt(e.touches[0].screenY, 10); | ||
} | ||
|
||
// how far have we dragged so far? | ||
deltaY = parseInt(e.touches[0].screenY, 10) - startY; | ||
|
||
// if we've dragged up and back down in to native scroll territory | ||
if (deltaY - dragOffset <= 0 || scrollParent.scrollTop !== 0) { | ||
|
||
if (isOverscrolling) { | ||
isOverscrolling = false; | ||
setScrollLock(false); | ||
} | ||
|
||
if (isDragging) { | ||
nativescroll(scrollParent,parseInt(deltaY - dragOffset, 10) * -1); | ||
} | ||
|
||
// if we're not at overscroll 0 yet, 0 out | ||
if (lastOverscroll !== 0) { | ||
overscroll(0); | ||
} | ||
|
||
return true; | ||
|
||
} else if (deltaY > 0 && scrollParent.scrollTop === 0 && !isOverscrolling) { | ||
// starting overscroll, but drag started below scrollTop 0, so we need to offset the position | ||
dragOffset = deltaY; | ||
} | ||
|
||
// prevent native scroll events while overscrolling | ||
e.preventDefault(); | ||
|
||
// if not overscrolling yet, initiate overscrolling | ||
if (!isOverscrolling) { | ||
isOverscrolling = true; | ||
setScrollLock(true); | ||
} | ||
|
||
isDragging = true; | ||
// overscroll according to the user's drag so far | ||
overscroll(parseInt(deltaY - dragOffset, 10)); | ||
|
||
// update the icon accordingly | ||
if (!activated && lastOverscroll > ptrThreshold) { | ||
activated = true; | ||
ionic.requestAnimationFrame(activate); | ||
|
||
} else if (activated && lastOverscroll < ptrThreshold) { | ||
activated = false; | ||
ionic.requestAnimationFrame(deactivate); | ||
} | ||
} | ||
|
||
function handleScroll(e) { | ||
// canOverscrol is used to greatly simplify the drag handler during normal scrolling | ||
canOverscroll = (e.target.scrollTop === 0) || isDragging; | ||
} | ||
|
||
function overscroll(val) { | ||
scrollChild.style[ionic.CSS.TRANSFORM] = 'translateY(' + val + 'px)'; | ||
lastOverscroll = val; | ||
} | ||
|
||
function nativescroll(target, newScrollTop) { | ||
// creates a scroll event that bubbles, can be cancelled, and with its view | ||
// and detail property initialized to window and 1, respectively | ||
target.scrollTop = newScrollTop; | ||
var e = document.createEvent("UIEvents"); | ||
e.initUIEvent("scroll", true, true, window, 1); | ||
target.dispatchEvent(e); | ||
} | ||
|
||
function setScrollLock(enabled) { | ||
// set the scrollbar to be position:fixed in preparation to overscroll | ||
// or remove it so the app can be natively scrolled | ||
if (enabled) { | ||
ionic.requestAnimationFrame(function() { | ||
scrollChild.classList.add('overscroll'); | ||
show(); | ||
}); | ||
|
||
} else { | ||
ionic.requestAnimationFrame(function() { | ||
scrollChild.classList.remove('overscroll'); | ||
hide(); | ||
deactivate(); | ||
}); | ||
} | ||
} | ||
|
||
$scope.$on('scroll.refreshComplete', function() { | ||
// prevent the complete from firing before the scroll has started | ||
$timeout(function() { | ||
|
||
ionic.requestAnimationFrame(tail); | ||
|
||
// scroll back to home during tail animation | ||
scrollTo(0, scrollTime, deactivate); | ||
|
||
// return to native scrolling after tail animation has time to finish | ||
$timeout(function() { | ||
|
||
if (isOverscrolling) { | ||
isOverscrolling = false; | ||
setScrollLock(false); | ||
} | ||
|
||
}, scrollTime); | ||
|
||
}, scrollTime); | ||
}); | ||
|
||
function scrollTo(Y, duration, callback) { | ||
// scroll animation loop w/ easing | ||
// credit https://gist.github.com/dezinezync/5487119 | ||
var start = Date.now(), | ||
from = lastOverscroll; | ||
|
||
if (from === Y) { | ||
callback(); | ||
return; /* Prevent scrolling to the Y point if already there */ | ||
} | ||
|
||
// decelerating to zero velocity | ||
function easeOutCubic(t) { | ||
return (--t) * t * t + 1; | ||
} | ||
|
||
// scroll loop | ||
function scroll() { | ||
var currentTime = Date.now(), | ||
time = Math.min(1, ((currentTime - start) / duration)), | ||
// where .5 would be 50% of time on a linear scale easedT gives a | ||
// fraction based on the easing method | ||
easedT = easeOutCubic(time); | ||
|
||
overscroll(parseInt((easedT * (Y - from)) + from, 10)); | ||
|
||
if (time < 1) { | ||
ionic.requestAnimationFrame(scroll); | ||
|
||
} else { | ||
|
||
if (Y < 5 && Y > -5) { | ||
isOverscrolling = false; | ||
setScrollLock(false); | ||
} | ||
|
||
callback && callback(); | ||
} | ||
} | ||
|
||
// start scroll loop | ||
ionic.requestAnimationFrame(scroll); | ||
} | ||
|
||
|
||
self.init = function() { | ||
scrollParent = $element.parent().parent()[0]; | ||
scrollChild = $element.parent()[0]; | ||
|
||
if (!scrollParent.classList.contains('ionic-scroll') || | ||
!scrollChild.classList.contains('scroll')) { | ||
throw new Error('Refresher must be immediate child of ion-content or ion-scroll'); | ||
} | ||
|
||
ionic.on('touchmove', handleTouchmove, scrollChild); | ||
ionic.on('touchend', handleTouchend, scrollChild); | ||
ionic.on('scroll', handleScroll, scrollParent); | ||
}; | ||
|
||
|
||
$scope.$on('$destroy', destroy); | ||
|
||
function destroy() { | ||
ionic.off('dragdown', handleTouchmove, scrollChild); | ||
ionic.off('dragend', handleTouchend, scrollChild); | ||
ionic.off('scroll', handleScroll, scrollParent); | ||
scrollParent = null; | ||
scrollChild = null; | ||
} | ||
|
||
// DOM manipulation and broadcast methods shared by JS and Native Scrolling | ||
// getter used by JS Scrolling | ||
self.getRefresherDomMethods = function() { | ||
return { | ||
activate: activate, | ||
deactivate: deactivate, | ||
start: start, | ||
show: show, | ||
hide: hide, | ||
tail: tail | ||
}; | ||
}; | ||
|
||
function activate() { | ||
$element[0].classList.add('active'); | ||
$scope.$onPulling(); | ||
} | ||
|
||
function deactivate() { | ||
// give tail 150ms to finish | ||
$timeout(function() { | ||
// deactivateCallback | ||
$element.removeClass('active refreshing refreshing-tail'); | ||
if (activated) activated = false; | ||
}, 150); | ||
} | ||
|
||
function start() { | ||
// startCallback | ||
$element[0].classList.add('refreshing'); | ||
$scope.$onRefresh(); | ||
} | ||
|
||
function show() { | ||
// showCallback | ||
$element[0].classList.remove('invisible'); | ||
} | ||
|
||
function hide() { | ||
// showCallback | ||
$element[0].classList.add('invisible'); | ||
} | ||
|
||
function tail() { | ||
// tailCallback | ||
$element[0].classList.add('refreshing-tail'); | ||
} | ||
|
||
// for testing | ||
self.__handleTouchmove = handleTouchmove; | ||
self.__getScrollChild = function() { return scrollChild; }; | ||
self.__getScrollParent= function() { return scrollParent; }; | ||
} | ||
]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.