WPF动画自动停止

本文关键字:动画 WPF | 更新日期: 2023-09-27 18:13:25

我有一个包含UserControl的WPF应用程序,它的边框是动画的:

<UserControl x:Class="CarSystem.CustomControls.AlarmItem"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:cs="clr-namespace:CarSystem.CustomControls"
             mc:Ignorable="d"
             DataContext="{Binding Path=Alarm, RelativeSource={RelativeSource Self}}">
    <UserControl.Resources>
        <Style TargetType="{x:Type cs:AlarmItem}">
            <Setter Property="IsFlashing" Value="False" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
                    <Setter Property="IsFlashing" Value="True" />
                </DataTrigger>
                <DataTrigger Binding="{Binding Path=IsPending}" Value="True">
                    <Setter Property="IsFlashing" Value="True" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </UserControl.Resources>
    <Border HorizontalAlignment="Center" 
            Margin="5" 
            Height="100"
            Name="Border" 
            VerticalAlignment="Center" 
            Width="100">
        <Border.Resources>
            <Storyboard x:Key="FlashingStoryboard"
                        AutoReverse="True"
                        RepeatBehavior="Forever">
                <ColorAnimationUsingKeyFrames BeginTime="00:00:00"
                                              Duration="00:00:00.5"
                                              Storyboard.TargetName="Border"
                                              Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)">
                    <DiscreteColorKeyFrame KeyTime="00:00:00.25" Value="Black" />
                </ColorAnimationUsingKeyFrames>
            </Storyboard>
        </Border.Resources>
        <Border.Style>
            <Style TargetType="Border">
                <Setter Property="BorderBrush" Value="Black" />
                <Setter Property="BorderThickness" Value="2" />
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
                        <Setter Property="BorderThickness" Value="4" />
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Path=IsPending}" Value="True">
                        <Setter Property="BorderThickness" Value="4" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Border.Style>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="FlashStates">
                <VisualState x:Name="FlashingOn"
                             Storyboard="{StaticResource ResourceKey=FlashingStoryboard}" />
                <VisualState x:Name="FlashingOff" />
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Image Grid.Row="0" 
                   Name="AlarmImage" 
                   Source="{Binding Path=Image, RelativeSource={RelativeSource AncestorType={x:Type cs:AlarmItem}}}" 
                   Stretch="Fill" />
            <cs:ResponseTimer Expired="Timer_Expired"
                              Grid.Row="1"
                              HideIfExpired="True"
                              IsTabStop="False"
                              MinHeight="10"
                              x:Name="TheTimer"
                              TimeoutPeriod="00:02:30"
                              VerticalAlignment="Bottom" />
        </Grid>
    </Border>
</UserControl>

应用程序从我公司生产的专有设备接收数据。对象被接收并加载到视图模型类实例中。为每个接收到的对象创建该控件的新实例,将对视图模型的引用放入新控件实例的DataContext属性中,并将新控件添加到Window中的ListBox中。

动画应该在对象的status属性为特定值时运行。还有第二个状态值,动画也应该在其中运行,并且在用户没有响应该项目的固定时间间隔后,状态会更改为该值。只有当状态值达到一个只能由用户交互设置的值时,动画才会停止。

当第一个对象被接收&显示,动画工作正常。如果没有接收到其他对象,动画将继续运行&当计时器到期时,边框颜色会按预期变化。

然而,动画在接收到2个或更多对象后自行停止。这既发生在改变导致边框颜色改变的状态的计时器到期之前,也发生在采取任何用户操作之前。请注意,它并不总是在第二个对象时停止,有时需要接收到3或4个对象才能停止动画。

有人知道为什么动画停止了吗?我如何让每一个都运行到最后?有没有更好的方法来获得同样的效果,而不存在这个问题?

WPF动画自动停止

与此最接近的场景是基于视图模型中的值触发动画。我在这里选择了"更好的方法",所以如果这不适合你的解决方案,我很抱歉,我只是基于我所看到的。

现在您提到为来自外部设备的每个警报创建一个新控件?所以我假设这个控件被绑定到一个单独的实例,并以类似于列表框的形式呈现。

