[Windows 8] A LiveTile control to use in your Windows Store applications !

November 19th, 2012 | Posted by Tom in .NET | Article | Windows 8 | WinRT | Read: 1,787

For one project I’m working on, I wanted to have some live tiles on the home page.

Unfortunately, I’ve not been able to find such control (at least for Windows 8) or, to be exact, I’ve not found a control that do exactly what I wanted. Indeed, after a quick search, I found a LiveTile control in Callisto, the Framework developed by Tim Heuer. But this control is, in my mind, not perfect because it works with IEnumerable/IList objects and display always the same template for each items. But in my case, I want to be able to display 2 differents templates (one for the front item and one for the back item).

So I’ve developed my own component using C#/XAML. This one might not be perfect but it’s a good start if you want to use it in your applications. Here is the C# code:

using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;

namespace DemoLiveTileControl
{
    public class SlideEventArgs : EventArgs
    {
    }

    [TemplatePart(Name = FRONTITEM_PARTNAME, Type = typeof(ContentPresenter))]
    [TemplatePart(Name = BACKITEM_PARTNAME, Type = typeof(ContentPresenter))]
    public sealed class LiveTile : Control
    {
        #region Constants

        const string FRONTITEM_PARTNAME = "PART_Front";
        const string BACKITEM_PARTNAME = "PART_Back";

        #endregion

        #region Enums

        public enum SlideDirection
        {
            Up,
            Left
        }

        #endregion

        #region Events

        public delegate void SlideEventHandler(object sender, SlideEventArgs args);

        public event SlideEventHandler SlideStarting;
        public event SlideEventHandler SlideEnded;

        #endregion

        #region Member Fields

        private DispatcherTimer _timer;
        private bool _isPlayingAnimation;
        private bool _isBackVisible;

        private ContentPresenter _frontItem;
        private ContentPresenter _backItem;

        private TranslateTransform _frontItemTransform;
        private TranslateTransform _backItemTransform;

        #endregion

        #region Dependency Properties

        public SlideDirection Direction
        {
            get { return (SlideDirection)GetValue(DirectionProperty); }
            set { SetValue(DirectionProperty, value); }
        }

        public static readonly DependencyProperty DirectionProperty =
            DependencyProperty.Register("Direction", typeof(SlideDirection), typeof(LiveTile), new PropertyMetadata(SlideDirection.Up));

        public object FrontContent
        {
            get { return (object)GetValue(FrontContentProperty); }
            set { SetValue(FrontContentProperty, value); }
        }

        public static readonly DependencyProperty FrontContentProperty =
            DependencyProperty.Register("FrontContent", typeof(object), typeof(LiveTile), new PropertyMetadata(null, (o, args) =>
            {
                var ctrl = o as LiveTile;
                if (ctrl != null && ctrl._frontItem != null)
                {
                    ctrl.ChangeDataContext();
                }
            }));

        public object BackContent
        {
            get { return (object)GetValue(BackContentProperty); }
            set { SetValue(BackContentProperty, value); }
        }

        public static readonly DependencyProperty BackContentProperty =
            DependencyProperty.Register("BackContent", typeof(object), typeof(LiveTile), new PropertyMetadata(null, (o, args) =>
            {
                var ctrl = o as LiveTile;
                if (ctrl != null && ctrl._backItem != null)
                {
                    ctrl.ChangeDataContext();
                }
            }));

        public DataTemplate FrontItemTemplate
        {
            get { return (DataTemplate)GetValue(FrontItemTemplateProperty); }
            set { SetValue(FrontItemTemplateProperty, value); }
        }

        public static readonly DependencyProperty FrontItemTemplateProperty =
            DependencyProperty.Register("FrontItemTemplate", typeof(DataTemplate), typeof(LiveTile), new PropertyMetadata(null, (o, args) =>
            {
                var ctrl = o as LiveTile;
                if (ctrl != null)
                {
                    ctrl.ChangeDataContext();
                }
            }));

        public DataTemplate BackItemTemplate
        {
            get { return (DataTemplate)GetValue(BackItemTemplateProperty); }
            set { SetValue(BackItemTemplateProperty, value); }
        }

        public static readonly DependencyProperty BackItemTemplateProperty =
            DependencyProperty.Register("BackItemTemplate", typeof(DataTemplate), typeof(LiveTile), new PropertyMetadata(null, (o, args) =>
            {
                var ctrl = o as LiveTile;
                if (ctrl != null)
                {
                    ctrl.ChangeDataContext();
                }
            }));

        private void ChangeDataContext()
        {
            if (this._frontItem != null && this._backItem != null)
            {
                this._frontItem.DataContext = this.FrontContent;
                this._backItem.DataContext = this.BackContent;
            }
        }

        #endregion

        #region Constructor

