在WPF中实现“下滑”动画

13

我正在尝试为Expander控件创建自己的模板。当控件被展开时,我希望内容能够缓慢地向下滑动。

内容所需的高度在编译时是未知的。

我认为我们可以将向下滑动定义为一种动画:

<Storyboard x:Key="ExpandContent">

    <DoubleAnimation 
        Storyboard.TargetName="_expanderContent" 
        Storyboard.TargetProperty="Height" 
        From="0.0" 
        To="{Binding ElementName=_expanderContent,Path=DesiredHeight}"
        Duration="0:0:1.0" />
</Storyboard>

但是不幸的是,我们遇到了错误。

无法冻结此故事板时间线树以供跨线程使用。

看起来我们不能在定义动画参数时使用绑定。(在这个问题中也讨论过。)
有人有什么想法可以解决吗?我不想使用LayoutTransform.ScaleY,因为那会创建一个扭曲的图像。
这类似于这个问题,但是这个问题的答案涉及编写代码后台,我认为在控件模板中不可能实现。
我想知道是否可以实现基于XAML的解决方案。
值得一提的是,这是我的控件模板的当前状态。
<ControlTemplate x:Key="ExpanderControlTemplate"  TargetType="{x:Type Expander}">
    <ControlTemplate.Resources>
            <!-- Here are the storyboards which don't work -->
            <Storyboard x:Key="ExpandContent">

                <DoubleAnimation 
                    Storyboard.TargetName="_expanderContent" 
                    Storyboard.TargetProperty="Height" 
                    From="0.0" 
                    To="{Binding ElementName=_expanderContent,Path=DesiredHeight}"
                    Duration="0:0:1.0" />
            </Storyboard>
            <Storyboard x:Key="ContractContent">

                <DoubleAnimation 
                    Storyboard.TargetName="_expanderContent" 
                    Storyboard.TargetProperty="Height" 
                    From="{Binding ElementName=_expanderContent,Path=DesiredHeight}"
                    To="0.0"
                    Duration="0:0:1.0" />

            </Storyboard>
        </ControlTemplate.Resources>
    <Grid Name="MainGrid" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Name="ContentRow" Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Border>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <ContentPresenter ContentSource="Header" />
                <ToggleButton Template="{StaticResource ProductButtonExpand}"
                              Grid.Column="1"
                              IsChecked="{Binding Path=IsExpanded,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}" 
                              />
                <Rectangle Grid.ColumnSpan="2" Fill="#FFDADADA" Height="1" Margin="8,0,8,2" VerticalAlignment="Bottom"/>

            </Grid>
        </Border>

            <ContentPresenter Grid.Row="1" HorizontalAlignment="Stretch" Name="_expanderContent">

            </ContentPresenter>

    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="IsExpanded" Value="True">
            <Setter TargetName="_expanderContent" Property="Height" Value="{Binding ElementName=_expanderContent,Path=DesiredHeight}" />

                <!-- Here is where I would activate the storyboard if they did work -->
                <Trigger.EnterActions>
                <!--<BeginStoryboard Storyboard="{StaticResource ExpandContent}"/>-->
            </Trigger.EnterActions>
                <Trigger.ExitActions>
                    <!--<BeginStoryboard x:Name="ContractContent_BeginStoryboard" Storyboard="{StaticResource ContractContent}"/>-->
                </Trigger.ExitActions>
        </Trigger>
            <Trigger Property="IsExpanded" Value="False">

                <Setter TargetName="_expanderContent" Property="Height" Value="0" />
            </Trigger>
        </ControlTemplate.Triggers>

</ControlTemplate>

1
为什么要将“To”和“From”设置为所需高度?如果您不设置它们,它们将自动完成。 - icebat
3个回答

10

这是一个有些陈旧的问题,但我今天遇到了这个问题,所以我想发布我的解决方案:

我必须对网格行的高度属性进行动画处理(向上和向下滑动),但需要动态绑定,以便该行再次滑动到与之前相同的位置。

在徒劳地与XAML斗争后,我发现这个答案非常有帮助:

http://go4answers.webhost4life.com/Question/found-solution-work-protected-override-190845.aspx

有时,在代码后台完成操作会更简单:

        Storyboard sb = new Storyboard();

        var animation = new GridLengthAnimation
        {
                Duration = new Duration(500.Milliseconds()),
                From = this.myGridRow.Height,
                To = new GridLength(IsGridRowVisible ? GridRowPreviousHeight : 0, GridUnitType.Pixel)
        };

        // Set the target of the animation
        Storyboard.SetTarget(animation, this.myGridRow);
        Storyboard.SetTargetProperty(animation, new PropertyPath("Height"));
        
        // Kick the animation off
        sb.Children.Add(animation);
        sb.Begin();

