├── Dec-04-2016 21-36-53.gif ├── Demo App ├── config.json ├── controllers │ └── index.js ├── styles │ └── index.tss └── views │ └── index.xml ├── README.md └── in.prashant.scrollableViews ├── controllers └── widget.js ├── styles └── widget.tss ├── views └── widget.xml └── widget.json /Dec-04-2016 21-36-53.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prashantsaini1/titanium-scrollable-views/5234617c5ab5df4788a0b02124406cfffef8d1d1/Dec-04-2016 21-36-53.gif -------------------------------------------------------------------------------- /Demo App/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": {}, 3 | "env:development": {}, 4 | "env:test": {}, 5 | "env:production": {}, 6 | "os:android": {}, 7 | "os:blackberry": {}, 8 | "os:ios": {}, 9 | "os:mobileweb": {}, 10 | "dependencies": { 11 | "in.prashant.scrollableViews" : "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo App/controllers/index.js: -------------------------------------------------------------------------------- 1 | 2 | $.index.open(); 3 | 4 | function add() { 5 | $.scrollWidget.add([Ti.UI.createView(), Ti.UI.createView(), Ti.UI.createView()], ["#a00", '#0a0', '#aa0'], true); 6 | } 7 | 8 | function remove() { 9 | $.scrollWidget.remove(0); 10 | $.scrollWidget.remove(0); 11 | $.scrollWidget.remove(0); 12 | } 13 | 14 | // master 15 | // another pull test 16 | -------------------------------------------------------------------------------- /Demo App/styles/index.tss: -------------------------------------------------------------------------------- 1 | "#scrollWidget": { 2 | // height : Ti.UI.FILL, 3 | width: Ti.UI.FILL, 4 | top: 50, 5 | bottom: 50, 6 | pagingPosition : 'top', 7 | pagingColor : 'white', 8 | pagingEffect : true, 9 | backdropEffect : true, 10 | backdropColors : ['teal', 'silver', 'cyan', 'pink'] 11 | } 12 | 13 | "Button" : { 14 | color : 'white' 15 | } 16 | -------------------------------------------------------------------------------- /Demo App/views/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # titanium-scrollable-views 2 | A cross platform Alloy - Titanium scrollable widget with background color transition effect and animated paging controls. 3 | 4 | ![Alt Text](https://github.com/prashantsaini1/titanium-scrollable-views/blob/master/Dec-04-2016%2021-36-53.gif) 5 | 6 | This widget provides you a customizable scrollable view container with background color transition effect (optional) and nice paging controls for iOS & Android. 7 | 8 | ## Installation 9 | 10 | Add in your *config.json*, under `dependencies`: 11 | 12 | ``` 13 | "dependencies": { 14 | "in.prashant.scrollableViews" : "*" 15 | } 16 | ``` 17 | 18 | ## Usage Titanium Classic 19 | ```javascript 20 | var widget = Alloy.createWidget('in.prashant.scrollableViews', { 21 | width: Ti.UI.FILL, 22 | top: 50, 23 | bottom: 50, 24 | pagingPosition : 'top', 25 | pagingColor : 'white', 26 | pagingEffect : true, 27 | backdropEffect : true, 28 | backdropColors : ['teal', 'silver', 'cyan', 'pink'], 29 | views : [Ti.UI.createView(), Ti.UI.createView(), Ti.UI.createView(), Ti.UI.createView()] 30 | }); 31 | ``` 32 | 33 | ## Usage Alloy 34 | *index.xml* 35 | ```xml 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ``` 45 | 46 | *index.tss* 47 | ```tss 48 | "#scrollWidget": { 49 | width: Ti.UI.FILL, 50 | top: 50, 51 | bottom: 50, 52 | pagingPosition : 'top', 53 | pagingColor : 'white', 54 | pagingEffect : true, 55 | backdropEffect : true, 56 | backdropColors : ['teal', 'silver', 'cyan', 'pink'] 57 | } 58 | 59 | "Button" : { 60 | color : 'white' 61 | } 62 | ``` 63 | 64 | Arguments 65 | * **backdropEffect** : set the background transition effect (true/false) - defaults to false - do not set background colors of Ti.UI.View to take effect of __backdropEffect__ property 66 | * **backdropColors** : array of colors to use with backdropEffect - defaults to transparent 67 | * **pagingEffect** : shows the paging control (true/false) - defaults to true 68 | * **pagerStyle** : pager style - pass 1 for ring type pagers or pass 2 for solid colored pagers, if value is 2 then optionally pass **pagingBackColor** for unselected pagers 69 | * **pagingPosition** : position of paging control - pass string "top" or "bottom" 70 | * **pagingPadding** : padding between pagers in paging control - defaults to 7dp 71 | * **pagingColor** : color of paging control - defaults to white 72 | * **pagingBackColor** : sets the color of unselected pagers if pagerStyle is 2, otherwise not effective 73 | * **clip** : iOS only - clip views to show them adjacently - defaults to false 74 | * **clipPadding** : padding of views when clip is true 75 | * **index** : index of the current page to set in widget 76 | 77 | Methods 78 | * **add**: Add view(s) at once with below arguments 79 | * *argument 1* : pass single view (Ti.UI.View) or an array of views to add. 80 | * *argument 2* : color or an array of colors for **backdropEffect**. _(optional)_ 81 | * *argument 3* : boolean to scroll to last view/page added. _(optional)_ 82 | * **remove**: Remove view by passing either View or index of the view - *iOS + Android* ( :stuck_out_tongue_winking_eye: but Ti SDK allows to remove the view by index on iOS only) 83 | * *argument 1* : Ti.UI.View object or index of the view to remove. 84 | 85 | Exposer Properties 86 | * **scrollableView**: instance of the Ti.UI.ScrollableView used in this widget. 87 | 88 | 89 | ## Contributions 90 | 91 | If you enjoy this module, feel free to contribute with your PR or [donate](https://paypal.me/prashantsainii) :-) 92 | -------------------------------------------------------------------------------- /in.prashant.scrollableViews/controllers/widget.js: -------------------------------------------------------------------------------- 1 | var args = $.args; 2 | var lastPage = 0, 3 | totalPages = 0, 4 | currentPage = 0, 5 | isRemoving = false, // to stop calling `onScrollAnimate` while removing the view 6 | pagerPadding = 5, 7 | pagerStyle = ((args.pagerStyle === undefined) || (args.pagerStyle == 1)) ? 1 : 2, // 1 for ring type pagers, 2 for solid pagers 8 | opacityEffect = ((args.backdropEffect === undefined) || (args.backdropEffect == false)) ? false : true, 9 | pagingEffect = ((args.pagingEffect === undefined) || (args.pagingEffect == true)) ? true : false, 10 | pagingTopBottom = (args.pagingPosition === undefined) || (args.pagingPosition === "bottom") ? 'bottom' : 'top', 11 | pagingColor = args.pagingColor || 'white', 12 | pagingBackColor = (pagerStyle == 1) ? pagingColor : (args.pagingBackColor || '#aaa'), 13 | defaultPadding = parseInt(args.pagingPadding) || 7, 14 | pagerPosition = 12 + defaultPadding; // default left position of animated pager control 15 | 16 | 17 | (function constructor() { 18 | var views = []; 19 | 20 | // get children if Alloy is used, or use `views` key to get pages views 21 | if ((args.views !== undefined) && _.isArray(args.views)) { 22 | views = _.filter(args.views, function(view) { 23 | return view.apiName == 'Ti.UI.View'; 24 | }); 25 | 26 | } else if (args.children !== undefined) { 27 | views = _.filter(args.children, function(view) { 28 | return view.apiName == 'Ti.UI.View'; 29 | }); 30 | } 31 | 32 | totalPages = views.length; 33 | 34 | // only iOS supports clipMode to show adjacent views 35 | if (OS_IOS && (args.clip !== undefined) && (args.clip != false)) { 36 | $.SCROLLABLE_VIEW.left = $.SCROLLABLE_VIEW.right = args.clipPadding || 0; 37 | $.SCROLLABLE_VIEW.clipViews = false; 38 | } 39 | 40 | // set pager controls - at bottom or top 41 | if (pagingTopBottom === 'bottom') { 42 | $.scrollView.bottom = pagerPadding; 43 | } else { 44 | $.scrollView.top = pagerPadding; 45 | } 46 | 47 | // set pager controls visible or invisible 48 | $.scrollView.visible = pagingEffect; 49 | $.pagerControl.borderColor = pagingColor; 50 | 51 | // set properties on main container 52 | var props = _.pick(args, 'width', 'height', 'top', 'bottom', 'left', 'right', 'backgroundColor', 'opacity', 'cacheSize'); 53 | $.container.applyProperties(props); 54 | 55 | if (totalPages > 0) { 56 | // set views 57 | $.SCROLLABLE_VIEW.views = views; 58 | 59 | // set lastPage to zero or defined one 60 | if ((args.index === undefined) || (args.index > (args.children.length - 1))) { 61 | currentPage = 0; 62 | 63 | } else { 64 | currentPage = parseInt(args.index) || 0; // parse valid index of current page 65 | currentPage = (currentPage < 0) ? 0 : currentPage; // if index is -ve, use default 0 index 66 | currentPage = (currentPage > (totalPages - 1)) ? 0 : currentPage; // if greater than total views, set to 0 67 | } 68 | 69 | $.SCROLLABLE_VIEW.currentPage = currentPage; 70 | } 71 | 72 | // create paging controls or remove pager from its parent container 73 | if (pagingEffect) { 74 | createPager(totalPages); 75 | $.pagerControl.left = currentPage * pagerPosition; 76 | $.SCROLLABLE_VIEW.addEventListener('scroll', animateControl); 77 | } 78 | 79 | // create backdrop views 80 | if (opacityEffect) { 81 | for (var i = 0; i < totalPages; ++i) { 82 | $.backdropViews.add(Ti.UI.createView({ 83 | backgroundColor : (args.backdropColors == undefined) ? 'transparent' : args.backdropColors[i], 84 | opacity : (i <= currentPage) ? 1 : 0 85 | })); 86 | } 87 | $.SCROLLABLE_VIEW.addEventListener('scroll', onScrollAnimate); 88 | 89 | } else { 90 | $.SCROLLABLE_VIEW.addEventListener('scrollend', function (e) { 91 | if (e.source.id !== "SCROLLABLE_VIEW") { return; } 92 | currentPage = e.currentPage; 93 | }); 94 | } 95 | 96 | })(); 97 | 98 | function createPager(_total) { 99 | var totalTempPagers = $.PAGING_VIEW.children.length; 100 | totalTempPagers = (totalTempPagers == 0) ? true : false; 101 | 102 | for (var i = 0; i < _total; ++i) { 103 | $.PAGING_VIEW.add(Ti.UI.createView({ 104 | width: 12, 105 | height: 12, 106 | borderRadius: 6, 107 | touchEnabled: false, 108 | left : totalTempPagers ? 0 : defaultPadding, 109 | borderWidth : (pagerStyle == 1) ? 1.2 : 6.5, 110 | borderColor : pagingBackColor 111 | })); 112 | 113 | totalTempPagers = false; 114 | } 115 | } 116 | 117 | function animateControl(e) { 118 | if ((totalPages <= 1) || (e.currentPageAsFloat < 0) || (e.currentPageAsFloat > (totalPages - 1))) { 119 | return; 120 | } 121 | 122 | $.pagerControl.left = e.currentPageAsFloat * pagerPosition; 123 | } 124 | 125 | function onScrollAnimate(e) { 126 | if (e.source.id !== "SCROLLABLE_VIEW") { return; } 127 | 128 | if (isRemoving) { return; } 129 | 130 | var cFloat = e.currentPageAsFloat; 131 | 132 | if ((totalPages <= 1) || (cFloat < 0) || (cFloat > $.SCROLLABLE_VIEW.views.length - 1)) { 133 | return; 134 | } 135 | 136 | var delta = cFloat - Math.floor(cFloat); 137 | 138 | // require('log')('Delta = ' + delta + ' : Float = ' + cFloat + ' : Page = ' + e.currentPage); 139 | 140 | if (cFloat > currentPage) { 141 | // on iOS, scrolling can be jumped slightly to next page when swiped very fastly 142 | // it gives currentPageAsFloat greater one page ahead and produces blink effect 143 | // so current float, while moving next, should not be greater than currentPage + 1 144 | // READ COMMENTS at bottom most of this file to see the above require('log') output 145 | if (cFloat > (currentPage + 1)) { 146 | return; 147 | } 148 | 149 | if (delta == 0) { 150 | delta = 1; 151 | } 152 | var nextPage = currentPage + 1; 153 | $.backdropViews.children[nextPage].opacity = delta; 154 | 155 | } else if (cFloat < currentPage) { 156 | // on iOS, scrolling can be jumped slightly to previous page slightly when swiped very fastly 157 | // it gives currentPageAsFloat lesser one page behind and produces blink effect 158 | // so current float, while moving to previous page, should not be less than (currentPage - 1) 159 | if (cFloat < (currentPage - 1)) { 160 | return; 161 | } 162 | 163 | var prevPage = !currentPage ? 0 : currentPage; 164 | $.backdropViews.children[prevPage].opacity = delta; 165 | } 166 | 167 | if (cFloat === e.currentPage) { 168 | // when the scrolling is finished 169 | currentPage = e.currentPage; 170 | } 171 | } 172 | 173 | function addView(_views, _backdropColors, _scrollToView) { 174 | // make an array of views and backdropcolors 175 | _views = _.isArray(_views) ? _views : [_views]; 176 | 177 | if (_backdropColors !== undefined) { 178 | _backdropColors = _.isArray(_backdropColors) ? _backdropColors : [_backdropColors]; 179 | } 180 | 181 | _.each(_views, function(_view, i) { 182 | if (_view.apiName == 'Ti.UI.View') { 183 | $.SCROLLABLE_VIEW.addView(_view); 184 | 185 | ++totalPages; 186 | 187 | if (pagingEffect) { 188 | $.scrollView.visible = true; 189 | createPager(1); 190 | } 191 | 192 | if (opacityEffect) { 193 | $.backdropViews.add(Ti.UI.createView({ 194 | backgroundColor : _backdropColors[i] || 'transparent', 195 | opacity : 0 196 | })); 197 | } 198 | } 199 | }); 200 | 201 | if ((_scrollToView !== undefined) && _.isBoolean(_scrollToView) && _scrollToView) { 202 | currentPage = totalPages - 1; 203 | 204 | $.SCROLLABLE_VIEW.currentPage = currentPage; 205 | 206 | // set opacity to 1 of all backdrop views, reverse views to set opacity of topmost view first to avoid blinking 207 | opacityEffect && _.each($.backdropViews.children.reverse(), function(_backView) { _backView.opacity = 1; }); 208 | 209 | // set pager control to last page 210 | if (pagingEffect) { $.pagerControl.left = currentPage * pagerPosition; } 211 | } 212 | } 213 | 214 | function removeView(_viewOrIndex) { 215 | if (totalPages === 0) { 216 | Ti.API.warn('No view to remove'); 217 | return; 218 | } 219 | 220 | var tempViewIndex = null; 221 | 222 | if (typeof _viewOrIndex === 'object') { 223 | if (_viewOrIndex.apiName !== 'Ti.UI.View') { 224 | Ti.API.warn('Cannot remove view. Invalid view type passed'); 225 | return; 226 | 227 | } else { 228 | // find the index of the view to be removed 229 | _.each($.SCROLLABLE_VIEW.views, function (_tempView, i) { 230 | (_tempView === _viewOrIndex) && (tempViewIndex = i); 231 | }); 232 | } 233 | 234 | } else if (typeof _viewOrIndex === 'number' && (_viewOrIndex !== NaN)) { 235 | tempViewIndex = _viewOrIndex; 236 | 237 | } else { 238 | Ti.API.warn('Cannot remove view. Pass either View or index'); 239 | return; 240 | } 241 | 242 | if ( (tempViewIndex < 0) || tempViewIndex > (totalPages - 1) ) { 243 | Ti.API.warn('Index/View is not valid'); 244 | return; 245 | } 246 | 247 | 248 | try { 249 | isRemoving = true; 250 | 251 | $.SCROLLABLE_VIEW.removeView($.SCROLLABLE_VIEW.views[tempViewIndex]); 252 | 253 | if ( (tempViewIndex < currentPage) || ((tempViewIndex == currentPage) && (tempViewIndex == (totalPages - 1))) ) { 254 | --currentPage; 255 | $.SCROLLABLE_VIEW.currentPage = currentPage; 256 | } 257 | 258 | --totalPages; 259 | 260 | // remove backdrop view and set opacity to 1 of all views till currentPage 261 | if (opacityEffect) { 262 | $.backdropViews.remove($.backdropViews.children[tempViewIndex]); 263 | var childs = $.backdropViews.children; 264 | for (var i=0; i<=currentPage; i++) { childs[i].opacity = 1; } 265 | childs = null; 266 | } 267 | 268 | // remove last pager, set left position of animated pager and if no views are then set it visible false 269 | if (pagingEffect) { 270 | if (totalPages == 0) { 271 | $.PAGING_VIEW.removeAllChildren(); 272 | $.scrollView.visible = false; 273 | 274 | } else { 275 | $.PAGING_VIEW.remove($.PAGING_VIEW.children[totalPages - 1]); 276 | if (currentPage == 0) { $.PAGING_VIEW.children[0].left = 0; } 277 | $.pagerControl.left = currentPage * pagerPosition; 278 | } 279 | } 280 | 281 | isRemoving = false; 282 | 283 | } catch(ex) { 284 | isRemoving = false; 285 | } 286 | } 287 | 288 | function setPage(_index) { 289 | currentPage = _index; 290 | $.SCROLLABLE_VIEW.currentPage = currentPage; 291 | 292 | if (pagingEffect) { 293 | $.pagerControl.left = currentPage * pagerPosition; 294 | } 295 | 296 | if (opacityEffect) { 297 | _.each($.backdropViews.children, function (child, i) { 298 | childs[i].opacity = (i <= currentPage) ? 1 : 0; 299 | }); 300 | } 301 | } 302 | 303 | 304 | // expose scrollable view pager 305 | $.scrollableView = $.SCROLLABLE_VIEW; 306 | $.add = addView; 307 | $.remove = removeView; 308 | $.currentPage = function () { return currentPage; }; 309 | $.totalPages = function () { return totalPages; }; 310 | $.setCurrentPage = setPage; 311 | 312 | 313 | 314 | 315 | // iOS swiping fast issue 316 | /* 317 | * 318 | 1 - ******* FAST ******* 319 | Delta = 0.208 : Float = 0.208 : Page = 0 320 | Delta = 0.538 : Float = 0.538 : Page = 1 321 | Delta = 0.745 : Float = 0.745 : Page = 1 322 | Delta = 0.991 : Float = 0.991 : Page = 1 323 | Delta = 0.033 : Float = 1.033 : Page = 1 324 | Delta = 0.045 : Float = 1.045 : Page = 1 325 | Delta = 0.005 : Float = 1.005 : Page = 1 326 | Delta = 0.001 : Float = 1.001 : Page = 1 327 | 328 | Delta = 0 : Float = 1 : Page = 1 329 | 330 | ******* SLOW ******* 331 | Delta = 0.030 : Float = 0.030 : Page = 0 332 | Delta = 0.326 : Float = 0.326 : Page = 0 333 | Delta = 0.463 : Float = 0.463 : Page = 0 334 | Delta = 0.620 : Float = 0.620 : Page = 1 335 | Delta = 0.761 : Float = 0.761 : Page = 1 336 | Delta = 0.998 : Float = 0.998 : Page = 1 337 | Delta = 0 : Float = 1 : Page = 1 338 | */ 339 | -------------------------------------------------------------------------------- /in.prashant.scrollableViews/styles/widget.tss: -------------------------------------------------------------------------------- 1 | "#pagerContainer": { 2 | width: Ti.UI.SIZE, 3 | height: Ti.UI.SIZE, 4 | } 5 | 6 | "#PAGING_VIEW": { 7 | width: Ti.UI.SIZE, 8 | layout: "horizontal", 9 | height: Ti.UI.SIZE, 10 | horizontalWrap: false 11 | } 12 | 13 | "#pagerControl": { 14 | width: 12, 15 | height: 12, 16 | borderRadius: 6, 17 | borderWidth: 7, 18 | touchEnabled: false 19 | } 20 | 21 | "#SCROLLABLE_VIEW" : { 22 | disableBounce : true, 23 | showPagingControl : false 24 | } 25 | 26 | "#scrollView": { 27 | left: 15, 28 | right: 15, 29 | height: Ti.UI.SIZE, 30 | scrollType: 'horizontal', 31 | disableBounce: true 32 | } 33 | 34 | "#backdropViews" : { 35 | width : Titanium.UI.FILL, 36 | height : Titanium.UI.FILL 37 | } 38 | -------------------------------------------------------------------------------- /in.prashant.scrollableViews/views/widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /in.prashant.scrollableViews/widget.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "in.prashant.scrollableViews", 3 | "name": "Scrollable Views", 4 | "description" : "A cross platform Alloy scrollable widget with nice background effect and paging controls", 5 | "author": "Prashant Saini", 6 | "version": "1.0.0", 7 | "copyright":"", 8 | "license":"", 9 | "min-alloy-version": "1.0", 10 | "min-titanium-version":"5.3.1", 11 | "tags":"", 12 | "platforms":"ios, android" 13 | } 14 | --------------------------------------------------------------------------------