Skip to content

Commit

Permalink
feat(refresher): Allow refrsher to work with native scrolling
Browse files Browse the repository at this point in the history
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
perrygovier committed Feb 5, 2015
1 parent e90477c commit 7134114
Show file tree
Hide file tree
Showing 9 changed files with 565 additions and 120 deletions.
313 changes: 313 additions & 0 deletions js/angular/controller/refresherController.js
@@ -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; };
}
]);
52 changes: 20 additions & 32 deletions js/angular/controller/scrollController.js
Expand Up @@ -12,7 +12,16 @@ IonicModule
'$document',
'$ionicScrollDelegate',
'$ionicHistory',
function($scope, scrollViewOptions, $timeout, $window, $location, $document, $ionicScrollDelegate, $ionicHistory) {
'$controller',
function($scope,
scrollViewOptions,
$timeout,
$window,
$location,
$document,
$ionicScrollDelegate,
$ionicHistory,
$controller) {

var self = this;
// for testing
Expand Down Expand Up @@ -171,38 +180,17 @@ function($scope, scrollViewOptions, $timeout, $window, $location, $document, $io
/**
* @private
*/
self._setRefresher = function(refresherScope, refresherElement) {
var refresher = self.refresher = refresherElement;
self._setRefresher = function(
refresherScope,
refresherElement,
refresherMethods
) {
self.refresher = refresherElement;
var refresherHeight = self.refresher.clientHeight || 60;
scrollView.activatePullToRefresh(refresherHeight, function() {
// activateCallback
refresher.classList.add('active');
refresherScope.$onPulling();
onPullProgress(1);
}, function() {
// deactivateCallback
refresher.classList.remove('active');
refresher.classList.remove('refreshing');
refresher.classList.remove('refreshing-tail');
}, function() {
// startCallback
refresher.classList.add('refreshing');
refresherScope.$onRefresh();
}, function() {
// showCallback
refresher.classList.remove('invisible');
}, function() {
// hideCallback
refresher.classList.add('invisible');
}, function() {
// tailCallback
refresher.classList.add('refreshing-tail');
}, onPullProgress);

function onPullProgress(progress) {
$scope.$broadcast('$ionicRefresher.pullProgress', progress);
refresherScope.$onPullProgress && refresherScope.$onPullProgress(progress);
}
scrollView.activatePullToRefresh(
refresherHeight,
refresherMethods
);
};

}]);

0 comments on commit 7134114

Please sign in to comment.