mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
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.
This commit is contained in:
313
js/angular/controller/refresherController.js
vendored
Normal file
313
js/angular/controller/refresherController.js
vendored
Normal file
@@ -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
js/angular/controller/scrollController.js
vendored
52
js/angular/controller/scrollController.js
vendored
@@ -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
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
Reference in New Issue
Block a user