        public LiveTile()
        {
            this.DefaultStyleKey = typeof(LiveTile);

            this.SizeChanged += OnLiveTileSizeChanged;
            this.Unloaded += OnLiveTileUnLoaded;
        }

        #endregion

        protected override void OnApplyTemplate()
        {
            this._frontItem = this.GetTemplateChild(FRONTITEM_PARTNAME) as ContentPresenter;
            this._backItem = this.GetTemplateChild(BACKITEM_PARTNAME) as ContentPresenter;

            if(this._frontItem != null && this._backItem != null)
            {
                this._frontItemTransform = this._frontItem.RenderTransform as TranslateTransform;
                this._backItemTransform = this._backItem.RenderTransform as TranslateTransform;

                if (this._backItemTransform != null)
                {
                    if (this.Direction == SlideDirection.Up)
                    {
                        this._backItemTransform.Y = this.Height;
                    }
                    else
                    {
                        this._backItemTransform.X = this.Width;
                    }
                }

                var startTimer = new DispatcherTimer
                {
                    Interval = TimeSpan.FromSeconds(2)
                };
                startTimer.Tick += (sender, o) =>
                {
                    this.RaiseSlideStarting();

                    // Show Back item
                    this.ShowBackItem();

                    this._isBackVisible = !this._isBackVisible;

                    startTimer.Stop();
                    startTimer = null;
                };
                startTimer.Start();

                this.StartAnimation();
                this.ChangeDataContext();
            }

            base.OnApplyTemplate();
        }

        #region Private Methods

        private void OnLiveTileUnLoaded(object sender, RoutedEventArgs args)
        {
            if(this._timer != null)
            {
                this._timer.Stop();
                this._timer = null;
            }

            if(this._frontItemTransform != null)
            {
                if(this.Direction == SlideDirection.Up)
                {
                    this._frontItemTransform.Y = 0;
                }
                else
                {
                    this._frontItemTransform.X = 0;
                }
            }

            if (this._backItemTransform != null)
            {
                if (this.Direction == SlideDirection.Up)
                {
                    this._backItemTransform.Y = this.Height;
                }
                else
                {
                    this._backItemTransform.X = this.Width;
                }
            }
        }

        private void OnLiveTileSizeChanged(object sender, SizeChangedEventArgs e)
        {
            if(this._frontItem != null && this._backItem != null)
            {
                this._frontItem.Width = this._backItem.Width = e.NewSize.Width;
                this._frontItem.Height = this._backItem.Height = e.NewSize.Height;
            }
        }

        private void RaiseSlideStarting()
        {
            var handler = this.SlideStarting;
            if (handler != null)
            {
                handler(this, new SlideEventArgs());
            }
        }

        private void RaiseSlideEnded()
        {
            var handler = this.SlideEnded;
            if (handler != null)
            {
                handler(this, new SlideEventArgs());
            }
        }

        private void StartAnimation()
        {
            if(this._timer == null)
            {
                this._timer = new DispatcherTimer
                {
                    Interval = new TimeSpan(0, 0, 6)
                };

                this._timer.Tick += (sender, o) =>
                {
                    if (!this._isPlayingAnimation)
                    {
                        this.RaiseSlideStarting();

                        if (this._isBackVisible)
                        {
                            // Show Front item
                            this.ShowFrontItem();
                        }
                        else
                        {
                            // Show Back item
                            this.ShowBackItem();
                        }

                        this._isBackVisible = !this._isBackVisible;
                    }
                };
                this._timer.Start();
            }
        }

        private void ShowFrontItem()
        {
            var sb = new Storyboard();

            var hideBackItemAnimation = new DoubleAnimation
            {
                Duration = new Duration(TimeSpan.FromMilliseconds(2000)),
                To = this.Direction == SlideDirection.Up ? this.Height : this.Width,
                FillBehavior = FillBehavior.HoldEnd,
                EasingFunction = new QuinticEase
                {
                    EasingMode = EasingMode.EaseOut
                }
            };

            Storyboard.SetTarget(hideBackItemAnimation, this._backItemTransform);
            Storyboard.SetTargetProperty(hideBackItemAnimation, this.Direction == SlideDirection.Up ? "Y" : "X");

            sb.Children.Add(hideBackItemAnimation);

            var showFrontItemAnimation = new DoubleAnimation
            {
                Duration = new Duration(TimeSpan.FromMilliseconds(2000)),
                To = 0,
                FillBehavior = FillBehavior.HoldEnd,
                EasingFunction = new QuinticEase
                {
                    EasingMode = EasingMode.EaseOut
                }
            };

            Storyboard.SetTarget(showFrontItemAnimation, this._frontItemTransform);
            Storyboard.SetTargetProperty(showFrontItemAnimation, this.Direction == SlideDirection.Up ? "Y" : "X");

            sb.Children.Add(showFrontItemAnimation);

            sb.Completed += (a, b) =>
            {
                this._isPlayingAnimation = false;

                this.RaiseSlideEnded();
            };
            sb.Begin();

            this._isPlayingAnimation = true;
        }

