diff --git a/js/angular/controller/headerBarController.js b/js/angular/controller/headerBarController.js index ae65e4272aa..16ddec64256 100644 --- a/js/angular/controller/headerBarController.js +++ b/js/angular/controller/headerBarController.js @@ -122,13 +122,14 @@ function($scope, $element, $attrs, $q, $ionicConfig, $ionicHistory) { }; - self.resetBackButton = function() { + self.resetBackButton = function(viewData) { if ($ionicConfig.backButton.previousTitleText()) { var previousTitleEle = getEle(PREVIOUS_TITLE); if (previousTitleEle) { previousTitleEle.classList.remove(HIDE); - var newPreviousTitleText = $ionicHistory.backTitle(); + var view = (viewData && $ionicHistory.getViewById(viewData.viewId)); + var newPreviousTitleText = $ionicHistory.backTitle(view); if (newPreviousTitleText !== previousTitleText) { previousTitleText = previousTitleEle.innerHTML = newPreviousTitleText; diff --git a/js/angular/controller/navBarController.js b/js/angular/controller/navBarController.js index f6763833d34..087a964318b 100644 --- a/js/angular/controller/navBarController.js +++ b/js/angular/controller/navBarController.js @@ -239,6 +239,7 @@ function($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $io self.transition(enteringHeaderBar, leavingHeaderBar, viewData); self.isInitialized = true; + navSwipeAttr(''); }; @@ -254,17 +255,39 @@ function($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $io ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', viewData.navBarTransition); ionic.DomUtil.cachedAttr($element, 'nav-bar-direction', viewData.direction); - if (navBarTransition.shouldAnimate) { + if (navBarTransition.shouldAnimate && viewData.renderEnd) { navBarAttr(enteringHeaderBar, 'stage'); } else { navBarAttr(enteringHeaderBar, 'entering'); navBarAttr(leavingHeaderBar, 'leaving'); } - enteringHeaderBarCtrl.resetBackButton(); + enteringHeaderBarCtrl.resetBackButton(viewData); navBarTransition.run(0); + self.activeTransition = { + run: function(step) { + navBarTransition.shouldAnimate = false; + navBarTransition.direction = 'back'; + navBarTransition.run(step); + }, + cancel: function(shouldAnimate, speed) { + navSwipeAttr(speed); + navBarAttr(leavingHeaderBar, 'active'); + navBarAttr(enteringHeaderBar, 'cached'); + navBarTransition.shouldAnimate = shouldAnimate; + navBarTransition.run(0); + self.activeTransition = navBarTransition = null; + }, + complete: function(shouldAnimate, speed) { + navSwipeAttr(speed); + navBarTransition.shouldAnimate = shouldAnimate; + navBarTransition.run(1); + queuedTransitionEnd = transitionEnd; + } + }; + $timeout(enteringHeaderBarCtrl.align, 16); queuedTransitionStart = function() { @@ -277,23 +300,26 @@ function($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $io queuedTransitionEnd = function() { if (latestTransitionId == transitionId || !navBarTransition.shouldAnimate) { - for (var x = 0; x < headerBars.length; x++) { - headerBars[x].isActive = false; - } - enteringHeaderBar.isActive = true; - - navBarAttr(enteringHeaderBar, 'active'); - navBarAttr(leavingHeaderBar, 'cached'); - - queuedTransitionEnd = null; + transitionEnd(); } }; queuedTransitionStart = null; }; - queuedTransitionStart(); + function transitionEnd() { + for (var x = 0; x < headerBars.length; x++) { + headerBars[x].isActive = false; + } + enteringHeaderBar.isActive = true; + + navBarAttr(enteringHeaderBar, 'active'); + navBarAttr(leavingHeaderBar, 'cached'); + + self.activeTransition = navBarTransition = queuedTransitionEnd = null; + } + queuedTransitionStart(); }; @@ -437,6 +463,10 @@ function($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $io ctrl && ionic.DomUtil.cachedAttr(ctrl.containerEle(), 'nav-bar', val); } + function navSwipeAttr(val) { + ionic.DomUtil.cachedAttr($element, 'nav-swipe', val); + } + $scope.$on('$destroy', function() { $scope.$parent.$hasHeader = false; diff --git a/js/angular/controller/navViewController.js b/js/angular/controller/navViewController.js index e3234645d82..c83da4c7164 100644 --- a/js/angular/controller/navViewController.js +++ b/js/angular/controller/navViewController.js @@ -9,7 +9,9 @@ IonicModule '$ionicNavViewDelegate', '$ionicHistory', '$ionicViewSwitcher', -function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $ionicNavViewDelegate, $ionicHistory, $ionicViewSwitcher) { + '$ionicConfig', + '$ionicScrollDelegate', +function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $ionicNavViewDelegate, $ionicHistory, $ionicViewSwitcher, $ionicConfig, $ionicScrollDelegate) { var DATA_ELE_IDENTIFIER = '$eleId'; var DATA_DESTROY_ELE = '$destroyEle'; @@ -23,6 +25,8 @@ function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, var navBarDelegate; var activeEleId; var navViewAttr = $ionicViewSwitcher.navViewAttr; + var disableRenderStartViewId, disableAnimation; + var transitionDuration, transitionTiming; self.scope = $scope; @@ -44,6 +48,12 @@ function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $scope.$on('$ionicHistory.deselect', self.cacheCleanup); + ionic.Platform.ready(function() { + if (ionic.Platform.isWebView() && $ionicConfig.views.swipeBackEnabled()) { + self.initSwipeBack(); + } + }); + return viewData; }; @@ -58,7 +68,10 @@ function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, self.update(registerData); // begin rendering and transitioning - self.render(registerData, viewLocals, leavingView); + var enteringView = $ionicHistory.getViewById(registerData.viewId) || {}; + + var renderStart = (disableRenderStartViewId !== registerData.viewId); + self.render(registerData, viewLocals, enteringView, leavingView, renderStart, true); }; @@ -94,19 +107,22 @@ function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, }; - self.render = function(registerData, viewLocals, leavingView) { - var enteringView = $ionicHistory.getViewById(registerData.viewId) || {}; - + self.render = function(registerData, viewLocals, enteringView, leavingView, renderStart, renderEnd) { // register the view and figure out where it lives in the various // histories and nav stacks, along with how views should enter/leave - var switcher = $ionicViewSwitcher.create(self, viewLocals, enteringView, leavingView); + var switcher = $ionicViewSwitcher.create(self, viewLocals, enteringView, leavingView, renderStart, renderEnd); // init the rendering of views for this navView directive switcher.init(registerData, function() { // the view is now compiled, in the dom and linked, now lets transition the views. // this uses a callback incase THIS nav-view has a nested nav-view, and after the NESTED // nav-view links, the NESTED nav-view would update which direction THIS nav-view should use - switcher.transition(self.direction(), registerData.enableBack); + + // kick off the transition of views + switcher.transition(self.direction(), registerData.enableBack, !disableAnimation); + + // reset private vars for next time + disableRenderStartViewId = disableAnimation = null; }); }; @@ -118,6 +134,7 @@ function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, navBarDelegate = transitionData.navBarDelegate; var associatedNavBarCtrl = getAssociatedNavBarCtrl(); associatedNavBarCtrl && associatedNavBarCtrl.update(transitionData); + navSwipeAttr(''); } }; @@ -152,6 +169,8 @@ function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, } } } + + navSwipeAttr(''); }; @@ -261,6 +280,142 @@ function($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, }; + self.initSwipeBack = function() { + var swipeBackHitWidth = $ionicConfig.views.swipeBackHitWidth(); + var viewTransition, associatedNavBarCtrl, backView; + var deregDragStart, deregDrag, deregRelease; + var windowWidth, startDragX, dragPoints; + + function onDragStart(ev) { + if (!isPrimary) return; + + startDragX = getDragX(ev); + if (startDragX > swipeBackHitWidth) return; + + backView = $ionicHistory.backView(); + + if (!backView || backView.historyId !== $ionicHistory.currentView().historyId) return; + + if (!windowWidth) windowWidth = window.innerWidth; + + freezeScrolls(true); + + var registerData = { + direction: 'back' + }; + + dragPoints = []; + + var switcher = $ionicViewSwitcher.create(self, registerData, backView, $ionicHistory.currentView(), true, false); + switcher.loadViewElements(registerData); + switcher.render(registerData); + + viewTransition = switcher.transition('back', $ionicHistory.enabledBack(backView), true); + + associatedNavBarCtrl = getAssociatedNavBarCtrl(); + + deregDrag = ionic.onGesture('drag', onDrag, $element[0]); + deregRelease = ionic.onGesture('release', onRelease, $element[0]); + } + + function onDrag(ev) { + if (isPrimary && viewTransition) { + var dragX = getDragX(ev); + + dragPoints.push({ + t: Date.now(), + x: dragX + }); + + if (dragX >= windowWidth - 15) { + onRelease(ev); + + } else { + var step = Math.min(Math.max(getSwipeCompletion(dragX), 0), 1); + viewTransition.run(step); + associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.run(step); + } + + } + } + + function onRelease(ev) { + if (isPrimary && viewTransition && dragPoints && dragPoints.length > 1) { + + var now = Date.now(); + var releaseX = getDragX(ev); + var startDrag = dragPoints[dragPoints.length - 1]; + + for (var x = dragPoints.length - 2; x >= 0; x--) { + if (now - startDrag.t > 200) { + break; + } + startDrag = dragPoints[x]; + } + + var isSwipingRight = (releaseX >= dragPoints[dragPoints.length - 2].x); + var releaseSwipeCompletion = getSwipeCompletion(releaseX); + var velocity = Math.abs(startDrag.x - releaseX) / (now - startDrag.t); + + // private variables because ui-router has no way to pass custom data using $state.go + disableRenderStartViewId = backView.viewId; + disableAnimation = (releaseSwipeCompletion < 0.03 || releaseSwipeCompletion > 0.97); + + if (isSwipingRight && (releaseSwipeCompletion > 0.5 || velocity > 0.1)) { + var speed = (velocity > 0.5 || velocity < 0.05 || releaseX > windowWidth - 45) ? 'fast' : 'slow'; + navSwipeAttr(disableAnimation ? '' : speed); + backView.go(); + associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.complete(!disableAnimation, speed); + + } else { + navSwipeAttr(disableAnimation ? '' : 'fast'); + disableRenderStartViewId = null; + viewTransition.cancel(!disableAnimation); + associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.cancel(!disableAnimation, 'fast'); + disableAnimation = null; + } + + } + + ionic.offGesture(deregDrag, 'drag', onDrag); + ionic.offGesture(deregRelease, 'release', onRelease); + + windowWidth = viewTransition = dragPoints = null; + + freezeScrolls(false); + } + + function getDragX(ev) { + return ionic.tap.pointerCoord(ev.gesture.srcEvent).x; + } + + function getSwipeCompletion(dragX) { + return (dragX - startDragX) / windowWidth; + } + + deregDragStart = ionic.onGesture('dragstart', onDragStart, $element[0]); + + $scope.$on('$destroy', function() { + ionic.offGesture(deregDragStart, 'dragstart', onDragStart); + ionic.offGesture(deregDrag, 'drag', onDrag); + ionic.offGesture(deregRelease, 'release', onRelease); + viewTransition = associatedNavBarCtrl = null; + }); + }; + + + function freezeScrolls(freeze) { + forEach($ionicScrollDelegate._instances, function(instance) { + instance.freezeScroll(freeze); + }); + } + + + function navSwipeAttr(val) { + ionic.DomUtil.cachedAttr($element, 'nav-swipe', val); + } + + function getAssociatedNavBarCtrl() { if (navBarDelegate) { for (var x=0; x < $ionicNavBarDelegate._instances.length; x++) { diff --git a/js/angular/controller/scrollController.js b/js/angular/controller/scrollController.js index 7d8d7d29e20..60bd0592af5 100644 --- a/js/angular/controller/scrollController.js +++ b/js/angular/controller/scrollController.js @@ -45,7 +45,7 @@ function($scope, } ); - if (!angular.isDefined(scrollViewOptions.bouncing)) { + if (!isDefined(scrollViewOptions.bouncing)) { ionic.Platform.ready(function() { if (scrollView.options) { scrollView.options.bouncing = true; @@ -76,22 +76,11 @@ function($scope, $scope.$on('$destroy', function() { deregisterInstance(); - //Windows: make sure the scrollView.__cleanup exists before calling it - if (scrollView.__cleanup) { - scrollView.__cleanup(); - } + scrollView && scrollView.__cleanup && scrollView.__cleanup(); ionic.off('resize', resize, $window); $window.removeEventListener('resize', resize); - scrollViewOptions = null; - self._scrollViewOptions.el = null; - self._scrollViewOptions = null; $element.off('scroll', scrollFunc); - $element = null; - self.$element = null; - element = null; - self.element = null; - self.scrollView = null; - scrollView = null; + scrollView = self.scrollView = scrollViewOptions = self._scrollViewOptions = scrollViewOptions.el = self._scrollViewOptions.el = $element = self.$element = element = null; }); $timeout(function() { @@ -99,11 +88,11 @@ function($scope, }); self.getScrollView = function() { - return self.scrollView; + return scrollView; }; self.getScrollPosition = function() { - return self.scrollView.getValues(); + return scrollView.getValues(); }; self.resize = function() { @@ -169,6 +158,11 @@ function($scope, }); }; + self.freezeScroll = function(shouldFreeze) { + if (arguments.length) scrollView.options.freeze = shouldFreeze; + return scrollView.options.freeze; + }; + /** * @private diff --git a/js/angular/controller/viewController.js b/js/angular/controller/viewController.js index 8f17abbb87c..f600598285a 100644 --- a/js/angular/controller/viewController.js +++ b/js/angular/controller/viewController.js @@ -51,20 +51,14 @@ function($scope, $element, $attrs, $compile, $rootScope, $ionicViewSwitcher) { navBarItems[n] = generateNavBarItem(navElementHtml[n]); } - navViewCtrl.beforeEnter({ + navViewCtrl.beforeEnter(extend(transData, { title: viewTitle, - direction: transData.direction, - transition: transData.transition, - navBarTransition: transData.navBarTransition, - transitionId: transData.transitionId, - shouldAnimate: transData.shouldAnimate, - enableBack: transData.enableBack, showBack: !attrTrue('hideBackButton'), navBarItems: navBarItems, navBarDelegate: navBarDelegateHandle || null, showNavBar: !attrTrue('hideNavBar'), hasHeaderBar: !!hasViewHeaderBar - }); + })); // make sure any existing observers are cleaned up deregisterFns(); diff --git a/js/angular/service/clickBlock.js b/js/angular/service/clickBlock.js index defc3bb4826..9ec33202ab5 100644 --- a/js/angular/service/clickBlock.js +++ b/js/angular/service/clickBlock.js @@ -7,6 +7,11 @@ function($document, $ionicBody, $timeout) { var CSS_HIDE = 'click-block-hide'; var cbEle, fallbackTimer, pendingShow; + function preventClick(ev) { + ev.preventDefault(); + ev.stopPropagation(); + } + function addClickBlock() { if (pendingShow) { if (cbEle) { @@ -15,6 +20,8 @@ function($document, $ionicBody, $timeout) { cbEle = $document[0].createElement('div'); cbEle.className = 'click-block'; $ionicBody.append(cbEle); + cbEle.addEventListener('touchstart', preventClick); + cbEle.addEventListener('mousedown', preventClick); } pendingShow = false; } @@ -29,12 +36,12 @@ function($document, $ionicBody, $timeout) { pendingShow = true; $timeout.cancel(fallbackTimer); fallbackTimer = $timeout(this.hide, autoExpire || 310); - ionic.requestAnimationFrame(addClickBlock); + addClickBlock(); }, hide: function() { pendingShow = false; $timeout.cancel(fallbackTimer); - ionic.requestAnimationFrame(removeClickBlock); + removeClickBlock(); } }; }]); diff --git a/js/angular/service/history.js b/js/angular/service/history.js index e886146c292..2d42e8bef4b 100644 --- a/js/angular/service/history.js +++ b/js/angular/service/history.js @@ -410,7 +410,7 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ action: action, direction: direction, historyId: historyId, - enableBack: !!(viewHistory.backView && viewHistory.backView.historyId === viewHistory.currentView.historyId), + enableBack: this.enabledBack(viewHistory.currentView), isHistoryRoot: (viewHistory.currentView.index === 0), ele: ele }; @@ -498,10 +498,9 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ * @description Gets the back view's title. * @returns {string} Returns the back view's title. */ - backTitle: function() { - if (viewHistory.backView) { - return viewHistory.backView.title; - } + backTitle: function(view) { + var backView = (view && getViewById(view.backViewId)) || viewHistory.backView; + return backView && backView.title; }, /** @@ -560,6 +559,12 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ viewHistory.backView && viewHistory.backView.go(); }, + + enabledBack: function(view) { + var backView = getBackView(view); + return !!(backView && backView.historyId === view.historyId); + }, + /** * @ngdoc method * @name $ionicHistory#clearHistory @@ -643,7 +648,7 @@ function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ nextViewOptions = nextViewOptions || {}; extend(nextViewOptions, opts); if (nextViewOptions.expire) { - nextViewExpireTimer = $timeout(function(){ + nextViewExpireTimer = $timeout(function() { nextViewOptions = null; }, nextViewOptions.expire); } diff --git a/js/angular/service/ionicConfig.js b/js/angular/service/ionicConfig.js index 3ff4fb566b9..765c4c01cfd 100644 --- a/js/angular/service/ionicConfig.js +++ b/js/angular/service/ionicConfig.js @@ -222,7 +222,9 @@ IonicModule views: { maxCache: PLATFORM, forwardCache: PLATFORM, - transition: PLATFORM + transition: PLATFORM, + swipeBackEnabled: PLATFORM, + swipeBackHitWidth: PLATFORM }, navBar: { alignTitle: PLATFORM, @@ -262,7 +264,9 @@ IonicModule views: { maxCache: 10, forwardCache: false, - transition: 'ios' + transition: 'ios', + swipeBackEnabled: true, + swipeBackHitWidth: 45 }, navBar: { @@ -311,7 +315,8 @@ IonicModule setPlatformConfig('android', { views: { - transition: 'android' + transition: 'android', + swipeBackEnabled: false }, navBar: { @@ -348,42 +353,45 @@ IonicModule // iOS Transitions // ----------------------- provider.transitions.views.ios = function(enteringEle, leavingEle, direction, shouldAnimate) { - shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back'); - function setStyles(ele, opacity, x) { + function setStyles(ele, opacity, x, boxShadowOpacity) { var css = {}; - css[ionic.CSS.TRANSITION_DURATION] = shouldAnimate ? '' : 0; + css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0; css.opacity = opacity; + if (boxShadowOpacity > -1) { + css.boxShadow = '0 0 10px rgba(0,0,0,' + (d.shouldAnimate ? boxShadowOpacity * 0.45 : 0.3) + ')'; + } css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)'; ionic.DomUtil.cachedStyles(ele, css); } - return { + var d = { run: function(step) { if (direction == 'forward') { - setStyles(enteringEle, 1, (1 - step) * 99); // starting at 98% prevents a flicker - setStyles(leavingEle, (1 - 0.1 * step), step * -33); + setStyles(enteringEle, 1, (1 - step) * 99, 1 - step); // starting at 98% prevents a flicker + setStyles(leavingEle, (1 - 0.1 * step), step * -33, -1); } else if (direction == 'back') { - setStyles(enteringEle, (1 - 0.1 * (1 - step)), (1 - step) * -33); - setStyles(leavingEle, 1, step * 100); + setStyles(enteringEle, (1 - 0.1 * (1 - step)), (1 - step) * -33, -1); + setStyles(leavingEle, 1, step * 100, 1 - step); } else { // swap, enter, exit - setStyles(enteringEle, 1, 0); - setStyles(leavingEle, 0, 0); + setStyles(enteringEle, 1, 0, -1); + setStyles(leavingEle, 0, 0, -1); } }, - shouldAnimate: shouldAnimate + shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back') }; + + return d; }; provider.transitions.navBar.ios = function(enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) { - shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back'); function setStyles(ctrl, opacity, titleX, backTextX) { var css = {}; - css[ionic.CSS.TRANSITION_DURATION] = shouldAnimate ? '' : 0; + css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0; css.opacity = opacity === 1 ? '' : opacity; ctrl.setCss('buttons-left', css); @@ -410,11 +418,11 @@ IonicModule setStyles(ctrlA, 1 - step, titleX, 0); } - return { + var d = { run: function(step) { var enteringHeaderCtrl = enteringHeaderBar.controller(); var leavingHeaderCtrl = leavingHeaderBar && leavingHeaderBar.controller(); - if (direction == 'back') { + if (d.direction == 'back') { leave(enteringHeaderCtrl, leavingHeaderCtrl, 1 - step); enter(leavingHeaderCtrl, enteringHeaderCtrl, 1 - step); } else { @@ -422,8 +430,11 @@ IonicModule leave(leavingHeaderCtrl, enteringHeaderCtrl, step); } }, - shouldAnimate: shouldAnimate + direction: direction, + shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back') }; + + return d; }; @@ -435,12 +446,12 @@ IonicModule function setStyles(ele, x) { var css = {}; - css[ionic.CSS.TRANSITION_DURATION] = shouldAnimate ? '' : 0; + css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0; css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)'; ionic.DomUtil.cachedStyles(ele, css); } - return { + var d = { run: function(step) { if (direction == 'forward') { setStyles(enteringEle, (1 - step) * 99); // starting at 98% prevents a flicker @@ -458,10 +469,11 @@ IonicModule }, shouldAnimate: shouldAnimate }; + + return d; }; provider.transitions.navBar.android = function(enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) { - shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back'); function setStyles(ctrl, opacity) { if (!ctrl) return; @@ -480,7 +492,7 @@ IonicModule setStyles(enteringHeaderBar.controller(), step); setStyles(leavingHeaderBar && leavingHeaderBar.controller(), 1 - step); }, - shouldAnimate: true + shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back') }; }; @@ -492,7 +504,8 @@ IonicModule return { run: function(step) { provider.transitions.views.android(enteringEle, leavingEle, false, false).run(step); - } + }, + shouldAnimate: false }; }; @@ -501,7 +514,8 @@ IonicModule run: function(step) { provider.transitions.navBar.ios(enteringHeaderBar, leavingHeaderBar, false, false).run(step); provider.transitions.navBar.android(enteringHeaderBar, leavingHeaderBar, false, false).run(step); - } + }, + shouldAnimate: false }; }; diff --git a/js/angular/service/scrollDelegate.js b/js/angular/service/scrollDelegate.js index e5f38739f84..f4f179a2f11 100644 --- a/js/angular/service/scrollDelegate.js +++ b/js/angular/service/scrollDelegate.js @@ -130,12 +130,19 @@ IonicModule * @param {boolean=} shouldAnimate Whether the scroll should animate. */ 'anchorScroll', + /** + * @ngdoc method + * @name $ionicScrollDelegate#freezeScroll + * @description Does not allow the scrollView to scroll in either x or y. + * @returns {object} If the scroll view is being prevented from scrolling or not. + */ + 'freezeScroll', /** * @ngdoc method * @name $ionicScrollDelegate#getScrollView * @returns {object} The scrollView associated with this delegate. */ - 'getScrollView', + 'getScrollView' /** * @ngdoc method * @name $ionicScrollDelegate#$getByHandle diff --git a/js/angular/service/viewSwitcher.js b/js/angular/service/viewSwitcher.js index 9ab2c45a7dc..2af4698a032 100644 --- a/js/angular/service/viewSwitcher.js +++ b/js/angular/service/viewSwitcher.js @@ -33,10 +33,11 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe var isActiveTimer; var cachedAttr = ionic.DomUtil.cachedAttr; var transitionPromises = []; + var defaultTimeout = 1100; var ionicViewSwitcher = { - create: function(navViewCtrl, viewLocals, enteringView, leavingView) { + create: function(navViewCtrl, viewLocals, enteringView, leavingView, renderStart, renderEnd) { // get a reference to an entering/leaving element if they exist // loop through to see if the view is already in the navViewElement var enteringEle, leavingEle; @@ -56,11 +57,12 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe }, loadViewElements: function(registerData) { - var viewEle, viewElements = navViewCtrl.getViewElements(); + var x, l, viewEle; + var viewElements = navViewCtrl.getViewElements(); var enteringEleIdentifier = getViewElementIdentifier(viewLocals, enteringView); var navViewActiveEleId = navViewCtrl.activeEleId(); - for (var x = 0, l = viewElements.length; x < l; x++) { + for (x = 0, l = viewElements.length; x < l; x++) { viewEle = viewElements.eq(x); if (viewEle.data(DATA_ELE_IDENTIFIER) === enteringEleIdentifier) { @@ -92,7 +94,9 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe enteringEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier); } - navViewCtrl.activeEleId(enteringEleIdentifier); + if (renderEnd) { + navViewCtrl.activeEleId(enteringEleIdentifier); + } registerData.ele = null; }, @@ -143,15 +147,15 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe callback && callback(); }, - transition: function(direction, enableBack) { - var deferred = $q.defer(); - transitionPromises.push(deferred.promise); - + transition: function(direction, enableBack, allowAnimate) { + var deferred; var enteringData = getTransitionData(viewLocals, enteringEle, direction, enteringView); var leavingData = extend(extend({}, enteringData), getViewData(leavingView)); enteringData.transitionId = leavingData.transitionId = transitionId; enteringData.fromCache = !!alreadyInDom; enteringData.enableBack = !!enableBack; + enteringData.renderStart = renderStart; + enteringData.renderEnd = renderEnd; cachedAttr(enteringEle.parent(), 'nav-view-transition', enteringData.transition); cachedAttr(enteringEle.parent(), 'nav-view-direction', enteringData.direction); @@ -159,42 +163,87 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe // cancel any previous transition complete fallbacks $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); - switcher.emit('before', enteringData, leavingData); - - // 1) get the transition ready and see if it'll animate + // get the transition ready and see if it'll animate var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none; - var viewTransition = transitionFn(enteringEle, leavingEle, enteringData.direction, enteringData.shouldAnimate); + var viewTransition = transitionFn(enteringEle, leavingEle, enteringData.direction, + enteringData.shouldAnimate && allowAnimate && renderEnd); if (viewTransition.shouldAnimate) { - // 2) attach transitionend events (and fallback timer) + // attach transitionend events (and fallback timer) enteringEle.on(TRANSITIONEND_EVENT, transitionComplete); - enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, 1000)); - $ionicClickBlock.show(); + enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, defaultTimeout)); + $ionicClickBlock.show(defaultTimeout); + } + + if (renderStart) { + // notify the views "before" the transition starts + switcher.emit('before', enteringData, leavingData); + + // stage entering element, opacity 0, no transition duration + navViewAttr(enteringEle, VIEW_STATUS_STAGED); + + // render the elements in the correct location for their starting point + viewTransition.run(0); + } + + if (renderEnd) { + // create a promise so we can keep track of when all transitions finish + // only required if this transition should complete + deferred = $q.defer(); + transitionPromises.push(deferred.promise); } - // 3) stage entering element, opacity 0, no transition duration - navViewAttr(enteringEle, VIEW_STATUS_STAGED); + if (renderStart && renderEnd) { + // CSS "auto" transitioned, not manually transitioned + // wait a frame so the styles apply before auto transitioning + $timeout(onReflow, 16); + + } else if (!renderEnd) { + // just the start of a manual transition + // but it will not render the end of the transition + navViewAttr(enteringEle, 'entering'); + navViewAttr(leavingEle, 'leaving'); + + // return the transition run method so each step can be ran manually + return { + run: viewTransition.run, + cancel: function(shouldAnimate) { + if (shouldAnimate) { + enteringEle.on(TRANSITIONEND_EVENT, cancelTransition); + enteringEle.data(DATA_FALLBACK_TIMER, $timeout(cancelTransition, defaultTimeout)); + $ionicClickBlock.show(defaultTimeout); + } else { + cancelTransition(); + } + viewTransition.shouldAnimate = shouldAnimate; + viewTransition.run(0); + viewTransition = null; + } + }; - // 4) place the elements in the correct step to begin - viewTransition.run(0); + } else if (renderEnd) { + // just the end of a manual transition + // happens after the manual transition has completed + // and a full history change has happened + onReflow(); + } - // 5) wait a frame so the styles apply - $timeout(onReflow, 16); function onReflow() { - // 6) remove that we're staging the entering element so it can transition + // remove that we're staging the entering element so it can auto transition navViewAttr(enteringEle, viewTransition.shouldAnimate ? 'entering' : VIEW_STATUS_ACTIVE); navViewAttr(leavingEle, viewTransition.shouldAnimate ? 'leaving' : VIEW_STATUS_CACHED); - // 7) start the transition + // start the auto transition and let the CSS take over viewTransition.run(1); + // trigger auto transitions on the associated nav bars $ionicNavBarDelegate._instances.forEach(function(instance) { instance.triggerTransitionStart(transitionId); }); if (!viewTransition.shouldAnimate) { - // no animated transition + // no animated auto transition transitionComplete(); } } @@ -207,20 +256,21 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); leavingEle && $timeout.cancel(leavingEle.data(DATA_FALLBACK_TIMER)); - // 8) emit that the views have finished transitioning + // emit that the views have finished transitioning // each parent nav-view will update which views are active and cached switcher.emit('after', enteringData, leavingData); - // 9) resolve that this one transition (there could be many w/ nested views) - deferred.resolve(navViewCtrl); + // resolve that this one transition (there could be many w/ nested views) + deferred && deferred.resolve(navViewCtrl); - // 10) the most recent transition added has completed and all the active + // the most recent transition added has completed and all the active // transition promises should be added to the services array of promises if (transitionId === transitionCounter) { $q.all(transitionPromises).then(ionicViewSwitcher.transitionEnd); switcher.cleanup(enteringData); } + // tell the nav bars that the transition has ended $ionicNavBarDelegate._instances.forEach(function(instance) { instance.triggerTransitionEnd(); }); @@ -229,6 +279,14 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe nextTransition = nextDirection = enteringView = leavingView = enteringEle = leavingEle = null; } + function cancelTransition() { + navViewAttr(enteringEle, VIEW_STATUS_CACHED); + navViewAttr(leavingEle, VIEW_STATUS_ACTIVE); + enteringEle.off(TRANSITIONEND_EVENT, cancelTransition); + $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); + ionicViewSwitcher.transitionEnd([navViewCtrl]); + } + }, emit: function(step, enteringData, leavingData) { @@ -295,7 +353,7 @@ function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDe }, transitionEnd: function(navViewCtrls) { - forEach(navViewCtrls, function(navViewCtrl){ + forEach(navViewCtrls, function(navViewCtrl) { navViewCtrl.transitionEnd(); }); diff --git a/js/utils/activator.js b/js/utils/activator.js index bc0069c80ed..84f1c7a13c0 100644 --- a/js/utils/activator.js +++ b/js/utils/activator.js @@ -11,6 +11,11 @@ start: function(e) { var self = this; + var hitX = ionic.tap.pointerCoord(e).x; + if (hitX > 0 && hitX < 45) { + return; + } + // when an element is touched/clicked, it climbs up a few // parents to see if it is an .item or .button element ionic.requestAnimationFrame(function() { diff --git a/js/views/scrollView.js b/js/views/scrollView.js index 6c397d3aa19..d3f3a973bd2 100644 --- a/js/views/scrollView.js +++ b/js/views/scrollView.js @@ -377,6 +377,8 @@ ionic.views.Scroll = ionic.views.View.inherit({ // The ms interval for triggering scroll events scrollEventInterval: 10, + freeze: false, + getContentWidth: function() { return Math.max(self.__content.scrollWidth, self.__content.offsetWidth); }, @@ -742,7 +744,7 @@ ionic.views.Scroll = ionic.views.View.inherit({ }; self.touchMove = function(e) { - if (!self.__isDown || + if (self.options.freeze || !self.__isDown || (!self.__isDown && e.defaultPrevented) || (e.target.tagName === 'TEXTAREA' && e.target.parentElement.querySelector(':focus')) ) { return; @@ -843,7 +845,7 @@ ionic.views.Scroll = ionic.views.View.inherit({ }; self.mouseMove = function(e) { - if (!mousedown || (!mousedown && e.defaultPrevented)) { + if (self.options.freeze || !mousedown || (!mousedown && e.defaultPrevented)) { return; } @@ -935,13 +937,13 @@ ionic.views.Scroll = ionic.views.View.inherit({ delete self.__indicatorY; delete self.options.el; - self.__callback = self.scrollChildIntoView = self.resetScrollView = angular.noop; + self.__callback = self.scrollChildIntoView = self.resetScrollView = NOOP; self.mouseMove = self.mouseDown = self.mouseUp = self.mouseWheel = - self.touchStart = self.touchMove = self.touchEnd = self.touchCancel = angular.noop; + self.touchStart = self.touchMove = self.touchEnd = self.touchCancel = NOOP; self.resize = self.scrollTo = self.zoomTo = - self.__scrollingComplete = angular.noop; + self.__scrollingComplete = NOOP; container = null; }, diff --git a/scss/_transitions.scss b/scss/_transitions.scss index fc212b0c057..35881d8b38e 100644 --- a/scss/_transitions.scss +++ b/scss/_transitions.scss @@ -13,8 +13,8 @@ $ios-transition-container-bg-color: #000 !default; [nav-view="leaving"] { @include transition-duration( $ios-transition-duration ); @include transition-timing-function( $ios-transition-timing-function ); - -webkit-transition-property: opacity, -webkit-transform; - transition-property: opacity, transform; + -webkit-transition-property: opacity, -webkit-transform, box-shadow; + transition-property: opacity, transform, box-shadow; } &[nav-view-direction="forward"], @@ -138,6 +138,31 @@ $android-transition-timing-function: cubic-bezier(0.4, 0.6, 0.2, 1) !defaul +// Nav Swipe +// ------------------------------- + +[nav-swipe="fast"] { + [nav-view], + .title, + .buttons, + .back-text { + @include transition-duration(50ms); + @include transition-timing-function(linear); + } +} + +[nav-swipe="slow"] { + [nav-view], + .title, + .buttons, + .back-text { + @include transition-duration(160ms); + @include transition-timing-function(linear); + } +} + + + // Transition Settings // ------------------------------- diff --git a/scss/_util.scss b/scss/_util.scss index 30b17fdcd4a..ca7326b4b6a 100644 --- a/scss/_util.scss +++ b/scss/_util.scss @@ -65,12 +65,13 @@ .click-block { position: absolute; top: 0; + right: 0; + bottom: 0; left: 0; - z-index: $z-index-click-block; - width: 100%; - height: 100%; opacity: 0; + z-index: $z-index-click-block; @include translate3d(0, 0, 0); + overflow: hidden; } .click-block-hide { @include translate3d(-9999px, 0, 0);