Last active
July 17, 2017 16:10
-
-
Save manumaticx/6125a8398fe9486f0eed8200ea3d2d2a to your computer and use it in GitHub Desktop.
port of PagingControl Alloy Widget for Titanium Classic
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Original Alloy Widget: | |
* https://github.com/manumaticx/pagingcontrol | |
* | |
* This implementation is a port for Classic Titanium. | |
* @author: Manuel Lehner | |
*/ | |
var OS_ANDROID = Ti.Platform.osname === 'android'; | |
var OS_IOS = !OS_ANDROID; | |
var PagingControl = function(args) { | |
this.view = Ti.UI.createScrollView({ | |
id : 'pagingcontrol', | |
scrollType: 'horizontal', | |
width: Ti.UI.FILL, | |
contentWidth: 'auto', | |
contentHeight: Ti.UI.FILL, | |
showHorizontalScrollIndicator: false, | |
showVerticalScrollIndicator: false | |
}); | |
this.scrollableView = undefined; | |
this.tabsCtrl = undefined; | |
this.indicator = undefined; | |
this.indicatorWidth = undefined; | |
this.previousPage = undefined; | |
// xml boolean args is string ("false" == true) | |
['tabs', 'findScrollableView'].forEach(function(key){ | |
if (!args.hasOwnProperty(key)){ | |
return; | |
} | |
try { | |
args[key] = JSON.parse(args[key]); | |
} catch (e) { | |
delete args[key]; | |
Ti.API.error("Unable to set argument '" + key + "'. It must be boolean."); | |
} | |
}); | |
// fill undefined args with defaults | |
this.args = Object.assign({ | |
bottomColor: "#ededed", | |
dividerColor: "#ccc", | |
indicatorColor: "#000", | |
indicatorHeight: 5, | |
tabs: false, | |
scrollOffset: 40, | |
width: Ti.UI.FILL, | |
findScrollableView: true, | |
color: "#000", | |
font : { | |
fontSize : 13, | |
fontWeight : 'bold' | |
} | |
}, args); | |
this.args.height = this.args.tabs ? 48 : 5; | |
// additional adjustments for tabs | |
if (this.args.tabs) { | |
this.args.tabs = { | |
dividerColor: this.args.dividerColor, | |
width: this.args.tabWidth, | |
backgroundColor : this.args.backgroundColor, | |
activeColor: this.args.activeColor, | |
color: this.args.color, | |
font : this.args.font | |
}; | |
} | |
// apply properties of Ti.UI.View that can be applied to paging control view | |
[ | |
"backgroundColor", | |
"backgroundImage", | |
"backgroundLeftCap", | |
"backgroundRepeat", | |
"backgroundTopCap", | |
"borderRadius", | |
"borderWidth", | |
"bottom", | |
"height", | |
"horizontalWrap", | |
"left", | |
"opacity", | |
"right", | |
"top", | |
"visible", | |
"width", | |
"zIndex" | |
].forEach(function(prop){ | |
this.args.hasOwnProperty(prop) && (this.view[prop] = this.args[prop]); | |
}.bind(this)); | |
// assign passed reference of scrollable view | |
(this.args.hasOwnProperty("scrollableView")) && (this.scrollableView = this.args.scrollableView); | |
return this; | |
}; | |
/** | |
* calls back after postlayout | |
* @param {Function} callback | |
* @param {Boolean} orientationChange wether called from oc callback | |
*/ | |
PagingControl.prototype.postLayout = function(callback, orientationChange){ | |
if (!orientationChange && this.view.size.width){ | |
callback(); | |
} else { | |
// wait for postlayout event to get the pagingcontrol size | |
this.view.addEventListener('postlayout', function onPostLayout(evt){ | |
// callback | |
callback(); | |
// remove eventlistener | |
evt.source.removeEventListener('postlayout', onPostLayout); | |
}); | |
} | |
}; | |
/** | |
* initialization method | |
*/ | |
PagingControl.prototype.init = function(){ | |
if (this.args.tabs) { | |
// create tabs | |
this.tabsCtrl = new TabsControl({ | |
tabs: this.args.tabs, | |
titles: this.scrollableView.getViews().map(function(v){ return v.title; }) | |
}); | |
// add tabs | |
this.view.add(this.tabsCtrl.getView()); | |
// add bottom border | |
this.view.add(Ti.UI.createView({ | |
width: Ti.UI.FILL, | |
height: 2, | |
bottom: 0, | |
backgroundColor: this.args.bottomColor | |
})); | |
// add tab select listener | |
this.tabsCtrl.getView().addEventListener('select', this.onTabSelect.bind(this)); | |
} | |
// create the indicator view | |
this.indicator = Ti.UI.createView({ | |
backgroundColor: this.args.indicatorColor, | |
height: this.args.indicatorHeight, | |
width: Ti.UI.SIZE, | |
bottom: 0, | |
left: 0, | |
zIndex: 2 | |
}); | |
this.adjustPositions(); | |
// add the indicator | |
this.view.add(this.indicator); | |
// add scroll listener to scrollable view | |
this.scrollableView.addEventListener('scroll', this.onScroll.bind(this)); | |
this.scrollableView.addEventListener('scrollend', this.onScrollEnd.bind(this)); | |
Ti.Gesture.addEventListener('orientationchange', this.onOrientationChange.bind(this)); | |
}; | |
PagingControl.prototype.onTabSelect = function(e){ | |
this.view.fireEvent('select', { tab: e.tab, view: e.view }); | |
this.scrollableView.currentPage = e.tab; | |
this.indicator.setLeft(e.tab * this.indicatorWidth); | |
}; | |
/** | |
* Callback for scroll event | |
*/ | |
PagingControl.prototype.onScroll = function(e){ | |
// restrict this to $.scrollableView to support nesting scrollableViews | |
if(e.source !== this.scrollableView) | |
return; | |
// update the indicator position | |
this.indicator.setLeft(e.currentPageAsFloat * this.indicatorWidth); | |
this.args.tabs && this.updateOffset(e.currentPageAsFloat); | |
}; | |
/** | |
* Callback for scrollend event | |
*/ | |
PagingControl.prototype.onScrollEnd = function(e){ | |
if (this.previousPage !== e.currentPage) { | |
this.tabsCtrl.selectColor(e.currentPage); | |
this.previousPage = e.currentPage; | |
} | |
}; | |
/** | |
* sets the tab bar offset | |
* @param {Number} index | |
*/ | |
PagingControl.prototype.updateOffset = function(index){ | |
if (this.args.tabWidth === 'auto'){ | |
return; | |
} | |
var dpToPX = function (val) { | |
if (OS_ANDROID) { | |
return val * Ti.Platform.displayCaps.dpi / 160; | |
} else if (OS_IOS) { | |
switch (Ti.Platform.displayCaps.density) { | |
case 'xhigh': return val * 3; | |
case 'high': return val * 2; | |
default: return val; | |
} | |
} else { | |
return val; | |
} | |
}; | |
var width = this.view.size.width, | |
tabsWidth = this.tabsCtrl.getWidth(), | |
maxOffset = tabsWidth - width, | |
tabSpace = tabsWidth * index / this.scrollableView.views.length; | |
if (width < tabsWidth){ | |
var offset = tabSpace - this.args.scrollOffset, | |
offsetDp = offset < maxOffset ? offset : maxOffset, | |
newOffset = OS_IOS ? (offsetDp < 0 ? 0 : offsetDp) : dpToPX(offsetDp); | |
this.view.setContentOffset( | |
{ x: newOffset, y: 0}, | |
{ animated: false } | |
); | |
} | |
}; | |
/** | |
* Callback for orientationchange event | |
*/ | |
PagingControl.prototype.onOrientationChange = function(e){ | |
this.postLayout(function(){ | |
this.tabsCtrl.updateWidth(); | |
this.adjustPositions(); | |
}.bind(this), true); | |
}; | |
/** | |
* Adjust initial layout positions | |
*/ | |
PagingControl.prototype.adjustPositions = function() { | |
var totalWidth = this.args.tabs ? this.tabsCtrl.getWidth() : this.view.size.width; | |
this.indicatorWidth = Math.floor(totalWidth / this.scrollableView.views.length); | |
this.indicator.setWidth(this.indicatorWidth); | |
this.indicator.setLeft(this.scrollableView.getCurrentPage() * this.indicatorWidth); | |
}; | |
/** | |
* update the paging control if views were added / removed from scrollableView | |
*/ | |
PagingControl.prototype.refresh = function(){ | |
this.view.removeAllChildren(); | |
this.cleanup(); | |
this.init(); | |
}; | |
/** | |
* if you need to set it in the controller | |
* @param {Ti.UI.Scrollableview} _scrollableView | |
*/ | |
PagingControl.prototype.setScrollableView = function(_scrollableView){ | |
if(this.scrollableView) { | |
Ti.API.error("Already initialized"); | |
return; | |
} | |
this.scrollableView = _scrollableView; | |
this.postLayout(this.init); | |
}; | |
/** | |
* removes orientationchange Listener | |
*/ | |
PagingControl.prototype.cleanup = function(){ | |
this.scrollableView.removeEventListener('scroll', this.onScroll); | |
this.scrollableView.removeEventListener('scrollend', this.onScrollEnd); | |
Ti.Gesture.removeEventListener('orientationchange', this.onOrientationChange); | |
this.args.tabs | |
&& this.tabsCtrl | |
&& this.tabsCtrl.getView().removeEventListener('select', PagingControl.prototype.onTabSelect) | |
&& (this.tabsCtrl = null); | |
this.indicator = null; | |
}; | |
/** | |
* A getter for the paging control view | |
* @returns {*} | |
*/ | |
PagingControl.prototype.getView = function(){ | |
return this.view; | |
}; | |
/** --------------------------------------------------------------------- **/ | |
/** The following part belongs to the Tabs Control ---------------------- **/ | |
/** --------------------------------------------------------------------- **/ | |
function TabsControl(args){ | |
this.view = Ti.UI.createView({ | |
layout: 'horizontal' | |
}); | |
this.tabs = []; | |
this.opts = {}; | |
this.labels = []; | |
this.tabWidth = undefined; | |
function getTabWidth(num){ | |
var displayWidth = Ti.Platform.displayCaps.platformWidth, | |
orientation = Ti.Gesture.orientation, | |
denominator, | |
width; | |
OS_ANDROID && (displayWidth /= Ti.Platform.displayCaps.logicalDensityFactor); | |
// there is more space in landscape, so we show more tabs then | |
if (orientation === Ti.UI.LANDSCAPE_LEFT || orientation === Ti.UI.LANDSCAPE_RIGHT){ | |
denominator = num || 7; | |
} else { | |
denominator = num || 4; | |
} | |
width = Math.floor(displayWidth / denominator) - 1; | |
return width; | |
} | |
Object.assign(this.opts, args || {}); | |
// 'auto' makes the tabs fill the available width | |
if (args.tabs.width === 'auto'){ | |
this.opts.fitWidth = true; | |
} | |
if (this.opts.fitWidth){ | |
this.tabWidth = getTabWidth(args.titles.length); | |
}else{ | |
this.tabWidth = args.tabs.width || getTabWidth(); | |
} | |
if(typeof this.tabWidth === "string" && this.tabWidth.indexOf('%')>0) { | |
var newWidth = parseInt(this.tabWidth.slice(0, this.tabWidth.indexOf('%')))/100; | |
OS_ANDROID && (newWidth /= Ti.Platform.displayCaps.logicalDensityFactor); | |
this.tabWidth = newWidth * Ti.Platform.displayCaps.platformWidth; | |
} | |
this.view.applyProperties({ | |
left: 0, | |
width: this.getWidth(), | |
height: Ti.UI.FILL | |
}); | |
for (var i = 0; i < args.titles.length; i++){ | |
this.tabs[i] = Ti.UI.createView({ | |
width: this.tabWidth, | |
height: Ti.UI.FILL | |
}); | |
var label = Ti.UI.createLabel({ | |
color: (i === 0 && !!args.tabs.activeColor) ? args.tabs.activeColor : args.tabs.color, | |
font: args.tabs.font, | |
text: args.titles[i] | |
}); | |
this.labels.push(label); | |
this.tabs[i].add(label); | |
var addEvent = function(index){ | |
var that = this; | |
this.tabs[i].addEventListener('click', function(){ | |
var view = this; | |
that.view.fireEvent('select', { tab: index, view: view }); | |
}); | |
}; | |
addEvent.apply(this, i); | |
this.view.add(this.tabs[i]); | |
if (i < args.titles.length - 1){ | |
// add divider | |
this.view.add(Ti.UI.createView({ | |
backgroundColor: args.tabs.dividerColor, | |
height: 32, | |
width: 1 | |
})); | |
} | |
} | |
return this; | |
} | |
TabsControl.prototype.getWidth = function getWidth(){ | |
return this.tabWidth * this.opts.titles.length + this.opts.titles.length; | |
}; | |
TabsControl.prototype.updateWidth = function updateWidth(){ | |
this.view.setWidth(Ti.UI.FILL); | |
if (this.opts.fitWidth){ | |
this.tabWidth = this.getTabWidth(this.opts.titles.length); | |
this.tabs.forEach(function(tab){ | |
tab.setWidth(this.tabWidth - 1); | |
}.bind(this)); | |
} | |
}; | |
TabsControl.prototype.selectColor = function selectColor(index) { | |
if (!this.opts.tabs.activeColor) { | |
return; | |
} | |
for (var j = 0; j < this.labels.length; j++) { | |
this.labels[j].color = (j === index) ? this.opts.tabs.activeColor : this.opts.tabs.color; | |
} | |
}; | |
TabsControl.prototype.getView = function () { | |
return this.view; | |
}; | |
module.exports = PagingControl; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment