Sunday, November 23, 2008

In my previous post, we talked about one approach to visualize the change of content. So, we had a control with the content ‘Ruurd’ and when we changed the content to ‘RJ’, we could very easily setup a fade out for ‘Ruurd’ and a fade in for ‘RJ’. Or get more fancy and add a small translate transform.
In this post I want to share some of my thinking on a related subject: how do you visualize the movement of an object? For example: we have a button in a grid on column 0 and we move it to column 1. That move is instant, but in many cases you would like an actual animation between those locations.

There is precedence here, namely in the many tweening libraries for Flash and nowadays for Silverlight. Also, Nikhil has written a lot about this subject with his great attached behavior series.

Sample

I like typing in that textbox a LOT. I’m sure you will too.  :)

Power to the item or does the panel still rule?

It is hard to determine when you should use a panel to do animation and when you should not. I live by this rule:

When the movement itself has meaning, use a panel. If it is an effect, let the item take care of it.

This means: if during the movement you should be able to stop the movement and use the items in that position, than the visual tree should not have any translate transforms going on. The layout system should be managing the location of these items.
If on the other hand the movement is just a transition, the panel should not need to be aware of this transition and just be used to determine the logical positions of an item. That is many times more efficient.

To use examples: a carousel is showing items and we let the user ‘nudge’ the items a few pixels –> the new positions are the real positions and thus a panel should be used.
Items in a dockpanel or a stackpanel that move around within that panel should be managing their transition themselves.

So, I believe effects (visual position) should be managed by the item, logical position should be determined by the panel.

This has the added technical advantage that we do not have to rewrite all panels. This is illustrated by the xaml for this sample:

    <Grid x:Name="LayoutRoot">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <ItemMovementSpike:MoveableItem HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" >
            <Button Click="Button_Click" Content="Move me" />
        </ItemMovementSpike:MoveableItem>
        <ItemMovementSpike:MoveableItem Grid.Row="1" HorizontalContentAlignment="Stretch" >
            <TextBox TextChanged="TextBox_TextChanged" Text="TypeMe" />
        </ItemMovementSpike:MoveableItem>
</Grid>
You can see I just use the regular Grid layout panel, nothing to see there!
Efficient location determination

Because we do not want to change our panels, we will have to determine our position on the screen ourselves. This is a somewhat inefficient process, so it is important to start thinking about where we want to take that hit.

The WPF/Silverlight layout system is very cautious about forcing a layout pass in a panel. It only does so when it is forced, so you can be certain that something has changed. The part of the layout pass that we are interested in is the Arrange part. During the arrange, the panel asks each child to arrange itself given a certain Size and then determines its position. So:

Panel.Arrange
  for each item: Item.Arrange
Panel –> set location and size for item (using a rectangle)

During this time, the most beloved of events is fired continuously on the item: CompositionTarget.Rendering.

The Rending event is called each time before the item is drawn on the screen and therefore comes with great power! If we were to do anything that is using cpu-cycles in this event, we will see some serious performance problems in our application. So, we need to be clever about it.

It is important to know that during the Item.Arrange, our item is still in its old location: The panel has not yet set its position, that will come a few Rendering events later. So, we can use the Item.Arrange to determine the current position with a TransformToVisual call. This is an expensive call, but since we only use it in the Arrange method (which is called only when necessary), it is cheap enough for our purposes.

   78 protected override Size ArrangeOverride(Size finalSize)

   79 {

   80     fromLocation = TransformToVisual(((UIElement) this.Parent)).Transform(new Point(0, 0));

   81     fromLocation.X += tt.X;

   82     moved = true;

   83     return base.ArrangeOverride(finalSize);

   84 }

I determine the current location on line 80 as it relates to its parent (the panel). You could do a quick visual tree walk to find our plugin root, which would enable some cool scenarios!!

Line 81 corrects for our current translatetransform (tt) and then we set a boolean ‘moved’ to true, indicating that something has happened.

The important stuff happens in our Rendering event:

   55 void CompositionTarget_Rendering(object sender, EventArgs e)

   56 {

   57     if(moved)

   58     {

   59         moved = false;

   60         toLocation = TransformToVisual(((UIElement) this.Parent)).Transform(new Point(0, 0));

   61         startmove = DateTime.Now;

   62         moving = true;

   63     }

   64 

   65     if(moving)

   66     {

   67         double fraction = DateTime.Now.Subtract(startmove).TotalMilliseconds/200d;

   68         tt.X = (fromLocation.X - toLocation.X)*(1 - fraction);

   69 

   70         if(fraction >= 1)

   71         {

   72             moving = false;

   73             tt.X = 0;

   74         }

   75     }

   76 }

It happens to be that in the first rendering event that is called, the move has occured. I’m not sure if that is a general rule, or something that just happens consistently on my machine. I haven’t looked into that yet.

We do one more inefficient TransformToVisual to find out where we currently are. Then we need the current time (startMove) and we indicate that we are moving. In the moving code we set the appropriate translateTransform (tt) and make sure that we stop moving when time is up.
If you’re into penner equations, you would plug them in after line 68.

No source for this one though, it’s just a spike. There are many things yet to be considered. But do let me know your thoughts.

Monday, November 24, 2008 5:53:47 PM (Romance Standard Time, UTC+01:00)
Works fine. But it is more work as I thought.
Tuesday, November 25, 2008 8:16:03 AM (Romance Standard Time, UTC+01:00)
Thank you, Great post!

Robert
<a href="http://www.amazooz.com">www.amazooz.com</a>
Wednesday, November 26, 2008 10:15:56 AM (Romance Standard Time, UTC+01:00)
Silverlight Travel: How can I make it less work for you? All you have to do is wrap your uielements in this:
<ItemMovementSpike:MoveableItem>your control </> and everything works..
No panels to inherit from.. nothing!!?
Ruurd
Name
E-mail
Home page

Comment (HTML not allowed)  

Enter the code shown (prevents robots):