Skip to content

Commit 7134114

Browse files
committed
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.
1 parent e90477c commit 7134114

File tree

9 files changed

+565
-120
lines changed

9 files changed

+565
-120
lines changed

Diff for: js/angular/controller/refresherController.js

+313
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
IonicModule
2+
.controller('$ionicRefresher', [
3+
'$scope',
4+
'$attrs',
5+
'$element',
6+
'$ionicBind',
7+
'$timeout',
8+
function($scope, $attrs, $element, $ionicBind, $timeout) {
9+
var self = this,
10+
isDragging = false,
11+
isOverscrolling = false,
12+
dragOffset = 0,
13+
lastOverscroll = 0,
14+
ptrThreshold = 60,
15+
activated = false,
16+
scrollTime = 500,
17+
startY = null,
18+
deltaY = null,
19+
canOverscroll = true,
20+
scrollParent,
21+
scrollChild;
22+
23+
if (!isDefined($attrs.pullingIcon)) {
24+
$attrs.$set('pullingIcon', 'ion-android-arrow-down');
25+
}
26+
27+
$scope.showSpinner = !isDefined($attrs.refreshingIcon);
28+
29+
$ionicBind($scope, $attrs, {
30+
pullingIcon: '@',
31+
pullingText: '@',
32+
refreshingIcon: '@',
33+
refreshingText: '@',
34+
spinner: '@',
35+
disablePullingRotation: '@',
36+
$onRefresh: '&onRefresh',
37+
$onPulling: '&onPulling'
38+
});
39+
40+
function handleTouchend() {
41+
// if this wasn't an overscroll, get out immediately
42+
if (!canOverscroll && !isDragging) {
43+
return;
44+
}
45+
// reset Y
46+
startY = null;
47+
// the user has overscrolled but went back to native scrolling
48+
if (!isDragging) {
49+
dragOffset = 0;
50+
isOverscrolling = false;
51+
setScrollLock(false);
52+
return true;
53+
}
54+
isDragging = false;
55+
dragOffset = 0;
56+
57+
// the user has scroll far enough to trigger a refresh
58+
if (lastOverscroll > ptrThreshold) {
59+
start();
60+
scrollTo(ptrThreshold, scrollTime);
61+
62+
// the user has overscrolled but not far enough to trigger a refresh
63+
} else {
64+
scrollTo(0, scrollTime, deactivate);
65+
isOverscrolling = false;
66+
}
67+
return true;
68+
}
69+
70+
function handleTouchmove(e) {
71+
// if multitouch or regular scroll event, get out immediately
72+
if (!canOverscroll || e.touches.length > 1) {
73+
return;
74+
}
75+
//if this is a new drag, keep track of where we start
76+
if (startY === null) {
77+
startY = parseInt(e.touches[0].screenY, 10);
78+
}
79+
80+
// how far have we dragged so far?
81+
deltaY = parseInt(e.touches[0].screenY, 10) - startY;
82+
83+
// if we've dragged up and back down in to native scroll territory
84+
if (deltaY - dragOffset <= 0 || scrollParent.scrollTop !== 0) {
85+
86+
if (isOverscrolling) {
87+
isOverscrolling = false;
88+
setScrollLock(false);
89+
}
90+
91+
if (isDragging) {
92+
nativescroll(scrollParent,parseInt(deltaY - dragOffset, 10) * -1);
93+
}
94+
95+
// if we're not at overscroll 0 yet, 0 out
96+
if (lastOverscroll !== 0) {
97+
overscroll(0);
98+
}
99+
100+
return true;
101+
102+
} else if (deltaY > 0 && scrollParent.scrollTop === 0 && !isOverscrolling) {
103+
// starting overscroll, but drag started below scrollTop 0, so we need to offset the position
104+
dragOffset = deltaY;
105+
}
106+
107+
// prevent native scroll events while overscrolling
108+
e.preventDefault();
109+
110+
// if not overscrolling yet, initiate overscrolling
111+
if (!isOverscrolling) {
112+
isOverscrolling = true;
113+
setScrollLock(true);
114+
}
115+
116+
isDragging = true;
117+
// overscroll according to the user's drag so far
118+
overscroll(parseInt(deltaY - dragOffset, 10));
119+
120+
// update the icon accordingly
121+
if (!activated && lastOverscroll > ptrThreshold) {
122+
activated = true;
123+
ionic.requestAnimationFrame(activate);
124+
125+
} else if (activated && lastOverscroll < ptrThreshold) {
126+
activated = false;
127+
ionic.requestAnimationFrame(deactivate);
128+
}
129+
}
130+
131+
function handleScroll(e) {
132+
// canOverscrol is used to greatly simplify the drag handler during normal scrolling
133+
canOverscroll = (e.target.scrollTop === 0) || isDragging;
134+
}
135+
136+
function overscroll(val) {
137+
scrollChild.style[ionic.CSS.TRANSFORM] = 'translateY(' + val + 'px)';
138+
lastOverscroll = val;
139+
}
140+
141+
function nativescroll(target, newScrollTop) {
142+
// creates a scroll event that bubbles, can be cancelled, and with its view
143+
// and detail property initialized to window and 1, respectively
144+
target.scrollTop = newScrollTop;
145+
var e = document.createEvent("UIEvents");
146+
e.initUIEvent("scroll", true, true, window, 1);
147+
target.dispatchEvent(e);
148+
}
149+
150+
function setScrollLock(enabled) {
151+
// set the scrollbar to be position:fixed in preparation to overscroll
152+
// or remove it so the app can be natively scrolled
153+
if (enabled) {
154+
ionic.requestAnimationFrame(function() {
155+
scrollChild.classList.add('overscroll');
156+
show();
157+
});
158+
159+
} else {
160+
ionic.requestAnimationFrame(function() {
161+
scrollChild.classList.remove('overscroll');
162+
hide();
163+
deactivate();
164+
});
165+
}
166+
}
167+
168+
$scope.$on('scroll.refreshComplete', function() {
169+
// prevent the complete from firing before the scroll has started
170+
$timeout(function() {
171+
172+
ionic.requestAnimationFrame(tail);
173+
174+
// scroll back to home during tail animation
175+
scrollTo(0, scrollTime, deactivate);
176+
177+
// return to native scrolling after tail animation has time to finish
178+
$timeout(function() {
179+
180+
if (isOverscrolling) {
181+
isOverscrolling = false;
182+
setScrollLock(false);
183+
}
184+
185+
}, scrollTime);
186+
187+
}, scrollTime);
188+
});
189+
190+
function scrollTo(Y, duration, callback) {
191+
// scroll animation loop w/ easing
192+
// credit https://gist.github.com/dezinezync/5487119
193+
var start = Date.now(),
194+
from = lastOverscroll;
195+
196+
if (from === Y) {
197+
callback();
198+
return; /* Prevent scrolling to the Y point if already there */
199+
}
200+
201+
// decelerating to zero velocity
202+
function easeOutCubic(t) {
203+
return (--t) * t * t + 1;
204+
}
205+
206+
// scroll loop
207+
function scroll() {
208+
var currentTime = Date.now(),
209+
time = Math.min(1, ((currentTime - start) / duration)),
210+
// where .5 would be 50% of time on a linear scale easedT gives a
211+
// fraction based on the easing method
212+
easedT = easeOutCubic(time);
213+
214+
overscroll(parseInt((easedT * (Y - from)) + from, 10));
215+
216+
if (time < 1) {
217+
ionic.requestAnimationFrame(scroll);
218+
219+
} else {
220+
221+
if (Y < 5 && Y > -5) {
222+
isOverscrolling = false;
223+
setScrollLock(false);
224+
}
225+
226+
callback && callback();
227+
}
228+
}
229+
230+
// start scroll loop
231+
ionic.requestAnimationFrame(scroll);
232+
}
233+
234+
235+
self.init = function() {
236+
scrollParent = $element.parent().parent()[0];
237+
scrollChild = $element.parent()[0];
238+
239+
if (!scrollParent.classList.contains('ionic-scroll') ||
240+
!scrollChild.classList.contains('scroll')) {
241+
throw new Error('Refresher must be immediate child of ion-content or ion-scroll');
242+
}
243+
244+
ionic.on('touchmove', handleTouchmove, scrollChild);
245+
ionic.on('touchend', handleTouchend, scrollChild);
246+
ionic.on('scroll', handleScroll, scrollParent);
247+
};
248+
249+
250+
$scope.$on('$destroy', destroy);
251+
252+
function destroy() {
253+
ionic.off('dragdown', handleTouchmove, scrollChild);
254+
ionic.off('dragend', handleTouchend, scrollChild);
255+
ionic.off('scroll', handleScroll, scrollParent);
256+
scrollParent = null;
257+
scrollChild = null;
258+
}
259+
260+
// DOM manipulation and broadcast methods shared by JS and Native Scrolling
261+
// getter used by JS Scrolling
262+
self.getRefresherDomMethods = function() {
263+
return {
264+
activate: activate,
265+
deactivate: deactivate,
266+
start: start,
267+
show: show,
268+
hide: hide,
269+
tail: tail
270+
};
271+
};
272+
273+
function activate() {
274+
$element[0].classList.add('active');
275+
$scope.$onPulling();
276+
}
277+
278+
function deactivate() {
279+
// give tail 150ms to finish
280+
$timeout(function() {
281+
// deactivateCallback
282+
$element.removeClass('active refreshing refreshing-tail');
283+
if (activated) activated = false;
284+
}, 150);
285+
}
286+
287+
function start() {
288+
// startCallback
289+
$element[0].classList.add('refreshing');
290+
$scope.$onRefresh();
291+
}
292+
293+
function show() {
294+
// showCallback
295+
$element[0].classList.remove('invisible');
296+
}
297+
298+
function hide() {
299+
// showCallback
300+
$element[0].classList.add('invisible');
301+
}
302+
303+
function tail() {
304+
// tailCallback
305+
$element[0].classList.add('refreshing-tail');
306+
}
307+
308+
// for testing
309+
self.__handleTouchmove = handleTouchmove;
310+
self.__getScrollChild = function() { return scrollChild; };
311+
self.__getScrollParent= function() { return scrollParent; };
312+
}
313+
]);

