Click here to Skip to main content
15,881,715 members
Articles / Programming Languages / C#

Cocktail Flow

Rate me:
Please Sign up or sign in to vote.
4.33/5 (3 votes)
7 Oct 2012CPOL4 min read 22.5K   6   6
Cocktail Flow the Windows 8 Metro App

Introduction 

Cocktail Flow is an application developed to help users browse, find and discover cocktails in a way they never experienced before!

The application features beautifully presented drink recipes with step by step guides and is packed with features you’ll love whether you’re a novice bartender or a professional mixologist. Cocktail recipes are continuously being added both as expanding existing cocktail packages and introducing new ones. 

Tailored for real life usage 

Cocktail Flow contains a special “Cabinet” page, that helps users at their home to decide which cocktails they can make from the available ingredients. Just mark the cocktail ingredients you have at home in the cabinet page and see what drinks you can make from them. The cabinet contains a large number of spirits, liquors and mixers. More ingredients are added when new packages are installed.

Building on top of our experience and making the most of Windows 8 

Cocktail Flow has been available on Windows Phone 7, iPhone and Android for a while – based on the usage data and user feedback we are developing the Windows 8 version to shine on the OS, while focusing on the need of users. We have a basic/showcase version currently available on Windows 8, but are planning to improve it to give you the best Windows 8/ UltraBook experience possible. 

Main features  

  • Browse cocktails by different types or search for it with the built-in Windows 8 search charm
  • Support for Live Tiles and marking cocktails as favourites gives you easier access to your most loved cocktails
  • Cabinet feature shows you what ingredients you have at hand and which cocktails you can make from them
  • Guides contain basic knowledge to learn the basics & best tricks for creating cocktails at your own bar

