design

Can a developer simplify the Blend design process?

The problem

I was reading the blog entry A little interface animation fun which contains a part describing how a line of text was animated. Essentially the designer has had to create a separate UI Element for each letter and associate separate animations too.

This makes a large amount of noise in the designer and XAML that just seems unnecessary; at least I'd like it to be easier. So how can I make it easier to animate some text?

Solution 1 - User Control

The least complicated solution for me, and therefore via Occam's razor the best solution, is to create a user control. However, I dismissed this out-of-hand because I *wanted* to write a Behaviour ;)

Solution 2 - Behaviour

A new Behaviour is seductive because a designer can 'just' apply to a TextBlock and magically the letters in the text will be correctly animated. This post is about some of the problems I encountered.

The Target

The XAML I want to attach the Behaviour to looks like this;

<TextBlock x:Name="myTest" Text="THIS IS SOME TEXT">

After applying the Behaviour the text should appear character by character as if written by an old teletype printer. So I wrote a quick Behaviour called TextBehave;

<TextBlock x:Name="myTest" Text="THIS IS SOME TEXT">

    <i:Interaction.Behaviors>

        <local:TextBehave StoryBoardToPlay="Storyboard1" />

    </i:Interaction.Behaviors>

</TextBlock>

I broke the problem into solving the following tasks;

a) Create an Element per letter of the text content
b) Apply the storyboard to each letter-element in turn

Sounds simple right?

The problem with Behaviours is they attach to the object before the page has been constructed. So where I want to get hold of the loaded elements, such as the Storyboard called Storyboard1, in the OnAttached event I can't just FindName() them. Time for a painful compromise, I need the Loaded event for the 'page' to tell the attached behaviours that they can now run their code to both inspect the created elements and to add additional elements of their own. I decided to do this by allowing the page to discover which behaviours want to know this information by looking for a particular interface, IContentChangingBehaviour. Unfortunately this means the designer has to add a little stub code into the page load event (I'm thinking about getting the behaviours to hook into their pages load event themselves, but that's for the future);

void MainPage_Loaded(object sender, RoutedEventArgs e)

        {           

            UIElementCollection uIElementCollection = LayoutRoot.Children;

            CaptureContentChangers(uIElementCollection);

            iBeamMove.Begin();

            ExecuteContentChangers();           

        }

 

        private void CaptureContentChangers(UIElementCollection uIElementCollection)

        {

            this.contentChangingBehaviours = new List<IContentChangingBehaviour>();

            foreach (UIElement element in uIElementCollection)

            {

                DependencyObject dep = element;

                BehaviorCollection behCol = Interaction.GetBehaviors(dep);

                foreach (Behavior behaviour in behCol)

                {

                    IContentChangingBehaviour contentChanger = behaviour as
                         IContentChangingBehaviour
;

                    if (contentChanger != null)

                    {

                        contentChangingBehaviours.Add(contentChanger);

                    }

                }

            }

        }

 

        private void ExecuteContentChangers()

        {

            foreach (IContentChangingBehaviour contentChanger in
                 this.contentChangingBehaviours)

            {

                contentChanger.Load(LayoutRoot);

            }

        }

Ok, so now the behaviours are attached and are getting poked to create elements it's time to get it to do something!

Create an Element per letter of text content

#region IContentChangingBehaviour Members

public void Load(Panel rootPanel)

{

    Show(rootPanel);

}


public void Show(Panel layoutRoot)

{

    TextBlock attachedObject = (TextBlock)this.AssociatedObject;

    content = attachedObject.Text;

    AddControlPerLetter(layoutRoot);

    attachedObject.Visibility = Visibility.Collapsed;

}


private void AddControlPerLetter(Panel layoutRoot)

{

    double position = 0;

    this.createdTextBlocks = new List<TextBlock>();

    foreach (char c in content)

    {

        // Clone it to grab all the non-default properties set on the

        // associated text block

    TextBlock letterBlock =

        CloneObject.Clone‹TextBlock›(this.AssociatedObject);

    letterBlock.Text = "";

    letterBlock.Text += c;

    letterBlock.SetValue(Canvas.LeftProperty, position);

    position += this.FixedSpacing;

    layoutRoot.Children.Add(letterBlock);

    this.createdTextBlocks.Add(letterBlock);

    }

}

The behaviour now creates an element per letter, I've also added a property to the behaviour, it's a fixed spacing property - not great but another change for the future.

<local:TextBehave FixedSpacing="20" />

Apply the storyboard to each letter-element in turn

Now the fun begins, one of the most frustrating non-features of Silverlight is its inability to share storyboards. What I want to do is for the designer to choose what storyboard they want to apply to each letter, and for the behaviour to apply the storyboard to each letter in-turn. However, I can't simply change the target of the storyboard from one element to another. Well you can but you cannot reassign a storyboard if it's still running. So you have to stop it, but as soon as you stop the storyboard the element will revert back to its original state - no good at all. The solution, clone the storyboard. Ah again the simplest things in Silverlight are the hardest. Cloning is a peculiar beast because of the dependency properties, fortunately I discovered Jim McCurdy's CloneObject class which does a good job (with a few additional tweaks) of cloning a storyboard. Therefore I can now allow the designer to specify the storyboard;

<local:TextBehave FixedSpacing="20" StoryBoardToPlay="animateLetterOn" />

...and programmatically control the animation of the letter;

private void AnimateCreatedContent()

{

    TextBlock target = this.createdTextBlocks[this.currentlyPlaying];

    Storyboard targetStoryBoard = this.storyboard;

    ChangeAnimationTarget(target, targetStoryBoard);

}

private void ChangeAnimationTarget(TextBlock target, Storyboard targetStoryboard)

{

    Storyboard clone = targetStoryboard.Clone<Storyboard>(typeof(Storyboard));

    foreach (Timeline timeLine in clone.Children)

    {

        Storyboard.SetTarget(timeLine, target);

    }

    clone.Completed += storyboard_Completed;

    clone.Begin();

}

void storyboard_Completed(object sender, EventArgs e)

{

    this.currentlyPlaying++;

    if (this.currentlyPlaying == this.createdTextBlocks.Count)

    {

        this.currentlyPlaying = 0;

    }

    else

    {

        System.Diagnostics.Debug.WriteLine("playing : " + this.currentlyPlaying);

        AnimateCreatedContent();

    }

}

So there you go, the original TextBlock has vanished to be replaced by letters that are individually animated onto the screen. Hurray. Except...

The original storyboard has a flashing cursor that moves along with the letters. Now I've split the letter-showing animation out of this storyboard how do you keep the animations in sync? Well I haven't solved that one yet, apart from calculating the time period (yuk). Hopefully I just need to get my head around relative animations and I'll get this working ;)

Well although this might not be a complete solution it does work, and maybe it will help someone else create a better version.

View the animated text

Article by Paulio

Coming soon - applying the refined code to the original design.

Return to site