GridLengthAnimation类可以在这里找到:http://social.msdn.microsoft.com/forums/en-US/wpf/thread/da47a4b8-4d39-4d6e-a570-7dbe51a842e4/

/// <summary>
    /// Animates a grid length value just like the DoubleAnimation animates a double value
    /// </summary>
    public class GridLengthAnimation : AnimationTimeline
    {
        /// <summary>
        /// Returns the type of object to animate
        /// </summary>
        public override Type TargetPropertyType
        {
            get
            {
                return typeof(GridLength);
            }
        }

        /// <summary>
        /// Creates an instance of the animation object
        /// </summary>
        /// <returns>Returns the instance of the GridLengthAnimation</returns>
        protected override System.Windows.Freezable CreateInstanceCore()
        {
            return new GridLengthAnimation();
        }

        /// <summary>
        /// Dependency property for the From property
        /// </summary>
        public static readonly DependencyProperty FromProperty = DependencyProperty.Register("From", typeof(GridLength),
                typeof(GridLengthAnimation));

        /// <summary>
        /// CLR Wrapper for the From depenendency property
        /// </summary>
        public GridLength From
        {
            get
            {
                return (GridLength)GetValue(GridLengthAnimation.FromProperty);
            }
            set
            {
                SetValue(GridLengthAnimation.FromProperty, value);
            }
        }

        /// <summary>
        /// Dependency property for the To property
        /// </summary>
        public static readonly DependencyProperty ToProperty = DependencyProperty.Register("To", typeof(GridLength),
                typeof(GridLengthAnimation));

        /// <summary>
        /// CLR Wrapper for the To property
        /// </summary>
        public GridLength To
        {
            get
            {
                return (GridLength)GetValue(GridLengthAnimation.ToProperty);
            }
            set
            {
                SetValue(GridLengthAnimation.ToProperty, value);
            }
        }

        /// <summary>
        /// Animates the grid let set
        /// </summary>
        /// <param name="defaultOriginValue">The original value to animate</param>
        /// <param name="defaultDestinationValue">The final value</param>
        /// <param name="animationClock">The animation clock (timer)</param>
        /// <returns>Returns the new grid length to set</returns>
        public override object GetCurrentValue(object defaultOriginValue,
            object defaultDestinationValue, AnimationClock animationClock)
        {
            double fromVal = ((GridLength)GetValue(GridLengthAnimation.FromProperty)).Value;
            //check that from was set from the caller
            if (fromVal == 1)
                //set the from as the actual value
                fromVal = ((GridLength)defaultDestinationValue).Value;

            double toVal = ((GridLength)GetValue(GridLengthAnimation.ToProperty)).Value;

            if (fromVal > toVal)
                return new GridLength((1 - animationClock.CurrentProgress.Value) * (fromVal - toVal) + toVal, GridUnitType.Star);
            else
                return new GridLength(animationClock.CurrentProgress.Value * (toVal - fromVal) + fromVal, GridUnitType.Star);
        }
    }

1
非常感谢!我终于让我的参数化动画工作了。事实上,在XAML中似乎不可能做到这一点。 - Dimitri C.
很高兴这有所帮助。是的,在学习WPF中哪些事情在XAML中太麻烦,以及反之需要一段时间。 - Sverrir Sigmundarson
4
做得很好!比起使用XAML技巧来做同样的事情,简单多了。对于所有信奉MVVM“无需代码后台”的狂热者,这只是UI的代码 - 放下吧。 - Mark Bonafe
1
挽救了我的一天,与XAML搏斗。感谢@SverrirSigmundarson。 - glihm

6
如果您可以将InteractionsFluidLayoutBlend 4 SDK)一起使用,那么您很幸运,这对于那些花哨的动画效果非常有用。
首先将内容CP的高度设置为0:
<ContentPresenter Grid.Row="1"
    HorizontalAlignment="Stretch"
    x:Name="_expanderContent"
    Height="0"/>

为了实现动画效果,只需要在表示展开状态的VisualState中将Height属性动画到NaN即可(非离散动画不能使用NaN):
xmlns:is="http://schemas.microsoft.com/expression/2010/interactions"