如果你还和我在一起,你的控制实例有它自己的视图模型实例,通过它的外观,你的触发从一个IsPending和IsExpired属性,当其中一个为真,使其flash。

我要做的第一件事是简化ViewModel的绑定,添加isalerrequired属性,当你需要警报时设置,可以在现有属性的setter中更新。这里的原因是单个触发器比多绑定简单得多。

然后使用DataTrigger来启动你的故事板。在你的应用程序中添加一个对Microsoft.Expression.Interactions的引用,然后在你的XAML中导入它们:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" 

然后在layoutroot元素前添加一个触发器:

    <i:Interaction.Triggers>
    <ei:DataTrigger Binding="{Binding ViewModel.IsAlertRequired}" Value="True">
        <ei:ControlStoryboardAction ControlStoryboardOption="Play" Storyboard="{StaticResource FlashingAnimation}"/>
    </ei:DataTrigger>

应该基于视图模型数据绑定来启动动画。

假设你得到了那么远,它开始你的动画工作,如果它仍然停止,然后我会看看是否有东西在你的视图模型或外部组件是阻塞UI线程。

多亏了kidshaw提供的信息,我才想出了一个可行的解决方案。从本质上讲,动画应该在两种颜色之间来回切换Border控件的BorderBrush,黑色和依赖于DataContext中对象的属性的颜色。这个逻辑在后面的代码中,因为逻辑依赖于数据上下文对象的两个不同属性,而我无法想出一个能够正常工作的XAML触发器。原来的问题中没有包含这个问题,因为它似乎不相关。

正如kidshaw回答的评论中提到的,问题是当额外的警报进入窗口时,动画失去了BorderBrush应该是什么颜色的跟踪。而不是切换回&Red & &;以黑色为例,它会认为自己必须在黑色&黑色或红色&红色的。所以看起来好像没有动画,而实际上是。

为了解决这个问题,并且由于需要根据数据上下文中对象的属性选择颜色,我最终为另一种颜色在动画中添加了第二个DiscreteColorKeyFrame。我试图将DiscreteColorKeyFrameValue属性绑定到我添加到控件中的DependencyProperty,这将由逻辑背后的代码设置,但这不起作用。动画不停地切换回来& &;黑& &;透明,而VS中的Output窗口一直记录CanFreeze为假的错误。

所以我最终为每种不同的颜色制作一个动画,并为每种颜色添加一个VisualStateVisualStateManager。后面的代码也有变化,但我让它都工作了。