Diff for: js/angular/controller/scrollController.js

+20-32
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@ IonicModule
1212
'$document',
1313
'$ionicScrollDelegate',
1414
'$ionicHistory',
15-
function($scope, scrollViewOptions, $timeout, $window, $location, $document, $ionicScrollDelegate, $ionicHistory) {
15+
'$controller',
16+
function($scope,
17+
scrollViewOptions,
18+
$timeout,
19+
$window,
20+
$location,
21+
$document,
22+
$ionicScrollDelegate,
23+
$ionicHistory,
24+
$controller) {
1625

1726
var self = this;
1827
// for testing
@@ -171,38 +180,17 @@ function($scope, scrollViewOptions, $timeout, $window, $location, $document, $io
171180
/**
172181
* @private
173182
*/
174-
self._setRefresher = function(refresherScope, refresherElement) {
175-
var refresher = self.refresher = refresherElement;
183+
self._setRefresher = function(
184+
refresherScope,
185+
refresherElement,
186+
refresherMethods
187+
) {
188+
self.refresher = refresherElement;
176189
var refresherHeight = self.refresher.clientHeight || 60;
177-
scrollView.activatePullToRefresh(refresherHeight, function() {
178-
// activateCallback
179-
refresher.classList.add('active');
180-
refresherScope.$onPulling();
181-
onPullProgress(1);
182-
}, function() {
183-
// deactivateCallback
184-
refresher.classList.remove('active');
185-
refresher.classList.remove('refreshing');
186-
refresher.classList.remove('refreshing-tail');
187-
}, function() {
188-
// startCallback
189-
refresher.classList.add('refreshing');
190-
refresherScope.$onRefresh();
191-
}, function() {
192-
// showCallback
193-
refresher.classList.remove('invisible');
194-
}, function() {
195-
// hideCallback
196-
refresher.classList.add('invisible');
197-
}, function() {
198-
// tailCallback
199-
refresher.classList.add('refreshing-tail');
200-
}, onPullProgress);
201-
202-
function onPullProgress(progress) {
203-
$scope.$broadcast('$ionicRefresher.pullProgress', progress);
204-
refresherScope.$onPullProgress && refresherScope.$onPullProgress(progress);
205-
}
190+
scrollView.activatePullToRefresh(
191+
refresherHeight,
192+
refresherMethods
193+
);
206194
};
207195

208196
}]);

0 commit comments

Comments
 (0)