        private void ShowBackItem()
        {
            var sb = new Storyboard();

            var showBackItemAnimation = new DoubleAnimation
            {
                Duration = new Duration(TimeSpan.FromMilliseconds(2000)),
                To = 0,
                FillBehavior = FillBehavior.HoldEnd,
                EasingFunction = new QuinticEase
                {
                    EasingMode = EasingMode.EaseOut
                }
            };

            Storyboard.SetTarget(showBackItemAnimation, this._backItemTransform);
            Storyboard.SetTargetProperty(showBackItemAnimation, this.Direction == SlideDirection.Up ? "Y" : "X");

            sb.Children.Add(showBackItemAnimation);

            var hideFrontItemAnimation = new DoubleAnimation
            {
                Duration = new Duration(TimeSpan.FromMilliseconds(2000)),
                To = this.Direction == SlideDirection.Up ? -this.Height : -this.Width,
                FillBehavior = FillBehavior.HoldEnd,
                EasingFunction = new QuinticEase
                {
                    EasingMode = EasingMode.EaseOut
                }
            };

            Storyboard.SetTarget(hideFrontItemAnimation, this._frontItemTransform);
            Storyboard.SetTargetProperty(hideFrontItemAnimation, this.Direction == SlideDirection.Up ? "Y" : "X");

            sb.Children.Add(hideFrontItemAnimation);

            sb.Completed += (a, b) =>
                {
                    this._isPlayingAnimation = false;

                    this.RaiseSlideEnded();
                };
            sb.Begin();

            this._isPlayingAnimation = true;
        }

        #endregion
    }
}

And here is the XAML code:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:DemoLiveTileControl">

    <Style TargetType="local:LiveTile">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:LiveTile">
                    <Grid Background="{TemplateBinding Background}"
                          Height="{TemplateBinding Height}"
                          Width="{TemplateBinding Width}">
                        <ContentPresenter x:Name="PART_Back"
                                          DataContext="{x:Null}"
                                          Content="{Binding}"
                                          ContentTemplate="{TemplateBinding BackItemTemplate}"
                                          RenderTransformOrigin="0.5,0.5">
                            <ContentPresenter.RenderTransform>
                                <TranslateTransform />
                            </ContentPresenter.RenderTransform>
                        </ContentPresenter>
                        <ContentPresenter x:Name="PART_Front"
                                          DataContext="{x:Null}"
                                          Content="{Binding}"
                                          ContentTemplate="{TemplateBinding FrontItemTemplate}"
                                          RenderTransformOrigin="0.5,0.5">
                            <ContentPresenter.RenderTransform>
                                <TranslateTransform />
                            </ContentPresenter.RenderTransform>
                        </ContentPresenter>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

As you can see, there are different properties that you can use but the most important are:

  1. Direction (Up or Left)
  2. FrontContent
  3. BackContent
  4. FrontItemTemplate
  5. BackItemTemplate

Here is a simple usage of the control:

<local:LiveTile FrontContent="Yoda"
                            BackContent="Dark Vador"
                            Direction="Up"
                            Height="245"
                            Width="220">
                <local:LiveTile.FrontItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition />
                                <RowDefinition Height="Auto" />
                            </Grid.RowDefinitions>
                            <Image Source="/Assets/Yoda.jpg" Height="220" />
                            <TextBlock Text="{Binding}"
                                        Grid.Row="1"
                                       Foreground="White"
                                       HorizontalAlignment="Center"
                                       VerticalAlignment="Center" />
                        </Grid>
                    </DataTemplate>
                </local:LiveTile.FrontItemTemplate>
                <local:LiveTile.BackItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition />
                                <RowDefinition Height="Auto" />
                            </Grid.RowDefinitions>
                            <Image Source="/Assets/DarkVador.png"
                                   Height="220" />
                            <TextBlock Text="{Binding}"
                                       Grid.Row="1"
                                       Foreground="White"
                                       HorizontalAlignment="Center"
                                       VerticalAlignment="Center" />
                        </Grid>
                    </DataTemplate>
                </local:LiveTile.BackItemTemplate>
            </local:LiveTile>

Using the control is really simple (you can even get  notified when the slide action will be performed or when it has ended). Here is a simple video of the control in action:

Of course, there might be some bugs so don’t hesitate to drop me a line !

 

Happy coding!

You can follow any responses to this entry through the RSS 2.0 You can leave a response, or trackback.

One Response

Add Comment Register



Leave a Reply