├── LICENSE ├── PagingControl ├── res │ └── shadowBar.png ├── tabs.js └── widget.js ├── README.md └── app.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Douglas Alves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /PagingControl/res/shadowBar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deckameron/TiPagingControl/2125b248c089d6cf902bc7aa969df6dbd7010018/PagingControl/res/shadowBar.png -------------------------------------------------------------------------------- /PagingControl/tabs.js: -------------------------------------------------------------------------------- 1 | exports.init = function(args) { 2 | 3 | var opts = {}; 4 | var tabWidth; 5 | var OS_ANDROID = Titanium.Platform.osname == "android"; 6 | 7 | var tabs = Titanium.UI.createView({ 8 | layout : "horizontal" 9 | }); 10 | 11 | if (args) { 12 | Titanium.API.info(JSON.stringify(args)); 13 | Titanium.API.info(args); 14 | } 15 | 16 | opts = args; 17 | 18 | if (args.tabs.width === 'auto'){ 19 | args.tabs.width = getTabWidth(args.titles.length); 20 | } 21 | 22 | tabWidth = args.tabs.width || getTabWidth(); 23 | 24 | if ( typeof tabWidth == "string" && tabWidth.indexOf('%') > 0) { 25 | var newWidth = parseInt(tabWidth.slice(0, tabWidth.indexOf('%'))) / 100; 26 | 27 | if (OS_ANDROID) { 28 | newWidth /= Ti.Platform.displayCaps.logicalDensityFactor; 29 | }; 30 | 31 | tabWidth = newWidth * Ti.Platform.displayCaps.platformWidth; 32 | } 33 | 34 | tabs.applyProperties({ 35 | left : 0, 36 | width : getWidth(), 37 | height : Ti.UI.FILL 38 | }); 39 | 40 | for ( i = 0; i < args.titles.length; i++) { 41 | 42 | var t = Ti.UI.createView({ 43 | width : tabWidth, 44 | height : Ti.UI.FILL 45 | }); 46 | 47 | t.add(Ti.UI.createLabel({ 48 | text : args.titles[i], 49 | font : args.font, 50 | color : args.labelsColor, 51 | opacity : args.highlightEffect ? 0.5 : 1, 52 | touchEnabled : false 53 | })); 54 | 55 | (function(index) { 56 | t.addEventListener('click', function(e) { 57 | RippleEffect(e); 58 | var view = this; 59 | tabs.fireEvent('select', { 60 | tab : index, 61 | view : view 62 | }); 63 | }); 64 | })(i); 65 | 66 | tabs.add(t); 67 | 68 | if (i < args.titles.length - 1) { 69 | // add divider 70 | tabs.add(Ti.UI.createView({ 71 | backgroundColor : args.tabs.dividerColor, 72 | height : 32, 73 | width : 1 74 | })); 75 | } 76 | } 77 | 78 | function getTabWidth(num) { 79 | var displayWidth = Ti.Platform.displayCaps.platformWidth, 80 | orientation = Ti.Gesture.orientation, 81 | denominator, 82 | width; 83 | 84 | OS_ANDROID && (displayWidth /= Ti.Platform.displayCaps.logicalDensityFactor); 85 | 86 | // there is more space in landscape, so we show more tabs then 87 | if (orientation == Ti.UI.LANDSCAPE_LEFT || orientation == Ti.UI.LANDSCAPE_RIGHT) { 88 | denominator = num || 7; 89 | } else { 90 | denominator = num || 4; 91 | } 92 | 93 | width = Math.floor(displayWidth / denominator); 94 | 95 | return width; 96 | } 97 | 98 | tabs.getWidth = getWidth(); 99 | 100 | function getWidth() { 101 | return tabWidth * opts.titles.length + opts.titles.length; 102 | }; 103 | 104 | return tabs; 105 | }; 106 | 107 | function RippleEffect(e) { 108 | 109 | if(e && e.source){ 110 | e.source.touchEnabled = false; 111 | } 112 | 113 | var OS_IOS = Titanium.Platform.osname != 'android'; 114 | var _x = (OS_IOS || e.dp) ? e.x : (e.x / Ti.Platform.displayCaps.logicalDensityFactor); 115 | var _y = (OS_IOS || e.dp) ? e.y : (e.y / Ti.Platform.displayCaps.logicalDensityFactor); 116 | 117 | // Max & Min value from Width and Height of our clicked view. 118 | // This way we can make the circle big enough to fit the view. 119 | var maxHeightWidth = Math.max(e.source.rect.width, e.source.rect.height); 120 | var minHeightWidth = Math.min(e.source.rect.width, e.source.rect.height); 121 | 122 | // Our circle that will be scaled up using 2dMartix. 123 | e.source.ripple = Titanium.UI.createView({ 124 | borderRadius : minHeightWidth / 2, 125 | height : minHeightWidth, 126 | width : minHeightWidth, 127 | center : { 128 | x : _x, 129 | y : _y 130 | }, 131 | backgroundColor : "#FFFFFF", 132 | zIndex : 999, 133 | opacity : 0, 134 | touchEnabled : false 135 | }); 136 | // Add the ripple view inside the clicked view 137 | if(e && e.source){ 138 | e.source.add(e.source.ripple); 139 | } 140 | 141 | // Use chainAnimate to sequence the animation steps. 142 | // We'll position the view at the center of the click position, by using the center property). 143 | e.source.ripple.anim_1 = Titanium.UI.createAnimation({ 144 | center : { 145 | x : _x, 146 | y : _y 147 | }, 148 | duration : 0, 149 | opacity : 0.3, 150 | transform : Ti.UI.create2DMatrix().scale(20 / maxHeightWidth) 151 | }); 152 | 153 | e.source.ripple.anim_1.addEventListener('complete', function() { 154 | if(e.source.ripple && e.source.ripple.anim_2){ 155 | e.source.ripple.animate(e.source.ripple.anim_2); 156 | } 157 | }); 158 | 159 | e.source.ripple.anim_2 = Titanium.UI.createAnimation({ 160 | curve : Ti.UI.ANIMATION_CURVE_EASE_IN, 161 | duration : 250, 162 | opacity : 0.0, 163 | transform : Ti.UI.create2DMatrix().scale((maxHeightWidth * 2) / minHeightWidth) 164 | }); 165 | 166 | e.source.ripple.anim_2.addEventListener('complete', function() { 167 | if(e.source.ripple && e.source.ripple.anim_3){ 168 | try{ 169 | e.source.ripple.animate(e.source.ripple.anim_3); 170 | }catch(e){ 171 | Titanium.API.error(e); 172 | } 173 | } 174 | }); 175 | 176 | e.source.ripple.anim_3 = Titanium.UI.createAnimation({ 177 | opacity : 0.0, 178 | duration : 100, 179 | curve : Ti.UI.ANIMATION_CURVE_LINEAR 180 | }); 181 | 182 | e.source.ripple.anim_3.addEventListener('complete', function() { 183 | if(e && e.source){ 184 | e.source.touchEnabled = true; 185 | e.source.remove(e.source.ripple); 186 | e.source.ripple = null; 187 | } 188 | }); 189 | 190 | if(e.source.ripple && e.source.ripple.anim_1){ 191 | e.source.ripple.animate(e.source.ripple.anim_1); 192 | } 193 | }; 194 | -------------------------------------------------------------------------------- /PagingControl/widget.js: -------------------------------------------------------------------------------- 1 | exports.create = function(args) { 2 | 3 | var tabsCtrl = require("/services/PagingControl/tabs"); 4 | var OS_IOS = Titanium.Platform.osname != "android"; 5 | 6 | localArgs = args; 7 | 8 | // fill undefined args with defaults 9 | if (localArgs.indicatorColor == null) { 10 | localArgs.indicatorColor = "#000"; 11 | }; 12 | if (localArgs.indicatorHeight == null) { 13 | localArgs.indicatorHeight = 5; 14 | }; 15 | if (localArgs.top == null) { 16 | localArgs.top = 0; 17 | }; 18 | if (localArgs.hasTabs == null) { 19 | localArgs.hasTabs = true; 20 | }; 21 | if (localArgs.scrollOffset == null) { 22 | localArgs.scrollOffset = 40; 23 | }; 24 | if (localArgs.height == null) { 25 | localArgs.height = localArgs.hasTabs ? 48 : 5; 26 | }; 27 | if (localArgs.scrollableViewHeight == null) { 28 | localArgs.scrollableViewHeight = Titanium.UI.FILL; 29 | }; 30 | if (localArgs.width == null) { 31 | localArgs.width = Ti.UI.FILL; 32 | }; 33 | if (localArgs.findScrollableView == null) { 34 | localArgs.findScrollableView = true; 35 | }; 36 | if (localArgs.tabsColor == null) { 37 | localArgs.tabsColor = "#EDEDED"; 38 | }; 39 | if (localArgs.dividerColor == null) { 40 | localArgs.dividerColor = "#CCC"; 41 | }; 42 | if (localArgs.tabWidth == null) { 43 | localArgs.tabWidth = "80"; 44 | }; 45 | if (localArgs.labelsColor == null) { 46 | localArgs.labelsColor = "#000"; 47 | }; 48 | if (localArgs.highlightEffect == null) { 49 | localArgs.highlightEffect = false; 50 | }; 51 | if (localArgs.shadowBar == null) { 52 | localArgs.shadowBar = true; 53 | }; 54 | if (localArgs.font == null) { 55 | localArgs.font = { 56 | fontSize: "14dp", 57 | fontFamily: 'Roboto-Medium', 58 | fontWeight: "normal" 59 | }; 60 | }; 61 | 62 | var iWidth, 63 | indicator, 64 | localArgs, 65 | anterior = 0, 66 | atual = 0; 67 | 68 | var scrollableView = Titanium.UI.createScrollableView({ 69 | top: localArgs.top, 70 | backgroundColor: 'transparent', 71 | bubbleParent: false, 72 | cacheSize: 1, 73 | currentPage: 0, 74 | height: localArgs.scrollableViewHeight, 75 | overScrollMode: Titanium.UI.Android.OVER_SCROLL_NEVER, 76 | zIndex: 100 77 | //showPagingControl: true 78 | }); 79 | 80 | var pagingcontrol = Titanium.UI.createScrollView({ 81 | scrollType : 'horizontal', 82 | bubbleParent: false, 83 | scrollingEnabled: true, 84 | width : Ti.UI.FILL, 85 | contentWidth : 'auto', 86 | contentHeight : Ti.UI.FILL, 87 | showHorizontalScrollIndicator : false, 88 | showVerticalScrollIndicator : false, 89 | top: 0 90 | }); 91 | 92 | function postLayout(callback) { 93 | pagingcontrol.addEventListener('postlayout', function onPostLayout(evt) { 94 | //Titanium.API.info('---------- PagingControl ScrollView PostLayout ----------'); 95 | // callback 96 | callback(); 97 | 98 | // remove eventlistener 99 | evt.source.removeEventListener('postlayout', onPostLayout); 100 | }); 101 | } 102 | 103 | 104 | ////Titanium.API.info(JSON.stringify(localArgs)); 105 | 106 | // xml boolean localArgs is string ("false" == true) 107 | 108 | var checkBoolArgs = ['hasTabs', 'findScrollableView']; 109 | for (item in checkBoolArgs) { 110 | try { 111 | localArgs[checkBoolArgs[item]] = JSON.parse(localArgs[checkBoolArgs[item]]); 112 | } catch (e) { 113 | delete localArgs[checkBoolArgs[item]]; 114 | Ti.API.error("Unable to set argument '" + checkBoolArgs[item] + "'. It must be boolean."); 115 | } 116 | } 117 | 118 | // additional adjustments for tabs 119 | if (localArgs.hasTabs) { 120 | localArgs.tabProps = { 121 | dividerColor : localArgs.dividerColor, 122 | width : localArgs.tabWidth.toString() 123 | }; 124 | } 125 | 126 | // apply properties of Ti.UI.View that can be applied to paging control view 127 | var propsArray = ["backgroundColor", "backgroundImage", "backgroundLeftCap", "backgroundRepeat", "backgroundTopCap", "borderRadius", "borderWidth", "bottom", "height", "horizontalWrap", "left", "opacity", "right", "visible", "width", "zIndex"]; 128 | 129 | for (prop in propsArray) { 130 | if (localArgs[propsArray[prop]]) { 131 | pagingcontrol[propsArray[prop]] = localArgs[propsArray[prop]]; 132 | } 133 | } 134 | 135 | // assign passed reference of scrollable view 136 | if (localArgs["scrollableView"]) { 137 | scrollableView = localArgs.scrollableView; 138 | } 139 | 140 | if (localArgs.hasTabs) { 141 | 142 | scrollableView.setViews(localArgs.tabs); 143 | 144 | var titlesArray = []; 145 | var tempViewArray = localArgs.tabs; 146 | for (v in tempViewArray) { 147 | if(tempViewArray[v].title){ 148 | titlesArray.push(tempViewArray[v].title.toUpperCase()); 149 | }else{ 150 | Titanium.API.error('A View de índice ' + v + ' não tem o atributo "title"'); 151 | } 152 | } 153 | tempViewArray = null; 154 | 155 | // create tabs 156 | tabsCtrl = tabsCtrl.init({ 157 | tabs : localArgs.tabProps, 158 | titles : titlesArray, 159 | font: localArgs.font, 160 | labelsColor: localArgs.labelsColor, 161 | highlightEffect: localArgs.highlightEffect 162 | }); 163 | 164 | // add tabs 165 | pagingcontrol.setBackgroundColor(localArgs.tabsColor); 166 | pagingcontrol.add(tabsCtrl); 167 | 168 | // add bottom border 169 | pagingcontrol.add(Ti.UI.createView({ 170 | width : Ti.UI.FILL, 171 | height : 2, 172 | bottom : 0, 173 | backgroundColor : localArgs.tabsColor 174 | })); 175 | 176 | scrollableView.add(pagingcontrol); 177 | 178 | if(localArgs.shadowBar){ 179 | var shadowBar = Titanium.UI.createImageView({ 180 | top: pagingcontrol.height, 181 | left: 0, 182 | right: 0, 183 | image: "/shadowBar.png", 184 | height: Titanium.UI.SIZE, 185 | touchEnabled: false 186 | }); 187 | scrollableView.add(shadowBar); 188 | } 189 | 190 | // add tab select listener 191 | tabsCtrl.addEventListener('select', function(e) { 192 | 193 | //Titanium.API.info("============= EVENT =============="); 194 | ////Titanium.API.info(JSON.stringify(e)); 195 | //Titanium.API.info("============= EVENT =============="); 196 | 197 | scrollableView.fireEvent('select', { 198 | tab : e.tab, 199 | view : e.view 200 | }); 201 | 202 | if(localArgs.fancyScroll){ 203 | scrollableView.scrollToView(e.tab); 204 | }else{ 205 | scrollableView.currentPage = e.tab; 206 | indicator.setLeft(e.tab * iWidth); 207 | } 208 | }); 209 | } 210 | 211 | // create the indicator view 212 | indicator = Ti.UI.createView({ 213 | backgroundColor : localArgs.indicatorColor, 214 | height : localArgs.indicatorHeight, 215 | width : Ti.UI.SIZE, 216 | 217 | bottom : 0, 218 | left : 0, 219 | zIndex : 2 220 | }); 221 | 222 | adjustePositions(); 223 | 224 | // add the indicator 225 | pagingcontrol.add(indicator); 226 | 227 | // add scroll listener to scrollable view 228 | scrollableView.addEventListener('scroll', onScroll); 229 | 230 | // FUNCTIONS SECTIO ====================================================================== 231 | /** 232 | * Callback for scroll event 233 | */ 234 | function onScroll(e) { 235 | //Titanium.API.info("========== PagingControl ScrollableView Scroll ==========="); 236 | 237 | // restrict this to scrollableView to support nesting scrollableViews 238 | if (e.source !== scrollableView){ 239 | return; 240 | } 241 | 242 | // update the indicator position 243 | indicator.setLeft(e.currentPageAsFloat * iWidth); 244 | updateOffset(e.currentPageAsFloat); 245 | 246 | if(e.currentPage != undefined){ 247 | atual = e.currentPage; 248 | if(anterior != atual && localArgs.highlightEffect){ 249 | this.getChildren()[0].getChildren()[0].getChildren()[anterior * 2].getChildren()[0].opacity = 0.4; 250 | anterior = atual; 251 | } 252 | this.getChildren()[0].getChildren()[0].getChildren()[atual * 2].getChildren()[0].opacity = 1; 253 | } 254 | } 255 | 256 | /** 257 | * sets the tab bar offset 258 | * @param {Number} index 259 | */ 260 | function updateOffset(index) { 261 | var width = pagingcontrol.size.width, 262 | tabsWidth = tabsCtrl.getWidth, 263 | maxOffset = tabsWidth - width, 264 | tabSpace = tabsWidth * index / scrollableView.views.length; 265 | 266 | if (width < tabsWidth) { 267 | 268 | var offset = tabSpace - localArgs.scrollOffset, 269 | offsetDp = offset < maxOffset ? offset : maxOffset, 270 | newOffset = OS_IOS ? (offsetDp < 0 ? 0 : offsetDp) : DPUnitsToPixels(offsetDp); 271 | 272 | pagingcontrol.setContentOffset({ 273 | x : newOffset, 274 | y : 0 275 | }, { 276 | animated : false 277 | }); 278 | } 279 | } 280 | 281 | function DPUnitsToPixels(TheDPUnits) 282 | { 283 | return (TheDPUnits * (Titanium.Platform.displayCaps.dpi / 160)); 284 | } 285 | 286 | /** 287 | * Adjust initial layout positions 288 | */ 289 | function adjustePositions() { 290 | var totalWidth = localArgs.hasTabs ? tabsCtrl.getWidth : pagingcontrol.size.width; 291 | iWidth = Math.floor(totalWidth / scrollableView.views.length); 292 | indicator.setWidth(iWidth); 293 | indicator.setLeft(scrollableView.getCurrentPage() * iWidth); 294 | } 295 | 296 | scrollableView.cleanup = function() { 297 | scrollableView.setViews([]); 298 | scrollableView.removeAllChildren(); 299 | localArgs.tabs && tabsCtrl && tabsCtrller.removeAllChildren(); 300 | }; 301 | 302 | return scrollableView; 303 | }; 304 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TiPagingControl 2 | 3 | CommonJS Module for a page indication on ScrollableViews. 4 | 5 | ### The credits for this code goes entirely to [manumaticx](https://github.com/manumaticx) 6 | 7 | This lib is a conversion of [pagingcontrol](https://github.com/manumaticx/pagingcontrol) into a non-Alloy javascript. I converted this code in order to help the developers who are not so familiarized with Titanium Alloy development. 8 | 9 | ![](https://github.com/manumaticx/pagingcontrol/blob/master/demo_android.gif) 10 | 11 | ## Quick Start 12 | 13 | ### Usage 14 | 15 | Here's the complete example from the above gif: 16 | 17 | `app.js` 18 | ```javascript 19 | var pagingControl = require("/PagingControl/widget"); 20 | 21 | var window = Titanium.UI.createWindow({ 22 | backgroundColor: "#fff", 23 | layout: "vertical" 24 | }); 25 | 26 | var tabsProps = [ 27 | {title: "Atlético MG", backgroundColor: "yellow"}, 28 | {title: "Corinthinas", backgroundColor: "black"}, 29 | {title: "São Paulo", backgroundColor: "red"}, 30 | {title: "Cruzeiro", backgroundColor: "purple"}, 31 | {title: "Grêmio", backgroundColor: "blue"}, 32 | {title: "Palmeiras", backgroundColor: "yellow"}, 33 | {title: "Flamengo", backgroundColor: "black"} 34 | ]; 35 | 36 | var tabs = []; 37 | 38 | for(var i=0,j=tabsProps.length; i