Upcoming / new features  

  • Additional downloadable packages (e.g.  Christmas, New Years, St Patrick’s Day, Valentine's Day) with over 200 new cocktails, with a lot more to come over time
  • Smart shopping assistant - You want to try out new cocktails? With the help of the shopping assistant you can get ideas on what ingredients to buy to prepare new drink recipes.
  • Synchronise your preferences / Barstock across multiple Windows 8 devices
  • Use voice commands / Text-to-Speech to navigate through the app & listen to instructions (especially handy when your hands are dirty) 
  • Ability to rate cocktails & upload custom photos to them, that other users can also view.  

Background 

A minimal knowledge of developing applications user interface with XAML (WinRT/Silverlight/WPF) and also basic C# knowledge is required to fully understand the implementations and the concepts. 

Also it is recommended that you already familiar with Windows 8 extra features such as, live tiles, snapped view, integrated search function and built in animation possibilities for developers.

Using the code 

In Cocktail Flow we wanted to add a nice reflection-effect for all the ingredients and cocktails, that's why we created the ReflectionImage UserControl.  

The xaml part:

XML
<UserControl x:Class="CocktailFlow.Windows8.Controls.Primitives.ReflectionImage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="756" d:DesignWidth="498">
 
    <Grid x:Name="LayoutRoot" Background="White">
        <Canvas>
            <Image Canvas.Left="0" Canvas.Top="0"  Canvas.ZIndex="1"  Height="220" Source="/Resources/Images/test.png" Stretch="None" Name="image1" CacheMode="BitmapCache">
                <Image.RenderTransform>
                    <TranslateTransform Y="0"></TranslateTransform>
                </Image.RenderTransform>
            </Image>
            <Image Canvas.Left="0" Canvas.Top="440" Canvas.ZIndex="0"  Height="220" Source="/Resources/Images/test.png" Stretch="None" Opacity="0.12" Name="image2"  CacheMode="BitmapCache">
                <Image.RenderTransform>
                    <CompositeTransform ScaleY="-1" TranslateY="0" Rotation="0"></CompositeTransform>
                </Image.RenderTransform>
            </Image>
        </Canvas>
    </Grid>
</UserControl>

In order to create a reusable control, we used DependencyProperty wherever we could.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Input;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
 
namespace CocktailFlow.Windows8.Controls.Primitives
{
    public partial class ReflectionImage : UserControl
    {
        public ReflectionImage()
        {
            InitializeComponent();
        }
 
        #region ClipImages (DependencyProperty)
 
        /// <summary>
        /// The image
        /// </summary>
        public bool ClipImages
        {
            get { return (bool)GetValue(ClipImagesProperty); }
            set { SetValue(ClipImagesProperty, value); }
        }
        public static readonly DependencyProperty ClipImagesProperty =
            DependencyProperty.Register("ClipImages", typeof(bool), typeof(ReflectionImage),
            new PropertyMetadata(false, new PropertyChangedCallback(OnClipImagesChanged)));
 
        private static void OnClipImagesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ReflectionImage)d).OnClipImagesChanged(e);
        }
 
        protected virtual void OnClipImagesChanged(DependencyPropertyChangedEventArgs e)
        {
        }
 
        #endregion
 
        public ImageSource ImageSource
        {
            get { return (ImageSource)GetValue(ImageSourceProperty); }
            set { SetValue(ImageSourceProperty, value); }
        }
 
        // Using a DependencyProperty as the backing store for ImageSource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ImageSourceProperty =
            DependencyProperty.Register("ImageSource", typeof(ImageSource), typeof(ReflectionImage), new PropertyMetadata(null, ImageSourceChanged));
 
        private static void ImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ReflectionImage control = d as ReflectionImage;
            if (null == control)
            {
                return;
            }
 
            control.image1.Source = control.ImageSource;
            control.image2.Source = control.ImageSource;
        }
 
        #region ReflectionOffset (DependencyProperty)
 
        /// <summary>
        /// The distance between the real image and the reflection.
        /// </summary>
        public double ReflectionOffset
        {
            get { return (double)GetValue(ReflectionOffsetProperty); }
            set { SetValue(ReflectionOffsetProperty, value); }
        }
        public static readonly DependencyProperty ReflectionOffsetProperty =
            DependencyProperty.Register("ReflectionOffset", typeof(double), typeof(ReflectionImage),
            new PropertyMetadata(-1, new PropertyChangedCallback(OnReflectionOffsetChanged)));
 
        private static void OnReflectionOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ReflectionImage)d).OnReflectionOffsetChanged(e);
        }
 
        protected virtual void OnReflectionOffsetChanged(DependencyPropertyChangedEventArgs e)
        {
	    // Calculate the new position of the normal image 
            (image1.RenderTransform as TranslateTransform).Y = ImageHeight - (double)e.NewValue;
            // Calculate the new position of the reflection
	    (image2.RenderTransform as CompositeTransform).TranslateY = (double)e.NewValue - ImageHeight;
 
            if (ClipImages)
            {
                if (image1.Clip == null)
                    image1.Clip = new RectangleGeometry();
                
                if (image2.Clip == null)
                    image2.Clip = new RectangleGeometry();
 
                Rect rect = (image1.Clip as RectangleGeometry).Rect;
                (image1.Clip as RectangleGeometry).Rect = new Rect(rect.Left, rect.Top, 1000, (double)e.NewValue);
                (image2.Clip as RectangleGeometry).Rect = new Rect(rect.Left, rect.Top, 1000, (double)e.NewValue);
            }
        }
 
        #endregion
 
        #region ImageHeight (DependencyProperty)
 
        /// <summary>
        /// Height of the image
        /// </summary>
        public double ImageHeight
        {
            get { return (double)GetValue(ImageHeightProperty); }
            set { SetValue(ImageHeightProperty, value); }
        }
        
	public static readonly DependencyProperty ImageHeightProperty =
            DependencyProperty.Register("ImageHeight", typeof(double), typeof(ReflectionImage),
            new PropertyMetadata(-1, new PropertyChangedCallback(OnImageHeightChanged)));
 
        private static void OnImageHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ReflectionImage)d).OnImageHeightChanged(e);
        }
 
        protected virtual void OnImageHeightChanged(DependencyPropertyChangedEventArgs e)
        {
            image1.Height = (double)e.NewValue;
            image2.Height = (double)e.NewValue;
            Canvas.SetTop(image2, image1.Height + image2.Height);
        }
 
        #endregion
    }
}

After the component is ready to use, you can use it very simply: 

XML
<cfPrimitives:ReflectionImage Margin="0,0,0,0"  Grid.Row="1"  ImageHeight="375" ReflectionOffset="360" VerticalAlignment="Top" >
	<cfPrimitives:ReflectionImage.ImageSource>
		<BitmapImage UriSource="{Binding CocktailBitmapImage}"></BitmapImage>
	</cfPrimitives:ReflectionImage.ImageSource>
</cfPrimitives:ReflectionImage>   

The built-in animations are very powerful and you can get easily the Windows 8 experience with them, but if you need more flexibility you have to dig a little deeper and write your own custom animation.

StartMenuAnimator is used for the first animation in Cocktail Flow. This is very similar to the EntranceThemeTransition, but it gives you more flexibility:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
 
namespace CocktailFlow.Windows8.Animation
{
    public class StartMenuAnimator
    {
        private Random random = new Random();
        Storyboard sb = new Storyboard();
 
