Skip to content

Instantly share code, notes, and snippets.

@iantearle
Created April 17, 2012 13:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iantearle/2406021 to your computer and use it in GitHub Desktop.
Save iantearle/2406021 to your computer and use it in GitHub Desktop.
Titanium Path Centered
// Path menu for Titanium
// Tony Lukasavage - @tonylukasavage
//
// Notes:
// - Transforms must be declared outside the animation to
// work on Android. (http://jira.appcelerator.org/browse/TIMOB-5796)
// There MUST be more than 1 icon or the math breaks
var DEFAULTS = {
ICON_IMAGE: '/images/star.png',
ICON_SIZE: 52, // Ti.Platform.displayCaps.platformWidth > 600 ? 104 : 52,
ICON_NUMBER: 12,
ICON_ROTATION: 0,
BUTTON_IMAGE: '/images/sm/add.png',
BUTTON_SIZE: 60, //Ti.Platform.displayCaps.platformWidth > 600 ? 120 : 60,
MENU_DURATION: 300,
FADE_DURATION: 400,
BOUNCE_DISTANCE: 25,
STAGGER: 60
};
var isAndroid = Ti.Platform.osname === 'android';
var isIOS = Ti.Platform.osname === 'iphone' || Ti.Platform.osname === 'ipad';
/////////////////////////////////////////
////////// Module dependencies //////////
/////////////////////////////////////////
if (isIOS) {
var pathAnimator = require('path.animator');
}
/////////////////////////////////////////
////////// "Private" variables //////////
/////////////////////////////////////////
var settings = {},
isAnimating = false,
menu,
menuButton,
menuIcons,
fadeOut,
fadeIn;
//////////////////////////////////////
////////// "Public" members //////////
//////////////////////////////////////
exports.EVENT_ICONCLICK = 'iconClick';
exports.createMenu = function(o) {
// Configure the settings for the menu
settings.iconList = o.iconList || createDefaultIconList();
settings.iconRotation = o.iconRotation || DEFAULTS.ICON_ROTATION;
settings.iconSize = o.iconSize || DEFAULTS.ICON_SIZE;
settings.buttonImage = o.buttonImage || DEFAULTS.BUTTON_IMAGE;
settings.buttonSize = o.buttonSize || DEFAULTS.BUTTON_SIZE;
settings.menuDuration = o.menuDuration || DEFAULTS.MENU_DURATION;
settings.fadeDuration = o.fadeDuration || DEFAULTS.FADE_DURATION;
settings.radius = o.radius || 260;
settings.bounceDistance = o.bounceDistance || DEFAULTS.BOUNCE_DISTANCE;
settings.stagger = o.stagger || DEFAULTS.STAGGER;
// Create reusable fade & scale animations. Need to declare
// the transforms outside of the animation. See notes at the beginning
// of this file.
fadeOut = Ti.UI.createAnimation({
duration: settings.fadeDuration,
opacity: 0
});
fadeOut.transform = Ti.UI.create2DMatrix().scale(0, 0);
fadeLarge = Ti.UI.createAnimation({
duration: settings.fadeDuration,
opacity: 0
});
fadeLarge.transform = Ti.UI.create2DMatrix().scale(4, 4);
// Construct menu UI components and establish view hierarchy
menu = Ti.UI.createView({
height: Ti.UI.SIZE,
width: Ti.UI.SIZE,
backgroundColor: 'red',
opacity: 1
});
menuButton = createMenuButton({
center:{x:50,y:50},
height: Ti.UI.SIZE,
width: Ti.UI.SIZE,
backgroundColor: 'blue'
});
menuIcons = [];
menuButton.addEventListener('click', handleMenuButtonClick);
for (var i = 0; i < o.iconList.length; i++) {
var menuIcon = createMenuIcon(i);
menuIcon.addEventListener('click', handleMenuIconClick);
menuIcons.push(menuIcon);
menu.add(menuIcon);
}
menu.add(menuButton);
menu.initMenu = initMenu;
return menu;
};
/////////////////////////////////////////
////////// "Private" functions //////////
/////////////////////////////////////////
var resetIconVisibility = function(icon) {
// use a short timeout to prevent flicker
setTimeout(function() {
icon.opacity = 1;
icon.transform = Ti.UI.create2DMatrix().scale(1,1);
if (isAndroid) {
icon.show();
}
}, 100);
};
var initMenu = function() {
menuButton.isOpen = false;
menuButton.opacity = 1;
menuButton.transform = Ti.UI.create2DMatrix().rotate(0);
menuButton.show();
for (var i = 0; i < menuIcons.length; i++) {
var icon = menuIcons[i];
resetIconVisibility(icon);
}
isAnimating = false;
};
var handleMenuButtonClick = function(e) {
// Make sure we don't have other menu animations running
if (isAndroid && isAnimating === true) {
return;
}
isAnimating = true;
var i, icon;
var anim = menuButton.isOpen ? 'close' : 'open';
// change the menu button state
menuButton.isOpen = !menuButton.isOpen;
menuButton.animate(menuButton.animations[anim]);
// Open/close all the icons with animation
for (i = 0; i < menuIcons.length; i++) {
icon = menuIcons[i];
icon.animations[anim + 'Bounce'].addEventListener(
'complete',
anim === 'open' ? doCompleteOpen : doCompleteClose,
anim === 'open' ? menu.opacity = 1 : menu.opacity = 1
);
icon.animate(icon.animations[anim + 'Bounce']);
// ios uses the path.animator module for iOS rotations so that they can be
// greater than 180 degrees
if (isIOS) {
icon.rotate({
angle: settings.iconRotation,
duration: settings.menuDuration + (settings.menuDuration / 3.5)
});
}
}
};
var handleMenuIconClick = function(e) {
var i, radians, icon;
// Make sure we don't have other menu animations running
if (isAndroid && isAnimating === true) {
return;
}
isAnimating = true;
menu.fireEvent(exports.EVENT_ICONCLICK, {
source: menu,
icon: e.source,
index: e.source.index,
id: e.source.id
});
// fade and scale out the menuButton
if (isAndroid) {
fadeOut.left = (menuButton.width * 0.5);
fadeOut.bottom = -1 * (menuButton.height * 0.5);
}
menuButton.animate(fadeOut);
// iterate through icons, fade and scale down the ones that weren't clicked,
// fade and scale up the one that was.
for (i = 0; i < menuIcons.length; i++) {
radians = (360 / (menuIcons.length - 1)) * i * Math.PI / 180;
icon = menuIcons[i];
// android scales from the top left, not the center like ios,
// hence the extra left/bottom animations
if (i !== e.source.index) {
if (isAndroid) {
fadeOut.left = Math.sin(radians) * settings.radius + (icon.width * 0.5);
fadeOut.bottom = Math.cos(radians) * settings.radius - (icon.height * 0.5);
}
icon.animate(fadeOut);
} else {
if (isAndroid) {
fadeLarge.left = Math.sin(radians) * settings.radius - (icon.width * 1.5);
fadeLarge.bottom = Math.cos(radians) * settings.radius + (icon.height * 1.5);
}
icon.animate(fadeLarge);
}
}
};
var createMenuButton = function() {
var animations = {
open: Ti.UI.createAnimation({
duration: settings.menuDuration
}),
close: Ti.UI.createAnimation({
duration: settings.menuDuration
})
};
animations.open.transform = Ti.UI.create2DMatrix().rotate(45);
animations.open.addEventListener('complete', function() {
isAnimating = false;
});
// In Titanium, Android rotations always start at zero, regardless of last position.
// In Android Titanium apps you can pass two arguments to the rotate() function,
// the first being the starting rotation, the second being the final rotation.
// This is not a cross-platform method, so you need to make sure you are on Android
// before using 2 arguments.
// Jira Issue: http://jira.appcelerator.org/browse/TIMOB-6843
if (isAndroid) {
animations.close.transform = Ti.UI.create2DMatrix().rotate(45, 0);
} else {
animations.close.transform = Ti.UI.create2DMatrix().rotate(0);
}
animations.close.addEventListener('complete', function() {
isAnimating = false;
});
var menuButton = Ti.UI.createImageView({
image: settings.buttonImage,
height: settings.buttonSize,
width: settings.buttonSize,
isOpen: false,
animations: animations
});
return menuButton;
};
var createMenuIcon = function(index) {
var length = settings.iconList.length;
var id = settings.iconList[index].id;
var radians = (360 / (length - 1)) * index * Math.PI / 196;
var bounceLeft = Math.sin(radians) * (settings.radius + settings.bounceDistance);
var bounceBottom = Math.cos(radians) * (settings.radius + settings.bounceDistance);
var finalLeft = Math.sin(radians) * settings.radius;
var finalBottom = Math.cos(radians) * settings.radius;
var animations = {
openBounce: Ti.UI.createAnimation({
duration: settings.menuDuration,
bottom: bounceBottom,
left: bounceLeft,
delay: index * settings.stagger
}),
openFinal: Ti.UI.createAnimation({
duration: settings.menuDuration / 3.5,
bottom: finalBottom,
left: finalLeft
}),
closeBounce: Ti.UI.createAnimation({
duration: settings.menuDuration / 3.5,
bottom: bounceBottom,
left: bounceLeft,
delay: (length - (index+1)) * settings.stagger,
}),
closeFinal: Ti.UI.createAnimation({
duration: settings.menuDuration,
bottom: 0,
left: 0,
top: 0,
right: 0
})
};
// iOS uses path.animator module for rotations
if (!isIOS) {
animations.openBounce.transform = Ti.UI.create2DMatrix().rotate(settings.iconRotation);
animations.closeFinal.transform = Ti.UI.create2DMatrix().rotate(-1 * settings.iconRotation);
}
// Use path.animator view for iOS, which can be rotated more than 180 degrees,
// by default 720 degrees
var icon;
if (isIOS) {
icon = pathAnimator.createView({
backgroundImage: settings.iconList[index].image,
height: settings.iconSize,
width: settings.iconSize,
left: 0,
bottom: 0,
right: 0,
top: 0,
animations: animations,
index: index,
id: id
});
} else {
icon = Ti.UI.createImageView({
image: settings.iconList[index].image,
height: settings.iconSize,
width: settings.iconSize,
left: 0,
bottom: 0,
right: 0,
top: 0,
animations: animations,
index: index,
id: id
});
}
icon.animations.openBounce.icon = icon;
icon.animations.closeBounce.icon = icon;
return icon;
};
var doCompleteOpen = function(e) {
e.source.removeEventListener('complete', doCompleteOpen);
e.source.icon.animate(e.source.icon.animations.openFinal);
};
var doCompleteClose = function(e) {
e.source.removeEventListener('complete', doCompleteClose);
e.source.icon.animate(e.source.icon.animations.closeFinal);
setTimeout(function(){menu.opacity = 0},1000)
};
var createDefaultIconList = function() {
var icons = [];
for (var i = 0; i < DEFAULTS.ICON_NUMBER; i++) {
icons.push({
image: DEFAULTS.ICON_IMAGE,
id: undefined
});
}
return icons;
};
Ti.App.addEventListener('animate_open', handleMenuButtonClick);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment