Tuesday, September 2, 2008

“Shield” Yourself from Animation Problems in Silverlight Visual State Manager


While working on Vertigo's high definition video player for the Democratic National Convention, our team encountered some challenges with Silverlight 2 Beta 2's Visual State Manager. In this post, I'll discuss how we overcame these challenges, and our solution to make working with VSM simple, quick, and easy. 

For an introduction to Visual State Manager, check out Scott Guthrie's Silverlight 2 Beta 2 intro here.

Problem #1: Radio Button and ListBoxItem Selection States
Radio buttons and ListBoxItems both have a "selected" state. The XAML for the VisualStateGroups for a ListBoxItem look like this (the same states are used for Radio buttons):

<vsm:VisualStateManager.VisualStateGroups>
<
vsm:VisualStateGroup x:Name="CommonStates">
<
vsm:VisualState x:Name="Normal" />
<
vsm:VisualState x:Name="MouseOver">
<
Storyboard>
<!-- Animate the MouseOver -->
</Storyboard>
</
vsm:VisualState>
</
vsm:VisualStateGroup>
<
vsm:VisualStateGroup x:Name="SelectionStates">
<
vsm:VisualState x:Name="Selected">
<
Storyboard> <!-- Animate the Selection -->
</Storyboard>
</
vsm:VisualState>
<
vsm:VisualState x:Name="Unselected"/>
</
vsm:VisualStateGroup>
</
vsm:VisualStateManager.VisualStateGroups>


Notice that there are 2 VisualStateGroups – one for "CommonStates", and one for "SelectionStates". Keep in mind that a control must be in one state from each VisualStateGroup at all times. This means that when a ListBoxItem or RadioButton is in the "Selected" state, it must also be in either the Normal/default state or in the MouseOver state:

Possible StatesNormal & Unselected
MouseOver & Unselected
Normal & Selected
MouseOver & Selected


This becomes a problem when you need to define a 3-state setup for your buttons, when the same property has different values in all 3 states. If the "Selected" Visual State modifies a property that is also modified by the "MouseOver" Visual State, a conflict occurs.

For example, let's say we want to modify the text color on a ListBox Item or button. When you select the button, it will appear in the "selected" state:



However, when you mouse out of the button's area, the button will return to the "normal" state, even though the button is also in the "selected" state. Since the button is both "selected" and "normal", and both those states modify the text color, the last state to transition "wins". Your selected button will show both states, with the normal state on top, making the text appear thin and unreadable:



Note that the same problem exists for CheckBoxes, which have a "CheckStates" VisualStateGroup.

Problem #2: Animating Brushes
We noticed that when using a ColorAnimation within VSM to change the color of a brush, strange behavior occurs. Either the animation does not occur, or it occurs on an adjacent control. This only happens on 1 out of every 10 transitions - a representative from Microsoft acknowledges the problem in this Silverlight forum discussion.

To see it for yourself, take a look at this sample application built by Page Brooks to illustrate the problem. Mouse over the dots back and forth - you'll notice states getting stuck. The source code is also available.



Our Solution to Both Problems: Opacity-Only Animations and the "Selected Shield"
To circumvent these problems, we only ever animate opacity. We build a unique element (Grid, Canvas, whatever) for each Visual State, all having an opacity set to 0 other than the element for the "Normal" state. We then simply animate using a DoubleAnimation, changing the opacity on each element.

If our elements include only opaque items, such as images, this would be enough. However, if our elements contain transparent items, we still have a problem. Our selected state will be set to an opacity of 1 along with the MouseOver state, so both will appear to bleed into each other. To solve this, we also include an opaque "Shield" element in the selected state. We set the Z-orders of these elements so that the selected element appears on top of the shield, and the shield element appears on top of the MouseOver and Normal elements. See below for an example of this setup in the XAML.

