Yet another blog about WPF, Surface, SL, MVVM, NUI....

2011

2010

2009

2008

Tag - scaling

Entries feed - Comments feed

 

How to create an animated expander

21 September 2010

The expander control can be used in a lot of situations but the one proposed by default is quite "rigid".

In this post we will discover how to animate it quite simply just via XAML !

The WPF engine lets us redefine the template of the controls and we'll just do that.

The goal aimed

What we aim is to get the same functionnality as the original expander.
This is not as simple as we tought and I've seen a lot of expander loosing some of their behaviors when they became "animated" : original value of IsExpanded ignored, ExpandDirection ignored, etc...

Getting the necessary files

The files needed are :

  1. the original control template of the Expander
  2. the expander's button style which are linked to it


To get them, I used Expression Blend folowing the MSDN steps on this page : http://msdn.microsoft.com/en-us/library/cc294908.aspx

Especially for you, they are also linked to the post :) !. Here is the expander original template :

<ControlTemplate TargetType="{x:Type Expander}">
  <Border SnapsToDevicePixels="true" 
      Background="{TemplateBinding Background}" 
      BorderBrush="{TemplateBinding BorderBrush}" 
      BorderThickness="{TemplateBinding BorderThickness}" 
      CornerRadius="3">
    <DockPanel>
      <ToggleButton FocusVisualStyle="{StaticResource ExpanderHeaderFocusVisual}" 
              Margin="1" 
              MinHeight="0" 
              MinWidth="0" 
              x:Name="HeaderSite" 
              Style="{StaticResource ExpanderDownHeaderStyle}" />
      <ContentPresenter Focusable="false" 
                Visibility="Collapsed" 
                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                Margin="{TemplateBinding Padding}" 
                x:Name="ExpandSite"   />
    </DockPanel>
  </Border>
  <ControlTemplate.Triggers>
    <Trigger Property="IsExpanded" Value="true">
      <Setter Property="Visibility" TargetName="ExpandSite" Value="Visible"/>
    </Trigger>
    <Trigger Property="ExpandDirection" Value="Right">
      <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Right"/>
      <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Left"/>
      <Setter Property="Style" TargetName="HeaderSite" 
                     Value="{StaticResource ExpanderRightHeaderStyle}"/>
    </Trigger>
    <Trigger Property="ExpandDirection" Value="Up">
      <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Top"/>
      <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Bottom"/>
      <Setter Property="Style" TargetName="HeaderSite" 
                    Value="{StaticResource ExpanderUpHeaderStyle}"/>
    </Trigger>
    <Trigger Property="ExpandDirection" Value="Left">
      <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Left"/>
      <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Right"/>
      <Setter Property="Style" TargetName="HeaderSite" 
                    Value="{StaticResource ExpanderLeftHeaderStyle}"/>
    </Trigger>
    <Trigger Property="IsEnabled" Value="false">
      <Setter Property="Foreground" 
               Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
    </Trigger>
  </ControlTemplate.Triggers>
</ControlTemplate>



Add the new behavior to the expander

As you can see, the original template plays on the Visibility property to expand or collapse the "expandable" part.

We will change that and add a scaling transformation on the "expendable part" of the control that we'll animate at the right moment (when the IsExpanded property value change).
Also we'll not use simple DataTrigger but MultiTrigger because we have to starts differents animation depending of the Expand direction.

The result is a quite simple but lenghty XAML file (the AnimatedExpanderStyles is linked to the post) :

<Style x:Key="ourAnimatedExpanderStyle" TargetType="{x:Type Expander}">
<Setter Property="Foreground"
             Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type Expander}">
 
      <ControlTemplate.Resources>
        <ResourceDictionary>
          <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary
     Source="/My.Assembly;component/AnimatedExpander/AnimatedExpanderStyles.xaml" />
        </ResourceDictionary.MergedDictionaries>
          <Storyboard x:Key="scaleYUp">
            <DoubleAnimation From="0" To="1" Duration="0:0:0.25" 
Storyboard.TargetName="ExpandSite"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleY)" />
          </Storyboard>
          <Storyboard x:Key="scaleYDown">
            <DoubleAnimation Fr  om="1" To="0" Duration="0:0:0.25" 
Storyboard.TargetName="ExpandSite"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleY)" />
          </Storyboard>
          <Storyboard x:Key="scaleXUp">
            <DoubleAnimation From="0" To="1" Duration="0:0:0.25"
Storyboard.TargetName="ExpandSite"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleX)" />
          </Storyboard>
          <Storyboard x:Key="scaleXDown">
            <DoubleAnimation From="1" To="0" Duration="0:0:0.25"  
Storyboard.TargetName="ExpandSite"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleX)" />
          </Storyboard>
        </ResourceDictionary>
      </ControlTemplate.Resources>
 
      <Border BorderBrush="{TemplateBinding BorderBrush}"
          BorderThickness="{TemplateBinding BorderThickness}"
          Background="{TemplateBinding Background}" CornerRadius="3"
                SnapsToDevicePixels="true">
        <DockPanel>
          <ToggleButton
              IsChecked="{Binding Path=IsExpanded, Mode=TwoWay, 
                                    RelativeSource={RelativeSource TemplatedParent}}"
              Margin="1" MinHeight="0" MinWidth="0" x:Name="HeaderSite"
              Style="{StaticResource ExpanderDownHeaderStyle}">
            <ContentPresenter Content="{TemplateBinding Header}"
                ContentTemplate="{TemplateBinding HeaderTemplate}"
                ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}" Margin="1"
                Focusable="false" />
          </ToggleButton>
 
          <ContentPresenter x:Name="ExpandSite"
              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
              Margin="{TemplateBinding Padding}" Focusable="false">
            <ContentPresenter.LayoutTransform>
              <ScaleTransform x:Name="scaleTransform" ScaleX="1" ScaleY="1" />
            </ContentPresenter.LayoutTransform>
          </ContentPresenter>
        </DockPanel>
      </Border>
      <ControlTemplate.Triggers>
        <MultiTrigger>
          <MultiTrigger.Conditions>
            <Condition Property="IsExpanded" Value="True" />
            <Condition Property="ExpandDirection" Value="Up" />
          </MultiTrigger.Conditions>
          <MultiTrigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource scaleYUp}" />
          </MultiTrigger.EnterActions>
          <MultiTrigger.ExitActions>
            <BeginStoryboard Storyboard="{StaticResource scaleYDown}" />
          </MultiTrigger.ExitActions>
        </MultiTrigger>
 
        <MultiTrigger>
          <MultiTrigger.Conditions>
            <Condition Property="IsExpanded" Value="True" />
            <Condition Property="ExpandDirection" Value="Down" />
          </MultiTrigger.Conditions>
          <MultiTrigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource scaleYUp}" />
          </MultiTrigger.EnterActions>
          <MultiTrigger.ExitActions>
            <BeginStoryboard Storyboard="{StaticResource scaleYDown}" />
          </MultiTrigger.ExitActions>
        </MultiTrigger>
 
        <MultiTrigger>
          <MultiTrigger.Conditions>
            <Condition Property="IsExpanded" Value="True" />
            <Condition Property="ExpandDirection" Value="Left" />
          </MultiTrigger.Conditions>
          <MultiTrigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource scaleXUp}" />
          </MultiTrigger.EnterActions>
          <MultiTrigger.ExitActions>
            <BeginStoryboard Storyboard="{StaticResource scaleXDown}" />
          </MultiTrigger.ExitActions>
        </MultiTrigger>
        <MultiTrigger>
          <MultiTrigger.Conditions>
            <Condition Property="IsExpanded" Value="True" />
            <Condition Property="ExpandDirection" Value="Right" />
          </MultiTrigger.Conditions>
          <MultiTrigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource scaleXUp}" />
          </MultiTrigger.EnterActions>
          <MultiTrigger.ExitActions>
            <BeginStoryboard Storyboard="{StaticResource scaleXDown}" />
          </MultiTrigger.ExitActions>
        </MultiTrigger>
 
        <Trigger Property="ExpandDirection" Value="Down">
          <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Bottom" />
          <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Top" />
 
        </Trigger>
        <Trigger Property="ExpandDirection" Value="Up">
          <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Top" />
          <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Bottom" />
          <Setter Property="Style" TargetName="HeaderSite"
              Value="{DynamicResource ExpanderUpHeaderStyle}" />
 
        </Trigger>
        <Trigger Property="ExpandDirection" Value="Right">
          <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Right" />
          <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Left" />
          <Setter Property="Style" TargetName="HeaderSite"
              Value="{DynamicResource ExpanderRightHeaderStyle}" />
 
        </Trigger>
 
        <Trigger Property="ExpandDirection" Value="Left">
          <Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Left" />
          <Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Right" />
          <Setter Property="Style" TargetName="HeaderSite"
              Value="{DynamicResource ExpanderLeftHeaderStyle}" />
        </Trigger>
 
      </ControlTemplate.Triggers>
    </ControlTemplate>
  </Setter.Value>
</Setter>
</Style>

Conclusion

To use this Expander we have to use this little snippet :

<Expander Header="Our text" ExpandDirection="Up" 
               Style="{StaticResource ourAnimatedExpanderStyle}"    >
    <!--- content here ! --->
</Expander>



Everythings seems to works fine and all the behaviors of the original expander are still here !

Shout it kick it on DotNetKicks.com




 

How to scale around a specific point and not the center of the Element

11 October 2009

The problem


The most popular controls which has been brought by the Microsoft SDK is certainly the scatterView. Each item is positioned at a random place with a random orientation.
ExampleOfScatterView