这里是XAML:
<UserControl x:Class="CarSystem.CustomControls.AlarmItem"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:cs="clr-namespace:CarSystem.CustomControls"
             mc:Ignorable="d"
             DataContext="{Binding Path=Alarm, RelativeSource={RelativeSource Self}}">
    <UserControl.Resources>
        <Style TargetType="{x:Type cs:AlarmItem}">
            <Setter Property="IsFlashing" Value="False" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
                    <Setter Property="IsFlashing" Value="True" />
                </DataTrigger>
                <DataTrigger Binding="{Binding Path=IsPending}" Value="True">
                    <Setter Property="IsFlashing" Value="True" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </UserControl.Resources>
    <Border HorizontalAlignment="Center" 
            Margin="5" 
            Height="100"
            Name="Border" 
            VerticalAlignment="Center" 
            Width="100">
        <Border.BorderBrush>
            <SolidColorBrush x:Name="AnimatedBrush" Color="Black" />
        </Border.BorderBrush>
        <Border.Resources>
            <Storyboard x:Key="ExpiredAnimation"
                        AutoReverse="False"
                        RepeatBehavior="Forever">
                <ColorAnimationUsingKeyFrames BeginTime="00:00:00"
                                              Duration="00:00:01"
                                              Storyboard.TargetName="AnimatedBrush"
                                              Storyboard.TargetProperty="Color">
                    <DiscreteColorKeyFrame KeyTime="00:00:00" Value="#FFFFFF78" />
                    <DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
                </ColorAnimationUsingKeyFrames>
            </Storyboard>
            <Storyboard x:Key="HistoricalAnimation"
                        AutoReverse="False"
                        RepeatBehavior="Forever">
                <ColorAnimationUsingKeyFrames BeginTime="00:00:00"
                                              Duration="00:00:01"
                                              Storyboard.TargetName="AnimatedBrush"
                                              Storyboard.TargetProperty="Color">
                    <DiscreteColorKeyFrame KeyTime="00:00:00"   Value="#FFFFFF78" />
                    <DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
                </ColorAnimationUsingKeyFrames>
            </Storyboard>
            <Storyboard x:Key="PendingAnimation"
                        AutoReverse="False"
                        RepeatBehavior="Forever">
                <ColorAnimationUsingKeyFrames BeginTime="00:00:00"
                                              Duration="00:00:01"
                                              Storyboard.TargetName="AnimatedBrush"
                                              Storyboard.TargetProperty="Color">
                    <DiscreteColorKeyFrame KeyTime="00:00:00" Value="Red" />
                    <DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
                </ColorAnimationUsingKeyFrames>
            </Storyboard>
            <Storyboard x:Key="WhiteListAnimation"
                        AutoReverse="False"
                        RepeatBehavior="Forever">
                <ColorAnimationUsingKeyFrames BeginTime="00:00:00"
                                              Duration="00:00:01"
                                              Storyboard.TargetName="AnimatedBrush"
                                              Storyboard.TargetProperty="Color">
                    <DiscreteColorKeyFrame KeyTime="00:00:00" Value="#FF5819" />
                    <DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
                </ColorAnimationUsingKeyFrames>
            </Storyboard>
        </Border.Resources>
        <Border.Style>
            <Style TargetType="Border">
                <Setter Property="BorderThickness" Value="2" />
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
                        <Setter Property="BorderThickness" Value="4" />
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Path=IsPending}" Value="True">
                        <Setter Property="BorderThickness" Value="4" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Border.Style>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="FlashStates">
                <VisualState x:Name="ExpiredState"    Storyboard="{StaticResource ResourceKey=ExpiredAnimation}" />
                <VisualState x:Name="HistoricalState" Storyboard="{StaticResource ResourceKey=HistoricalAnimation}" />
                <VisualState x:Name="PendingState"    Storyboard="{StaticResource ResourceKey=PendingAnimation}" />
                <VisualState x:Name="WhiteListState"  Storyboard="{StaticResource ResourceKey=WhiteListAnimation}" />
                <VisualState x:Name="FlashingOff" />
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Image Grid.Row="0" 
                   Name="AlarmImage" 
                   Source="{Binding Path=Image, RelativeSource={RelativeSource AncestorType={x:Type cs:AlarmItem}}}" 
                   Stretch="Fill" />
            <cs:ResponseTimer Expired="Timer_Expired"
                              Grid.Row="1"
                              HideIfExpired="True"
                              IsTabStop="False"
                              MinHeight="10"
                              x:Name="TheTimer"
                              TimeoutPeriod="00:02:30"
                              VerticalAlignment="Bottom" />
        </Grid>
    </Border>
</UserControl>

后端代码选择&启动正确的动画

private void StartStatusAnimation() {
    if ( condition1 ) {
        // It is.  Display the WhiteListAnimation.
        if ( !VisualStateManager.GoToElementState( Border, "WhiteListState", true ) ) {
            // Log error
        }
    } else if ( condition2 ) {
        if ( !VisualStateManager.GoToElementState( Border, "ExpiredState", true ) ) {
            // Log error
        }
    } else if ( condition3 ) {
        if ( !VisualStateManager.GoToElementState( Border, "HistoricalState", true ) ) {
            // Log error
        }
    } else if ( condition4 ) {
        if ( !VisualStateManager.GoToElementState( Border, "PendingState", true ) ) {
            // Log error
        }
    } else {
        // We don't know what state this is.  Stop flashing now
        if ( !VisualStateManager.GoToElementState( Border, "FlashingOff", true ) ) {
            // Log error
        }
    }
}

请注意,数据上下文对象属性的值可能会由于用户交互或计时器过期而改变,在第一种情况下,这可能会停止闪烁,或者在第二种情况下,导致当前动画停止而另一个动画开始。在第二种情况下,当Style中的触发器将控件的IsFlashing设置为false时,VisualSTateManager仅被设置为FlashingOff状态,在第二种情况下,当计时器到期时再次调用上述方法。