QQ群大家都用过,先看下目前QQ的群文件列表容器的效果:
细心点大家就会发现,这玩意收缩和展开是带动画的,并不是很僵硬地直接收缩或者直接展开,毫无疑问,如果用WPF实现这样的效果,这里的最佳控件是Expander,WPF的Expander控件自带Collapse和Expand功能,但是用过Expander的人都知道,这玩意的Collapse或者Expand是瞬间完成的,找遍Expander所有的属性,没有发现能设置为动画伸缩的,于是想到它里面一探究竟。用Blend编辑样式如下图:
通过Blend可以清楚地看到Expand的时候发生了什么,当它Expand的时候,把ExpandSite的Visibility改成了Visible,ExpandSite是什么呢,它就是Expander的ContentPresenter,即内容载体,难怪呢,Visibility是瞬间的,没有什么动画可言。
看到这也许你会很失望,因为没法对Visibility这个属性做动画。是不是真的没有办法了呢,当然不是,没有条件,可以创造条件。想想能做动画的是什么,最直接的,高度,或者——Transform,那么,即使我能对单个Expander做动画伸缩,怎么保证其他的Expander能动画上下位移呢?现在有3个问题需要解决:
1.如何去掉Expander本身的Expander和Collapse效果,因为自带的效果是单纯的设置Visibility,这个属性是没法做动画的
2.如何对单个Expander做动画伸缩,也就是使用它的哪个属性做动画
3.对某个Expander伸缩的时候如何让其他的Expander自动位移
这三个问题解决了,那么这种效果就实现了。
问题1的解决思路
似乎这三个问题都涉及到了控件内部的一些逻辑,最直接的想法是写个类继承Expander,然后去override相关函数,我试过,没什么作用,即使不用调base的函数,该出现的还是会出现。如果我有源码,或者我会在Measure里做些什么,也许可以改动一些逻辑,可惜我不会,我只会改改样式什么的。于是我想到了继承和样式相结合——事实上这种办法很大程度上简化了控件的开发(相对于游离在VisualTree和LogicTree之间的程序员来说),因为Style能快速增减控件,但是实现的逻辑有限,而继承控件能轻易实现逻辑,但对于一些人来说,继承后再加个控件,在哪里加,位置、背景、Margin、BorderThickness如何这些都太TM难了,调试难度也不低,所以继承和样式结合,各取所优,利益最大化。这样的话,我可以写个样式,把IsExpanded触发的逻辑去掉,然后写个类继承Expander,在构造函数里找到这个样式并设置为自己的Style,那么第一个问题就解决了。
问题2的解决思路
我想选高度来做动画吧,收缩好办,变为0就可以了,展开呢,高度该变为多少呢,Expander的高度是Auto,也就是根据内容来的,内容有多高展开就有多高,DoubleAnimation的To只是一个Double,没法绑定,这条路似乎有点难度。那么我用变形效果做动画呢,收缩的时候Y轴缩放为0,展开的时候Y轴缩放为1,这样我根本不用关心Expander的高度具体是多少,这样一来,问题2也得到了解决。
问题3的解决思路
单个的Expander行为怎么去影响别人的行为呢,这似乎有点为难。其实也不难,选好容器就可以,你把它们放在Grid里肯定是不行的,Gird只提供行列和Margin,如果要我关联Expander的伸缩事件然后挨个去设行列或者margin,那是会死人的。很显然,StackPanel最适合不过了,StackPanel提供Children了自动占用空间的特性,当一个child的高度变小,其他控件是会跟着移动的。但是还有个问题,我是选Expander的变形来做动画,印象中控件的变形效果其实不是发生了真正的布局改变,所以还有一点要注意,就是使用LayoutTransform,这个变形效果是会影响到布局的,而这正是我想要的结果,这样一来,问题3也解决了。以下是效果图:
以下是部分代码:
1 <Setter Property="Template"> 2 <Setter.Value> 3 <ControlTemplate TargetType="{x:Type Expander}"> 4 <ControlTemplate.Resources> 5 <Storyboard x:Key="STHide"> 6 <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)" 7 Storyboard.TargetName="ExpandSite"> 8 <EasingDoubleKeyFrame KeyTime="0:0:0.2" 9 Value="0" /> 10 </DoubleAnimationUsingKeyFrames> 11 <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" 12 Storyboard.TargetName="ExpandSite"> 13 <EasingDoubleKeyFrame KeyTime="0:0:0.2" 14 Value="1" /> 15 </DoubleAnimationUsingKeyFrames> 16 </Storyboard> 17 <Storyboard x:Key="STShow"> 18 <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)" 19 Storyboard.TargetName="ExpandSite"> 20 <EasingDoubleKeyFrame KeyTime="0" 21 Value="0" /> 22 <EasingDoubleKeyFrame KeyTime="0:0:0.2" 23 Value="1" /> 24 </DoubleAnimationUsingKeyFrames> 25 <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" 26 Storyboard.TargetName="ExpandSite"> 27 <EasingDoubleKeyFrame KeyTime="0" 28 Value="0" /> 29 <EasingDoubleKeyFrame KeyTime="0:0:0.2" 30 Value="1" /> 31 </DoubleAnimationUsingKeyFrames> 32 </Storyboard> 33 </ControlTemplate.Resources> 34 <Border BorderBrush="{TemplateBinding BorderBrush}" 35 BorderThickness="{TemplateBinding BorderThickness}" 36 Background="{TemplateBinding Background}" 37 CornerRadius="3" 38 SnapsToDevicePixels="true"> 39 <DockPanel> 40 <ToggleButton x:Name="HeaderSite" 41 ContentTemplate="{TemplateBinding HeaderTemplate}" 42 ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}" 43 Content="{TemplateBinding Header}" 44 DockPanel.Dock="Top" 45 Foreground="{TemplateBinding Foreground}" 46 FontWeight="{TemplateBinding FontWeight}" 47 FocusVisualStyle="{StaticResource ExpanderHeaderFocusVisual}" 48 FontStyle="{TemplateBinding FontStyle}" 49 FontStretch="{TemplateBinding FontStretch}" 50 FontSize="{TemplateBinding FontSize}" 51 FontFamily="{TemplateBinding FontFamily}" 52 HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" 53 IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" 54 Margin="1" 55 MinWidth="0" 56 MinHeight="0" 57 Padding="{TemplateBinding Padding}" 58 Style="{StaticResource ExpanderDownHeaderStyle}" 59 VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> 60 <ContentPresenter x:Name="ExpandSite" 61 DockPanel.Dock="Bottom" 62 Focusable="false" 63 HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 64 Margin="{TemplateBinding Padding}" 65 Visibility="Visible" 66 VerticalAlignment="{TemplateBinding VerticalContentAlignment}"> 67 <ContentPresenter.LayoutTransform> 68 <TransformGroup> 69 <ScaleTransform /> 70 <SkewTransform /> 71 <RotateTransform /> 72 <TranslateTransform /> 73 </TransformGroup> 74 </ContentPresenter.LayoutTransform> 75 </ContentPresenter> 76 </DockPanel> 77 </Border> 78 <ControlTemplate.Triggers> 79 <EventTrigger RoutedEvent="FrameworkElement.Loaded"> 80 <BeginStoryboard Storyboard="{StaticResource STHide}" /> 81 </EventTrigger> 82 <EventTrigger RoutedEvent="Expander.Expanded"> 83 <BeginStoryboard x:Name="STShow_BeginStoryboard" 84 Storyboard="{StaticResource STShow}" /> 85 </EventTrigger> 86 <EventTrigger RoutedEvent="Expander.Collapsed"> 87 <BeginStoryboard Storyboard="{StaticResource STHide}" /> 88 </EventTrigger> 89 <Trigger Property="ExpandDirection" 90 Value="Right"> 91 <Setter Property="DockPanel.Dock" 92 TargetName="ExpandSite" 93 Value="Right" /> 94 <Setter Property="DockPanel.Dock" 95 TargetName="HeaderSite" 96 Value="Left" /> 97 <Setter Property="Style" 98 TargetName="HeaderSite" 99 Value="{StaticResource ExpanderRightHeaderStyle}" /> 100 </Trigger> 101 <Trigger Property="ExpandDirection" 102 Value="Up"> 103 <Setter Property="DockPanel.Dock" 104 TargetName="ExpandSite" 105 Value="Top" /> 106 <Setter Property="DockPanel.Dock" 107 TargetName="HeaderSite" 108 Value="Bottom" /> 109 <Setter Property="Style" 110 TargetName="HeaderSite" 111 Value="{StaticResource ExpanderUpHeaderStyle}" /> 112 </Trigger> 113 <Trigger Property="ExpandDirection" 114 Value="Left"> 115 <Setter Property="DockPanel.Dock" 116 TargetName="ExpandSite" 117 Value="Left" /> 118 <Setter Property="DockPanel.Dock" 119 TargetName="HeaderSite" 120 Value="Right" /> 121 <Setter Property="Style" 122 TargetName="HeaderSite" 123 Value="{StaticResource ExpanderLeftHeaderStyle}" /> 124 </Trigger> 125 <Trigger Property="IsEnabled" 126 Value="false"> 127 <Setter Property="Foreground" 128 Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" /> 129 </Trigger> 130 </ControlTemplate.Triggers> 131 </ControlTemplate> 132 </Setter.Value> 133 </Setter>
这个样式主要是用过LayoutTransform下的ScaleTransForm做了伸缩动画,然后关联Expanded和Collapsed事件执行动画。
1 <EventTrigger RoutedEvent="Expander.Expanded"> 2 <BeginStoryboard x:Name="STShow_BeginStoryboard" 3 Storyboard="{StaticResource STShow}" /> 4 </EventTrigger> 5 <EventTrigger RoutedEvent="Expander.Collapsed"> 6 <BeginStoryboard Storyboard="{StaticResource STHide}" /> 7 </EventTrigger>
到这里似乎不需要再写个继承类来实现什么逻辑了,的确,这样的功能一个样式就搞定了,不过这里面有个缺陷,至于是什么缺陷,有什么办法弥补,将在下一篇阐述,敬请期待。
本篇源码已在QQ群里共享,如有需要可以下载来研究。