Of course, having to make multiple copies of the exact same XAML elements is not optimal, and ends up requiring more lines of code. However, it is the only way we have found to totally squash both of these problems.

<ControlTemplate TargetType="ListBoxItem">
<Grid x:Name="LBGrid" Background="Transparent" Cursor="Hand">

<
vsm:VisualStateManager.VisualStateGroups>
<
vsm:VisualStateGroup x:Name="CommonStates">
<
vsm:VisualState x:Name="Normal" />
<
vsm:VisualState x:Name="MouseOver">
<
Storyboard>
<
DoubleAnimation Storyboard.TargetName="canvasNormal" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="0"/>
<
DoubleAnimation Storyboard.TargetName="canvasMouseOver" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="1"/>
</
Storyboard>
</
vsm:VisualState>
</
vsm:VisualStateGroup>
<
vsm:VisualStateGroup x:Name="SelectionStates">
<
vsm:VisualState x:Name="Selected">
<
Storyboard>
<
DoubleAnimation Storyboard.TargetName="canvasNormal" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="0"/>
<
DoubleAnimation Storyboard.TargetName="canvasMouseOver" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="0"/>
<
DoubleAnimation Storyboard.TargetName="canvasSelectedShield" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="1"/>
<
DoubleAnimation Storyboard.TargetName="canvasSelected" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="00:00:00.001" To="1"/>
</
Storyboard>
</
vsm:VisualState>
<
vsm:VisualState x:Name="Unselected"/>
</
vsm:VisualStateGroup>
</
vsm:VisualStateManager.VisualStateGroups>

<
Grid.ColumnDefinitions>
<
ColumnDefinition Width="255"/>
</
Grid.ColumnDefinitions>
<
Grid.RowDefinitions>
<
RowDefinition Height="24"/>
<
RowDefinition Height="3"/>
</
Grid.RowDefinitions>

<Canvas x:Name="canvasMouseOver" Grid.Column="0" Grid.Row="0" Opacity="0">
<
Rectangle Canvas.Left="0" Canvas.Top="0" Stretch="Fill" VerticalAlignment="Stretch" Width="255" Height="24" >

<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.511514,-0.00480887" EndPoint="0.511514,1.0625">
<LinearGradientBrush.GradientStops>
<GradientStop Color="#80FFFFFF" Offset="0"/>
<GradientStop Color="#805B7F9C" Offset="0.34322"/>
<GradientStop Color="#80FFFFFF" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</
Canvas>

<Canvas x:Name="canvasNormal" Grid.Column="0" Grid.Row="0" Background="Transparent" Opacity="1">
<
Rectangle />
</
Canvas>

<
Canvas x:Name="canvasSelectedShield" Grid.Column="0" Grid.Row="0" Background="#ABBDD1" Opacity="0"/>

<
Canvas x:Name="canvasSelected" Grid.Column="0" Grid.Row="0" Opacity="0"> <Rectangle Canvas.Left="0" Canvas.Top="0" Stretch="Fill" VerticalAlignment="Stretch" Width="255" Height="24" >
<
Rectangle.Fill>
<
LinearGradientBrush StartPoint="0.525718,0.0128409" EndPoint="0.525718,1.0513">
<
LinearGradientBrush.GradientStops>
<
GradientStop Color="#80FFFFFF" Offset="0"/>
<
GradientStop Color="#807E9EB8" Offset="0.360731"/>
<
GradientStop Color="#80FDFDFD" Offset="1"/>
</
LinearGradientBrush.GradientStops>
</
LinearGradientBrush>
</
Rectangle.Fill>
</
Rectangle>
</
Canvas>

</Grid></ControlTemplate>

1 comment:

  1. Verify you're creating a trial your own specific coolest pay stub or maybe proof of cash stream at the side of your driver's alter or maybe check cashing corona impart PROGNOSIS. entirely around development, having the money would love will clearly be our the specified target.

    ReplyDelete

Note: Only a member of this blog may post a comment.