<Grid x:Name="MainGrid" Background="White">
    <VisualStateManager.CustomVisualStateManager>
        <is:ExtendedVisualStateManager/>
    </VisualStateManager.CustomVisualStateManager>
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="ExpansionStates" is:ExtendedVisualStateManager.UseFluidLayout="True">
            <VisualStateGroup.Transitions>
                <VisualTransition GeneratedDuration="0:0:1"/>
            </VisualStateGroup.Transitions>
            <VisualState x:Name="Expanded">
                <Storyboard>
                    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Height)"
                                                   Storyboard.TargetName="_expanderContent">
                        <DiscreteDoubleKeyFrame KeyTime="0" Value="NaN"/>
                    </DoubleAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
            <VisualState x:Name="Collapsed"/>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <!-- ... --->

这应该就是必要的一切了,流式布局会为您创建过渡效果。


如果你有一个代码后台解决方案,那就没问题了,你甚至可以在字典中使用代码后台:

<!-- TestDictionary.xaml -->
<ResourceDictionary x:Class="Test.TestDictionary"
                    ...>

//TestDictionary.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;

namespace Test
{
    partial class TestDictionary : ResourceDictionary
    {
        //Handlers and such here
    }
}

这个解决方案根本不起作用。仍然会报NaN的错误。 - Chris Bordeman

2

有一个现成的、仅限于XAML的解决方案在CodeProject上:

样式:

    <local:MultiplyConverter x:Key="MultiplyConverter" />
    <Style TargetType="Expander" x:Key="VerticalSlidingEmptyExpander">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Expander}">
                    <ScrollViewer x:Name="ExpanderContentScrollView"
                  HorizontalScrollBarVisibility="Hidden"
                  VerticalScrollBarVisibility="Hidden"
                  HorizontalContentAlignment="Stretch"
                  VerticalContentAlignment="Top"
                  >
                        <ScrollViewer.Tag>
                            <system:Double>0.0</system:Double>
                        </ScrollViewer.Tag>
                        <ScrollViewer.Height>
                            <MultiBinding Converter="{StaticResource MultiplyConverter}">
                                <Binding Path="ActualHeight" ElementName="ExpanderContent"/>
                                <Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
                            </MultiBinding>
                        </ScrollViewer.Height>
                        <ContentPresenter x:Name="ExpanderContent" ContentSource="Content"/>
                    </ScrollViewer>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsExpanded" Value="True">
                            <Trigger.EnterActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation 
                       Storyboard.TargetName="ExpanderContentScrollView"
                       Storyboard.TargetProperty="Tag"
                       To="1"
                       Duration="0:0:0.2"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.EnterActions>
                            <Trigger.ExitActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation 
                         Storyboard.TargetName="ExpanderContentScrollView"
                         Storyboard.TargetProperty="Tag"
                         To="0"
                         Duration="0:0:0.2"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.ExitActions>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="Expander" x:Key="HorizontalSlidingEmptyExpander">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Expander}">
                    <ScrollViewer x:Name="ExpanderContentScrollView"
                  HorizontalScrollBarVisibility="Hidden"
                  VerticalScrollBarVisibility="Hidden"
                  HorizontalContentAlignment="Left"
                  VerticalContentAlignment="Stretch"
                  >
                        <ScrollViewer.Tag>
                            <system:Double>0.0</system:Double>
                        </ScrollViewer.Tag>
                        <ScrollViewer.Width>
                            <MultiBinding Converter="{StaticResource MultiplyConverter}">
                                <Binding Path="ActualWidth" ElementName="ExpanderContent"/>
                                <Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
                            </MultiBinding>
                        </ScrollViewer.Width>
                        <ContentPresenter x:Name="ExpanderContent" ContentSource="Content"/>
                    </ScrollViewer>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsExpanded" Value="True">
                            <Trigger.EnterActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation 
                       Storyboard.TargetName="ExpanderContentScrollView"
                       Storyboard.TargetProperty="Tag"
                       To="1"
                       Duration="0:0:0.2"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.EnterActions>
                            <Trigger.ExitActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation 
                         Storyboard.TargetName="ExpanderContentScrollView"
                         Storyboard.TargetProperty="Tag"
                         To="0"
                         Duration="0:0:0.2"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </Trigger.ExitActions>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

MultiplyConverter:

public class MultiplyConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType,
           object parameter, CultureInfo culture)
    {
        double result = 1.0;
        for (int i = 0; i < values.Length; i++)
        {
            if (values[i] is double)
                result *= (double)values[i];
        }

        return result;
    }

    public object[] ConvertBack(object value, Type[] targetTypes,
           object parameter, CultureInfo culture)
    {
        throw new Exception("Not implemented");
    }
}

我复制了这个样式,得到了水平和垂直版本,并省略了ToggleButtons,但你可以轻松从原始帖子中获取它们。


使用此解决方案会出现无切换按钮和无标题的问题。 - igorGIS

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接