        public StartMenuAnimator(GridView gridView)
        {
            GridViewItem firstItem = (gridView.ItemContainerGenerator.ContainerFromIndex(0) as GridViewItem);
            var rootPanel = FindParent<StackPanel>(firstItem, "rootPanel");
 
            int l = 0;
 
	    // Custom TranslateX animation for header element(s) 
	    // If you don't have header element, you can skip this foreach in your code
            foreach (var header in rootPanel.Children)
            {
		// You have to name your header element to "headerRoot"
                var headerRoot = FindControlByType<Grid>(header, "headerRoot");
 
                headerRoot.RenderTransform = new CompositeTransform() { };
                DoubleAnimation translateX = new DoubleAnimation();
                translateX.SetValue(Storyboard.TargetPropertyProperty, "(UIElement.RenderTransform).(CompositeTransform.TranslateX)");
                Storyboard.SetTarget(translateX, headerRoot);
                translateX.EasingFunction = new ExponentialEase() { EasingMode = Windows.UI.Xaml.Media.Animation.EasingMode.EaseInOut, Exponent = 6 };
                translateX.From = 2000;
                translateX.To = 0;
                translateX.Duration = TimeSpan.FromSeconds(1);
                translateX.BeginTime = TimeSpan.FromSeconds(0.05 * l);
                sb.Children.Add(translateX);
 
                l++;
            }
 
	    // Custom TranslateX and Opacity animation for item(s) in GridView
	    // You can get a more flexible animation with this solution than the built-in EntranceThemeTransition
	    // For example, you can set each item's BeginTime with such an array, like panels
            int[] panels = new int[] { 2, 4, 7, 5, 3, 2, 0, 4, 5, 10, 5, 4, 2, 3, 1, 0, 2, 3, 7, 7, 7, 7 };
            
	    for (int i = 0; i < 22; i++)
            {
                GridViewItem item = (gridView.ItemContainerGenerator.ContainerFromIndex(i) as GridViewItem);
                if (item != null)
                {
                    var x = VisualTreeHelper.GetChild(item, 0);
 
		    // Your item's name should be root  
                    var rootControl = FindControlByType<FrameworkElement>(item, "root");
 
                    rootControl.RenderTransform = new CompositeTransform() { CenterX = rootControl.ActualWidth * 0.7, CenterY = rootControl.ActualHeight * 0.9 };
 
                    DoubleAnimation da = new DoubleAnimation();
                    da.SetValue(Storyboard.TargetPropertyProperty, "Opacity");
                    Storyboard.SetTarget(da, rootControl);
                    da.From = 0.1;
                    da.To = 1;
                    da.Duration = TimeSpan.FromSeconds(1);
                    da.BeginTime = TimeSpan.FromSeconds(0.05 * panels[i]);
                    sb.Children.Add(da);
 
                    DoubleAnimation translateX = new DoubleAnimation();
                    translateX.SetValue(Storyboard.TargetPropertyProperty, "(UIElement.RenderTransform).(CompositeTransform.TranslateX)");
                    Storyboard.SetTarget(translateX, rootControl);
                    translateX.EasingFunction = new ExponentialEase() { EasingMode = Windows.UI.Xaml.Media.Animation.EasingMode.EaseInOut, Exponent = 6 };
                    translateX.From = 2000;
                    translateX.To = 0;
                    translateX.Duration = TimeSpan.FromSeconds(1);
                    translateX.BeginTime = TimeSpan.FromSeconds(0.05 * panels[i]);
                    sb.Children.Add(translateX);
                }
            }
 
            sb.Begin();
        }
 
        public void Animate()
        {
            sb.Begin();
        }
 
        public static T FindControlByType<T>(DependencyObject container) where T : DependencyObject
        {
            return FindControlByType<T>(container, null);
        }
 
        public static T FindControlByType<T>(DependencyObject container, string name) where T : DependencyObject
        {
            T foundControl = null;
 
            //for each child object in the container
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(container); i++)
            {
                //is the object of the type we are looking for?
                if (VisualTreeHelper.GetChild(container, i) is T && (VisualTreeHelper.GetChild(container, i).GetValue(FrameworkElement.NameProperty).Equals(name) || name == null))
                {
                    foundControl = (T)VisualTreeHelper.GetChild(container, i);
                    break;
                }
                //if not, does it have children?
                else if (VisualTreeHelper.GetChildrenCount(VisualTreeHelper.GetChild(container, i)) > 0)
                {
                    //recursively look at its children
                    foundControl = FindControlByType<T>(VisualTreeHelper.GetChild(container, i), name);
                    if (foundControl != null)
                        break;
                }
            }
 
            return foundControl;
        }
 
