├── .gitignore ├── README.md ├── adapters ├── NativePageTransitionsIonicAdapter.js ├── NativePageTransitionsIonicAdapter.min.js ├── NativePageTransitionsKendoAdapter.js └── NativePageTransitionsKendoAdapter.min.js ├── demo ├── index.html ├── jqmdemo.html ├── kendouidemo.html └── standalone.html ├── package.json ├── plugin.xml ├── src ├── android │ ├── NativePageTransitions.java │ └── lib │ │ ├── AnimationFactory.java │ │ └── FlipAnimation.java ├── ios │ ├── AppDelegate+nativepagetransitions.h │ ├── AppDelegate+nativepagetransitions.m │ ├── NativePageTransitions.h │ └── NativePageTransitions.m └── winphone │ └── NativePageTransitions.cs └── www └── NativePageTransitions.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Native Page Transitions Cordova / PhoneGap Plugin 2 | by [Telerik](http://www.telerik.com) 3 | 4 | > **WARNING**: This plugin is no longer maintained, and we now recommend using [NativeScript](https://www.nativescript.org/) as you get native transitions (and UI) out of the box. 5 | 6 | Using the Cordova CLI? 7 | 8 | ``` 9 | cordova plugin add com.telerik.plugins.nativepagetransitions 10 | ``` 11 | 12 | Using PGB? 13 | 14 | ```xml 15 | 16 | ``` 17 | 18 | [The MIT License (MIT)](http://www.opensource.org/licenses/mit-license.html) 19 | -------------------------------------------------------------------------------- /adapters/NativePageTransitionsIonicAdapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class automatically wires up your Ionic Framework project 3 | * to the Native Page Transitions plugin. 4 | * 5 | * 6 | * We scan the code on deviceready for any animation tags. 7 | * We expect a direction as well: animation="slide-left-right" 8 | * Slide default: left, Flip default: right. 9 | * 10 | * If you specify a default transitions, we will use that as the default as expected: 11 | * 12 | * 13 | * Prevent anchors ( tags) or back buttons from being auto-enhanced by adding: 14 | * animation-native="false" to the tag: 15 | * 16 | * (untested feature:) To add a delay for ios or android, add: 17 | * animation-native-androiddelay="200" to the tag (200 ms for android in this case) 18 | * 19 | * TODO: add attributes for things like duration and slowdownfactor 20 | * 21 | * 22 | ************************************************************************************ 23 | * PRO TIP: specify details in the $ionicPlatform.ready function of app.js: 24 | * 25 | * window.plugins.nativepagetransitions.globalOptions.duration = 350; 26 | * window.plugins.nativepagetransitions.globalOptions.slowdownfactor = 8; 27 | * // window.plugins.nativepagetransitions.globalOptions.fixedPixelsTop = 64; 28 | * window.plugins.nativepagetransitions.globalOptions.fixedPixelsBottom = 48; 29 | * 30 | */ 31 | 32 | (function () { 33 | 34 | "use strict"; 35 | 36 | var transitionStack = []; 37 | var defaultTransition = null; 38 | 39 | // poor mans autowiring trigger.. we need dominserted stuff for enhancing dynamic links 40 | ionic.on("click", function (event) { 41 | // timeout because the dom needs to update before the buttons can be enhanced 42 | setTimeout(function () { 43 | window.NativePageTransitionsIonicAdapter.apply(); 44 | }, 400); 45 | }); 46 | 47 | var NativePageTransitionsIonicAdapter = function () { 48 | window.NativePageTransitionsIonicAdapter = this; 49 | }; 50 | 51 | NativePageTransitionsIonicAdapter.prototype = { 52 | 53 | enhanceBackbuttons: function (from) { 54 | var backbuttons = document.querySelectorAll('button.back-button'); 55 | for (var j = 0; j < backbuttons.length; j++) { 56 | var backbutton = backbuttons[j]; 57 | if (backbutton.getAttribute("animation-native") !== "false") { 58 | var transition = transitionStack.pop() || "slide:right"; // default, when stack is empty 59 | var direction = transition.substring(transition.indexOf(":") + 1); 60 | if (transition.indexOf("flip") > -1) { 61 | backbutton.setAttribute("onclick", 'event.preventDefault ? event.preventDefault() : event.returnValue = false;'); 62 | backbutton.setAttribute("ontouchend", 'event.preventDefault ? event.preventDefault() : event.returnValue = false; setTimeout(function(){window.kendo.mobile.application.pane.navigate("#:back")},20); window.NativePageTransitionsIonicAdapter.flip(\'' + direction + '\', \'' + from + '\', \'' + 100 + '\', \'' + 100 + '\')'); 63 | } else { 64 | backbutton.setAttribute("onclick", 'event.stopPropagation(); event.preventDefault ? event.preventDefault() : event.returnValue = false;'); 65 | backbutton.setAttribute("ontouchend", 'event.preventDefault ? event.preventDefault() : event.returnValue = false; window.NativePageTransitionsIonicAdapter.slide(\'' + direction + '\', \'' + from + '\', \'140\', \'140\')'); 66 | } 67 | } 68 | } 69 | }, 70 | 71 | apply: function () { 72 | if (defaultTransition == null) { 73 | defaultTransition = document.body.getAttribute("animation"); 74 | if (defaultTransition == null) { 75 | defaultTransition = "none"; 76 | } 77 | } 78 | 79 | var transAnchors; 80 | if (defaultTransition == "none") { 81 | // if there is no default, we only need to enhance the specific tags 82 | transAnchors = document.querySelectorAll("a[animation]"); 83 | } else { 84 | // if there is a default, enhance all tags (except backbuttons and data-rel's (like modalview)), and honor the specific overrides if they exist 85 | transAnchors = document.querySelectorAll('a[href]:not([data-rel])'); 86 | // add an animation attribute to all anchors without one, so the processing below is uniform 87 | for (var t = 0; t < transAnchors.length; t++) { 88 | var theAnchor = transAnchors[t]; 89 | // exclude and links with window.open 90 | var lowerhref = theAnchor.getAttribute('href').toLowerCase(); 91 | if (lowerhref.indexOf("window.open") == -1 && lowerhref.indexOf("url.loadurl") == -1) { 92 | if (!theAnchor.hasAttribute("animation")) { 93 | theAnchor.setAttribute("animation", defaultTransition); 94 | } 95 | } 96 | } 97 | } 98 | for (var i = 0; i < transAnchors.length; i++) { 99 | var transAnchor = transAnchors[i]; 100 | if (transAnchor.getAttribute("animation-native") !== "false") { 101 | var transition = transAnchor.getAttribute("animation"); 102 | if (transition != null && transition != "none") { 103 | var href = transAnchor.getAttribute("href"); 104 | 105 | var androiddelay = transAnchor.getAttribute("animation-native-androiddelay"); 106 | if (androiddelay == null) { 107 | androiddelay = 100; 108 | } 109 | var iosdelay = transAnchor.getAttribute("animation-native-iosdelay"); 110 | if (iosdelay == null) { 111 | iosdelay = 100; 112 | } 113 | 114 | if (transition.indexOf("slide") > -1) { 115 | this._addSlideEvent(transAnchor, transition, href, androiddelay, iosdelay); 116 | } else if (transition.indexOf("flip") > -1) { 117 | this._addFlipEvent(transAnchor, transition, href, androiddelay, iosdelay); 118 | } else { 119 | // unsupported transition for now, so leave it be 120 | continue; 121 | } 122 | // removing these will prevent these element to be processed again in this lifecycle 123 | transAnchor.removeAttribute("animation"); 124 | if (href != null) { 125 | transAnchor.removeAttribute("href"); 126 | } 127 | } 128 | } 129 | } 130 | }, 131 | 132 | _addSlideEvent: function (transAnchor, transition, href, androiddelay, iosdelay) { 133 | var direction = "left"; 134 | if (transition.indexOf("slide-right") > -1) { 135 | direction = "right"; 136 | } else if (transition.indexOf("slide-up") > -1) { 137 | direction = "up"; 138 | } else if (transition.indexOf("slide-down") > -1) { 139 | direction = "down"; 140 | } 141 | transAnchor.setAttribute("onclick", 'window.NativePageTransitionsIonicAdapter.slide(\'' + direction + '\', \'' + href + '\', \'' + androiddelay + '\', \'' + iosdelay + '\')'); 142 | }, 143 | 144 | _addFlipEvent: function (transAnchor, transition, href, androiddelay, iosdelay) { 145 | var direction = "left"; 146 | if (transition.indexOf("flip-right") > -1) { 147 | direction = "right"; 148 | } else if (transition.indexOf("flip-up") > -1) { 149 | direction = "up"; 150 | } else if (transition.indexOf("flip-down") > -1) { 151 | direction = "down"; 152 | } 153 | transAnchor.setAttribute("onclick", 'window.NativePageTransitionsIonicAdapter.flip(\'' + direction + '\', \'' + href + '\', \'' + androiddelay + '\', \'' + iosdelay + '\')'); 154 | }, 155 | 156 | getOppositeDirection: function (direction) { 157 | if (direction == "right") { 158 | return "left"; 159 | } else if (direction == "up") { 160 | return "down"; 161 | } else if (direction == "down") { 162 | return "up"; 163 | } else { 164 | return "right"; 165 | } 166 | }, 167 | 168 | slide: function (direction, href, androiddelay, iosdelay) { 169 | event.preventDefault ? event.preventDefault() : event.returnValue = false; 170 | transitionStack.push("slide:" + this.getOppositeDirection(direction)); 171 | window.plugins.nativepagetransitions.slide({ 172 | 'direction': direction, 173 | 'androiddelay': androiddelay, 174 | 'iosdelay': iosdelay, 175 | // 'winphonedelay': winphonedelay, 176 | 'href': href 177 | }, 178 | function () { 179 | console.log('slide transition finished'); 180 | }, 181 | function (errmsg) { 182 | console.log('slide transition failed: ' + errmsg); 183 | }); 184 | }, 185 | 186 | flip: function (direction, href, androiddelay, iosdelay) { 187 | event.preventDefault ? event.preventDefault() : event.returnValue = false; 188 | transitionStack.push("flip:" + this.getOppositeDirection(direction)); 189 | window.plugins.nativepagetransitions.flip({ 190 | 'direction': direction, 191 | 'androiddelay': androiddelay, 192 | 'iosdelay': iosdelay, 193 | // 'winphonedelay': winphonedelay, 194 | 'href': href 195 | }, 196 | function () { 197 | console.log('flip transition finished'); 198 | }, 199 | function (errmsg) { 200 | console.log('flip transition failed: ' + errmsg); 201 | }); 202 | } 203 | }; 204 | 205 | var adapter = new NativePageTransitionsIonicAdapter(); 206 | 207 | // wait for cordova (and its plugins) to be ready 208 | document.addEventListener( 209 | "deviceready", 210 | function () { 211 | if (window.ionic && window.plugins && window.plugins.nativepagetransitions) { 212 | adapter.apply(); 213 | adapter.enhanceBackbuttons(); 214 | 215 | window.ionic.on("hashchange", function (event) { 216 | // timeout because the dom needs to update before the buttons can be enhanced 217 | var from = event.oldURL.substring(event.oldURL.indexOf("#")); 218 | setTimeout(function () { 219 | window.NativePageTransitionsIonicAdapter.enhanceBackbuttons(from); 220 | }, 100); 221 | }); 222 | 223 | } else { 224 | console.log("window.plugins.nativepagetransitions is not available, so no native transitions will be applied"); 225 | } 226 | }, 227 | false); 228 | })(); 229 | -------------------------------------------------------------------------------- /adapters/NativePageTransitionsIonicAdapter.min.js: -------------------------------------------------------------------------------- 1 | (function(){"use strict";var t=[],n=null,i,r;ionic.on("click",function(){setTimeout(function(){window.NativePageTransitionsIonicAdapter.apply()},400)});i=function(){window.NativePageTransitionsIonicAdapter=this};i.prototype={enhanceBackbuttons:function(n){for(var i,r,f,e=document.querySelectorAll("button.back-button"),u=0;u-1?(i.setAttribute("onclick","event.preventDefault ? event.preventDefault() : event.returnValue = false;"),i.setAttribute("ontouchend",'event.preventDefault ? event.preventDefault() : event.returnValue = false; setTimeout(function(){window.kendo.mobile.application.pane.navigate("#:back")},20); window.NativePageTransitionsIonicAdapter.flip(\''+f+"', '"+n+"', '100', '100')")):(i.setAttribute("onclick","event.stopPropagation(); event.preventDefault ? event.preventDefault() : event.returnValue = false;"),i.setAttribute("ontouchend","event.preventDefault ? event.preventDefault() : event.returnValue = false; window.NativePageTransitionsIonicAdapter.slide('"+f+"', '"+n+"', '140', '140')")))},apply:function(){var r,e,o,c,s,t,i,h,u,f;if(n==null&&(n=document.body.getAttribute("animation"),n==null&&(n="none")),n=="none")r=document.querySelectorAll("a[animation]");else for(r=document.querySelectorAll("a[href]:not([data-rel])"),e=0;e-1)this._addSlideEvent(t,i,h,u,f);else if(i.indexOf("flip")>-1)this._addFlipEvent(t,i,h,u,f);else continue;t.removeAttribute("animation");h!=null&&t.removeAttribute("href")}},_addSlideEvent:function(n,t,i,r,u){var f="left";t.indexOf("slide-right")>-1?f="right":t.indexOf("slide-up")>-1?f="up":t.indexOf("slide-down")>-1&&(f="down");n.setAttribute("onclick","window.NativePageTransitionsIonicAdapter.slide('"+f+"', '"+i+"', '"+r+"', '"+u+"')")},_addFlipEvent:function(n,t,i,r,u){var f="left";t.indexOf("flip-right")>-1?f="right":t.indexOf("flip-up")>-1?f="up":t.indexOf("flip-down")>-1&&(f="down");n.setAttribute("onclick","window.NativePageTransitionsIonicAdapter.flip('"+f+"', '"+i+"', '"+r+"', '"+u+"')")},getOppositeDirection:function(n){return n=="right"?"left":n=="up"?"down":n=="down"?"up":"right"},slide:function(n,i,r,u){event.preventDefault?event.preventDefault():event.returnValue=!1;t.push("slide:"+this.getOppositeDirection(n));window.plugins.nativepagetransitions.slide({direction:n,androiddelay:r,iosdelay:u,href:i},function(){console.log("slide transition finished")},function(n){console.log("slide transition failed: "+n)})},flip:function(n,i,r,u){event.preventDefault?event.preventDefault():event.returnValue=!1;t.push("flip:"+this.getOppositeDirection(n));window.plugins.nativepagetransitions.flip({direction:n,androiddelay:r,iosdelay:u,href:i},function(){console.log("flip transition finished")},function(n){console.log("flip transition failed: "+n)})}};r=new i;document.addEventListener("deviceready",function(){if(window.ionic&&window.plugins&&window.plugins.nativepagetransitions){r.apply();r.enhanceBackbuttons();window.ionic.on("hashchange",function(n){var t=n.oldURL.substring(n.oldURL.indexOf("#"));setTimeout(function(){window.NativePageTransitionsIonicAdapter.enhanceBackbuttons(t)},100)})}else console.log("window.plugins.nativepagetransitions is not available, so no native transitions will be applied")},!1)})(); -------------------------------------------------------------------------------- /adapters/NativePageTransitionsKendoAdapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class automatically wires up your KendoUI Mobile project 3 | * to the Native Page Transitions plugin. 4 | * 5 | * We scan the code on deviceready for any data-transition tags. 6 | * We expect a direction as well: data-transition="slide:left" 7 | * If no direction is set, a default is used: data-transition="slide". 8 | * Slide default: left, Flip default: right. 9 | * 10 | * If you specify a default transitions, we will use that as the default as expected: 11 | * new kendo.mobile.Application(document.body, {transition: 'slide'}); 12 | * 13 | * Prevent anchors ( tags) from being auto-enhanced by adding: 14 | * data-transition-native="false" to the tag. 15 | * 16 | * To add a delay for ios or android, add: 17 | * data-transition-native-androiddelay="200" to the tag (200 ms for android in this case) 18 | * 19 | * To enable transitions for links in remote views, you must add the data-transition attribute to those links. 20 | * 21 | * TODO: add data- attributes for things like duration and slowdownfactor 22 | * TODO: auto-enhance drawers based on data-rel="drawer" & data-align="right" 23 | */ 24 | 25 | (function() { 26 | 27 | "use strict"; 28 | 29 | var transitionStack = []; 30 | var defaultTransition = null; 31 | 32 | window.addEventListener('hashchange', function(hashchangeevent) { 33 | // timeout because the dom needs to update before the backbutton can be enhanced 34 | setTimeout(function() { 35 | window.NativePageTransitionsKendoAdapter.enhanceBackbuttons(); 36 | }, 100); 37 | }); 38 | 39 | var NativePageTransitionsKendoAdapter = function() { 40 | window.NativePageTransitionsKendoAdapter = this; 41 | }; 42 | 43 | NativePageTransitionsKendoAdapter.prototype = { 44 | 45 | enhanceBackbuttons : function() { 46 | // find all views.. 47 | var backbuttonViews = document.querySelectorAll('div[data-role="view"]'); 48 | for (var i = 0; i < backbuttonViews.length; i++) { 49 | var backbuttonView = backbuttonViews[i]; 50 | // find the view which is currently showing (not hidden) 51 | if (backbuttonView.style.display != "none") { 52 | var backbuttons = backbuttonView.querySelectorAll('a[data-role="backbutton"]:not([data-rel])'); 53 | for (var j = 0; j < backbuttons.length; j++) { 54 | var backbutton = backbuttons[j]; 55 | var href = backbutton.getAttribute("href"); 56 | if (href != null && backbutton.getAttribute("data-transition-native") !== "false") { 57 | var transition = transitionStack.pop() || "slide:right"; 58 | if (href == "#:back") { 59 | if (transition.indexOf("flip") > -1) { 60 | backbutton.setAttribute("onclick", 'event.preventDefault ? event.preventDefault() : event.returnValue = false;'); 61 | backbutton.setAttribute("ontouchend", 'event.preventDefault ? event.preventDefault() : event.returnValue = false; setTimeout(function(){window.kendo.mobile.application.pane.navigate("#:back")},20); window.NativePageTransitionsKendoAdapter.flip(\'right\', null, \'' + 100 + '\', \'' + 100 + '\')'); 62 | } else { 63 | backbutton.setAttribute("onclick", 'event.preventDefault ? event.preventDefault() : event.returnValue = false;'); 64 | backbutton.setAttribute("ontouchend", 'event.preventDefault ? event.preventDefault() : event.returnValue = false; setTimeout(function(){window.kendo.mobile.application.pane.navigate("#:back")},20); window.NativePageTransitionsKendoAdapter.slide(\'right\', null, \'' + 140 + '\', \'' + 140 + '\')'); 65 | } 66 | } else { 67 | // this branch is for remote views 68 | if (href.indexOf("#") == -1) { 69 | href = "#" + href; 70 | } 71 | if (transition.indexOf("flip") > -1) { 72 | backbutton.setAttribute("onclick", 'event.preventDefault ? event.preventDefault() : event.returnValue = false;'); 73 | backbutton.setAttribute("ontouchend", 'event.preventDefault ? event.preventDefault() : event.returnValue = false; window.NativePageTransitionsKendoAdapter.flip(\'right\', \''+href+'\', \'' + 100 + '\', \'' + 100 + '\')'); 74 | } else { 75 | backbutton.setAttribute("onclick", 'event.preventDefault ? event.preventDefault() : event.returnValue = false;'); 76 | backbutton.setAttribute("ontouchend", 'event.preventDefault ? event.preventDefault() : event.returnValue = false; window.NativePageTransitionsKendoAdapter.slide(\'right\', \''+href+'\', \'' + 140 + '\', \'' + 140 + '\')'); 77 | } 78 | } 79 | } 80 | } 81 | return false; // found the right view, so break the loop 82 | } 83 | } 84 | }, 85 | 86 | apply : function () { 87 | if (this._checkPluginLoaded() && window.kendo.mobile.application) { 88 | 89 | if (defaultTransition == null) { 90 | // figure out the default transition and use that as our default 91 | defaultTransition = window.kendo.mobile.application.options.transition; 92 | if (defaultTransition == "") { 93 | defaultTransition = "none"; 94 | } 95 | // make sure the Kendo transitions don't interfere with ours by disabling them 96 | window.kendo.effects.enabled = false; 97 | } 98 | 99 | // hijack programmatic navigation 100 | if (!window.originalAppNavigate) { 101 | window.originalAppNavigate = window.app.navigate; 102 | window.app.navigate = function (href, transition) { 103 | if (href.charAt(0) !== '#') { 104 | //if remote view with no # prefix, prepend it 105 | href = '#' + href; 106 | } 107 | if (transition === undefined) { 108 | transition = defaultTransition; 109 | } 110 | if (transition === undefined) { 111 | transition = "slide"; 112 | } 113 | if (transition.indexOf("flip") > -1) { 114 | var direction = "right"; // define a default 115 | if (transition.indexOf("flip:") > -1) { 116 | direction = transition.substring(5); 117 | } 118 | window.NativePageTransitionsKendoAdapter.flip(direction, href); 119 | } else if (transition.indexOf("slide") > -1) { 120 | var direction = "left"; // define a default 121 | if (transition.indexOf("slide:") > -1) { 122 | direction = transition.substring(6); 123 | } 124 | window.NativePageTransitionsKendoAdapter.slide(direction, href); 125 | } else { 126 | // unsupported by the adapter, invoke the original function 127 | window.originalAppNavigate(href, transition); 128 | } 129 | } 130 | } 131 | 132 | // enhance tags 133 | var transAnchors; 134 | if (defaultTransition == "none") { 135 | // if there is no default, we only need to enhance the specific tags 136 | transAnchors = document.querySelectorAll("a[data-transition]"); 137 | } else { 138 | // if there is a default, enhance all tags (except backbuttons and data-rel's (like modalview)), and honor the specific overrides if they exist 139 | transAnchors = document.querySelectorAll('a[href]:not([data-role="backbutton"]):not([data-rel])'); 140 | // add a data-transition attribute to all anchors without one, so the processing below is uniform 141 | for (var t = 0; t < transAnchors.length; t++) { 142 | var theAnchor = transAnchors[t]; 143 | // exclude and links with window.open 144 | var lowerhref = theAnchor.getAttribute('href').toLowerCase(); 145 | if (lowerhref.indexOf("window.open") == -1 && lowerhref.indexOf("url.loadurl") == -1) { 146 | if (!theAnchor.hasAttribute("data-transition")) { 147 | theAnchor.setAttribute("data-transition", defaultTransition); 148 | } 149 | } 150 | } 151 | } 152 | for (var i = 0; i < transAnchors.length; i++) { 153 | var transAnchor = transAnchors[i]; 154 | if (transAnchor.getAttribute("data-transition-native") !== "false") { 155 | var transition = transAnchor.getAttribute("data-transition"); 156 | if (transition != null && transition != "none") { 157 | var href = transAnchor.getAttribute("href"); 158 | 159 | if (href != null) { 160 | // Kendo remote view support 161 | if (href.indexOf("#") == -1 && href.indexOf(".") > -1) { 162 | href = "#" + href; 163 | } 164 | } 165 | 166 | var androiddelay = transAnchor.getAttribute("data-transition-native-androiddelay"); 167 | var iosdelay = transAnchor.getAttribute("data-transition-native-iosdelay"); 168 | 169 | if (transition.indexOf("slide") > -1) { 170 | this._addSlideEvent(transAnchor, transition, href, androiddelay, iosdelay); 171 | } else if (transition.indexOf("flip") > -1) { 172 | this._addFlipEvent(transAnchor, transition, href, androiddelay, iosdelay); 173 | } else { 174 | // unsupported transition for now, so leave it be 175 | continue; 176 | } 177 | // removing these will prevent these element to be processed again in this lifecycle 178 | if (href != null) { 179 | transAnchor.removeAttribute("href"); 180 | } 181 | transAnchor.removeAttribute("data-transition"); 182 | } 183 | } 184 | } 185 | } 186 | }, 187 | 188 | _addSlideEvent : function (transAnchor, transition, href, androiddelay, iosdelay) { 189 | var direction = "left"; // define a default 190 | if (transition.indexOf("slide:") > -1) { 191 | direction = transition.substring(6); 192 | } 193 | transAnchor.setAttribute("onclick", 'window.NativePageTransitionsKendoAdapter.slide(\'' + direction + '\', \'' + href + '\', \'' + androiddelay + '\', \'' + iosdelay + '\')'); 194 | }, 195 | 196 | _addFlipEvent : function (transAnchor, transition, href, androiddelay, iosdelay) { 197 | var direction = "right"; // define a default 198 | if (transition.indexOf("flip:") > -1) { 199 | direction = transition.substring(5); 200 | } 201 | transAnchor.setAttribute("onclick", 'window.NativePageTransitionsKendoAdapter.flip(\'' + direction + '\', \'' + href + '\', \'' + androiddelay + '\', \'' + iosdelay + '\')'); 202 | }, 203 | 204 | slide : function (direction, href, androiddelay, iosdelay) { 205 | event.preventDefault ? event.preventDefault() : event.returnValue = false; 206 | transitionStack.push("slide:" + (direction == 'left' ? 'right' : 'left')); 207 | window.plugins.nativepagetransitions.slide({ 208 | 'direction': direction, 209 | 'androiddelay': androiddelay, 210 | 'iosdelay': iosdelay, 211 | // 'winphonedelay': winphonedelay, 212 | 'href': href 213 | }, 214 | function () { 215 | console.log('slide transition finished'); 216 | }, 217 | function (errmsg) { 218 | console.log('slide transition failed: ' + errmsg); 219 | }); 220 | }, 221 | 222 | flip : function (direction, href, androiddelay, iosdelay) { 223 | event.preventDefault ? event.preventDefault() : event.returnValue = false; 224 | transitionStack.push("flip:" + (direction == 'right' ? 'left' : 'right')); 225 | window.plugins.nativepagetransitions.flip({ 226 | 'direction': direction, 227 | 'androiddelay': androiddelay, 228 | 'iosdelay': iosdelay, 229 | // 'winphonedelay': winphonedelay, 230 | 'href': href 231 | }, 232 | function () { 233 | console.log('flip transition finished'); 234 | }, 235 | function (errmsg) { 236 | console.log('flip transition failed: ' + errmsg); 237 | }); 238 | }, 239 | 240 | _checkPluginLoaded : function () { 241 | if (window.plugins && window.plugins.nativepagetransitions) { 242 | return true; 243 | } else { 244 | console.log("window.plugins.nativepagetransitions is not available, so no native transitions will be applied"); 245 | return false; 246 | } 247 | }, 248 | 249 | // inlined minified version of fastclick, needed because we bind to onclick which has a delay 250 | loadFastClick : function() { 251 | if (!window.FastClick && !window.Origami) { 252 | /* 253 | FastClick: polyfill to remove click delays on browsers with touch UIs. 254 | @version 1.0.3 255 | @codingstandard ftlabs-jsv2 256 | @copyright The Financial Times Limited [All Rights Reserved] 257 | @license MIT License 258 | */ 259 | (function e$$0(g,m,b){function h(f,k){if(!m[f]){if(!g[f]){var a="function"==typeof require&&require;if(!k&&a)return a(f,!0);if(e)return e(f,!0);a=Error("Cannot find module '"+f+"'");throw a.code="MODULE_NOT_FOUND",a;}a=m[f]={exports:{}};g[f][0].call(a.exports,function(a){var d=g[f][1][a];return h(d?d:a)},a,a.exports,e$$0,g,m,b)}return m[f].exports}for(var e="function"==typeof require&&require,k=0;kd.offsetHeight){c=d;a.fastClickScrollParent=d;break}d=d.parentElement}while(d)}c&&(c.fastClickLastScrollTop=c.scrollTop)};b.prototype.getTargetElementFromEventTarget=function(a){return a.nodeType=== 266 | Node.TEXT_NODE?a.parentNode:a};b.prototype.onTouchStart=function(a){var c,d,b;if(1c||Math.abs(a.pageY-this.touchStartY)>c?!0:!1};b.prototype.onTouchMove=function(a){if(!this.trackingClick)return!0;if(this.targetElement!==this.getTargetElementFromEventTarget(a.target)||this.touchHasMoved(a))this.trackingClick=!1,this.targetElement=null;return!0};b.prototype.findControl= 268 | function(a){return void 0!==a.control?a.control:a.htmlFor?document.getElementById(a.htmlFor):a.querySelector("button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea")};b.prototype.onTouchEnd=function(a){var c,d,b=this.targetElement;if(!this.trackingClick)return!0;if(a.timeStamp-this.lastClickTime 0) { 303 | var thisIndex = ++dispatchIndex; 304 | setTimeout(function () { 305 | // enhance the anchors if there is no newer pending event within this timeout 306 | if (dispatchIndex == thisIndex) { 307 | window.NativePageTransitionsKendoAdapter.apply(); 308 | } 309 | }, 20); 310 | } 311 | }, true); 312 | }, 313 | false); 314 | })(); 315 | -------------------------------------------------------------------------------- /adapters/NativePageTransitionsKendoAdapter.min.js: -------------------------------------------------------------------------------- 1 | (function(){"use strict";var t=[],n=null,i;window.addEventListener("hashchange",function(){setTimeout(function(){window.NativePageTransitionsKendoAdapter.enhanceBackbuttons()},100)});i=function(){window.NativePageTransitionsKendoAdapter=this};i.prototype={enhanceBackbuttons:function(){for(var f,e,r,n,i,o,s=document.querySelectorAll('div[data-role="view"]'),u=0;u-1?(n.setAttribute("onclick","event.preventDefault ? event.preventDefault() : event.returnValue = false;"),n.setAttribute("ontouchend","event.preventDefault ? event.preventDefault() : event.returnValue = false; setTimeout(function(){window.kendo.mobile.application.pane.navigate(\"#:back\")},20); window.NativePageTransitionsKendoAdapter.flip('right', null, '100', '100')")):(n.setAttribute("onclick","event.preventDefault ? event.preventDefault() : event.returnValue = false;"),n.setAttribute("ontouchend","event.preventDefault ? event.preventDefault() : event.returnValue = false; setTimeout(function(){window.kendo.mobile.application.pane.navigate(\"#:back\")},20); window.NativePageTransitionsKendoAdapter.slide('right', null, '140', '140')")):(i.indexOf("#")==-1&&(i="#"+i),o.indexOf("flip")>-1?(n.setAttribute("onclick","event.preventDefault ? event.preventDefault() : event.returnValue = false;"),n.setAttribute("ontouchend","event.preventDefault ? event.preventDefault() : event.returnValue = false; window.NativePageTransitionsKendoAdapter.flip('right', '"+i+"', '100', '100')")):(n.setAttribute("onclick","event.preventDefault ? event.preventDefault() : event.returnValue = false;"),n.setAttribute("ontouchend","event.preventDefault ? event.preventDefault() : event.returnValue = false; window.NativePageTransitionsKendoAdapter.slide('right', '"+i+"', '140', '140')"))));return!1}},apply:function(){var u,f,e,s,o,t,r,i,h,c;if(this._checkPluginLoaded()&&window.kendo.mobile.application){if(n==null&&(n=window.kendo.mobile.application.options.transition,n==""&&(n="none"),window.kendo.effects.enabled=!1),window.originalAppNavigate||(window.originalAppNavigate=window.app.navigate,window.app.navigate=function(t,i){var r;t.charAt(0)!=="#"&&(t="#"+t);i===undefined&&(i=n);i===undefined&&(i="slide");i.indexOf("flip")>-1?(r="right",i.indexOf("flip:")>-1&&(r=i.substring(5)),window.NativePageTransitionsKendoAdapter.flip(r,t)):i.indexOf("slide")>-1?(r="left",i.indexOf("slide:")>-1&&(r=i.substring(6)),window.NativePageTransitionsKendoAdapter.slide(r,t)):window.originalAppNavigate(t,i)}),n=="none")u=document.querySelectorAll("a[data-transition]");else for(u=document.querySelectorAll('a[href]:not([data-role="backbutton"]):not([data-rel])'),f=0;f-1&&(i="#"+i),h=t.getAttribute("data-transition-native-androiddelay"),c=t.getAttribute("data-transition-native-iosdelay"),r.indexOf("slide")>-1)this._addSlideEvent(t,r,i,h,c);else if(r.indexOf("flip")>-1)this._addFlipEvent(t,r,i,h,c);else continue;i!=null&&t.removeAttribute("href");t.removeAttribute("data-transition")}}},_addSlideEvent:function(n,t,i,r,u){var f="left";t.indexOf("slide:")>-1&&(f=t.substring(6));n.setAttribute("onclick","window.NativePageTransitionsKendoAdapter.slide('"+f+"', '"+i+"', '"+r+"', '"+u+"')")},_addFlipEvent:function(n,t,i,r,u){var f="right";t.indexOf("flip:")>-1&&(f=t.substring(5));n.setAttribute("onclick","window.NativePageTransitionsKendoAdapter.flip('"+f+"', '"+i+"', '"+r+"', '"+u+"')")},slide:function(n,i,r,u){event.preventDefault?event.preventDefault():event.returnValue=!1;t.push("slide:"+(n=="left"?"right":"left"));window.plugins.nativepagetransitions.slide({direction:n,androiddelay:r,iosdelay:u,href:i},function(){console.log("slide transition finished")},function(n){console.log("slide transition failed: "+n)})},flip:function(n,i,r,u){event.preventDefault?event.preventDefault():event.returnValue=!1;t.push("flip:"+(n=="right"?"left":"right"));window.plugins.nativepagetransitions.flip({direction:n,androiddelay:r,iosdelay:u,href:i},function(){console.log("flip transition finished")},function(n){console.log("flip transition failed: "+n)})},_checkPluginLoaded:function(){return window.plugins&&window.plugins.nativepagetransitions?!0:(console.log("window.plugins.nativepagetransitions is not available, so no native transitions will be applied"),!1)},loadFastClick:function(){if(!window.FastClick&&!window.Origami){ 2 | /* 3 | FastClick: polyfill to remove click delays on browsers with touch UIs. 4 | @version 1.0.3 5 | @codingstandard ftlabs-jsv2 6 | @copyright The Financial Times Limited [All Rights Reserved] 7 | @license MIT License 8 | */ 9 | (function n(t,i,r){function u(f,o){if(!i[f]){if(!t[f]){var s="function"==typeof require&&require;if(!o&&s)return s(f,!0);if(e)return e(f,!0);s=Error("Cannot find module '"+f+"'");throw s.code="MODULE_NOT_FOUND",s;}s=i[f]={exports:{}};t[f][0].call(s.exports,function(n){var i=t[f][1][n];return u(i?i:n)},s,s.exports,n,t,i,r)}return i[f].exports}for(var e="function"==typeof require&&require,f=0;ft.offsetHeight){i=t;n.fastClickScrollParent=t;break}t=t.parentElement}while(t)}i&&(i.fastClickLastScrollTop=i.scrollTop)};i.prototype.getTargetElementFromEventTarget=function(n){return n.nodeType===Node.TEXT_NODE?n.parentNode:n};i.prototype.onTouchStart=function(n){var i,t,u;if(1t||Math.abs(n.pageY-this.touchStartY)>t?!0:!1};i.prototype.onTouchMove=function(n){return this.trackingClick?((this.targetElement!==this.getTargetElementFromEventTarget(n.target)||this.touchHasMoved(n))&&(this.trackingClick=!1,this.targetElement=null),!0):!0};i.prototype.findControl=function(n){return void 0!==n.control?n.control:n.htmlFor?document.getElementById(n.htmlFor):n.querySelector("button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea")};i.prototype.onTouchEnd=function(n){var i,o,t=this.targetElement;if(!this.trackingClick)return!0;if(n.timeStamp-this.lastClickTime0&&(r=++n,setTimeout(function(){n==r&&window.NativePageTransitionsKendoAdapter.apply()},20)))},!0)},!1)})(); -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Hello World 10 | 15 | 16 | 17 |
18 |

First page

19 | 54 |
55 | 56 | 57 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /demo/jqmdemo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Transitions - JQM demo 7 | 8 | 9 | 18 | 19 | 20 | 31 | 32 | 33 | 34 |
35 |
36 | Back 37 |

Login page

38 |
39 |
40 |
    41 |
  • JQM slide
  • 42 |
  • Native slide
  • 43 |
  • This is the first page
  • 44 |
  • This is the first page
  • 45 |
  • This is the first page
  • 46 |
  • This is the first page
  • 47 |
  • This is the first page
  • 48 |
  • This is the first page
  • 49 |
  • This is the first page
  • 50 |
  • This is the first page
  • 51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 |
60 |
61 | Back 62 |

First page

63 |
64 |
65 |
    66 |
  • JQM slide
  • 67 |
  • Native slide
  • 68 |
  • This is the first page
  • 69 |
  • This is the first page
  • 70 |
  • This is the first page
  • 71 |
  • This is the first page
  • 72 |
  • This is the first page
  • 73 |
  • This is the first page
  • 74 |
  • This is the first page
  • 75 |
  • This is the first page
  • 76 |
77 |
78 |
79 | 80 | 81 |
82 |
83 | 84 |
85 |
86 |

Second page

87 |
88 |
89 |
    90 |
  • JQM slide
  • 91 |
  • Native slide
  • 92 |
  • This is the second page
  • 93 |
  • This is the second page
  • 94 |
  • This is the second page
  • 95 |
  • This is the second page
  • 96 |
  • This is the second page
  • 97 |
98 |
99 |
100 | 101 |

created by Eddy Verbruggen

102 |
103 |
104 | 105 | 106 | -------------------------------------------------------------------------------- /demo/kendouidemo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kendo UI Demo 5 | 6 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 100 | 101 | 102 | 103 |
104 | Back 105 |
106 |
107 |
108 |
109 | Forward (css) 110 |
111 |
112 | 113 | Forward (native) 114 |
115 | 116 |
117 |
118 |
119 | Back (css) 120 |
121 | Forward (css) 122 |
123 |
124 |
125 | Back (native) 126 |
127 | Forward (native) 128 |
129 |
130 |
131 |
132 |

Just some Kendo widgets..

133 |
134 | 135 | 140 | 141 |
142 | 143 | 148 | 149 |
150 | 151 |
152 |
153 |
154 |

And another page

155 |
156 | Back (css) 157 |
158 |
159 | Back (native) 160 |
161 |
162 |
163 | 164 | 165 | 286 | 287 | 288 | 294 | 295 | -------------------------------------------------------------------------------- /demo/standalone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hello World 11 | 16 | 17 | 18 |
19 |

Standalone page

20 | 27 |
28 | 29 | 30 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.7.0", 3 | "name": "com.telerik.plugins.nativepagetransitions", 4 | "cordova_name": "Native Page Transitions", 5 | "description": "Slide out the current page to reveal the next one. By a native transitions.", 6 | "license": "MIT", 7 | "author": "Telerik / Eddy Verbruggen (https://github.com/EddyVerbruggen)", 8 | "repo": "https://github.com/Telerik-Verified-Plugins/WKWebView.git", 9 | "issue": "https://github.com/Telerik-Verified-Plugins/WKWebView/issues", 10 | "keywords": [ 11 | "Native transitions", 12 | "Native page transitions", 13 | "Slide", 14 | "Flip", 15 | "Drawer", 16 | "Menu", 17 | "Curl", 18 | "ecosystem:cordova", 19 | "cordova-android", 20 | "cordova-ios", 21 | "cordova-wp8", 22 | "cordova-windows" 23 | ], 24 | "platforms": [ 25 | "ios", 26 | "android", 27 | "wp8", 28 | "windows" 29 | ], 30 | "engines": [ 31 | { 32 | "name": "cordova", 33 | "version": ">=3.0.0" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Native Page Transitions 7 | 8 | 9 | Slide out the current page to reveal the next one. By a native transitions. 10 | 11 | 12 | Eddy Verbruggen / Telerik 13 | 14 | MIT 15 | 16 | Native transitions, Native page transitions, Slide, Flip, Drawer, Menu, Curl 17 | 18 | https://github.com/Telerik-Verified-Plugins/NativePageTransitions.git 19 | 20 | https://github.com/Telerik-Verified-Plugins/NativePageTransitions/issues 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/android/NativePageTransitions.java: -------------------------------------------------------------------------------- 1 | package com.telerik.plugins.nativepagetransitions; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Color; 5 | import android.os.Build; 6 | import android.util.DisplayMetrics; 7 | import android.view.Gravity; 8 | import android.view.TextureView; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.view.WindowManager; 12 | import android.view.animation.AlphaAnimation; 13 | import android.view.animation.Animation; 14 | import android.view.animation.AnimationSet; 15 | import android.view.animation.TranslateAnimation; 16 | import android.widget.FrameLayout; 17 | import android.widget.ImageView; 18 | import com.telerik.plugins.nativepagetransitions.lib.AnimationFactory; 19 | import org.apache.cordova.*; 20 | import org.json.JSONArray; 21 | import org.json.JSONException; 22 | import org.json.JSONObject; 23 | 24 | import java.util.Timer; 25 | import java.util.TimerTask; 26 | 27 | public class NativePageTransitions extends CordovaPlugin { 28 | 29 | private static final String ACTION_EXECUTE_PENDING_TRANSITION = "executePendingTransition"; 30 | private static final String ACTION_CANCEL_PENDING_TRANSITION = "cancelPendingTransition"; 31 | 32 | private static final String ACTION_SLIDE = "slide"; 33 | private static final String ACTION_FADE = "fade"; 34 | private static final String ACTION_FLIP = "flip"; 35 | private static final String ACTION_DRAWER = "drawer"; 36 | 37 | private ImageView imageView; 38 | private ImageView imageView2; 39 | private ImageView fixedImageViewTop; 40 | private ImageView fixedImageViewBottom; 41 | private float retinaFactor; 42 | private long duration; 43 | private long delay; 44 | private String drawerAction; 45 | private String drawerOrigin; 46 | private String direction; 47 | private String backgroundColor; 48 | private int slowdownfactor; 49 | private int slidePixels; 50 | private int fixedPixelsTop; 51 | private int fixedPixelsBottom; 52 | private CallbackContext _callbackContext; 53 | private String _action; 54 | // this plugin listens to page changes, so only kick in a transition when it was actually requested by the JS bridge 55 | // TODO remove this bool as we no longer rely on it due to xwalk and delay=-1 complexities 56 | private boolean calledFromJS; 57 | private FrameLayout layout; 58 | private static final boolean BEFORE_KITKAT = Build.VERSION.SDK_INT < 19; 59 | private final boolean requiresRedraw = BEFORE_KITKAT; 60 | // this plugin listens to page changes, so only kick in a transition when it was actually requested by the JS bridge 61 | private String lastCallbackID; 62 | private static boolean isCrosswalk; 63 | 64 | static { 65 | try { 66 | Class.forName("org.crosswalk.engine.XWalkWebViewEngine"); 67 | isCrosswalk = true; 68 | } catch (Exception ignore) { 69 | } 70 | } 71 | 72 | // Helper to be compile-time compatible with both Cordova 3.x and 4.x. 73 | private View cachedView; 74 | private View getView() { 75 | if (cachedView == null) { 76 | try { 77 | cachedView = (View) webView.getClass().getMethod("getView").invoke(webView); 78 | } catch (Exception e) { 79 | cachedView = (View) webView; 80 | } 81 | } 82 | return cachedView; 83 | } 84 | 85 | @Override 86 | public void initialize(CordovaInterface cordova, CordovaWebView webView) { 87 | super.initialize(cordova, webView); 88 | imageView = new ImageView(cordova.getActivity().getBaseContext()); 89 | imageView2 = new ImageView(cordova.getActivity().getBaseContext()); 90 | 91 | // Transitions are below par when this is switched off in the manifest, so enabling it here. 92 | // We may need to have developers suppress this via a param in the future. 93 | enableHardwareAcceleration(); 94 | 95 | layout = new FrameLayout(cordova.getActivity()); 96 | layout.setLayoutParams(getView().getLayoutParams()); 97 | ViewGroup vg = (ViewGroup) getView().getParent(); 98 | if (vg != null) { 99 | vg.addView(layout, getView().getLayoutParams()); 100 | vg.removeView(getView()); 101 | } 102 | layout.addView(getView()); 103 | layout.addView(imageView); 104 | layout.addView(imageView2); 105 | 106 | DisplayMetrics metrics = new DisplayMetrics(); 107 | cordova.getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics); 108 | retinaFactor = metrics.density; 109 | } 110 | 111 | int drawerNonOverlappingSpace; 112 | 113 | @Override 114 | public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException { 115 | _callbackContext = callbackContext; 116 | 117 | if (ACTION_EXECUTE_PENDING_TRANSITION.equalsIgnoreCase(action)) { 118 | delay = 0; 119 | if (ACTION_SLIDE.equalsIgnoreCase(_action)) { 120 | doSlideTransition(); 121 | } else if (ACTION_FADE.equalsIgnoreCase(_action)) { 122 | doFadeTransition(); 123 | } else if (ACTION_FLIP.equalsIgnoreCase(_action)) { 124 | doFlipTransition(); 125 | } else if (ACTION_DRAWER.equalsIgnoreCase(_action)) { 126 | doDrawerTransition(); 127 | } 128 | return true; 129 | } else if (ACTION_CANCEL_PENDING_TRANSITION.equalsIgnoreCase(action)) { 130 | lastCallbackID = null; 131 | cordova.getActivity().runOnUiThread(new Runnable() { 132 | @Override 133 | public void run() { 134 | if (fixedImageViewTop != null) { 135 | fixedImageViewTop.setImageBitmap(null); 136 | } 137 | if (fixedImageViewBottom != null) { 138 | fixedImageViewBottom.setImageBitmap(null); 139 | } 140 | imageView.setImageBitmap(null); 141 | _callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 142 | } 143 | }); 144 | return true; 145 | } 146 | 147 | _action = action; 148 | 149 | final JSONObject json = args.getJSONObject(0); 150 | final String href = json.isNull("href") ? null : json.getString("href"); 151 | 152 | calledFromJS = true; 153 | 154 | // TODO move effects to separate classes, and reuse lots of code 155 | if (ACTION_SLIDE.equalsIgnoreCase(action)) { 156 | duration = json.getLong("duration"); 157 | direction = json.getString("direction"); 158 | delay = json.getLong("androiddelay"); 159 | slowdownfactor = json.getInt("slowdownfactor"); 160 | slidePixels = json.optInt("slidePixels"); 161 | fixedPixelsTop = json.getInt("fixedPixelsTop"); 162 | fixedPixelsBottom = json.getInt("fixedPixelsBottom"); 163 | 164 | cordova.getActivity().runOnUiThread(new Runnable() { 165 | @Override 166 | public void run() { 167 | Bitmap bitmap = getBitmap(); 168 | imageView.setImageBitmap(bitmap); 169 | bringToFront(imageView); 170 | 171 | if (bitmap != null) { 172 | // crop the screenshot if fixed pixels have been passed when sliding left or right 173 | if (fixedPixelsTop > 0) { 174 | int cropHeight = (int)(fixedPixelsTop * retinaFactor); 175 | fixedImageViewTop = new ImageView(cordova.getActivity().getBaseContext()); 176 | fixedImageViewTop.setImageBitmap(Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), cropHeight)); 177 | final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.TOP); 178 | layout.addView(fixedImageViewTop, lp); 179 | if ("down".equals(direction)) { 180 | // in case we slide down, strip off the fixedPixelsTop from the top of the screenshot 181 | bitmap = Bitmap.createBitmap(bitmap, 0, cropHeight, bitmap.getWidth(), bitmap.getHeight()-cropHeight); 182 | imageView.setScaleType(ImageView.ScaleType.FIT_END); // affects the entire plugin but is only relevant here 183 | imageView.setImageBitmap(bitmap); 184 | } else if ("up".equals(direction)) { 185 | // TODO in case we slide up, strip off the fixedPixelsTop from the top of the webview 186 | // TODO .. but this seems it a bit impossible.. (see my email of jan 24 2016) 187 | } 188 | } 189 | if (fixedPixelsBottom > 0) { 190 | int cropHeight = (int)(fixedPixelsBottom * retinaFactor); 191 | fixedImageViewBottom = new ImageView(cordova.getActivity().getBaseContext()); 192 | fixedImageViewBottom.setImageBitmap(Bitmap.createBitmap(bitmap, 0, bitmap.getHeight()-cropHeight, bitmap.getWidth(), cropHeight)); 193 | final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM); 194 | layout.addView(fixedImageViewBottom, lp); 195 | } 196 | } 197 | 198 | if (href != null && !"null".equals(href)) { 199 | if (!href.startsWith("#")) { 200 | webView.loadUrlIntoView(webView.getUrl().substring(0, webView.getUrl().lastIndexOf('/')+1) + href, false); 201 | } 202 | } 203 | 204 | if (delay > -1) { 205 | doSlideTransition(); 206 | } else { 207 | _callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 208 | } 209 | } 210 | }); 211 | 212 | } else if (ACTION_DRAWER.equalsIgnoreCase(action)) { 213 | 214 | if (drawerNonOverlappingSpace == 0) { 215 | drawerNonOverlappingSpace = getView().getWidth()/8; 216 | } 217 | duration = json.getLong("duration"); 218 | drawerAction = json.getString("action"); 219 | drawerOrigin = json.getString("origin"); 220 | delay = json.getLong("androiddelay"); 221 | 222 | cordova.getActivity().runOnUiThread(new Runnable() { 223 | @Override 224 | public void run() { 225 | Bitmap bitmap; 226 | if ("open".equals(drawerAction)) { 227 | bitmap = getBitmap(); 228 | } else { 229 | // TODO Crosswalk compat 230 | getView().setDrawingCacheEnabled(true); 231 | bitmap = Bitmap.createBitmap(getView().getDrawingCache(), "left".equals(drawerOrigin) ? 0 : drawerNonOverlappingSpace, 0, getView().getWidth()- drawerNonOverlappingSpace, getView().getHeight()); 232 | if ("left".equals(drawerOrigin)) { 233 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 234 | imageView2.setX(-drawerNonOverlappingSpace / 2); 235 | } 236 | } else { 237 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 238 | imageView2.setX(drawerNonOverlappingSpace / 2); 239 | } 240 | } 241 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { 242 | bitmap.setHasAlpha(false); 243 | } 244 | getView().setDrawingCacheEnabled(false); 245 | } 246 | if ("open".equals(drawerAction)) { 247 | imageView.setImageBitmap(bitmap); 248 | bringToFront(imageView); 249 | } else { 250 | imageView2.setImageBitmap(bitmap); 251 | bringToFront(imageView2); 252 | } 253 | 254 | if (href != null && !"null".equals(href)) { 255 | if (!href.startsWith("#") && href.contains(".html")) { 256 | webView.loadUrlIntoView(webView.getUrl().substring(0, webView.getUrl().lastIndexOf('/')+1) + href, false); 257 | } 258 | } 259 | 260 | if (delay > -1) { 261 | doDrawerTransition(); 262 | } else { 263 | _callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 264 | } 265 | } 266 | }); 267 | 268 | } else if (ACTION_FADE.equalsIgnoreCase(action)) { 269 | 270 | duration = json.getLong("duration"); 271 | delay = json.getLong("androiddelay"); 272 | 273 | cordova.getActivity().runOnUiThread(new Runnable() { 274 | @Override 275 | public void run() { 276 | imageView.setImageBitmap(getBitmap()); 277 | bringToFront(imageView); 278 | 279 | if (href != null && !"null".equals(href)) { 280 | if (!href.startsWith("#") && href.contains(".html")) { 281 | webView.loadUrlIntoView(webView.getUrl().substring(0, webView.getUrl().lastIndexOf('/')+1) + href, false); 282 | } 283 | } 284 | 285 | if (delay > -1) { 286 | doFadeTransition(); 287 | } else { 288 | _callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 289 | } 290 | } 291 | }); 292 | 293 | } else if (ACTION_FLIP.equalsIgnoreCase(action)) { 294 | 295 | duration = json.getLong("duration"); 296 | direction = json.getString("direction"); 297 | delay = json.getLong("androiddelay"); 298 | backgroundColor = json.optString("backgroundColor"); 299 | 300 | cordova.getActivity().runOnUiThread(new Runnable() { 301 | @Override 302 | public void run() { 303 | if (backgroundColor != null && backgroundColor.startsWith("#")) { 304 | ((View)getView().getParent()).setBackgroundColor(Color.parseColor(backgroundColor)); 305 | } 306 | imageView.setImageBitmap(getBitmap()); 307 | bringToFront(imageView); 308 | if (href != null && !"null".equals(href)) { 309 | if (!href.startsWith("#") && href.contains(".html")) { 310 | webView.loadUrlIntoView(webView.getUrl().substring(0, webView.getUrl().lastIndexOf('/')+1) + href, false); 311 | } 312 | } 313 | 314 | if (delay > -1) { 315 | doFlipTransition(); 316 | } else { 317 | _callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 318 | } 319 | } 320 | }); 321 | } 322 | return true; 323 | } 324 | 325 | private void doFadeTransition() { 326 | if (!calledFromJS || this._callbackContext.getCallbackId().equals(lastCallbackID)) { 327 | return; 328 | } 329 | lastCallbackID = this._callbackContext.getCallbackId(); 330 | 331 | new Timer().schedule(new TimerTask() { 332 | public void run() { 333 | // manipulations of the imageView need to be done by the same thread 334 | // as the one that created it - the uithread in this case 335 | cordova.getActivity().runOnUiThread(new Runnable() { 336 | @Override 337 | public void run() { 338 | 339 | final Animation[] animations = new Animation[] { 340 | AnimationFactory.fadeOutAnimation(duration, imageView), 341 | AnimationFactory.fadeInAnimation(duration, getView()) 342 | }; 343 | 344 | animations[0].setAnimationListener(new Animation.AnimationListener() { 345 | @Override 346 | public void onAnimationStart(Animation animation) { 347 | } 348 | 349 | @Override 350 | public void onAnimationEnd(Animation animation) { 351 | bringToFront(getView()); 352 | // animation.reset(); 353 | getView().clearAnimation(); 354 | _callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 355 | } 356 | 357 | @Override 358 | public void onAnimationRepeat(Animation animation) { 359 | } 360 | }); 361 | animations[1].setAnimationListener(new Animation.AnimationListener() { 362 | @Override 363 | public void onAnimationStart(Animation animation) { 364 | } 365 | 366 | @Override 367 | public void onAnimationEnd(Animation animation) { 368 | imageView.setImageBitmap(null); 369 | // animation.reset(); 370 | imageView.clearAnimation(); 371 | } 372 | 373 | @Override 374 | public void onAnimationRepeat(Animation animation) { 375 | } 376 | }); 377 | 378 | imageView.startAnimation(animations[0]); 379 | getView().startAnimation(animations[1]); 380 | 381 | calledFromJS = false; 382 | } 383 | }); 384 | } 385 | }, delay); 386 | } 387 | 388 | private void doFlipTransition() { 389 | if (!calledFromJS || this._callbackContext.getCallbackId().equals(lastCallbackID)) { 390 | return; 391 | } 392 | lastCallbackID = this._callbackContext.getCallbackId(); 393 | 394 | new Timer().schedule(new TimerTask() { 395 | public void run() { 396 | // manipulations of the imageView need to be done by the same thread 397 | // as the one that created it - the uithread in this case 398 | cordova.getActivity().runOnUiThread(new Runnable() { 399 | @Override 400 | public void run() { 401 | 402 | AnimationFactory.FlipDirection flipDirection; 403 | if ("left".equals(direction)) { 404 | flipDirection = AnimationFactory.FlipDirection.RIGHT_LEFT; 405 | } else if ("up".equals(direction)) { 406 | flipDirection = AnimationFactory.FlipDirection.LEFT_RIGHT; // TODO impl UP_DOWN; 407 | } else if ("down".equals(direction)) { 408 | flipDirection = AnimationFactory.FlipDirection.RIGHT_LEFT; // TODO impl DOWN_UP; 409 | } else { 410 | flipDirection = AnimationFactory.FlipDirection.LEFT_RIGHT; 411 | } 412 | 413 | final Animation[] animations = AnimationFactory.flipAnimation(imageView, getView(), flipDirection, duration, null); 414 | 415 | animations[0].setAnimationListener(new Animation.AnimationListener() { 416 | @Override 417 | public void onAnimationStart(Animation animation) { 418 | } 419 | 420 | @Override 421 | public void onAnimationEnd(Animation animation) { 422 | imageView.setImageBitmap(null); 423 | animation.reset(); 424 | imageView.clearAnimation(); 425 | } 426 | 427 | @Override 428 | public void onAnimationRepeat(Animation animation) { 429 | } 430 | }); 431 | animations[1].setAnimationListener(new Animation.AnimationListener() { 432 | @Override 433 | public void onAnimationStart(Animation animation) { 434 | } 435 | 436 | @Override 437 | public void onAnimationEnd(Animation animation) { 438 | animation.reset(); 439 | getView().clearAnimation(); 440 | if (backgroundColor != null && backgroundColor.startsWith("#")) { 441 | backgroundColor = null; 442 | ((View)getView().getParent()).setBackgroundColor(Color.BLACK); 443 | } 444 | _callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 445 | } 446 | 447 | @Override 448 | public void onAnimationRepeat(Animation animation) { 449 | } 450 | }); 451 | 452 | imageView.startAnimation(animations[0]); 453 | getView().startAnimation(animations[1]); 454 | 455 | calledFromJS = false; 456 | } 457 | }); 458 | } 459 | }, delay); 460 | } 461 | 462 | private void doSlideTransition() { 463 | if (!calledFromJS || this._callbackContext.getCallbackId().equals(lastCallbackID)) { 464 | return; 465 | } 466 | lastCallbackID = this._callbackContext.getCallbackId(); 467 | 468 | new Timer().schedule(new TimerTask() { 469 | public void run() { 470 | // manipulations of the imageView need to be done by the same thread 471 | // as the one that created it - the uithread in this case 472 | cordova.getActivity().runOnUiThread(new Runnable() { 473 | @Override 474 | public void run() { 475 | float transitionToX = 0; 476 | float transitionToY = 0; 477 | int translateAnimationY = TranslateAnimation.RELATIVE_TO_PARENT; 478 | int screenshotSlowdownFactor = 1; 479 | int webviewSlowdownFactor = 1; 480 | 481 | if ("left".equals(direction)) { 482 | bringToFront(getView()); 483 | transitionToX = -1; 484 | screenshotSlowdownFactor = slowdownfactor; 485 | } else if ("right".equals(direction)) { 486 | bringToFront(imageView); 487 | transitionToX = 1; 488 | webviewSlowdownFactor = slowdownfactor; 489 | } else if ("up".equals(direction)) { 490 | bringToFront(getView()); 491 | transitionToY = -getView().getHeight(); 492 | translateAnimationY = TranslateAnimation.ABSOLUTE; 493 | screenshotSlowdownFactor = slowdownfactor; 494 | } else if ("down".equals(direction)) { 495 | bringToFront(imageView); 496 | transitionToY = getView().getHeight(); 497 | translateAnimationY = TranslateAnimation.ABSOLUTE; 498 | webviewSlowdownFactor = slowdownfactor; 499 | } 500 | 501 | if (fixedImageViewTop != null) { 502 | bringToFront(fixedImageViewTop); 503 | } 504 | if (fixedImageViewBottom != null) { 505 | bringToFront(fixedImageViewBottom); 506 | } 507 | 508 | // imageview animation 509 | final AnimationSet imageViewAnimation = new AnimationSet(true); 510 | 511 | if (screenshotSlowdownFactor > 0) { 512 | final Animation imageViewAnimation1 = new TranslateAnimation( 513 | TranslateAnimation.RELATIVE_TO_PARENT, 0f, 514 | TranslateAnimation.RELATIVE_TO_PARENT, transitionToX / screenshotSlowdownFactor, 515 | translateAnimationY, 0, 516 | translateAnimationY, slidePixels > 0 ? slidePixels : (transitionToY / screenshotSlowdownFactor)); 517 | imageViewAnimation1.setDuration(duration); 518 | imageViewAnimation.addAnimation(imageViewAnimation1); 519 | } 520 | 521 | if (slowdownfactor != 1 && ("left".equals(direction) || "up".equals(direction))) { 522 | if (slidePixels <= 0) { 523 | final Animation imageViewAnimation2 = new AlphaAnimation(1, 0.4f); 524 | imageViewAnimation2.setDuration(duration); 525 | imageViewAnimation.addAnimation(imageViewAnimation2); 526 | } 527 | } 528 | 529 | // webview animation 530 | final AnimationSet webViewAnimationSet = new AnimationSet(true); 531 | 532 | if (webviewSlowdownFactor > 0) { 533 | final Animation webViewAnimation1 = new TranslateAnimation( 534 | TranslateAnimation.RELATIVE_TO_PARENT, -transitionToX / webviewSlowdownFactor, 535 | TranslateAnimation.RELATIVE_TO_PARENT, 0, 536 | TranslateAnimation.ABSOLUTE, slidePixels > 0 ? slidePixels : (-transitionToY / webviewSlowdownFactor), 537 | TranslateAnimation.ABSOLUTE, 0); 538 | webViewAnimation1.setDuration(duration); 539 | webViewAnimationSet.addAnimation(webViewAnimation1); 540 | //webViewAnimation1.setInterpolator(new OvershootInterpolator()); 541 | } 542 | 543 | if (slidePixels <= 0 && 544 | slowdownfactor != 1 && 545 | ("right".equals(direction) || "down".equals(direction))) { 546 | final Animation webViewAnimation2 = new AlphaAnimation(0.4f, 1f); 547 | webViewAnimation2.setDuration(duration); 548 | webViewAnimationSet.addAnimation(webViewAnimation2); 549 | } 550 | 551 | if (slidePixels > 0) { 552 | if ("up".equals(direction)) { 553 | webViewAnimationSet.addAnimation(AnimationFactory.fadeInAnimation(duration, getView())); 554 | } else if ("down".equals(direction)) { 555 | imageViewAnimation.addAnimation(AnimationFactory.fadeOutAnimation(duration, imageView)); 556 | } 557 | } 558 | 559 | webViewAnimationSet.setAnimationListener(new Animation.AnimationListener() { 560 | @Override 561 | public void onAnimationStart(Animation animation) { 562 | } 563 | 564 | @Override 565 | public void onAnimationEnd(Animation animation) { 566 | // prevent a flash by removing the optional fixed header/footer screenshots after a little delay 567 | if (fixedImageViewTop != null || fixedImageViewBottom != null) { 568 | new Timer().schedule(new TimerTask() { 569 | public void run() { 570 | cordova.getActivity().runOnUiThread(new Runnable() { 571 | @Override 572 | public void run() { 573 | if (fixedImageViewTop != null) { 574 | fixedImageViewTop.setImageBitmap(null); 575 | } 576 | if (fixedImageViewBottom != null) { 577 | fixedImageViewBottom.setImageBitmap(null); 578 | } 579 | imageView.setImageBitmap(null); 580 | } 581 | }); 582 | } 583 | }, 20); 584 | } 585 | bringToFront(getView()); 586 | _callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 587 | } 588 | 589 | @Override 590 | public void onAnimationRepeat(Animation animation) { 591 | } 592 | }); 593 | 594 | imageView.setAnimation(imageViewAnimation); 595 | if (slidePixels <=0 || !"down".equals(direction)) { 596 | getView().setAnimation(webViewAnimationSet); 597 | } 598 | 599 | if (BEFORE_KITKAT) { 600 | //fixes a problem where the animation didn't start unless the user touched the screen once 601 | layout.invalidate(); 602 | } 603 | 604 | layout.startLayoutAnimation(); 605 | 606 | if (BEFORE_KITKAT) { 607 | // This fixes an issue observed on a Samsung Galaxy S3 /w Android 4.3 where the img is shown, 608 | // but the transition doesn't kick in unless the screen is touched again. 609 | // note that with 'layout.invalidate();' this may be obsolete but I can't reproduce the issue anyway 610 | imageView.requestFocusFromTouch(); 611 | getView().requestFocus(); 612 | } 613 | 614 | calledFromJS = false; 615 | } 616 | }); 617 | } 618 | }, delay); 619 | } 620 | 621 | private void doDrawerTransition() { 622 | if (!calledFromJS || this._callbackContext.getCallbackId().equals(lastCallbackID)) { 623 | return; 624 | } 625 | lastCallbackID = this._callbackContext.getCallbackId(); 626 | 627 | new Timer().schedule(new TimerTask() { 628 | public void run() { 629 | // manipulations of the imageView need to be done by the same thread 630 | // as the one that created it - the uithread in this case 631 | cordova.getActivity().runOnUiThread(new Runnable() { 632 | @Override 633 | public void run() { 634 | 635 | float width = getView().getWidth(); 636 | float transitionToX = 0; 637 | float transitionFromX = 0; 638 | 639 | if ("open".equals(drawerAction)) { 640 | if ("right".equals(drawerOrigin)) { 641 | transitionToX = width - drawerNonOverlappingSpace; 642 | } else { 643 | transitionToX = -width + drawerNonOverlappingSpace; 644 | } 645 | } else if ("close".equals(drawerAction)) { 646 | if ("right".equals(drawerOrigin)) { 647 | transitionFromX = -width + drawerNonOverlappingSpace; 648 | } else { 649 | transitionFromX = width - drawerNonOverlappingSpace; 650 | } 651 | } 652 | 653 | final Animation animation = new TranslateAnimation( 654 | TranslateAnimation.ABSOLUTE, transitionFromX, 655 | TranslateAnimation.ABSOLUTE, -transitionToX, 656 | TranslateAnimation.ABSOLUTE, 0, 657 | TranslateAnimation.ABSOLUTE, 0); 658 | 659 | animation.setDuration(duration); 660 | 661 | animation.setAnimationListener(new Animation.AnimationListener() { 662 | @Override 663 | public void onAnimationStart(Animation animation) { 664 | } 665 | 666 | @Override 667 | public void onAnimationEnd(Animation animation) { 668 | if ("close".equals(drawerAction)) { 669 | imageView.setImageBitmap(null); 670 | imageView2.setImageBitmap(null); 671 | } 672 | _callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 673 | } 674 | 675 | @Override 676 | public void onAnimationRepeat(Animation animation) { 677 | } 678 | }); 679 | 680 | if ("open".equals(drawerAction)) { 681 | animation.setFillAfter(true); // persists the screenshot (not working on crosswalk btw..) 682 | imageView.startAnimation(animation); 683 | } else { 684 | // prevent a flash by moving the webview to the front with a little delay 685 | new Timer().schedule(new TimerTask() { 686 | public void run() { 687 | cordova.getActivity().runOnUiThread(new Runnable() { 688 | @Override 689 | public void run() { 690 | bringToFront(getView()); 691 | } 692 | }); 693 | } 694 | }, 80); 695 | getView().setAnimation(animation); 696 | layout.startLayoutAnimation(); 697 | } 698 | calledFromJS = false; 699 | } 700 | }); 701 | } 702 | }, delay); 703 | } 704 | 705 | private void bringToFront(View view) { 706 | view.bringToFront(); 707 | view.setVisibility(View.VISIBLE); 708 | if (requiresRedraw) { 709 | view.requestLayout(); 710 | } 711 | } 712 | 713 | private Bitmap getBitmap() { 714 | Bitmap bitmap = null; 715 | if (isCrosswalk) { 716 | try { 717 | TextureView textureView = findCrosswalkTextureView((ViewGroup) getView()); 718 | bitmap = textureView.getBitmap(); 719 | } catch(Exception ignore) { 720 | } 721 | } else { 722 | View view = getView(); 723 | view.setDrawingCacheEnabled(true); 724 | bitmap = Bitmap.createBitmap(view.getDrawingCache()); 725 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { 726 | bitmap.setHasAlpha(false); 727 | } 728 | view.setDrawingCacheEnabled(false); 729 | } 730 | return bitmap; 731 | } 732 | 733 | private TextureView findCrosswalkTextureView(ViewGroup group) { 734 | int childCount = group.getChildCount(); 735 | for(int i=0;i= Build.VERSION_CODES.HONEYCOMB) { 755 | cordova.getActivity().getWindow().setFlags( 756 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, 757 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); 758 | imageView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 759 | if (BEFORE_KITKAT && !isCrosswalk) { 760 | getView().setLayerType(View.LAYER_TYPE_SOFTWARE, null); 761 | } 762 | } 763 | } 764 | } -------------------------------------------------------------------------------- /src/android/lib/AnimationFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2012 Ephraim Tekle genzeb@gmail.com 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 5 | * associated documentation files (the "Software"), to deal in the Software without restriction, including 6 | * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 8 | * following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in all copies or substantial 11 | * portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 14 | * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 15 | * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 16 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 17 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | * 19 | * @author Ephraim A. Tekle 20 | * 21 | */ 22 | package com.telerik.plugins.nativepagetransitions.lib; 23 | 24 | import android.view.View; 25 | import android.view.animation.*; 26 | import android.view.animation.Animation.AnimationListener; 27 | import android.widget.ViewAnimator; 28 | 29 | /** 30 | * This class contains methods for creating {@link Animation} objects for some of the most common animation, including a 3D flip animation, {@link FlipAnimation}. 31 | * Furthermore, utility methods are provided for initiating fade-in-then-out and flip animations. 32 | * 33 | * @author Ephraim A. Tekle 34 | * 35 | */ 36 | public class AnimationFactory { 37 | 38 | /** 39 | * The {@code FlipDirection} enumeration defines the most typical flip view transitions: left-to-right and right-to-left. {@code FlipDirection} is used during the creation of {@link FlipAnimation} animations. 40 | * 41 | * @author Ephraim A. Tekle 42 | * 43 | */ 44 | public static enum FlipDirection { 45 | LEFT_RIGHT, 46 | RIGHT_LEFT, 47 | UP_DOWN, 48 | DOWN_UP; 49 | 50 | public float getStartDegreeForFirstView() { 51 | return 0; 52 | } 53 | 54 | public float getStartDegreeForSecondView() { 55 | switch(this) { 56 | case LEFT_RIGHT: 57 | return -90; 58 | case RIGHT_LEFT: 59 | return 90; 60 | default: 61 | return 0; 62 | } 63 | } 64 | 65 | public float getEndDegreeForFirstView() { 66 | switch(this) { 67 | case LEFT_RIGHT: 68 | return 90; 69 | case RIGHT_LEFT: 70 | return -90; 71 | default: 72 | return 0; 73 | } 74 | } 75 | 76 | public float getEndDegreeForSecondView() { 77 | return 0; 78 | } 79 | 80 | public FlipDirection theOtherDirection() { 81 | switch(this) { 82 | case LEFT_RIGHT: 83 | return RIGHT_LEFT; 84 | case RIGHT_LEFT: 85 | return LEFT_RIGHT; 86 | default: 87 | return null; 88 | } 89 | } 90 | } 91 | 92 | 93 | /** 94 | * Create a pair of {@link FlipAnimation} that can be used to flip 3D transition from {@code fromView} to {@code toView}. A typical use case is with {@link ViewAnimator} as an out and in transition. 95 | * 96 | * NOTE: Avoid using this method. Instead, use {@link #flipTransition}. 97 | * 98 | * @param fromView the view transition away from 99 | * @param toView the view transition to 100 | * @param dir the flip direction 101 | * @param duration the transition duration in milliseconds 102 | * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator) 103 | * @return 104 | */ 105 | public static Animation[] flipAnimation(final View fromView, final View toView, FlipDirection dir, long duration, Interpolator interpolator) { 106 | Animation[] result = new Animation[2]; 107 | float centerX; 108 | float centerY; 109 | 110 | centerX = fromView.getWidth() / 2.0f; 111 | centerY = fromView.getHeight() / 2.0f; 112 | 113 | Animation outFlip= new FlipAnimation(dir.getStartDegreeForFirstView(), dir.getEndDegreeForFirstView(), centerX, centerY, FlipAnimation.SCALE_DEFAULT, FlipAnimation.ScaleUpDownEnum.SCALE_DOWN); 114 | outFlip.setDuration(duration/2); 115 | outFlip.setFillAfter(true); 116 | outFlip.setInterpolator(interpolator==null?new LinearInterpolator():interpolator); 117 | 118 | AnimationSet outAnimation = new AnimationSet(true); 119 | outAnimation.addAnimation(outFlip); 120 | result[0] = outAnimation; 121 | 122 | // Uncomment the following if toView has its layout established (not the case if using ViewFlipper and on first show) 123 | centerX = toView.getWidth() / 2.0f; 124 | centerY = toView.getHeight() / 2.0f; 125 | 126 | Animation inFlip = new FlipAnimation(dir.getStartDegreeForSecondView(), dir.getEndDegreeForSecondView(), centerX, centerY, FlipAnimation.SCALE_DEFAULT, FlipAnimation.ScaleUpDownEnum.SCALE_UP); 127 | inFlip.setDuration(duration/2); 128 | inFlip.setFillAfter(true); 129 | inFlip.setInterpolator(interpolator==null?new LinearInterpolator():interpolator); 130 | inFlip.setStartOffset(duration/2); 131 | 132 | AnimationSet inAnimation = new AnimationSet(true); 133 | inAnimation.addAnimation(inFlip); 134 | result[1] = inAnimation; 135 | 136 | return result; 137 | 138 | } 139 | 140 | /** 141 | * Flip to the next view of the {@code ViewAnimator}'s subviews. A call to this method will initiate a {@link FlipAnimation} to show the next View. 142 | * If the currently visible view is the last view, flip direction will be reversed for this transition. 143 | * 144 | * @param viewAnimator the {@code ViewAnimator} 145 | * @param dir the direction of flip 146 | */ 147 | public static void flipTransition(final ViewAnimator viewAnimator, FlipDirection dir) { 148 | 149 | final View fromView = viewAnimator.getCurrentView(); 150 | final int currentIndex = viewAnimator.getDisplayedChild(); 151 | final int nextIndex = (currentIndex + 1)%viewAnimator.getChildCount(); 152 | 153 | final View toView = viewAnimator.getChildAt(nextIndex); 154 | 155 | Animation[] animc = AnimationFactory.flipAnimation(fromView, toView, (nextIndex < currentIndex?dir.theOtherDirection():dir), 500, null); 156 | 157 | viewAnimator.setOutAnimation(animc[0]); 158 | viewAnimator.setInAnimation(animc[1]); 159 | 160 | viewAnimator.showNext(); 161 | } 162 | 163 | ////////////// 164 | 165 | 166 | /** 167 | * Slide animations to enter a view from left. 168 | * 169 | * @param duration the animation duration in milliseconds 170 | * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator) 171 | * @return a slide transition animation 172 | */ 173 | public static Animation inFromLeftAnimation(long duration, Interpolator interpolator) { 174 | Animation inFromLeft = new TranslateAnimation( 175 | Animation.RELATIVE_TO_PARENT, -1.0f, Animation.RELATIVE_TO_PARENT, 0.0f, 176 | Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f 177 | ); 178 | inFromLeft.setDuration(duration); 179 | inFromLeft.setInterpolator(interpolator==null?new AccelerateInterpolator():interpolator); //AccelerateInterpolator 180 | return inFromLeft; 181 | } 182 | 183 | /** 184 | * Slide animations to hide a view by sliding it to the right 185 | * 186 | * @param duration the animation duration in milliseconds 187 | * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator) 188 | * @return a slide transition animation 189 | */ 190 | public static Animation outToRightAnimation(long duration, Interpolator interpolator) { 191 | Animation outtoRight = new TranslateAnimation( 192 | Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, +1.0f, 193 | Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f 194 | ); 195 | outtoRight.setDuration(duration); 196 | outtoRight.setInterpolator(interpolator==null?new AccelerateInterpolator():interpolator); 197 | return outtoRight; 198 | } 199 | 200 | /** 201 | * Slide animations to enter a view from right. 202 | * 203 | * @param duration the animation duration in milliseconds 204 | * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator) 205 | * @return a slide transition animation 206 | */ 207 | public static Animation inFromRightAnimation(long duration, Interpolator interpolator) { 208 | 209 | Animation inFromRight = new TranslateAnimation( 210 | Animation.RELATIVE_TO_PARENT, +1.0f, Animation.RELATIVE_TO_PARENT, 0.0f, 211 | Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f 212 | ); 213 | inFromRight.setDuration(duration); 214 | inFromRight.setInterpolator(interpolator==null?new AccelerateInterpolator():interpolator); 215 | return inFromRight; 216 | } 217 | 218 | /** 219 | * Slide animations to hide a view by sliding it to the left. 220 | * 221 | * @param duration the animation duration in milliseconds 222 | * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator) 223 | * @return a slide transition animation 224 | */ 225 | public static Animation outToLeftAnimation(long duration, Interpolator interpolator) { 226 | Animation outtoLeft = new TranslateAnimation( 227 | Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, -1.0f, 228 | Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f 229 | ); 230 | outtoLeft.setDuration(duration); 231 | outtoLeft.setInterpolator(interpolator==null?new AccelerateInterpolator():interpolator); 232 | return outtoLeft; 233 | } 234 | 235 | /** 236 | * Slide animations to enter a view from top. 237 | * 238 | * @param duration the animation duration in milliseconds 239 | * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator) 240 | * @return a slide transition animation 241 | */ 242 | public static Animation inFromTopAnimation(long duration, Interpolator interpolator) { 243 | Animation infromtop = new TranslateAnimation( 244 | Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f, 245 | Animation.RELATIVE_TO_PARENT, -1.0f, Animation.RELATIVE_TO_PARENT, 0.0f 246 | ); 247 | infromtop.setDuration(duration); 248 | infromtop.setInterpolator(interpolator==null?new AccelerateInterpolator():interpolator); 249 | return infromtop; 250 | } 251 | 252 | /** 253 | * Slide animations to hide a view by sliding it to the top 254 | * 255 | * @param duration the animation duration in milliseconds 256 | * @param interpolator the interpolator to use (pass {@code null} to use the {@link AccelerateInterpolator} interpolator) 257 | * @return a slide transition animation 258 | */ 259 | public static Animation outToTopAnimation(long duration, Interpolator interpolator) { 260 | Animation outtotop = new TranslateAnimation( 261 | Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, 0.0f, 262 | Animation.RELATIVE_TO_PARENT, 0.0f, Animation.RELATIVE_TO_PARENT, -1.0f 263 | ); 264 | outtotop.setDuration(duration); 265 | outtotop.setInterpolator(interpolator==null?new AccelerateInterpolator():interpolator); 266 | return outtotop; 267 | } 268 | 269 | /** 270 | * A fade animation that will fade the subject in by changing alpha from 0 to 1. 271 | * 272 | * @param duration the animation duration in milliseconds 273 | * @param delay how long to wait before starting the animation, in milliseconds 274 | * @return a fade animation 275 | */ 276 | public static Animation fadeInAnimation(long duration, long delay) { 277 | 278 | Animation fadeIn = new AlphaAnimation(0, 1); 279 | fadeIn.setInterpolator(new DecelerateInterpolator()); 280 | fadeIn.setDuration(duration); 281 | fadeIn.setStartOffset(delay); 282 | 283 | return fadeIn; 284 | } 285 | 286 | /** 287 | * A fade animation that will fade the subject out by changing alpha from 1 to 0. 288 | * 289 | * @param duration the animation duration in milliseconds 290 | * @param delay how long to wait before starting the animation, in milliseconds 291 | * @return a fade animation 292 | */ 293 | public static Animation fadeOutAnimation(long duration, long delay) { 294 | 295 | Animation fadeOut = new AlphaAnimation(1, 0); 296 | fadeOut.setInterpolator(new AccelerateInterpolator()); 297 | fadeOut.setStartOffset(delay); 298 | fadeOut.setDuration(duration); 299 | 300 | return fadeOut; 301 | } 302 | 303 | /** 304 | * A fade animation that will ensure the View starts and ends with the correct visibility 305 | * @param view the View to be faded in 306 | * @param duration the animation duration in milliseconds 307 | * @return a fade animation that will set the visibility of the view at the start and end of animation 308 | */ 309 | public static Animation fadeInAnimation(long duration, final View view) { 310 | Animation animation = fadeInAnimation(duration, 0); 311 | 312 | animation.setAnimationListener(new AnimationListener() { 313 | @Override 314 | public void onAnimationEnd(Animation animation) { 315 | view.setVisibility(View.VISIBLE); 316 | } 317 | 318 | @Override 319 | public void onAnimationRepeat(Animation animation) { 320 | } 321 | 322 | @Override 323 | public void onAnimationStart(Animation animation) { 324 | view.setVisibility(View.GONE); 325 | } 326 | }); 327 | 328 | return animation; 329 | } 330 | 331 | /** 332 | * A fade animation that will ensure the View starts and ends with the correct visibility 333 | * @param view the View to be faded out 334 | * @param duration the animation duration in milliseconds 335 | * @return a fade animation that will set the visibility of the view at the start and end of animation 336 | */ 337 | public static Animation fadeOutAnimation(long duration, final View view) { 338 | 339 | Animation animation = fadeOutAnimation(duration, 0); 340 | 341 | animation.setAnimationListener(new AnimationListener() { 342 | @Override 343 | public void onAnimationEnd(Animation animation) { 344 | view.setVisibility(View.GONE); 345 | } 346 | 347 | @Override 348 | public void onAnimationRepeat(Animation animation) { 349 | } 350 | 351 | @Override 352 | public void onAnimationStart(Animation animation) { 353 | view.setVisibility(View.VISIBLE); 354 | } 355 | }); 356 | 357 | return animation; 358 | 359 | } 360 | 361 | /** 362 | * Creates a pair of animation that will fade in, delay, then fade out 363 | * @param duration the animation duration in milliseconds 364 | * @param delay how long to wait after fading in the subject and before starting the fade out 365 | * @return a fade in then out animations 366 | */ 367 | public static Animation[] fadeInThenOutAnimation(long duration, long delay) { 368 | return new Animation[] {fadeInAnimation(duration,0), fadeOutAnimation(duration, duration+delay)}; 369 | } 370 | 371 | /** 372 | * Fades the view in. Animation starts right away. 373 | * @param v the view to be faded in 374 | */ 375 | public static void fadeOut(View v) { 376 | if (v==null) return; 377 | v.startAnimation(fadeOutAnimation(500, v)); 378 | } 379 | 380 | /** 381 | * Fades the view out. Animation starts right away. 382 | * @param v the view to be faded out 383 | */ 384 | public static void fadeIn(View v) { 385 | if (v==null) return; 386 | 387 | v.startAnimation(fadeInAnimation(500, v)); 388 | } 389 | 390 | /** 391 | * Fades the view in, delays the specified amount of time, then fades the view out 392 | * @param v the view to be faded in then out 393 | * @param delay how long the view will be visible for 394 | */ 395 | public static void fadeInThenOut(final View v, long delay) { 396 | if (v==null) return; 397 | 398 | v.setVisibility(View.VISIBLE); 399 | AnimationSet animation = new AnimationSet(true); 400 | Animation[] fadeInOut = fadeInThenOutAnimation(500,delay); 401 | animation.addAnimation(fadeInOut[0]); 402 | animation.addAnimation(fadeInOut[1]); 403 | animation.setAnimationListener(new AnimationListener() { 404 | @Override 405 | public void onAnimationEnd(Animation animation) { 406 | v.setVisibility(View.GONE); 407 | } 408 | @Override 409 | public void onAnimationRepeat(Animation animation) { 410 | } 411 | @Override 412 | public void onAnimationStart(Animation animation) { 413 | v.setVisibility(View.VISIBLE); 414 | } 415 | }); 416 | 417 | v.startAnimation(animation); 418 | } 419 | 420 | } 421 | -------------------------------------------------------------------------------- /src/android/lib/FlipAnimation.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2012 Ephraim Tekle genzeb@gmail.com 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 5 | * associated documentation files (the "Software"), to deal in the Software without restriction, including 6 | * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 8 | * following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in all copies or substantial 11 | * portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 14 | * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 15 | * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 16 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 17 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | * 19 | * @author Ephraim A. Tekle 20 | * 21 | */ 22 | package com.telerik.plugins.nativepagetransitions.lib; 23 | 24 | import android.view.animation.Animation; 25 | import android.graphics.Camera; 26 | import android.graphics.Matrix; 27 | import android.view.animation.Transformation; 28 | 29 | /** 30 | * This class extends Animation to support a 3D flip view transition animation. Two instances of this class is 31 | * required: one for the "from" view and another for the "to" view. 32 | * 33 | * NOTE: use {@link AnimationFactory} to use this class. 34 | * 35 | * @author Ephraim A. Tekle 36 | * 37 | */ 38 | public class FlipAnimation extends Animation { 39 | private final float mFromDegrees; 40 | private final float mToDegrees; 41 | private final float mCenterX; 42 | private final float mCenterY; 43 | private Camera mCamera; 44 | 45 | private final ScaleUpDownEnum scaleType; 46 | 47 | /** 48 | * How much to scale up/down. The default scale of 75% of full size seems optimal based on testing. Feel free to experiment away, however. 49 | */ 50 | public static final float SCALE_DEFAULT = 0.69f; // works fine on S3 and PRO 8 51 | 52 | private float scale; 53 | 54 | /** 55 | * Constructs a new {@code FlipAnimation} object.Two {@code FlipAnimation} objects are needed for a complete transition b/n two views. 56 | * 57 | * @param fromDegrees the start angle in degrees for a rotation along the y-axis, i.e. in-and-out of the screen, i.e. 3D flip. This should really be multiple of 90 degrees. 58 | * @param toDegrees the end angle in degrees for a rotation along the y-axis, i.e. in-and-out of the screen, i.e. 3D flip. This should really be multiple of 90 degrees. 59 | * @param centerX the x-axis value of the center of rotation 60 | * @param centerY the y-axis value of the center of rotation 61 | * @param scale to get a 3D effect, the transition views need to be zoomed (scaled). This value must be b/n (0,1) or else the default scale {@link #SCALE_DEFAULT} is used. 62 | * @param scaleType flip view transition is broken down into two: the zoom-out of the "from" view and the zoom-in of the "to" view. This parameter is used to determine which is being done. See {@link ScaleUpDownEnum}. 63 | */ 64 | public FlipAnimation(float fromDegrees, float toDegrees, float centerX, float centerY, float scale, ScaleUpDownEnum scaleType) { 65 | mFromDegrees = fromDegrees; 66 | mToDegrees = toDegrees; 67 | mCenterX = centerX; 68 | mCenterY = centerY; 69 | this.scale = (scale<=0||scale>=1)?SCALE_DEFAULT:scale; 70 | this.scaleType = scaleType==null?ScaleUpDownEnum.SCALE_CYCLE:scaleType; 71 | } 72 | 73 | @Override 74 | public void initialize(int width, int height, int parentWidth, int parentHeight) { 75 | super.initialize(width, height, parentWidth, parentHeight); 76 | mCamera = new Camera(); 77 | } 78 | 79 | @Override 80 | protected void applyTransformation(float interpolatedTime, Transformation t) { 81 | final float fromDegrees = mFromDegrees; 82 | float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime); 83 | 84 | final float centerX = mCenterX; 85 | final float centerY = mCenterY; 86 | final Camera camera = mCamera; 87 | 88 | final Matrix matrix = t.getMatrix(); 89 | 90 | camera.save(); 91 | 92 | camera.rotateY(degrees); // TODO EV use rotateX for a vertical flip 93 | 94 | camera.getMatrix(matrix); 95 | camera.restore(); 96 | 97 | matrix.preTranslate(-centerX, -centerY); 98 | matrix.postTranslate(centerX, centerY); 99 | 100 | matrix.preScale(scaleType.getScale(scale, interpolatedTime), scaleType.getScale(scale, interpolatedTime), centerX, centerY); 101 | 102 | } 103 | 104 | 105 | /** 106 | * This enumeration is used to determine the zoom (or scale) behavior of a {@link FlipAnimation}. 107 | * 108 | * @author Ephraim A. Tekle 109 | * 110 | */ 111 | public static enum ScaleUpDownEnum { 112 | /** 113 | * The view will be scaled up from the scale value until it's at 100% zoom level (i.e. no zoom). 114 | */ 115 | SCALE_UP, 116 | /** 117 | * The view will be scaled down starting at no zoom (100% zoom level) until it's at a specified zoom level. 118 | */ 119 | SCALE_DOWN, 120 | /** 121 | * The view will cycle through a zoom down and then zoom up. 122 | */ 123 | SCALE_CYCLE, 124 | /** 125 | * No zoom effect is applied. 126 | */ 127 | SCALE_NONE; 128 | 129 | /** 130 | * The intermittent zoom level given the current or desired maximum zoom level for the specified iteration 131 | * 132 | * @param max the maximum desired or current zoom level 133 | * @param iter the iteration (from 0..1). 134 | * @return the current zoom level 135 | */ 136 | public float getScale(float max, float iter) { 137 | switch(this) { 138 | case SCALE_UP: 139 | return max + (1-max)*iter; 140 | 141 | case SCALE_DOWN: 142 | return 1 - (1-max)*iter; 143 | 144 | case SCALE_CYCLE: { 145 | final boolean halfWay = (iter > 0.5); 146 | 147 | if (halfWay) { 148 | return max + (1-max)*(iter-0.5f)*2; 149 | } else { 150 | return 1 - (1-max)*(iter*2); 151 | } 152 | } 153 | 154 | default: 155 | return 1; 156 | } 157 | } 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/ios/AppDelegate+nativepagetransitions.h: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | @interface AppDelegate (nativepagetransitions) 4 | 5 | - (void)application:(UIApplication *)application willChangeStatusBarFrame:(CGRect)newStatusBarFrame; 6 | 7 | @end -------------------------------------------------------------------------------- /src/ios/AppDelegate+nativepagetransitions.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate+nativepagetransitions.h" 2 | #import "NativePageTransitions.h" 3 | 4 | @implementation AppDelegate (nativepagetransitions) 5 | 6 | - (void)application:(UIApplication *)application willChangeStatusBarFrame:(CGRect)newStatusBarFrame { 7 | NativePageTransitions *nativeTransitions = [self.viewController getCommandInstance:@"NativePageTransitions"]; 8 | nativeTransitions.webViewPushedDownPixels = newStatusBarFrame.size.height; 9 | } 10 | 11 | @end -------------------------------------------------------------------------------- /src/ios/NativePageTransitions.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "Cordova/CDV.h" 3 | 4 | @interface NativePageTransitions : CDVPlugin 5 | 6 | @property (retain) NSMutableDictionary *slideOptions; 7 | @property (retain) NSMutableDictionary *flipOptions; 8 | @property (retain) NSMutableDictionary *drawerOptions; 9 | @property (retain) NSMutableDictionary *fadeOptions; 10 | @property (retain) NSMutableDictionary *curlOptions; 11 | 12 | @property (retain) UIColor *originalColor; 13 | 14 | @property (strong, nonatomic) IBOutlet UIImageView *screenShotImageViewTop; 15 | @property (strong, nonatomic) IBOutlet UIImageView *screenShotImageViewBottom; 16 | @property (strong, nonatomic) IBOutlet UIImageView *screenShotImageView; 17 | @property (strong, nonatomic) IBOutlet CDVInvokedUrlCommand *command; 18 | 19 | @property (strong, nonatomic) IBOutlet UIView *transitionView; 20 | @property (strong, nonatomic) IBOutlet WKWebView *wkWebView; 21 | @property (nonatomic, assign) int nonWebViewHeight; 22 | @property (nonatomic, assign) int webViewPushedDownPixels; 23 | 24 | - (void) slide:(CDVInvokedUrlCommand*)command; 25 | - (void) drawer:(CDVInvokedUrlCommand*)command; 26 | - (void) flip:(CDVInvokedUrlCommand*)command; 27 | - (void) curl:(CDVInvokedUrlCommand*)command; 28 | - (void) fade:(CDVInvokedUrlCommand*)command; 29 | 30 | - (void) executePendingTransition:(CDVInvokedUrlCommand*)command; 31 | - (void) cancelPendingTransition:(CDVInvokedUrlCommand*)command; 32 | 33 | @end -------------------------------------------------------------------------------- /src/ios/NativePageTransitions.m: -------------------------------------------------------------------------------- 1 | #import "NativePageTransitions.h" 2 | 3 | @implementation NativePageTransitions 4 | 5 | #define IS_RETINA_DISPLAY() [[UIScreen mainScreen] respondsToSelector:@selector(scale)] && [[UIScreen mainScreen] scale] == 2.0f 6 | #define IS_RETINA_HD_DISPLAY() [[UIScreen mainScreen] respondsToSelector:@selector(scale)] && [[UIScreen mainScreen] scale] == 3.0f 7 | #define DISPLAY_SCALE IS_RETINA_HD_DISPLAY() ? 3.0f : (IS_RETINA_DISPLAY() ? 2.0f : 1.0f) 8 | 9 | - (void) pluginInitialize { 10 | CGRect screenBound = [[UIScreen mainScreen] bounds]; 11 | 12 | // Set our transitioning view (see #114) 13 | self.transitionView = self.webView; 14 | 15 | // Look to see if a WKWebView exists 16 | Class wkWebViewClass = NSClassFromString(@"WKWebView"); 17 | if (wkWebViewClass) { 18 | for (int i = 0; i < self.webView.superview.subviews.count; i++) { 19 | UIView *subview = [self.webView.superview.subviews objectAtIndex:i]; 20 | if ([subview isKindOfClass:wkWebViewClass]) { 21 | self.transitionView = self.wkWebView = (WKWebView *)subview; 22 | } 23 | } 24 | } 25 | 26 | // webview height may differ from screen height because of a statusbar 27 | _nonWebViewHeight = screenBound.size.width-self.transitionView.frame.size.width + screenBound.size.height-self.transitionView.frame.size.height; 28 | } 29 | 30 | - (void)dispose 31 | { 32 | // Cleanup 33 | self.transitionView = nil; 34 | self.wkWebView = nil; 35 | 36 | [super dispose]; 37 | } 38 | 39 | - (void) executePendingTransition:(CDVInvokedUrlCommand*)command { 40 | _command = command; 41 | if (_slideOptions != nil) { 42 | [self performSlideTransition]; 43 | } else if (_flipOptions != nil) { 44 | [self performFlipTransition]; 45 | } else if (_drawerOptions != nil) { 46 | [self performDrawerTransition]; 47 | } else if (_fadeOptions != nil) { 48 | [self performFadeTransition]; 49 | } else if (_curlOptions != nil) { 50 | [self performCurlTransition]; 51 | } 52 | } 53 | 54 | - (void) cancelPendingTransition:(CDVInvokedUrlCommand*)command { 55 | _slideOptions = nil; 56 | _flipOptions = nil; 57 | _drawerOptions = nil; 58 | _fadeOptions = nil; 59 | _curlOptions = nil; 60 | 61 | // hide the screenshot like you mean it 62 | [_screenShotImageView removeFromSuperview]; 63 | if (_originalColor != nil) { 64 | self.viewController.view.backgroundColor = _originalColor; 65 | } 66 | // doesn't matter if these weren't added, but if they were we need to remove them 67 | [_screenShotImageViewTop removeFromSuperview]; 68 | [_screenShotImageViewBottom removeFromSuperview]; 69 | 70 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 71 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 72 | } 73 | 74 | - (void) slide:(CDVInvokedUrlCommand*)command { 75 | // make sure incorrect usage doesn't leave artifacts (call setup, then slide with delay >= 0) 76 | if (_slideOptions != nil) { 77 | [_screenShotImageView removeFromSuperview]; 78 | [_screenShotImageViewTop removeFromSuperview]; 79 | [_screenShotImageViewBottom removeFromSuperview]; 80 | } 81 | 82 | _command = command; 83 | NSMutableDictionary *args = [command.arguments objectAtIndex:0]; 84 | NSString *direction = [args objectForKey:@"direction"]; 85 | NSString *href = [args objectForKey:@"href"]; 86 | NSNumber *fixedPixelsTopNum = [args objectForKey:@"fixedPixelsTop"]; 87 | NSNumber *fixedPixelsBottomNum = [args objectForKey:@"fixedPixelsBottom"]; 88 | int fixedPixelsTop = [fixedPixelsTopNum intValue]; 89 | int fixedPixelsBottom = [fixedPixelsBottomNum intValue]; 90 | 91 | _originalColor = self.viewController.view.backgroundColor; 92 | self.viewController.view.backgroundColor = [UIColor blackColor]; 93 | self.transitionView.layer.shadowOpacity = 0; 94 | 95 | // CGFloat totalHeight = self.viewController.view.frame.size.height; 96 | CGFloat width = self.viewController.view.frame.size.width; 97 | CGFloat height = self.viewController.view.frame.size.height; 98 | CGRect screenshotRect = [self.viewController.view.window frame]; 99 | 100 | // correct landscape detection on iOS < 8 101 | BOOL isLandscape = UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation); 102 | if (isLandscape && width < height) { 103 | screenshotRect = CGRectMake(screenshotRect.origin.x, screenshotRect.origin.y, screenshotRect.size.height, screenshotRect.size.width); 104 | CGFloat temp = width; 105 | width = height; 106 | height = temp; 107 | } 108 | 109 | 110 | UIImage *image =[self grabScreenshot]; 111 | screenshotRect.size.height -= _webViewPushedDownPixels == 40 ? 20 : 0; 112 | _screenShotImageView = [[UIImageView alloc]initWithFrame:screenshotRect]; 113 | [_screenShotImageView setImage:image]; 114 | CGFloat retinaFactor = DISPLAY_SCALE; 115 | 116 | // in case of a statusbar above the webview, crop off the top 117 | if ((_nonWebViewHeight > 0 || fixedPixelsTop > 0) && [direction isEqualToString:@"down"]) { 118 | CGRect rect = CGRectMake(0.0f, (_nonWebViewHeight+fixedPixelsTop)*retinaFactor, image.size.width*retinaFactor, (image.size.height-_nonWebViewHeight-fixedPixelsTop)*retinaFactor); 119 | CGRect rect2 = CGRectMake(0.0f, _nonWebViewHeight+fixedPixelsTop, image.size.width, image.size.height-_nonWebViewHeight-fixedPixelsTop); 120 | CGImageRef tempImage = CGImageCreateWithImageInRect([image CGImage], rect); 121 | _screenShotImageView = [[UIImageView alloc]initWithFrame:rect2]; 122 | [_screenShotImageView setImage:[UIImage imageWithCGImage:tempImage]]; 123 | CGImageRelease(tempImage); 124 | } 125 | 126 | [self.transitionView.superview insertSubview:_screenShotImageView aboveSubview:self.transitionView]; 127 | 128 | // Make a cropped version of the screenshot with only the top and/or bottom piece 129 | if (fixedPixelsTop > 0) { 130 | CGRect rect = CGRectMake(0.0f, _nonWebViewHeight*retinaFactor, image.size.width*retinaFactor, fixedPixelsTop*retinaFactor); 131 | CGRect rect2 = CGRectMake(0.0f, _nonWebViewHeight, image.size.width, fixedPixelsTop); 132 | CGImageRef tempImage = CGImageCreateWithImageInRect([image CGImage], rect); 133 | _screenShotImageViewTop = [[UIImageView alloc]initWithFrame:rect2]; 134 | [_screenShotImageViewTop setImage:[UIImage imageWithCGImage:tempImage]]; 135 | CGImageRelease(tempImage); 136 | [self.transitionView.superview insertSubview:_screenShotImageViewTop aboveSubview:([direction isEqualToString:@"left"] || [direction isEqualToString:@"up"] ? self.transitionView : self.screenShotImageView)]; 137 | } 138 | if (fixedPixelsBottom > 0) { 139 | CGRect rect = CGRectMake(0.0f, (image.size.height-fixedPixelsBottom)*retinaFactor, image.size.width*retinaFactor, fixedPixelsBottom*retinaFactor); 140 | CGRect rect2 = CGRectMake(0.0f, image.size.height-fixedPixelsBottom, image.size.width, fixedPixelsBottom); 141 | CGImageRef tempImage = CGImageCreateWithImageInRect([image CGImage], rect); 142 | _screenShotImageViewBottom = [[UIImageView alloc]initWithFrame:rect2]; 143 | [_screenShotImageViewBottom setImage:[UIImage imageWithCGImage:tempImage]]; 144 | CGImageRelease(tempImage); 145 | [self.transitionView.superview insertSubview:_screenShotImageViewBottom aboveSubview:([direction isEqualToString:@"left"] || [direction isEqualToString:@"up"] ? self.transitionView : self.screenShotImageView)]; 146 | } 147 | 148 | if ([self loadHrefIfPassed:href]) { 149 | // pass in -1 for manual (requires you to call executePendingTransition) 150 | NSTimeInterval delay = [[args objectForKey:@"iosdelay"] doubleValue]; 151 | _slideOptions = args; 152 | if (delay < 0) { 153 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 154 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 155 | } else { 156 | [self performSlideTransition]; 157 | } 158 | } 159 | } 160 | 161 | - (void) performSlideTransition { 162 | NSMutableDictionary *args = _slideOptions; 163 | _slideOptions = nil; 164 | NSTimeInterval duration = [[args objectForKey:@"duration"] doubleValue]; 165 | // duration/delay is passed in ms, but needs to be in sec here 166 | duration = duration / 1000; 167 | CGFloat lowerLayerAlpha = 0.4f; 168 | NSTimeInterval delay = [[args objectForKey:@"iosdelay"] doubleValue]; // pass in -1 for manual (requires you to call executePendingTransition) 169 | if (delay < 0) { 170 | delay = 0; 171 | } 172 | delay = delay / 1000; 173 | 174 | NSNumber *slidePixelsNum = [args objectForKey:@"slidePixels"]; 175 | int slidePixels = [slidePixelsNum intValue]; 176 | 177 | NSString *direction = [args objectForKey:@"direction"]; 178 | NSNumber *slowdownfactor = [args objectForKey:@"slowdownfactor"]; 179 | 180 | NSNumber *fixedPixelsTopNum = [args objectForKey:@"fixedPixelsTop"]; 181 | int fixedPixelsTop = [fixedPixelsTopNum intValue]; 182 | 183 | CGFloat transitionToX = 0; 184 | CGFloat transitionToY = 0; 185 | CGFloat webviewFromY = _nonWebViewHeight; 186 | CGFloat webviewToY = _nonWebViewHeight; 187 | int screenshotSlowdownFactor = 1; 188 | int webviewSlowdownFactor = 1; 189 | 190 | CGFloat width = self.viewController.view.frame.size.width; 191 | CGFloat height = self.viewController.view.frame.size.height; 192 | 193 | // correct landscape detection on iOS < 8 194 | BOOL isLandscape = UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation); 195 | if (isLandscape && width < height) { 196 | CGFloat temp = width; 197 | width = height; 198 | height = temp; 199 | } 200 | 201 | if ([direction isEqualToString:@"left"]) { 202 | transitionToX = -width; 203 | screenshotSlowdownFactor = [slowdownfactor intValue]; 204 | } else if ([direction isEqualToString:@"right"]) { 205 | transitionToX = width; 206 | webviewSlowdownFactor = [slowdownfactor intValue]; 207 | } else if ([direction isEqualToString:@"up"]) { 208 | screenshotSlowdownFactor = [slowdownfactor intValue]; 209 | transitionToY = (-height/screenshotSlowdownFactor)+_nonWebViewHeight; 210 | webviewToY = _nonWebViewHeight; 211 | webviewFromY = height/webviewSlowdownFactor; 212 | } else if ([direction isEqualToString:@"down"]) { 213 | transitionToY = (height/screenshotSlowdownFactor)+_nonWebViewHeight; 214 | webviewSlowdownFactor = [slowdownfactor intValue]; 215 | webviewFromY = (-height/webviewSlowdownFactor)+_nonWebViewHeight; 216 | } 217 | 218 | if (screenshotSlowdownFactor > 0) { 219 | [UIView animateWithDuration:duration 220 | delay:delay 221 | options:UIViewAnimationOptionCurveEaseInOut 222 | animations:^{ 223 | if ([direction isEqualToString:@"left"] || [direction isEqualToString:@"up"]) { 224 | // the screenshot was on top of the webview to hide any page changes, but now we need the webview on top again 225 | [self.transitionView.superview sendSubviewToBack:_screenShotImageView]; 226 | } 227 | if (slidePixels > 0) { 228 | [_screenShotImageView setAlpha:0]; 229 | } 230 | [_screenShotImageView setFrame:CGRectMake(transitionToX/screenshotSlowdownFactor, slidePixels > 0 ? fixedPixelsTop+slidePixels : transitionToY, width, height)]; 231 | } 232 | completion:^(BOOL finished) { 233 | [_screenShotImageView removeFromSuperview]; 234 | if (_originalColor != nil) { 235 | self.viewController.view.backgroundColor = _originalColor; 236 | } 237 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 238 | [self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId]; 239 | }]; 240 | } else { 241 | [self.transitionView.superview sendSubviewToBack:_screenShotImageView]; 242 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (delay+duration) * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 243 | [_screenShotImageView removeFromSuperview]; 244 | if (_originalColor != nil) { 245 | self.viewController.view.backgroundColor = _originalColor; 246 | } 247 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 248 | [self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId]; 249 | }); 250 | } 251 | 252 | // also, fade out the screenshot a bit to give it some depth 253 | if ([slowdownfactor intValue] != 1 && ([direction isEqualToString:@"left"] || [direction isEqualToString:@"up"])) { 254 | [UIView animateWithDuration:duration 255 | delay:delay 256 | options:UIViewAnimationOptionCurveEaseInOut 257 | animations:^{ 258 | _screenShotImageView.alpha = slidePixels > 0 ? 1.0f : lowerLayerAlpha; 259 | } 260 | completion:^(BOOL finished) { 261 | }]; 262 | } 263 | 264 | // without this on wkwebview the transition permanently cuts off the fixedPixelsTop 265 | if (self.wkWebView != nil) { 266 | fixedPixelsTop = 0; 267 | } 268 | 269 | if (webviewSlowdownFactor > 0) { 270 | if (fixedPixelsTop > 0) { 271 | [self.transitionView setBounds:CGRectMake(0, fixedPixelsTop, width, height-_nonWebViewHeight+fixedPixelsTop)]; 272 | [self.transitionView setClipsToBounds:YES]; 273 | } 274 | int corr = 0; 275 | if ([direction isEqualToString:@"left"] || [direction isEqualToString:@"right"]) { 276 | //corr = fixedPixelsTop; 277 | } 278 | if (slidePixels > 0) { 279 | [self.transitionView setAlpha:0]; 280 | [self.transitionView.superview bringSubviewToFront:self.transitionView]; 281 | } 282 | [self.transitionView setFrame:CGRectMake(-transitionToX/webviewSlowdownFactor, slidePixels > 0 ? fixedPixelsTop+slidePixels : webviewFromY+corr, width, height-_nonWebViewHeight)]; 283 | 284 | [UIView animateWithDuration:duration 285 | delay:delay 286 | options:UIViewAnimationOptionCurveEaseInOut 287 | animations:^{ 288 | [self.transitionView setFrame:CGRectMake(0, webviewToY, width, height-_nonWebViewHeight)]; 289 | if (slidePixels > 0) { 290 | [self.transitionView setAlpha:1.0f]; 291 | } 292 | } 293 | completion:^(BOOL finished) { 294 | }]; 295 | } 296 | 297 | // let's make sure these are removed (#110 indicated they won't always disappear after animation finished) 298 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (delay+duration) * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 299 | // doesn't matter if these weren't added 300 | [_screenShotImageViewTop removeFromSuperview]; 301 | [_screenShotImageViewBottom removeFromSuperview]; 302 | }); 303 | 304 | if (slidePixels <= 0 && [slowdownfactor intValue] != 1 && ([direction isEqualToString:@"right"] || [direction isEqualToString:@"down"])) { 305 | self.transitionView.alpha = lowerLayerAlpha; 306 | [UIView animateWithDuration:duration 307 | delay:delay 308 | options:UIViewAnimationOptionCurveEaseInOut 309 | animations:^{ 310 | self.transitionView.alpha = 1.0f; 311 | } 312 | completion:^(BOOL finished) { 313 | }]; 314 | } 315 | } 316 | 317 | - (void) flip:(CDVInvokedUrlCommand*)command { 318 | _command = command; 319 | 320 | UIImage *image =[self grabScreenshot]; 321 | CGFloat width = self.viewController.view.frame.size.width; 322 | CGFloat height = self.viewController.view.frame.size.height; 323 | [_screenShotImageView setFrame:CGRectMake(0, 0, width, height)]; 324 | 325 | _screenShotImageView = [[UIImageView alloc]initWithFrame:[self.viewController.view.window frame]]; 326 | [_screenShotImageView setImage:image]; 327 | [self.transitionView.superview insertSubview:_screenShotImageView aboveSubview:self.transitionView]; 328 | 329 | NSMutableDictionary *args = [command.arguments objectAtIndex:0]; 330 | if ([self loadHrefIfPassed:[args objectForKey:@"href"]]) { 331 | // pass in -1 for manual (requires you to call executePendingTransition) 332 | NSTimeInterval delay = [[args objectForKey:@"iosdelay"] doubleValue]; 333 | _flipOptions = args; 334 | if (delay < 0) { 335 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 336 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 337 | return; 338 | } else { 339 | [self performFlipTransition]; 340 | } 341 | } 342 | } 343 | 344 | // Assumes input like "#00FF00" (#RRGGBB) 345 | - (UIColor *)colorFromHexString:(NSString *)hexString { 346 | unsigned rgbValue = 0; 347 | NSScanner *scanner = [NSScanner scannerWithString:hexString]; 348 | [scanner setScanLocation:1]; // bypass '#' character 349 | [scanner scanHexInt:&rgbValue]; 350 | return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0f]; 351 | } 352 | 353 | - (void) performFlipTransition { 354 | NSMutableDictionary *args = _flipOptions; 355 | _flipOptions = nil; 356 | NSTimeInterval duration = [[args objectForKey:@"duration"] doubleValue]; 357 | NSString *direction = [args objectForKey:@"direction"]; 358 | NSString *backgroundColor = [args objectForKey:@"backgroundColor"]; 359 | NSTimeInterval delay = [[args objectForKey:@"iosdelay"] doubleValue]; 360 | if (delay < 0) { 361 | delay = 0; 362 | } 363 | 364 | // change the background color of the view if the user likes that (no need to change it back btw) 365 | if (backgroundColor != nil) { 366 | UIColor *theColor = [self colorFromHexString:backgroundColor]; 367 | self.transitionView.superview.superview.backgroundColor = theColor; 368 | } 369 | 370 | // duration is passed in ms, but needs to be in sec here 371 | duration = duration / 1000; 372 | 373 | CGFloat width = self.viewController.view.frame.size.width; 374 | CGFloat height = self.viewController.view.frame.size.height; 375 | 376 | UIViewAnimationOptions animationOptions; 377 | if ([direction isEqualToString:@"right"]) { 378 | if (width < height && UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) { 379 | animationOptions = UIViewAnimationOptionTransitionFlipFromTop; 380 | } else { 381 | animationOptions = UIViewAnimationOptionTransitionFlipFromLeft; 382 | } 383 | } else if ([direction isEqualToString:@"left"]) { 384 | if (width < height && UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) { 385 | animationOptions = UIViewAnimationOptionTransitionFlipFromBottom; 386 | } else { 387 | animationOptions = UIViewAnimationOptionTransitionFlipFromRight; 388 | } 389 | } else if ([direction isEqualToString:@"up"]) { 390 | if (width < height && UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) { 391 | animationOptions = UIViewAnimationOptionTransitionFlipFromRight; 392 | } else { 393 | animationOptions = UIViewAnimationOptionTransitionFlipFromTop; 394 | } 395 | } else if ([direction isEqualToString:@"down"]) { 396 | if (width < height && UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) { 397 | animationOptions = UIViewAnimationOptionTransitionFlipFromLeft; 398 | } else { 399 | animationOptions = UIViewAnimationOptionTransitionFlipFromBottom; 400 | } 401 | } else { 402 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"direction should be one of up|down|left|right"]; 403 | [self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId]; 404 | return; 405 | } 406 | 407 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ 408 | // remove the screenshot halfway during the transition 409 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (duration/2) * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ 410 | [_screenShotImageView removeFromSuperview]; 411 | }); 412 | [UIView transitionWithView:self.viewController.view 413 | duration:duration 414 | options:animationOptions | UIViewAnimationOptionAllowAnimatedContent 415 | animations:^{} 416 | completion:^(BOOL finished) { 417 | if (backgroundColor != nil) { 418 | self.transitionView.superview.superview.backgroundColor = [UIColor blackColor]; 419 | } 420 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 421 | [self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId]; 422 | }]; 423 | }); 424 | } 425 | 426 | - (void) drawer:(CDVInvokedUrlCommand*)command { 427 | _command = command; 428 | 429 | UIImage *image =[self grabScreenshot]; 430 | 431 | NSMutableDictionary *args = [command.arguments objectAtIndex:0]; 432 | NSString *action = [args objectForKey:@"action"]; 433 | NSTimeInterval duration = [[args objectForKey:@"duration"] doubleValue]; 434 | 435 | // duration is passed in ms, but needs to be in sec here 436 | duration = duration / 1000; 437 | 438 | CGFloat width = self.viewController.view.frame.size.width; 439 | CGFloat height = self.viewController.view.frame.size.height; 440 | CGRect screenshotRect = [self.viewController.view.window frame]; 441 | 442 | // correct landscape detection on iOS < 8 443 | BOOL isLandscape = UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation); 444 | if (isLandscape && width < height) { 445 | screenshotRect = CGRectMake(screenshotRect.origin.x, screenshotRect.origin.y, screenshotRect.size.height, screenshotRect.size.width); 446 | CGFloat temp = width; 447 | width = height; 448 | height = temp; 449 | } 450 | 451 | [_screenShotImageView setFrame:screenshotRect]; 452 | if ([action isEqualToString:@"open"]) { 453 | _screenShotImageView = [[UIImageView alloc]initWithFrame:screenshotRect]; 454 | // add a cool shadow 455 | UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:_screenShotImageView.bounds]; 456 | _screenShotImageView.layer.masksToBounds = NO; 457 | _screenShotImageView.layer.shadowColor = [UIColor blackColor].CGColor; 458 | _screenShotImageView.layer.shadowOffset = CGSizeMake(0.0f, 5.0f); 459 | _screenShotImageView.layer.shadowOpacity = 0.5f; 460 | _screenShotImageView.layer.shadowPath = shadowPath.CGPath; 461 | } 462 | [_screenShotImageView setImage:image]; 463 | if ([action isEqualToString:@"open"]) { 464 | [self.transitionView.superview insertSubview:_screenShotImageView aboveSubview:self.transitionView]; 465 | } else { 466 | // add a cool shadow here as well 467 | UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:self.transitionView.bounds]; 468 | self.transitionView.layer.masksToBounds = NO; 469 | self.transitionView.layer.shadowColor = [UIColor blackColor].CGColor; 470 | self.transitionView.layer.shadowOffset = CGSizeMake(0.0f, 5.0f); 471 | self.transitionView.layer.shadowOpacity = 0.5f; 472 | self.transitionView.layer.shadowPath = shadowPath.CGPath; 473 | } 474 | 475 | if ([self loadHrefIfPassed:[args objectForKey:@"href"]]) { 476 | // pass in -1 for manual (requires you to call executePendingTransition) 477 | NSTimeInterval delay = [[args objectForKey:@"iosdelay"] doubleValue]; 478 | _drawerOptions = args; 479 | if (delay < 0) { 480 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 481 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 482 | } else { 483 | [self performDrawerTransition]; 484 | } 485 | } 486 | } 487 | 488 | - (void) performDrawerTransition { 489 | NSMutableDictionary *args = _drawerOptions; 490 | _drawerOptions = nil; 491 | NSTimeInterval duration = [[args objectForKey:@"duration"] doubleValue]; 492 | NSString *action = [args objectForKey:@"action"]; 493 | NSString *origin = [args objectForKey:@"origin"]; 494 | NSTimeInterval delay = [[args objectForKey:@"iosdelay"] doubleValue]; 495 | if (delay < 0) { 496 | delay = 0; 497 | } 498 | // duration is passed in ms, but needs to be in sec here 499 | duration = duration / 1000; 500 | delay = delay / 1000; 501 | 502 | CGFloat width = self.viewController.view.frame.size.width; 503 | CGFloat height = self.viewController.view.frame.size.height; 504 | 505 | // correct landscape detection on iOS < 8 506 | BOOL isLandscape = UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation); 507 | if (isLandscape && width < height) { 508 | CGFloat temp = width; 509 | width = height; 510 | height = temp; 511 | } 512 | 513 | CGFloat transitionToX = 0; 514 | CGFloat webviewTransitionFromX = 0; 515 | int screenshotPx = 44; 516 | 517 | if ([action isEqualToString:@"open"]) { 518 | if ([origin isEqualToString:@"right"]) { 519 | transitionToX = -width+screenshotPx; 520 | } else { 521 | transitionToX = width-screenshotPx; 522 | } 523 | } else if ([action isEqualToString:@"close"]) { 524 | if ([origin isEqualToString:@"right"]) { 525 | transitionToX = screenshotPx; 526 | webviewTransitionFromX = -width+screenshotPx; 527 | } else { 528 | transitionToX = -width+screenshotPx; 529 | webviewTransitionFromX = width-screenshotPx; 530 | } 531 | } 532 | 533 | if ([action isEqualToString:@"open"]) { 534 | [UIView animateWithDuration:duration 535 | delay:delay 536 | options:UIViewAnimationOptionCurveEaseInOut 537 | animations:^{ 538 | [_screenShotImageView setFrame:CGRectMake(transitionToX, 0, width, height)]; 539 | } 540 | completion:^(BOOL finished) { 541 | if ([action isEqualToString:@"close"]) { 542 | _screenShotImageView = nil; 543 | // [_screenShotImageView removeFromSuperview]; 544 | } 545 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 546 | [self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId]; 547 | }]; 548 | } 549 | 550 | if ([action isEqualToString:@"close"]) { 551 | [self.transitionView setFrame:CGRectMake(webviewTransitionFromX, _nonWebViewHeight, width, height-_nonWebViewHeight)]; 552 | 553 | // position the webview above the screenshot just after the animation kicks in so no flash of the webview occurs 554 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay+50 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ 555 | [self.transitionView.superview bringSubviewToFront:self.transitionView]; 556 | }); 557 | 558 | [UIView animateWithDuration:duration 559 | delay:delay 560 | options:UIViewAnimationOptionCurveEaseInOut 561 | animations:^{ 562 | [self.transitionView setFrame:CGRectMake(0, _nonWebViewHeight, width, height-_nonWebViewHeight)]; 563 | } 564 | completion:^(BOOL finished) { 565 | [_screenShotImageView removeFromSuperview]; 566 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 567 | [self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId]; 568 | }]; 569 | } 570 | } 571 | 572 | - (void) fade:(CDVInvokedUrlCommand*)command { 573 | _command = command; 574 | 575 | UIImage *image =[self grabScreenshot]; 576 | 577 | NSMutableDictionary *args = [command.arguments objectAtIndex:0]; 578 | 579 | CGFloat width = self.viewController.view.frame.size.width; 580 | CGFloat height = self.viewController.view.frame.size.height; 581 | [_screenShotImageView setFrame:CGRectMake(0, 0, width, height)]; 582 | 583 | _screenShotImageView = [[UIImageView alloc]initWithFrame:[self.viewController.view.window frame]]; 584 | [_screenShotImageView setImage:image]; 585 | [self.transitionView.superview insertSubview:_screenShotImageView aboveSubview:self.transitionView]; 586 | 587 | if ([self loadHrefIfPassed:[args objectForKey:@"href"]]) { 588 | // pass in -1 for manual (requires you to call executePendingTransition) 589 | NSTimeInterval delay = [[args objectForKey:@"iosdelay"] doubleValue]; 590 | _fadeOptions = args; 591 | if (delay < 0) { 592 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 593 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 594 | } else { 595 | [self performFadeTransition]; 596 | } 597 | } 598 | } 599 | 600 | - (void) performFadeTransition { 601 | NSMutableDictionary *args = _fadeOptions; 602 | _fadeOptions = nil; 603 | NSTimeInterval delay = [[args objectForKey:@"iosdelay"] doubleValue]; 604 | NSTimeInterval duration = [[args objectForKey:@"duration"] doubleValue]; 605 | 606 | if (delay < 0) { 607 | delay = 0; 608 | } 609 | // delay/duration is passed in ms, but needs to be in sec here 610 | duration = duration / 1000; 611 | delay = delay / 1000; 612 | 613 | UIViewAnimationOptions animationOptions = UIViewAnimationOptionTransitionCrossDissolve; 614 | 615 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ 616 | // remove the screenshot halfway during the transition 617 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (duration/2) * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ 618 | [_screenShotImageView removeFromSuperview]; 619 | }); 620 | [UIView transitionWithView:self.viewController.view 621 | duration:duration 622 | options:animationOptions | UIViewAnimationOptionAllowAnimatedContent 623 | animations:^{} 624 | completion:^(BOOL finished) { 625 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 626 | [self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId]; 627 | }]; 628 | }); 629 | } 630 | 631 | - (void) curl:(CDVInvokedUrlCommand*)command { 632 | _command = command; 633 | 634 | UIImage *image =[self grabScreenshot]; 635 | 636 | NSMutableDictionary *args = [command.arguments objectAtIndex:0]; 637 | NSTimeInterval duration = [[args objectForKey:@"duration"] doubleValue]; 638 | 639 | // duration is passed in ms, but needs to be in sec here 640 | duration = duration / 1000; 641 | 642 | CGFloat width = self.viewController.view.frame.size.width; 643 | CGFloat height = self.viewController.view.frame.size.height; 644 | [_screenShotImageView setFrame:CGRectMake(0, 0, width, height)]; 645 | 646 | _screenShotImageView = [[UIImageView alloc]initWithFrame:[self.viewController.view.window frame]]; 647 | [_screenShotImageView setImage:image]; 648 | [self.transitionView.superview insertSubview:_screenShotImageView aboveSubview:self.transitionView]; 649 | 650 | if ([self loadHrefIfPassed:[args objectForKey:@"href"]]) { 651 | // pass in -1 for manual (requires you to call executePendingTransition) 652 | NSTimeInterval delay = [[args objectForKey:@"iosdelay"] doubleValue]; 653 | _curlOptions = args; 654 | if (delay < 0) { 655 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 656 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 657 | } else { 658 | [self performCurlTransition]; 659 | } 660 | } 661 | } 662 | 663 | - (void) performCurlTransition { 664 | NSMutableDictionary *args = _curlOptions; 665 | _curlOptions = nil; 666 | NSTimeInterval delay = [[args objectForKey:@"iosdelay"] doubleValue]; 667 | NSTimeInterval duration = [[args objectForKey:@"duration"] doubleValue]; 668 | NSString *direction = [args objectForKey:@"direction"]; 669 | 670 | if (delay < 0) { 671 | delay = 0; 672 | } 673 | // delay/duration is passed in ms, but needs to be in sec here 674 | duration = duration / 1000; 675 | delay = delay / 1000; 676 | 677 | UIViewAnimationOptions animationOptions; 678 | if ([direction isEqualToString:@"up"]) { 679 | animationOptions = UIViewAnimationOptionTransitionCurlUp; 680 | } else if ([direction isEqualToString:@"down"]) { 681 | animationOptions = UIViewAnimationOptionTransitionCurlDown; 682 | } else { 683 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"direction should be one of up|down"]; 684 | [self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId]; 685 | return; 686 | } 687 | 688 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ 689 | // remove the screenshot halfway during the transition 690 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (duration/2) * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ 691 | [_screenShotImageView removeFromSuperview]; 692 | }); 693 | [UIView transitionWithView:self.viewController.view 694 | duration:duration 695 | options:animationOptions | UIViewAnimationOptionAllowAnimatedContent 696 | animations:^{} 697 | completion:^(BOOL finished) { 698 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 699 | [self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId]; 700 | }]; 701 | }); 702 | } 703 | 704 | - (UIImage*) grabScreenshot { 705 | UIGraphicsBeginImageContextWithOptions(self.viewController.view.bounds.size, YES, 0.0f); 706 | 707 | // Since drawViewHierarchyInRect is slower than renderInContext we should only use it to overcome the bug in WKWebView 708 | if (self.wkWebView != nil) { 709 | [self.viewController.view drawViewHierarchyInRect:self.viewController.view.bounds afterScreenUpdates:NO]; 710 | } else { 711 | [self.viewController.view.layer renderInContext:UIGraphicsGetCurrentContext()]; 712 | } 713 | 714 | // Read the UIImage object 715 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 716 | UIGraphicsEndImageContext(); 717 | return image; 718 | } 719 | 720 | - (BOOL) loadHrefIfPassed:(NSString*) href { 721 | WKWebView *uiwebview = nil; 722 | if ([self.webView isKindOfClass:[WKWebView class]]) { 723 | uiwebview = ((WKWebView*)self.webView); 724 | } 725 | if (href != nil && ![href isEqual:[NSNull null]]) { 726 | if (![href hasPrefix:@"#"]) { 727 | // strip any params when looking for the file on the filesystem 728 | NSString *bareFileName = href; 729 | NSString *urlParams = nil; 730 | 731 | if (![bareFileName hasSuffix:@".html"]) { 732 | NSRange range = [href rangeOfString:@".html"]; 733 | bareFileName = [href substringToIndex:range.location+5]; 734 | urlParams = [href substringFromIndex:range.location+5]; 735 | } 736 | NSURL *url; 737 | NSURL *origUrl; 738 | if (self.wkWebView != nil) { 739 | origUrl = self.wkWebView.URL; 740 | } else { 741 | origUrl = uiwebview.URL; 742 | } 743 | if([origUrl.scheme isEqualToString:@"file"]) { 744 | NSString *currentUrl = origUrl.absoluteString; 745 | NSRange lastSlash = [currentUrl rangeOfString:@"/" options:NSBackwardsSearch]; 746 | NSString *path = [currentUrl substringToIndex:lastSlash.location+1]; 747 | url = [NSURL URLWithString:[path stringByAppendingString:bareFileName]]; 748 | } else { 749 | NSString *filePath = bareFileName; 750 | NSString *replaceWith = [@"/" stringByAppendingString:bareFileName]; 751 | filePath = [origUrl.absoluteString stringByReplacingOccurrencesOfString:origUrl.path withString:replaceWith]; 752 | url = [NSURL URLWithString:filePath]; 753 | } 754 | 755 | // re-attach the params when loading the url 756 | if (urlParams != nil) { 757 | NSString *absoluteURLString = [url absoluteString]; 758 | NSString *absoluteURLWithParams = [absoluteURLString stringByAppendingString: urlParams]; 759 | url = [NSURL URLWithString:absoluteURLWithParams]; 760 | } 761 | 762 | NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url]; 763 | 764 | // Utilize WKWebView for request if it exists 765 | if (self.wkWebView != nil) { 766 | [self.wkWebView loadRequest: urlRequest]; 767 | } else { 768 | [uiwebview loadRequest: urlRequest]; 769 | } 770 | } else if (![href hasPrefix:@"#"]) { 771 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"href must be null, a .html file or a #navigationhash"]; 772 | [self.commandDelegate sendPluginResult:pluginResult callbackId:_command.callbackId]; 773 | return NO; 774 | } else { 775 | // it's a hash, so load the url without any possible current hash 776 | NSString *url = nil; 777 | if (self.wkWebView != nil) { 778 | url = self.wkWebView.URL.absoluteString; 779 | } else { 780 | url = uiwebview.URL.absoluteString; 781 | } 782 | 783 | // remove the # if it's still there 784 | if ([url rangeOfString:@"#"].location != NSNotFound) { 785 | NSRange range = [url rangeOfString:@"#"]; 786 | url = [url substringToIndex:range.location]; 787 | } 788 | // attach the hash 789 | url = [url stringByAppendingString:href]; 790 | // and load it 791 | NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; 792 | 793 | if (self.wkWebView != nil) { 794 | [self.wkWebView loadRequest: urlRequest]; 795 | } else { 796 | [uiwebview loadRequest: urlRequest]; 797 | } 798 | } 799 | } 800 | return YES; 801 | } 802 | @end 803 | -------------------------------------------------------------------------------- /src/winphone/NativePageTransitions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Phone.Tasks; 2 | using Microsoft.Phone.Controls; 3 | using WPCordovaClassLib.Cordova; 4 | using WPCordovaClassLib.Cordova.Commands; 5 | using WPCordovaClassLib.Cordova.JSON; 6 | using System.Runtime.Serialization; 7 | using System; 8 | using System.ComponentModel; 9 | using System.IO; 10 | using System.Threading; 11 | using System.Windows; 12 | using System.Windows.Media; 13 | using System.Windows.Media.Animation; 14 | using System.Windows.Media.Imaging; 15 | using System.Windows.Controls; 16 | using System.Windows.Controls.Primitives; 17 | using System.Windows.Navigation; 18 | using WPCordovaClassLib; 19 | using Microsoft.Xna.Framework.Media; 20 | 21 | namespace Cordova.Extension.Commands 22 | { 23 | public class NativePageTransitions : BaseCommand 24 | { 25 | 26 | public NativePageTransitions() 27 | { 28 | cView = getCordovaView(); 29 | browser = cView.Browser; 30 | //browser.Navigated += Browser_Navigated; 31 | //browser.Navigating += Browser_Navigating; 32 | img = new Image(); 33 | } 34 | 35 | [DataContract] 36 | public class TransitionOptions 37 | { 38 | [DataMember(IsRequired = true, Name = "direction")] 39 | public string direction { get; set; } 40 | 41 | [DataMember(IsRequired = true, Name = "duration")] 42 | public int duration { get; set; } 43 | 44 | [DataMember(IsRequired = false, Name = "slowdownfactor")] 45 | public int slowdownfactor { get; set; } 46 | 47 | [DataMember(IsRequired = false, Name = "href")] 48 | public string href { get; set; } 49 | 50 | [DataMember(IsRequired = false, Name = "winphonedelay")] 51 | public int winphonedelay { get; set; } 52 | } 53 | 54 | private CordovaView cView; 55 | private WebBrowser browser; 56 | private TransitionOptions transitionOptions; 57 | private Image img; 58 | private Image img2; 59 | 60 | public void slide(string options) 61 | { 62 | try 63 | { 64 | String jsonOptions = JsonHelper.Deserialize(options)[0]; 65 | transitionOptions = JsonHelper.Deserialize(jsonOptions); 66 | } 67 | catch (Exception) 68 | { 69 | DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION)); 70 | return; 71 | } 72 | 73 | Deployment.Current.Dispatcher.BeginInvoke(() => 74 | { 75 | // grab a screenshot 76 | WriteableBitmap bmp = new WriteableBitmap(browser, null); 77 | var width = (int)bmp.PixelWidth; 78 | var height = (int)bmp.PixelHeight; 79 | 80 | img.Source = bmp; 81 | 82 | img2 = new Image(); 83 | img2.Source = bmp; 84 | 85 | // image animation 86 | img2.RenderTransform = new TranslateTransform(); 87 | DoubleAnimation imgAnimation = new DoubleAnimation(); 88 | imgAnimation.Duration = TimeSpan.FromMilliseconds(transitionOptions.duration); 89 | 90 | string animationAxis = "X"; 91 | double webviewAnimationFrom = 0; 92 | int screenshotSlowdownFactor = 1; 93 | int webviewSlowdownFactor = 1; 94 | int imgOrdering = 0; 95 | 96 | if (transitionOptions.slowdownfactor < 0) { 97 | transitionOptions.slowdownfactor = 1000; 98 | } 99 | 100 | if (transitionOptions.direction == "left") 101 | { 102 | screenshotSlowdownFactor = transitionOptions.slowdownfactor; 103 | webviewAnimationFrom = width; 104 | imgAnimation.To = -width / screenshotSlowdownFactor; // Application.Current.Host.Content.ActualWidth; 105 | } 106 | else if (transitionOptions.direction == "right") 107 | { 108 | webviewSlowdownFactor = transitionOptions.slowdownfactor; 109 | webviewAnimationFrom = -width; 110 | imgAnimation.To = width; // Application.Current.Host.Content.ActualWidth; 111 | imgOrdering = 1; 112 | } 113 | else if (transitionOptions.direction == "up") 114 | { 115 | animationAxis = "Y"; 116 | screenshotSlowdownFactor = transitionOptions.slowdownfactor; 117 | webviewAnimationFrom = height; 118 | imgAnimation.To = -height / screenshotSlowdownFactor; // Application.Current.Host.Content.ActualHeight; 119 | } 120 | else if (transitionOptions.direction == "down") 121 | { 122 | animationAxis = "Y"; 123 | webviewSlowdownFactor = transitionOptions.slowdownfactor; 124 | webviewAnimationFrom = -height; 125 | imgAnimation.To = height; // Application.Current.Host.Content.ActualHeight; 126 | imgOrdering = 1; 127 | } 128 | 129 | // inserting the image at index 0 makes it appear below the webview, 130 | // but we need to set it to 1 first so the webview is hidden and can be updated 131 | cView.LayoutRoot.Children.Insert(1, img); 132 | cView.LayoutRoot.Children.Insert(imgOrdering, img2); 133 | 134 | 135 | // now load the new content 136 | if (transitionOptions.href != null && transitionOptions.href != "" && transitionOptions.href != "null") 137 | { 138 | String to = transitionOptions.href; 139 | Uri currenturi = browser.Source; 140 | string path = currenturi.OriginalString; 141 | if (to.StartsWith("#")) 142 | { 143 | if (path.StartsWith("//")) 144 | { 145 | path = path.Substring(2); 146 | } 147 |  if (path.Contains("#")) 148 | { 149 | path = path.Substring(0, path.IndexOf("#")); 150 | } 151 | to = path + to; 152 | } 153 | else 154 | { 155 | to = path.Substring(0, path.LastIndexOf('/')+1) + to; 156 | } 157 | browser.Navigate(new Uri(to, UriKind.RelativeOrAbsolute)); 158 | } 159 | 160 | Storyboard.SetTarget(imgAnimation, img2); 161 | Storyboard.SetTargetProperty(imgAnimation, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform." + animationAxis + ")")); 162 | 163 | 164 | browser.RenderTransform = new TranslateTransform(); 165 | DoubleAnimation webviewAnimation = new DoubleAnimation(); 166 | webviewAnimation.Duration = TimeSpan.FromMilliseconds(transitionOptions.duration); 167 | webviewAnimation.From = webviewAnimationFrom / webviewSlowdownFactor; 168 | webviewAnimation.To = 0; 169 | Storyboard.SetTarget(webviewAnimation, browser); 170 | Storyboard.SetTargetProperty(webviewAnimation, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform." + animationAxis + ")")); 171 | 172 | 173 | Storyboard storyboard = new Storyboard(); 174 | storyboard.Completed += slideAnimationCompleted; 175 | storyboard.Children.Add(imgAnimation); 176 | storyboard.Children.Add(webviewAnimation); 177 | 178 | this.Perform(delegate() 179 | { 180 | cView.LayoutRoot.Children.Remove(img); 181 | storyboard.Begin(); 182 | }, transitionOptions.winphonedelay); 183 | }); 184 | } 185 | 186 | 187 | public void flip(string options) 188 | { 189 | try 190 | { 191 | String jsonOptions = JsonHelper.Deserialize(options)[0]; 192 | transitionOptions = JsonHelper.Deserialize(jsonOptions); 193 | } 194 | catch (Exception) 195 | { 196 | DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION)); 197 | return; 198 | } 199 | 200 | Deployment.Current.Dispatcher.BeginInvoke(() => 201 | { 202 | // grab a screenshot 203 | WriteableBitmap bmp = new WriteableBitmap(browser, null); 204 | 205 | img2 = new Image(); 206 | img2.Source = bmp; 207 | 208 | int direction = 1; 209 | DependencyProperty property = PlaneProjection.RotationYProperty; 210 | 211 | if (transitionOptions.direction == "right") 212 | { 213 | direction = -1; 214 | } 215 | else if (transitionOptions.direction == "up") 216 | { 217 | property = PlaneProjection.RotationXProperty; 218 | direction = -1; 219 | } 220 | else if (transitionOptions.direction == "down") 221 | { 222 | property = PlaneProjection.RotationXProperty; 223 | } 224 | 225 | // Insert the screenshot above the webview (index 1) 226 | cView.LayoutRoot.Children.Insert(1, img2); 227 | 228 | // now load the new content 229 | if (transitionOptions.href != null && transitionOptions.href != "" && transitionOptions.href != "null") 230 | { 231 | String to = transitionOptions.href; 232 | Uri currenturi = browser.Source; 233 | string path = currenturi.OriginalString; 234 | if (to.StartsWith("#")) 235 | { 236 | if (path.StartsWith("//")) 237 | { 238 | path = path.Substring(2); 239 | } 240 |  if (path.Contains("#")) 241 | { 242 | path = path.Substring(0, path.IndexOf("#")); 243 | } 244 | to = path + to; 245 | } 246 | else 247 | { 248 | to = path.Substring(0, path.LastIndexOf('/')+1) + to; 249 | } 250 | browser.Navigate(new Uri(to, UriKind.RelativeOrAbsolute)); 251 | } 252 | 253 | TimeSpan duration = TimeSpan.FromMilliseconds(transitionOptions.duration); 254 | Storyboard sb = new Storyboard(); 255 | sb.Completed += flipAnimationCompleted; 256 | 257 | // animation for the screenshot 258 | DoubleAnimation imgAnimation = new DoubleAnimation() 259 | { 260 | From = 0, 261 | To = direction * 180, 262 | Duration = new Duration(duration) 263 | }; 264 | Storyboard.SetTargetProperty(imgAnimation, new PropertyPath(property)); 265 | img2.Projection = new PlaneProjection(); 266 | Storyboard.SetTarget(imgAnimation, img2.Projection); 267 | sb.Children.Add(imgAnimation); 268 | 269 | // animation for the webview 270 | DoubleAnimation webviewAnimation = new DoubleAnimation() 271 | { 272 | From = direction * -180, 273 | To = 0, 274 | Duration = new Duration(duration) 275 | }; 276 | Storyboard.SetTargetProperty(webviewAnimation, new PropertyPath(property)); 277 | browser.Projection = new PlaneProjection(); 278 | Storyboard.SetTarget(webviewAnimation, browser.Projection); 279 | sb.Children.Add(webviewAnimation); 280 | 281 | // perform the transition after the specified delay 282 | this.Perform(delegate() 283 | { 284 | // remove the image halfway down the transition so we don't see the back of the image instead of the webview 285 | this.Perform(delegate() 286 | { 287 | Deployment.Current.Dispatcher.BeginInvoke(() => 288 | { 289 | CordovaView cView2 = getCordovaView(); 290 | cView2.LayoutRoot.Children.Remove(img2); 291 | }); 292 | }, transitionOptions.duration / 2); 293 | 294 | sb.Begin(); 295 | }, transitionOptions.winphonedelay); 296 | }); 297 | } 298 | 299 | // clean up resources 300 | private void slideAnimationCompleted(object sender, EventArgs e) 301 | { 302 | (sender as Storyboard).Completed -= slideAnimationCompleted; 303 | Deployment.Current.Dispatcher.BeginInvoke(() => 304 | { 305 | CordovaView cView = getCordovaView(); 306 | cView.LayoutRoot.Children.Remove(img2); 307 | }); 308 | DispatchCommandResult(new PluginResult(PluginResult.Status.OK)); 309 | } 310 | 311 | private void flipAnimationCompleted(object sender, EventArgs e) 312 | { 313 | (sender as Storyboard).Completed -= flipAnimationCompleted; 314 | DispatchCommandResult(new PluginResult(PluginResult.Status.OK)); 315 | } 316 | 317 | void Browser_Navigated(object sender, NavigationEventArgs e) 318 | { 319 | } 320 | 321 | void Browser_Navigating(object sender, NavigationEventArgs e) 322 | { 323 | } 324 | 325 | private CordovaView getCordovaView() 326 | { 327 | PhoneApplicationFrame frame = (PhoneApplicationFrame)Application.Current.RootVisual; 328 | PhoneApplicationPage page = (PhoneApplicationPage)frame.Content; 329 | return (CordovaView)page.FindName("CordovaView"); 330 | } 331 | 332 | private void Perform(Action myMethod, int delayInMilliseconds) 333 | { 334 | BackgroundWorker worker = new BackgroundWorker(); 335 | worker.DoWork += (s, e) => Thread.Sleep(delayInMilliseconds); 336 | worker.RunWorkerCompleted += (s, e) => myMethod.Invoke(); 337 | worker.RunWorkerAsync(); 338 | } 339 | } 340 | } -------------------------------------------------------------------------------- /www/NativePageTransitions.js: -------------------------------------------------------------------------------- 1 | function NativePageTransitions() { 2 | } 3 | 4 | NativePageTransitions.prototype.globalOptions = { 5 | duration: 400, 6 | iosdelay: 60, // a number of milliseconds, or -1 (call executePendingTransition() when ready) 7 | androiddelay: 70, // a number of milliseconds, or -1 (call executePendingTransition() when ready) 8 | winphonedelay: 200, 9 | slowdownfactor: 4, 10 | fixedPixelsTop: 0, // currently for slide left/right only 11 | fixedPixelsBottom: 0 // currently for slide left/right only 12 | }; 13 | 14 | NativePageTransitions.prototype.executePendingTransition = function (onSuccess, onError) { 15 | cordova.exec(onSuccess, onError, "NativePageTransitions", "executePendingTransition", []); 16 | }; 17 | 18 | NativePageTransitions.prototype.cancelPendingTransition = function (onSuccess, onError) { 19 | cordova.exec(onSuccess, onError, "NativePageTransitions", "cancelPendingTransition", []); 20 | }; 21 | 22 | NativePageTransitions.prototype.slide = function (options, onSuccess, onError) { 23 | var opts = options || {}; 24 | if (!this._validateHref(opts.href, onError)) { 25 | return; 26 | } 27 | opts.direction = opts.direction || "left"; 28 | if (opts.duration == undefined || opts.duration == "null") { 29 | opts.duration = this.globalOptions.duration; 30 | } 31 | if (opts.androiddelay == undefined || opts.androiddelay == "null") { 32 | opts.androiddelay = this.globalOptions.androiddelay; 33 | } 34 | if (opts.iosdelay == undefined || opts.iosdelay == "null") { 35 | opts.iosdelay = this.globalOptions.iosdelay; 36 | } 37 | if (opts.winphonedelay == undefined || opts.winphonedelay == "null") { 38 | opts.winphonedelay = this.globalOptions.winphonedelay; 39 | } 40 | if (opts.fixedPixelsTop == undefined || opts.fixedPixelsTop == "null") { 41 | opts.fixedPixelsTop = this.globalOptions.fixedPixelsTop; 42 | } 43 | if (opts.fixedPixelsBottom == undefined || opts.fixedPixelsBottom == "null") { 44 | opts.fixedPixelsBottom = this.globalOptions.fixedPixelsBottom; 45 | } 46 | // setting slowdownfactor > 1 makes the next page slide less pixels. Use 1 for side-by-side. 47 | opts.slowdownfactor = opts.slowdownfactor || this.globalOptions.slowdownfactor; 48 | cordova.exec(onSuccess, onError, "NativePageTransitions", "slide", [opts]); 49 | }; 50 | 51 | NativePageTransitions.prototype.drawer = function (options, onSuccess, onError) { 52 | var opts = options || {}; 53 | if (!this._validateHref(opts.href, onError)) { 54 | return; 55 | } 56 | opts.origin = opts.origin || "left"; 57 | opts.action = opts.action || "open"; 58 | if (opts.duration == undefined || opts.duration == "null") { 59 | opts.duration = this.globalOptions.duration; 60 | } 61 | if (opts.androiddelay == undefined || opts.androiddelay == "null") { 62 | opts.androiddelay = this.globalOptions.androiddelay; 63 | } 64 | if (opts.iosdelay == undefined || opts.iosdelay == "null") { 65 | opts.iosdelay = this.globalOptions.iosdelay; 66 | } 67 | if (opts.winphonedelay == undefined || opts.winphonedelay == "null") { 68 | opts.winphonedelay = this.globalOptions.winphonedelay; 69 | } 70 | cordova.exec(onSuccess, onError, "NativePageTransitions", "drawer", [opts]); 71 | }; 72 | 73 | NativePageTransitions.prototype.flip = function (options, onSuccess, onError) { 74 | var opts = options || {}; 75 | if (!this._validateHref(opts.href, onError)) { 76 | return; 77 | } 78 | opts.direction = opts.direction || "right"; 79 | if (opts.duration == undefined || opts.duration == "null") { 80 | opts.duration = this.globalOptions.duration; 81 | } 82 | if (opts.androiddelay == undefined || opts.androiddelay == "null") { 83 | opts.androiddelay = this.globalOptions.androiddelay; 84 | } 85 | if (opts.iosdelay == undefined || opts.iosdelay == "null") { 86 | opts.iosdelay = this.globalOptions.iosdelay; 87 | } 88 | if (opts.winphonedelay == undefined || opts.winphonedelay == "null") { 89 | opts.winphonedelay = this.globalOptions.winphonedelay; 90 | } 91 | cordova.exec(onSuccess, onError, "NativePageTransitions", "flip", [opts]); 92 | }; 93 | 94 | NativePageTransitions.prototype.curl = function (options, onSuccess, onError) { 95 | var opts = options || {}; 96 | if (!this._validateHref(opts.href, onError)) { 97 | return; 98 | } 99 | opts.direction = opts.direction || "up"; 100 | if (opts.duration == undefined || opts.duration == "null") { 101 | opts.duration = this.globalOptions.duration; 102 | } 103 | if (opts.iosdelay == undefined || opts.iosdelay == "null") { 104 | opts.iosdelay = this.globalOptions.iosdelay; 105 | } 106 | cordova.exec(onSuccess, onError, "NativePageTransitions", "curl", [opts]); 107 | }; 108 | 109 | NativePageTransitions.prototype.fade = function (options, onSuccess, onError) { 110 | var opts = options || {}; 111 | if (!this._validateHref(opts.href, onError)) { 112 | return; 113 | } 114 | if (opts.duration == undefined || opts.duration == "null") { 115 | opts.duration = this.globalOptions.duration; 116 | } 117 | if (opts.androiddelay == undefined || opts.androiddelay == "null") { 118 | opts.androiddelay = this.globalOptions.androiddelay; 119 | } 120 | if (opts.iosdelay == undefined || opts.iosdelay == "null") { 121 | opts.iosdelay = this.globalOptions.iosdelay; 122 | } 123 | cordova.exec(onSuccess, onError, "NativePageTransitions", "fade", [opts]); 124 | }; 125 | 126 | NativePageTransitions.prototype._validateHref = function (href, errCallback) { 127 | // if not contains www/ : dan zit je in een companion app.. 128 | var hrf = window.location.href; 129 | var currentHref; 130 | if (hrf.indexOf('www/') == -1) { 131 | // hrf is something like file:///data/.../index.html 132 | currentHref = hrf.substr(hrf.lastIndexOf('/')+1); 133 | } else { 134 | currentHref = hrf.substr(hrf.indexOf('www/')+4); 135 | } 136 | // if no href was passed the transition should always kick in 137 | if (href) { 138 | // if only hash nav, do it in JS for Android 139 | // (I'm a little reluctant to depend on 'device' only for this: device.platform == "Android") 140 | if (href.indexOf('#') == 0 && navigator.userAgent.indexOf("Android") > -1) { 141 | // starts with a #, so check if the current one has a hash with the same value 142 | if (currentHref.indexOf('#') > -1) { 143 | var file = currentHref.substr(0, currentHref.indexOf('#')); 144 | if (hrf.indexOf('www/') == -1) { 145 | var to = hrf.substr(0, hrf.lastIndexOf('/')+1); 146 | window.location.href = to+file+href; 147 | } else { 148 | window.location.href = "/android_asset/www/"+file+ href; 149 | } 150 | } else { 151 | // the current page has no #, so simply attach the href to current url 152 | if (hrf.indexOf('#') > -1) { 153 | hrf = hrf.substring(0, hrf.indexOf('#')); 154 | } 155 | window.location = hrf += href; 156 | } 157 | } 158 | } 159 | if (currentHref == href) { 160 | if (errCallback) { 161 | errCallback("The passed href is the same as the current"); 162 | } else { 163 | console.log("The passed href is the same as the current"); 164 | } 165 | return false; 166 | } 167 | return true; 168 | }; 169 | 170 | NativePageTransitions.install = function () { 171 | if (!window.plugins) { 172 | window.plugins = {}; 173 | } 174 | 175 | window.plugins.nativepagetransitions = new NativePageTransitions(); 176 | return window.plugins.nativepagetransitions; 177 | }; 178 | 179 | cordova.addConstructor(NativePageTransitions.install); --------------------------------------------------------------------------------