views:

2157

answers:

1

I'm animating a 'race' on a map. The race takes 45 minutes, but the animation runs for 60 seconds.

You can watch the 2008 City2Surf race demo to see what I mean.

The 'race clock' in the top-left must show "real time", and had to be set-up in the .xaml.cs with a System.Windows.Threading.DispatcherTimer which seems a bit of a hack.

I thought maybe there'd be a DependencyProperty on the animation rather than just StoryBoard.GetCurrentTime(), but instead I have had to

         // SET UP AND START TIMER, before StoryBoard.Begin()
         dt = new System.Windows.Threading.DispatcherTimer();
         dt.Interval = new TimeSpan(0, 0, 0, 0, 100); // 0.1 second
         dt.Tick +=new EventHandler(dt_Tick);
         winTimeRatio = (realWinTime.TotalSeconds * 1.0) / animWinTime.TotalSeconds;
         dt.Start();

and then the Tick event handler

    void dt_Tick(object sender, EventArgs e)
    {
        var sb = LayoutRoot.Resources["Timeline"] as Storyboard;
        TimeSpan ts = sb.GetCurrentTime();
        TimeSpan toDisplay = new TimeSpan(0,0, 
               Convert.ToInt32(ts.TotalSeconds * winTimeRatio));
        RaceTimeText.Text = toDisplay.ToString();
    }

This works and seems to perform OK - but my question is: am I missing something in the Silverlight animation/storyboard classes that would do this more neatly? I have to remember to stop the DispatcherTimer too!

Or to put the question another way: any better suggestions on 'animation' of TextBox content (the .Text itself, not the location/dimensions/etc)?

+3  A: 

That is one way. It's nice and simple, but a bit messy. You could get rid of the storyboard and on each tick, increment a local value by the tick interval and use that to set your time. You would then only have one time piece.

Or... A more elegant and re-usable way would be to create a helper class that is a DependencyObject. I would also just use a StoryBoard with a DoubleAnimation an bind the Storyboard.Target to an instance of the DoubleTextblockSetter. Set the storyboard Duration to your time and set the value to your time in seconds. Here is the DoublerBlockSetterCode.

public class DoubleTextBlockSetter : DependencyObject
{
    private TextBlock textBlock { get; private set; }
    private IValueConverter converter { get; private set; }
    private object converterParameter { get; private set; }

    public DoubleTextBlockSetter(
               TextBlock textBlock, 
               IValueConverter converter, 
               object converterParameter)
    {
        this.textBlock = textBlock;
        this.converter = converter;
        this.converterParameter = converterParameter;
    }

    #region Value

    public static readonly DependencyProperty ValueProperty = 
         DependencyProperty.Register(
             "Value", 
             typeof(double), 
             typeof(DoubleTextBlockSetter),
             new PropertyMetadata(
                 new PropertyChangedCallback(
                     DoubleTextBlockSetter.ValuePropertyChanged
                 )
             )
          );

    private static void ValuePropertyChanged(
        DependencyObject obj, 
        DependencyPropertyChangedEventArgs args)
    {
        DoubleTextBlockSetter control = obj as DoubleTextBlockSetter;
        if (control != null)
        {
            control.OnValuePropertyChanged();
        }
    }

    public double Value
    {
        get { return (double)this.GetValue(DoubleTextBlockSetter.ValueProperty); }
        set { base.SetValue(DoubleTextBlockSetter.ValueProperty, value); }
    }

    protected virtual void OnValuePropertyChanged()
    {
        this.textBlock.Text = this.converter.Convert(
            this.Value, 
            typeof(string), 
            this.converterParameter, 
            CultureInfo.CurrentCulture) as string;
    }

    #endregion
}

Then you might have a format converter:

public class TicksFormatConverter : IValueConverter
{
    TimeSpanFormatProvider formatProvider = new TimeSpanFormatProvider();

    public object Convert(object value, 
        Type targetType, 
        object parameter, 
        CultureInfo culture)
    {
        long numericValue = 0;

        if (value is int)
        {
            numericValue = (long)(int)value;
        }
        else if (value is long)
        {
            numericValue = (long)value;
        }
        else if (value is double)
        {
            numericValue = (long)(double)value;
        }
        else
            throw new ArgumentException("Expecting type of int, long, or double.");

        string formatterString = null;
        if (parameter != null)
        {
            formatterString = parameter.ToString();
        }
        else
        {
            formatterString = "{0:H:m:ss}";
        }

        TimeSpan timespan = new TimeSpan(numericValue);

        return string.Format(this.formatProvider, formatterString, timespan);
    }

    public object ConvertBack(
        object value, 
        Type targetType,  
        object parameter, 
        CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

I almost forgot the TimespanFormatProvider. There is no format provider for timespan in Silverlight, so it appears.

public class TimeSpanFormatProvider : IFormatProvider, ICustomFormatter
{
    public object GetFormat(Type formatType)
    {
        if (formatType != typeof(ICustomFormatter))
            return null;
        return this;
    }

    public string Format(string format, object arg, IFormatProvider formatProvider)
    {
        string formattedString;

        if (arg is TimeSpan)
        {
            TimeSpan ts = (TimeSpan)arg;
            DateTime dt = DateTime.MinValue.Add(ts);
            if (ts < TimeSpan.FromDays(1))
            {
                format = format.Replace("d.", "");
                format = format.Replace("d", "");
            }

            if (ts < TimeSpan.FromHours(1))
            {
                format = format.Replace("H:", "");
                format = format.Replace("H", "");
                format = format.Replace("h:", "");
                format = format.Replace("h", "");
            }

            // Uncomment of you want to minutes to disappear below 60 seconds.
            //if (ts < TimeSpan.FromMinutes(1))
            //{
            //    format = format.Replace("m:", "");
            //    format = format.Replace("m", "");
            //}

            if (string.IsNullOrEmpty(format))
            {
                formattedString = string.Empty;
            }
            else
            {
                formattedString = dt.ToString(format, formatProvider);
            }
        }
        else
            throw new ArgumentNullException();

        return formattedString;
    }
}

All that stuff is re-usable and should live in your tool box. I pulled it from mine. Then, of course, you wire it all together:

Storyboard sb = new Storyboard();
DoubleAnimation da = new DoubleAnimation();
sb.Children.Add(da);
DoubleTextBlockSetter textBlockSetter = new DoubleTextBlockSetter(
    Your_TextBlock, 
    new TicksFormatConverter(), 
    "{0:m:ss}"); // DateTime format

Storyboard.SetTarget(da, textBlockSetter);

da.From = Your_RefreshInterval_Secs * TimeSpan.TicksPerSecond;
da.Duration = new Duration(
    new TimeSpan(
        Your_RefreshInterval_Secs * TimeSpan.TicksPerSecond));
sb.begin();

And that should do the trick. An it's only like a million lines of code. And we haven't even written Hello World just yet...;) I didn't compile that, but I did Copy and Paste the 3 classes directly from my library. I've used them quite a lot. It works great. I also use those classes for other things. The TickFormatConverter comes in handy when data binding. I also have one that does Seconds. Very useful. The DoubleTextblockSetter allows me to animate numbers, which is really fun. Especially when you apply different types of interpolation.

Enjoy.

ptoinson
Wow very comprehensive answer! I will give that a try and see how I go...
CraigD