diff --git a/dist/js/ionic.js b/dist/js/ionic.js index 769e9ee043..d3fe05a45c 100644 --- a/dist/js/ionic.js +++ b/dist/js/ionic.js @@ -2484,30 +2484,24 @@ window.ionic = { (function(ionic) { 'use strict'; - /** - * The Scroll view is a container that suppoerts complex - * and customizable scroll behavior. - * - * This is a replacement for the buggy and shallow -webkit-overflow-scroll: touch. - * which is fine for web apps that want to have overflow scrolling containers, - * but HTML5 hybrid apps benefit from the same kind of scroll abstractions - * seen on iOS or Android. - */ ionic.views.ScrollView = function(opts) { var _this = this; // Extend the options with our defaults ionic.Utils.extend(opts, { - decelerationRate: ionic.views.Scroll.prototype.DECEL_RATE_NORMAL, + decelerationRate: ionic.views.ScrollView.prototype.DECEL_RATE_NORMAL, dragThresholdY: 10, resistance: 2, scrollEventName: 'momentumScrolled', intertialEventInterval: 50, - showScrollBar: true + + bounceTime: 600 //how long to take when bouncing back in a rubber band }); ionic.Utils.extend(this, opts); + this.el = opts.el; + // Listen for drag and release events ionic.onGesture('drag', function(e) { _this._handleDrag(e); @@ -2525,6 +2519,37 @@ window.ionic = { DECEL_RATE_FAST: 0.99, DECEL_RATE_SLOW: 0.996, + _getMomentum: function (current, start, time, lowerMargin, wrapperSize) { + var distance = current - start, + speed = Math.abs(distance) / time, + destination, + duration, + deceleration = 0.0006; + + // Calculate the final desination + destination = current + ( speed * speed ) / ( 2 * deceleration ) * ( distance < 0 ? -1 : 1 ); + duration = speed / deceleration; + + // Check if the final destination needs to be rubber banded + if ( destination < lowerMargin ) { + // We have dragged too far down, snap back to the maximum + destination = wrapperSize ? lowerMargin - ( wrapperSize / 2.5 * ( speed / 8 ) ) : lowerMargin; + distance = Math.abs(destination - current); + duration = distance / speed; + } else if ( destination > 0 ) { + // We have dragged too far up, snap back to 0 + destination = 0;//wrapperSize ? wrapperSize / 2.5 * ( speed / 8 ) : 0; + distance = Math.abs(current) + destination; + duration = distance / speed; + } + + console.log('Momentum of time', time, speed, destination, duration); + return { + destination: Math.round(destination), + duration: duration + }; + }, + /** * Scroll to the given X and Y point, taking * the given amount of time, with the given @@ -2570,6 +2595,8 @@ window.ionic = { this._isStopped = false; }, + + _endTransition: function() { this._isDragging = false; this._drag = null; @@ -2581,6 +2608,8 @@ window.ionic = { clearTimeout(this._momentumStepTimeout) }, + + /** * Initialize a drag by grabbing the content area to drag, and any other * info we might need for the dragging. @@ -2593,62 +2622,90 @@ window.ionic = { var scrollTop = parseFloat(this.el.style.webkitTransform.replace('translate3d(', '').split(',')[1]) || 0; this._drag = { + direction: 'v', + y: scrollTop, + pointY: e.gesture.touches[0].pageY, startY: scrollTop, resist: 1, - startTime: +(new Date) + startTime: Date.now() }; }, + + /** * Process the drag event to move the item to the left or right. */ _handleDrag: function(e) { var _this = this; - window.requestAnimationFrame(function() { - var content; + var content; - // The drag stopped already, don't process this one - if(_this._isStopped) { - _this._initDrag(); - return; - } + // The drag stopped already, don't process this one + if(_this._isStopped) { + _this._initDrag(); + return; + } - // We really aren't dragging - if(!_this._drag) { - _this._startDrag(e); - } + // We really aren't dragging + if(!_this._drag) { + _this._startDrag(e); + } - // Stop any default events during the drag - e.preventDefault(); + // Stop any default events during the drag + e.preventDefault(); - // Check if we should start dragging. Check if we've dragged past the threshold. - if(!_this._isDragging && (Math.abs(e.gesture.deltaY) > _this.dragThresholdY)) { - _this._isDragging = true; - } + var py = e.gesture.touches[0].pageY; + var deltaY = py - _this._drag.pointY; + console.log("Delta y", deltaY); - if(_this._isDragging) { + _this._drag.pointY = py; + + // Check if we should start dragging. Check if we've dragged past the threshold. + if(!_this._isDragging && (Math.abs(e.gesture.deltaY) > _this.dragThresholdY)) { + _this._isDragging = true; + } + + if(_this._isDragging) { + var drag = _this._drag; + window.requestAnimationFrame(function() { + // We are dragging, grab the current content height + // and the height of the parent container var totalHeight = _this.el.offsetHeight; var parentHeight = _this.el.parentNode.offsetHeight; + var timestamp = Date.now(); - var newY = _this._drag.startY + e.gesture.deltaY; + // Calculate the new Y point for the container + var newY = drag.y + deltaY; // Check if the dragging is beyond the bottom or top if(newY > 0 || (-newY + parentHeight) > totalHeight) { // Rubber band - newY = newY + e.gesture.deltaY / (-_this.resistance); + newY = drag.y + deltaY / 3;//(-_this.resistance); } // Update the new translated Y point of the container _this.el.style.webkitTransform = 'translate3d(0,' + newY + 'px, 0)'; + drag.y = newY; + + // Check if we need to reset the drag initial states if we've + // been dragging for a bit + if(timestamp - drag.startTime > 300) { + console.log('Resetting timer'); + drag.startTime = timestamp; + drag.startY = drag.y; + } + ionic.trigger(_this.scrollEventName, { target: _this.el, scrollTop: -newY }); - } - }); + }); + } }, + + _handleEndDrag: function(e) { // We didn't have a drag, so just init and leave if(!this._drag) { @@ -2666,56 +2723,59 @@ window.ionic = { }, + // Find the stopping point given the current velocity and acceleration rate, and + // animate to that position _animateToStop: function(e) { var _this = this; - var totalHeight = this.el.offsetHeight; - var parentHeight = this.el.parentNode.offsetHeight; - var scrollTop = parseFloat(this.el.style.webkitTransform.replace('translate3d(', '').split(',')[1]) || 0; - var duration = +(new Date) - this._drag.startTime; - - var newY = scrollTop; - - if(e.gesture.velocityY) { - // Get the final resting point - var vy = e.gesture.velocityY; - var speed = Math.abs(e.gesture.deltaY) / duration; - //newY = newY + (vy * vy) / (0.05 * this.decelerationRate); - - var destination = newY + ( speed * speed ) / ( 2 * (1-_this.decelerationRate)) * ( e.gesture.deltaY < 0 ? -1 : 1 ); - var dur = speed / (1-_this.decelerationRate); - - if((-destination + parentHeight) > totalHeight) { - destination = -(totalHeight - parentHeight); - } else if(destination > 0) { - destination = 0; - } - - console.log('Ending at velocity and point', speed, vy, destination, dur); - - - var el = this.el; - - window.requestAnimationFrame(function() { - _this.scrollTo(0, destination, dur); - }); + if(this._drag.direction == 'v') { + this._animateToStopVertical(e); + } else { + this._animateToStopHorizontal(e); } - /* - // Check if the dragging is beyond the bottom or top - */ + }, - /* - setTimeout(function() { - window.requestAnimationFrame(function() { - _this.el.style.webkitTransform = 'translate3d(0,' + newY + 'px, 0)'; - }); - }, 50); - */ + _animateToStopHorizontal: function(e) { + }, + + _animateToStopVertical: function(e) { + var _this = this; + window.requestAnimationFrame(function() { + var drag = _this._drag; - // Turn on animation - this.el.classList.add('scroll-scrolling'); + // Calculate the viewport height and the height of the content + var totalHeight = _this.el.offsetHeight; + var parentHeight = _this.el.parentNode.offsetHeight; + // Calculate how long we've been dragging for, with a max of 300ms + var duration = Math.min(300, (Date.now()) - _this._drag.startTime); + + + //var newX = Math.round(this.x), + var newY = Math.round(drag.y); + //distanceX = Math.abs(newX - this.startX), + //var distanceY = Math.abs(newY - drag.startY); + + + var momentum = _this._getMomentum(drag.y, drag.startY, duration, parentHeight - totalHeight, parentHeight); + //var newX = momentumX.destination; + newY = momentum.destination; + var time = momentum.duration; + + if(drag.y > 0) { + _this.scrollTo(0, 0, _this.bounceTime); + return; + } else if ((-drag.y + parentHeight) > totalHeight) { + _this.scrollTo(0, totalHeight - parentHeight, _this.bounceTime); + return; + } + + var el = _this.el; + + // Turn on animation + _this.scrollTo(0, newY, time); + }); } }; @@ -2725,7 +2785,10 @@ window.ionic = { * Adapted from the great iScroll for Ionic. iScroll is licensed under MIT just like Ionic. * * Think of ionic.views.Scroll like a Javascript version of UIScrollView or any - * scroll container in any UI library. + * scroll container in any UI library. You could just use -webkit-overflow-scrolling: touch, + * but you lose control over scroll behavior that native developers have with things + * like UIScrollView, and you don't get events after the finger stops touching the + * device (after a flick, for example) * * iScroll v5.0.5 ~ (c) 2008-2013 Matteo Spinelli ~ http://cubiq.org/license */ @@ -2739,6 +2802,9 @@ var rAF = window.requestAnimationFrame || window.msRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; +/** + * Utilities for calculating momentum, etc. + */ var utils = (function () { var me = {}; diff --git a/js/views/scrollView.js b/js/views/scrollView.js index 9e1a0c6136..c738e05867 100644 --- a/js/views/scrollView.js +++ b/js/views/scrollView.js @@ -1,30 +1,24 @@ (function(ionic) { 'use strict'; - /** - * The Scroll view is a container that suppoerts complex - * and customizable scroll behavior. - * - * This is a replacement for the buggy and shallow -webkit-overflow-scroll: touch. - * which is fine for web apps that want to have overflow scrolling containers, - * but HTML5 hybrid apps benefit from the same kind of scroll abstractions - * seen on iOS or Android. - */ ionic.views.ScrollView = function(opts) { var _this = this; // Extend the options with our defaults ionic.Utils.extend(opts, { - decelerationRate: ionic.views.Scroll.prototype.DECEL_RATE_NORMAL, + decelerationRate: ionic.views.ScrollView.prototype.DECEL_RATE_NORMAL, dragThresholdY: 10, resistance: 2, scrollEventName: 'momentumScrolled', intertialEventInterval: 50, - showScrollBar: true + + bounceTime: 600 //how long to take when bouncing back in a rubber band }); ionic.Utils.extend(this, opts); + this.el = opts.el; + // Listen for drag and release events ionic.onGesture('drag', function(e) { _this._handleDrag(e); @@ -42,6 +36,37 @@ DECEL_RATE_FAST: 0.99, DECEL_RATE_SLOW: 0.996, + _getMomentum: function (current, start, time, lowerMargin, wrapperSize) { + var distance = current - start, + speed = Math.abs(distance) / time, + destination, + duration, + deceleration = 0.0006; + + // Calculate the final desination + destination = current + ( speed * speed ) / ( 2 * deceleration ) * ( distance < 0 ? -1 : 1 ); + duration = speed / deceleration; + + // Check if the final destination needs to be rubber banded + if ( destination < lowerMargin ) { + // We have dragged too far down, snap back to the maximum + destination = wrapperSize ? lowerMargin - ( wrapperSize / 2.5 * ( speed / 8 ) ) : lowerMargin; + distance = Math.abs(destination - current); + duration = distance / speed; + } else if ( destination > 0 ) { + // We have dragged too far up, snap back to 0 + destination = 0;//wrapperSize ? wrapperSize / 2.5 * ( speed / 8 ) : 0; + distance = Math.abs(current) + destination; + duration = distance / speed; + } + + console.log('Momentum of time', time, speed, destination, duration); + return { + destination: Math.round(destination), + duration: duration + }; + }, + /** * Scroll to the given X and Y point, taking * the given amount of time, with the given @@ -87,6 +112,8 @@ this._isStopped = false; }, + + _endTransition: function() { this._isDragging = false; this._drag = null; @@ -98,6 +125,8 @@ clearTimeout(this._momentumStepTimeout) }, + + /** * Initialize a drag by grabbing the content area to drag, and any other * info we might need for the dragging. @@ -110,62 +139,90 @@ var scrollTop = parseFloat(this.el.style.webkitTransform.replace('translate3d(', '').split(',')[1]) || 0; this._drag = { + direction: 'v', + y: scrollTop, + pointY: e.gesture.touches[0].pageY, startY: scrollTop, resist: 1, - startTime: +(new Date) + startTime: Date.now() }; }, + + /** * Process the drag event to move the item to the left or right. */ _handleDrag: function(e) { var _this = this; - window.requestAnimationFrame(function() { - var content; + var content; - // The drag stopped already, don't process this one - if(_this._isStopped) { - _this._initDrag(); - return; - } + // The drag stopped already, don't process this one + if(_this._isStopped) { + _this._initDrag(); + return; + } - // We really aren't dragging - if(!_this._drag) { - _this._startDrag(e); - } + // We really aren't dragging + if(!_this._drag) { + _this._startDrag(e); + } - // Stop any default events during the drag - e.preventDefault(); + // Stop any default events during the drag + e.preventDefault(); - // Check if we should start dragging. Check if we've dragged past the threshold. - if(!_this._isDragging && (Math.abs(e.gesture.deltaY) > _this.dragThresholdY)) { - _this._isDragging = true; - } + var py = e.gesture.touches[0].pageY; + var deltaY = py - _this._drag.pointY; + console.log("Delta y", deltaY); - if(_this._isDragging) { + _this._drag.pointY = py; + + // Check if we should start dragging. Check if we've dragged past the threshold. + if(!_this._isDragging && (Math.abs(e.gesture.deltaY) > _this.dragThresholdY)) { + _this._isDragging = true; + } + + if(_this._isDragging) { + var drag = _this._drag; + window.requestAnimationFrame(function() { + // We are dragging, grab the current content height + // and the height of the parent container var totalHeight = _this.el.offsetHeight; var parentHeight = _this.el.parentNode.offsetHeight; + var timestamp = Date.now(); - var newY = _this._drag.startY + e.gesture.deltaY; + // Calculate the new Y point for the container + var newY = drag.y + deltaY; // Check if the dragging is beyond the bottom or top if(newY > 0 || (-newY + parentHeight) > totalHeight) { // Rubber band - newY = newY + e.gesture.deltaY / (-_this.resistance); + newY = drag.y + deltaY / 3;//(-_this.resistance); } // Update the new translated Y point of the container _this.el.style.webkitTransform = 'translate3d(0,' + newY + 'px, 0)'; + drag.y = newY; + + // Check if we need to reset the drag initial states if we've + // been dragging for a bit + if(timestamp - drag.startTime > 300) { + console.log('Resetting timer'); + drag.startTime = timestamp; + drag.startY = drag.y; + } + ionic.trigger(_this.scrollEventName, { target: _this.el, scrollTop: -newY }); - } - }); + }); + } }, + + _handleEndDrag: function(e) { // We didn't have a drag, so just init and leave if(!this._drag) { @@ -183,56 +240,59 @@ }, + // Find the stopping point given the current velocity and acceleration rate, and + // animate to that position _animateToStop: function(e) { var _this = this; - var totalHeight = this.el.offsetHeight; - var parentHeight = this.el.parentNode.offsetHeight; - var scrollTop = parseFloat(this.el.style.webkitTransform.replace('translate3d(', '').split(',')[1]) || 0; - var duration = +(new Date) - this._drag.startTime; - - var newY = scrollTop; - - if(e.gesture.velocityY) { - // Get the final resting point - var vy = e.gesture.velocityY; - var speed = Math.abs(e.gesture.deltaY) / duration; - //newY = newY + (vy * vy) / (0.05 * this.decelerationRate); - - var destination = newY + ( speed * speed ) / ( 2 * (1-_this.decelerationRate)) * ( e.gesture.deltaY < 0 ? -1 : 1 ); - var dur = speed / (1-_this.decelerationRate); - - if((-destination + parentHeight) > totalHeight) { - destination = -(totalHeight - parentHeight); - } else if(destination > 0) { - destination = 0; - } - - console.log('Ending at velocity and point', speed, vy, destination, dur); - - - var el = this.el; - - window.requestAnimationFrame(function() { - _this.scrollTo(0, destination, dur); - }); + if(this._drag.direction == 'v') { + this._animateToStopVertical(e); + } else { + this._animateToStopHorizontal(e); } - /* - // Check if the dragging is beyond the bottom or top - */ + }, - /* - setTimeout(function() { - window.requestAnimationFrame(function() { - _this.el.style.webkitTransform = 'translate3d(0,' + newY + 'px, 0)'; - }); - }, 50); - */ + _animateToStopHorizontal: function(e) { + }, + + _animateToStopVertical: function(e) { + var _this = this; + window.requestAnimationFrame(function() { + var drag = _this._drag; - // Turn on animation - this.el.classList.add('scroll-scrolling'); + // Calculate the viewport height and the height of the content + var totalHeight = _this.el.offsetHeight; + var parentHeight = _this.el.parentNode.offsetHeight; + // Calculate how long we've been dragging for, with a max of 300ms + var duration = Math.min(300, (Date.now()) - _this._drag.startTime); + + + //var newX = Math.round(this.x), + var newY = Math.round(drag.y); + //distanceX = Math.abs(newX - this.startX), + //var distanceY = Math.abs(newY - drag.startY); + + + var momentum = _this._getMomentum(drag.y, drag.startY, duration, parentHeight - totalHeight, parentHeight); + //var newX = momentumX.destination; + newY = momentum.destination; + var time = momentum.duration; + + if(drag.y > 0) { + _this.scrollTo(0, 0, _this.bounceTime); + return; + } else if ((-drag.y + parentHeight) > totalHeight) { + _this.scrollTo(0, totalHeight - parentHeight, _this.bounceTime); + return; + } + + var el = _this.el; + + // Turn on animation + _this.scrollTo(0, newY, time); + }); } }; diff --git a/js/views/scrollerView.js b/js/views/scrollerView.js index fd5b7ba7f1..4ad28155f3 100644 --- a/js/views/scrollerView.js +++ b/js/views/scrollerView.js @@ -2,7 +2,10 @@ * Adapted from the great iScroll for Ionic. iScroll is licensed under MIT just like Ionic. * * Think of ionic.views.Scroll like a Javascript version of UIScrollView or any - * scroll container in any UI library. + * scroll container in any UI library. You could just use -webkit-overflow-scrolling: touch, + * but you lose control over scroll behavior that native developers have with things + * like UIScrollView, and you don't get events after the finger stops touching the + * device (after a flick, for example) * * iScroll v5.0.5 ~ (c) 2008-2013 Matteo Spinelli ~ http://cubiq.org/license */ @@ -16,6 +19,9 @@ var rAF = window.requestAnimationFrame || window.msRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; +/** + * Utilities for calculating momentum, etc. + */ var utils = (function () { var me = {}; diff --git a/starters/weather/app.js b/starters/weather/app.js index a88b85c823..8132b5d413 100644 --- a/starters/weather/app.js +++ b/starters/weather/app.js @@ -18,7 +18,7 @@ angular.module('ionic.weather', ['ionic.weather.services', 'ionic.weather.direct $scope.getActiveBackgroundImage = function() { if($scope.activeBgImage) { var item = $scope.activeBgImage; - var url = "http://farm"+ item.farm +".static.flickr.com/"+ item.server +"/"+ item.id +"_"+ item.secret +"_m.jpg"; + var url = "http://farm"+ item.farm +".static.flickr.com/"+ item.server +"/"+ item.id +"_"+ item.secret +"_z.jpg"; return { 'background-image': 'url(' + url + ')' }; @@ -62,7 +62,7 @@ angular.module('ionic.weather', ['ionic.weather.services', 'ionic.weather.direct if($scope.bgImages) { $scope.activeBgImage = $scope.bgImages[$scope.activeBgImageIndex++ % $scope.bgImages.length]; } - $timeout(cycle, 10000); + //$timeout(cycle, 10000); }); }; diff --git a/starters/weather/index.html b/starters/weather/index.html index 08694ae512..8c21ab21be 100644 --- a/starters/weather/index.html +++ b/starters/weather/index.html @@ -47,7 +47,7 @@ var bgImage = document.getElementById('bg-image'); var header = document.getElementById('header'); - var scroll = new ionic.views.Scroll({ + var scroll = new ionic.views.ScrollView({ el: s, decelerationRate: 0.87, inertialEventInterval: 100 @@ -57,7 +57,7 @@ s.addEventListener('momentumScrolled', function(e) { setTimeout(function() { var bgAmount = e.detail.scrollTop; - bgImage.style.webkitTransform = 'translate3d(0, ' + -(bgAmount / 40) + 'px, 0)'; + //bgImage.style.webkitTransform = 'translate3d(0, ' + -(bgAmount / 20) + 'px, 0)'; //var blurAmount = Math.min(3, e.detail.scrollTop / 200); @@ -67,7 +67,7 @@ var per = 1 - (diff / 600); brightness = Math.max(0.7, per); } - bgImage.style.webkitFilter = 'brightness(' + brightness + ')'; + //bgImage.style.webkitFilter = 'brightness(' + brightness + ')'; }); }); diff --git a/starters/weather/weather.css b/starters/weather/weather.css index e436dd5223..07eeb9836d 100644 --- a/starters/weather/weather.css +++ b/starters/weather/weather.css @@ -1,5 +1,7 @@ body { } +#scroller { +} #bg-image { position: fixed; width: 120%; diff --git a/test/scroll.html b/test/scroll.html index 39db3beff3..b6d4ecc094 100644 --- a/test/scroll.html +++ b/test/scroll.html @@ -14,20 +14,6 @@ .list-item { padding: 10px; } - #wrapper { - position:absolute; z-index:1; - top:0px; bottom:0px; left:0; - width:100%; - background:#aaa; - overflow:hidden; - } - #scroller { - position:absolute; z-index:1; - /* -webkit-touch-callout:none;*/ - -webkit-tap-highlight-color:rgba(0,0,0,0); - width:100%; - padding:0; - }
@@ -37,28 +23,26 @@