You can then rotate, move or scale them with your fingers. Here we will focus on this last point : the scaling. This is a really nice feature and you may wants to put it in your application (it may also be replace my a mouse wheel or stylus events, etc.).

If an user wants to zoom-in on a specific part of the presented items, he wills do a 'scale manipulation' with it's fingers on the specific part.

Simple, will you think : we just have to change the width and the height of the control based on the scale delta ! But the problem is that, the control will grow but the specific part wanted by the user will no more be under it's fingers. A figure worth a thousand words :
Schema example



My solution

Here we are going to scale a scatterViewItem with the property 'CanMove' set to false. We do it because, the scatterView item does already what we wants and this is done by a translation.


Also we are going to use a Affine2DManipulationProcessor which will gives us the scale value for a manipulation done by multiple fingers. If some are catching stylus events, you could use a ManipulationProcessor from the multiTouch SDK (available here :http://www.microsoft.com/downloads/details.aspx?FamilyID=12100526-ed26-476b-8e20-69662b8546c1&displaylang=en).

The XAML :

<s:ScatterView VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
      <s:ScatterViewItem x:Name="_myObjectToScale" Orientation="0" CanRotate="False" 
            CanScale="False" CanMove="False"
            Center="512.0,384.0"  ShowsActivationEffects="False"
            PreviewContactDown="_myObjectToScale_ContactDown" 
            PreviewContactUp="_myObjectToScale_ContactUp">
         <Image Source="Resources/myself.jpg" />
      </s:ScatterViewItem>


The code :

private Affine2DManipulationProcessor _ourManipProc;
public Affine2DManipulationProcessor OurManipProc { 
   get { return _ourManipProc; } 
   set { _ourManipProc = value; }
 }
 
public SurfaceWindow1()
{
    InitializeComponent();
    DataContext = this;
    _ourManipProc = new Affine2DManipulationProcessor(Affine2DManipulations.Scale, this);
 
    //Catch the event from our manipulation processor
    OurManipProc.Affine2DManipulationDelta += OurManipProc_Affine2DManipulationDelta;
}
 
 
private void _myObjectToScale_ContactDown(object sender, ContactEventArgs e)
{
    //this contact is tracked by ou MP
    OurManipProc.BeginTrack(e.Contact);
}
 
private void _myObjectToScale_ContactUp(object sender, ContactEventArgs e)
{
    //this contact is no more tracked by ou MP
    OurManipProc.EndTrack(e.Contact);
}



Then the important part, the Affine2DManipulationDelta handler which will do what we wants, I will describe it below.

void OurManipProc_Affine2DManipulationDelta(object sender, Affine2DOperationDeltaEventArgs e)
{
    double scaleDelta = e.ScaleDelta;
    if (scaleDelta == 1.0) return;
 
 
    Point manipOrigin = e.ManipulationOrigin;
    Point oldCenter = new Point(_myObjectToScale.Center.X, _myObjectToScale.Center.Y);
 
    double oldHeight = _myObjectToScale.ActualHeight;
    double newHeight = _myObjectToScale.ActualHeight * scaleDelta;
 
    double oldWidth = _myObjectToScale.ActualWidth;
    double newWidth = _myObjectToScale.ActualWidth * scaleDelta;
 
    _myObjectToScale.Height = newHeight;
    _myObjectToScale.Width = newWidth;
 
    double ratioX = Math.Abs(manipOrigin.X - oldCenter.X) / (oldWidth / 2);
    double newCenterXD = ratioX
        * Math.Sign(oldCenter.X - manipOrigin.X) * (newWidth - oldWidth) / 2;
 
    double ratioY = Math.Abs(manipOrigin.Y - oldCenter.Y) / (oldHeight / 2);
    double newCenterYD = ratioY *
        Math.Sign(oldCenter.Y - manipOrigin.Y) * (newHeight - oldHeight) / 2;
 
    if (scaleDelta > 1.0)
        _myObjectToScale.Center += new Vector(newCenterXD, newCenterYD);
    else
        _myObjectToScale.Center += new Vector(newCenterXD, newCenterYD);
}



Explanation


First we need to calculate the new size of our control. This is done by multiplying it's actual size by the scaleDelta gived by our processor.

Then we store some interesting values as the old size, the old center position, etc.

Then we calculate the ration for X and for Y. What is it ? It's ratio of the aimed point (the point on top of which the manipulation is done) and the half of the control size. But why do we need it ? Because we wants the controls to grow on each side of the aimed point, not only the one near the center. If we does not calculate this, one side of the control would stay at the same position during our manipulation. algo explanation

Next we calculate the center delta which is the translation we must operate on our control for the focused point to stay under our fingers (or mouse pointer, or stylus, whatever you wants :D).



We finaly apply all this measure to our control. That's it !

kick it on DotNetKicks.com Shout it