        private T FindParent<T>(DependencyObject control, string name) where T : DependencyObject
        {
            T foundControl = null;
 
            //for each child object in the container

            //is the object of the type we are looking for?
            var parent = VisualTreeHelper.GetParent(control);
            if (parent == null)
                return null;
 
            if (VisualTreeHelper.GetParent(control) is T && (VisualTreeHelper.GetParent(control).GetValue(FrameworkElement.NameProperty).Equals(name) || name == null))
                foundControl = (T)VisualTreeHelper.GetParent(control);
            else
                foundControl = FindParent<T>(parent, name);
 
            return foundControl;
        }
    }
}

Here is the part of the code from the MainPage.xaml where we declare the GridView, what will use this animation (the naming convention is important: "rootPanel", "headerRoot" and root for the different elements):

XML
<cfControls:VariableGridView                  
	x:Name="itemGridView"
    AutomationProperties.AutomationId="ItemGridView"
	AutomationProperties.Name="Grouped Items"                            
	SelectionMode="None"                    
	IsItemClickEnabled="True"               
	ItemClick="ItemView_ItemClick"
	ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"                            
	ItemTemplateSelector="{StaticResource mainViewDataTemplateSelector}"                            
	ItemContainerStyle="{StaticResource GridViewDefaultItemStyle}">
 
	<GridView.ItemsPanel>
		<ItemsPanelTemplate>
			<StackPanel Orientation="Horizontal" Margin="96,96,40,46"  x:Name="rootPanel"/>
		</ItemsPanelTemplate>
	</GridView.ItemsPanel>
 
	<cfControls:VariableGridView.GroupStyle>
		<GroupStyle>
			<GroupStyle.HeaderTemplate>
				<DataTemplate>
					<Grid Margin="1,0,0,6" x:Name="headerRoot">
						<TextBlock  AutomationProperties.Name="Group Title" Text="{Binding Title}" FontFamily="Segoe UI" FontWeight="Light" FontSize="60" Foreground="{Binding ForegroundBrush}"/>
					</Grid>
				</DataTemplate>
			</GroupStyle.HeaderTemplate>
		<GroupStyle.Panel>		
			<ItemsPanelTemplate>
				<VariableSizedWrapGrid 
					Margin="0,0,80,0" 
					Orientation="Vertical" 
					ItemWidth="210"
					ItemHeight="240"/>
			</ItemsPanelTemplate>
		</GroupStyle.Panel>
		</GroupStyle>
	</cfControls:VariableGridView.GroupStyle>
</cfControls:VariableGridView>

You can start the animation at any time, by default, the Loaded event triggers it:

private void LayoutAwarePage_Loaded(object sender, RoutedEventArgs e)
{
    // This function could be called multiple times, to ensure the animation plays
    // only once we declared the isLoaded variable
    if (!isLoaded)
    {	
	isLoaded = true;
	StartMenuAnimator animator = new StartMenuAnimator(itemGridView);
	animator.Animate();
    }
}   

Screenshots     

Image 1
 

Image 2

 

Image 3

 Image 4

Points of Interest 

The most interesting challenge when developing Cocktail Flow was probably the constantly changing requirements over time: 

We started to develop Cocktail Flow for Windows 8 in early 2012 and at that time there wasn't much documentation and example code available and also, as the OS itself progressed different release builds re-implemented/ changed certain APIs (not huge differences between two release builds but enough to give us some sleepless nights :) ).

Aside from that the making of the app was a great experience for the whole team, it was great to see how easily we could transform the (business logic) code from the Windows Phone 7 version. 

Of course it wasn't just a Create New Project - Select All - Copy - Paste - Build method, because we had to completely re-write the UI layer, e.g. Both portrait and Landscape orientation mode were well known before Windows 8 but snapped view is a something new. Also, designing & developing an app that scales well to multiple resolutions wasn't an easy task.  


As the AppInnovation Contest progresses we are dedicated to publish more details about the parts that we already have and about what is in progress.

History 

Version 1.0 basic Cocktail Flow (see below Features)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionwill you let us download the full project code Pin
Tridip Bhattacharjee25-Sep-14 2:43
professionalTridip Bhattacharjee25-Sep-14 2:43 
QuestionIts rocking. Just a suggestion Pin
pepethepepe14-Oct-12 17:42
pepethepepe14-Oct-12 17:42 
QuestionWhich flavour? Pin
Chris Maunder9-Oct-12 14:49
cofounderChris Maunder9-Oct-12 14:49 
AnswerRe: Which flavour? Pin
Distinction Ltd.9-Oct-12 23:23
Distinction Ltd.9-Oct-12 23:23 
QuestionUltra Book Sensors Usage Pin
Ranjan.D8-Oct-12 1:17
professionalRanjan.D8-Oct-12 1:17 
AnswerRe: Ultra Book Sensors Usage Pin
Distinction Ltd.8-Oct-12 1:28
Distinction Ltd.8-Oct-12 1:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.