【笔记】WPF实现ViewPager引导界面效果及问题汇总
最近在开发项目的首次使用引导界面时,遇到了问题,引导界面类似于安卓手机ViewPager那样的效果,希望通过左右滑动手指来实现切换不同页面,其间伴随动画。
实现思路:
1、界面布局:新建一个UserControl,最外层为Grid,两行一列,内嵌一个Canvas和StackPanel。Canvas中放一个StackPanel用于存放大图列表,外层的StackPanel用于存放RadioButton组,Xaml代码如下:
1 <Grid x:Name="grid"> 2 <Grid.RowDefinitions> 3 <RowDefinition Height="7*"></RowDefinition> 4 <RowDefinition></RowDefinition> 5 </Grid.RowDefinitions> 6 <Canvas x:Name="canvas" Grid.Row="0" Grid.RowSpan="2" Background="#eaede6"> 7 <StackPanel x:Name="imageStack" Orientation="Horizontal"></StackPanel> 8 </Canvas> 9 <StackPanel x:Name="buttonStack" Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Center" > 10 <RadioButton></RadioButton> 11 </StackPanel> 12 </Grid>
2、后台代码:定义三个依赖属性,分别为ActiveItemIndex,TotalItemsCount,ItemsListSource,分别表示当前处于活动状态的条目ID,总的条目数量,及条目源,这里的后台代码我的ItemsListSource的数据类型是IEnumerable<BitmapImage>,为了方便看效果我直接把每个页面作为一张图片,到项目集成的时候应该用Page或其他控件,当ActiveItemIndex改变时,执行相应的动画,C#代码如下:
1 namespace UserInterface.UserControls 2 { 3 /// <summary> 4 /// IndicatorControl.xaml 的交互逻辑 5 /// </summary> 6 public partial class IndicatorControl : UserControl 7 { 8 #region 字段及属性 9 /// <summary> 10 /// 单张图片宽度 11 /// </summary> 12 private Double _width = 1300; 13 /// <summary> 14 /// 触摸起始点 15 /// </summary> 16 private TouchPoint _startTouchPoint; 17 /// <summary> 18 /// 触摸结束点 19 /// </summary> 20 private TouchPoint _endTouchPoint; 21 22 // Using a DependencyProperty as the backing store for ActiveButtonIndex. This enables animation, styling, binding, etc... 23 public static readonly DependencyProperty ActiveItemIndexProperty = 24 DependencyProperty.Register("ActiveItemIndex", typeof(Int32), typeof(IndicatorControl), new UIPropertyMetadata(-1, new PropertyChangedCallback((sender, e) => 25 { 26 IndicatorControl control = sender as IndicatorControl; 27 control.SetActiveItem(); 28 }))); 29 30 // Using a DependencyProperty as the backing store for TotalButtonCount. This enables animation, styling, binding, etc... 31 public static readonly DependencyProperty TotalItemsCountProperty = 32 DependencyProperty.Register("TotalItemsCount", typeof(Int32), typeof(IndicatorControl), new UIPropertyMetadata(-1, new PropertyChangedCallback((sender, e) => 33 { 34 IndicatorControl control = sender as IndicatorControl; 35 control.SetItemsByTotalCount(); 36 }))); 37 38 // Using a DependencyProperty as the backing store for ImageListProperty. This enables animation, styling, binding, etc... 39 public static readonly DependencyProperty ItemsListSourceProperty = 40 DependencyProperty.Register("ItemsListSource", typeof(IEnumerable<BitmapImage>), typeof(IndicatorControl), new UIPropertyMetadata(null, new PropertyChangedCallback((sender, e) => 41 { 42 IndicatorControl control = sender as IndicatorControl; 43 control.SetItemsList(); 44 }))); 45 /// <summary> 46 /// 当前处于激活状态的条目索引 47 /// </summary> 48 public Int32 ActiveItemIndex 49 { 50 get { return (Int32)GetValue(ActiveItemIndexProperty); } 51 set { SetValue(ActiveItemIndexProperty, value); } 52 } 53 /// <summary> 54 /// 总条目数量 55 /// </summary> 56 public Int32 TotalItemsCount 57 { 58 get { return (Int32)GetValue(TotalItemsCountProperty); } 59 set { SetValue(TotalItemsCountProperty, value); } 60 } 61 /// <summary> 62 /// 条目数据源 63 /// </summary> 64 public IEnumerable<BitmapImage> ItemsListSource 65 { 66 get { return (IEnumerable<BitmapImage>)GetValue(ItemsListSourceProperty); } 67 set { SetValue(ItemsListSourceProperty, value); } 68 } 69 #endregion 70 71 #region 构造函数 72 public IndicatorControl() 73 { 74 InitializeComponent(); 75 } 76 #endregion 77 78 #region 方法 79 /// <summary> 80 /// 设置当前活动的Item项 81 /// </summary> 82 public void SetActiveItem() 83 { 84 for (int i = 0; i < this.TotalItemsCount; i++) 85 { 86 if (i.Equals(this.ActiveItemIndex)) 87 { 88 (this.buttonStack.Children[i] as RadioButton).IsChecked = true; 89 } 90 } 91 MoveAnimation(ActiveItemIndex); 92 } 93 /// <summary> 94 /// 设置Item的总数 95 /// </summary> 96 public void SetItemsByTotalCount() 97 { 98 this.buttonStack.Children.Clear(); 99 for (Int32 i = 0; i < this.TotalItemsCount; i++) 100 { 101 RadioButton r = new RadioButton(); 102 r.IsEnabled = false; 103 r.GroupName = "Index"; 104 r.Margin = new Thickness(10); 105 this.buttonStack.Children.Add(r); 106 } 107 } 108 /// <summary> 109 /// 设置Items数据源 110 /// </summary> 111 public void SetItemsList() 112 { 113 this.imageStack.Children.Clear(); 114 for (Int32 i = 0; i < ItemsListSource.Count(); i++) 115 { 116 Image image = new Image(); 117 image.Source = ItemsListSource.ElementAt(i); 118 image.Width = _width; 119 image.Stretch = Stretch.Fill; 120 this.imageStack.Children.Add(image); 121 } 122 } 123 #endregion 124 125 #region 事件 126 /// <summary> 127 /// 控件加载时执行一些操作 128 /// </summary> 129 /// <param name="sender"></param> 130 /// <param name="e"></param> 131 private void UserControl_Loaded(object sender, RoutedEventArgs e) 132 { 133 this.ActiveItemIndex = 0; 134 this.imageStack.Width = _width * TotalItemsCount; 135 } 136 /// <summary> 137 /// 触摸按下 138 /// </summary> 139 /// <param name="sender"></param> 140 /// <param name="e"></param> 141 private void imageStack_TouchDown(object sender, TouchEventArgs e) 142 { 143 _startTouchPoint = e.GetTouchPoint(App.Current.MainWindow); 144 e.Handled = true; 145 } 146 /// <summary> 147 /// 长按并移动 148 /// </summary> 149 /// <param name="sender"></param> 150 /// <param name="e"></param> 151 private void imageStack_TouchMove(object sender, TouchEventArgs e) 152 { 153 TouchPoint tempPoint = e.GetTouchPoint(App.Current.MainWindow); 154 //得到前后两点X的平移距离 155 double distance = _startTouchPoint.Position.X - tempPoint.Position.X; 156 //计算相偏移量 157 Double offset = this._width * ActiveItemIndex + distance; 158 //释放属性,使其可以被设置 159 this.imageStack.BeginAnimation(Canvas.LeftProperty, null); 160 161 Canvas.SetLeft(this.imageStack, -offset); 162 e.Handled = true; 163 } 164 /// <summary> 165 /// 触摸释放 166 /// </summary> 167 /// <param name="sender"></param> 168 /// <param name="e"></param> 169 private void imageStack_TouchUp(object sender, TouchEventArgs e) 170 { 171 _endTouchPoint = e.GetTouchPoint(App.Current.MainWindow); 172 double x_offset = _startTouchPoint.Position.X - _endTouchPoint.Position.X; 173 //当X轴偏移量向右大于100且当前Index小于页总数 174 if (x_offset > 100 && ActiveItemIndex < TotalItemsCount - 1) 175 { 176 ++ActiveItemIndex; 177 } 178 //当X轴偏移量向左偏移量大于100且当前Index大于1 179 else if (x_offset < -100 && ActiveItemIndex > 0) 180 { 181 --ActiveItemIndex; 182 } 183 else 184 { 185 MoveAnimation(ActiveItemIndex); 186 } 187 e.Handled = true; 188 } 189 #endregion 190 191 #region 动画 192 /// <summary> 193 /// 动画 194 /// </summary> 195 /// <param name="index"></param> 196 private void MoveAnimation(Int32 index) 197 { 198 DoubleAnimation da = new DoubleAnimation(); 199 da.Duration = new Duration(TimeSpan.FromMilliseconds(300)); 200 da.DecelerationRatio = 0.2; 201 da.AccelerationRatio = 0.2; 202 da.From = Canvas.GetLeft(this.imageStack); 203 da.To = -(index * _width); 204 this.imageStack.BeginAnimation(Canvas.LeftProperty, da); 205 } 206 #endregion 207 } 208 }
3、数据绑定:有了依赖属性,在客户端的任何一个窗口中调用该控件,都可以进行数据绑定了,以下是调用该控件的窗口XAML:
1 <Grid> 2 <control:IndicatorControl ItemsListSource="{Binding ImageList}" TotalItemsCount="{Binding ImageList.Count}"></control:IndicatorControl> 3 </Grid>
如果控件的当前激活条目需要绑定到其他地方,也可以设置ActiveItemIndex的绑定对象,但这里暂时不需要设置了。
在开发这个引导界面的过程中,遇到了以下几点问题:
1、在Animation对象中的对Canvas.Left属性做动画,必须要给定一个From的初始值,在x:Name="imageStack"的StackPanel中我设置了Canvas.Left="0",这样后台在调用Canvas.GetLeft()方法时不会取到NaN,当时这个问题纠结了很久。
2、在TouchMove事件中,为了在每次移动手指时控件的水平位置进行一定量的偏移,所以每次都需要重新设置imageStack的Canvas.Left的值,但WPF一旦对某个属性执行了动画,如果没有显示指定Animation的FillBehavior属性为Stop,则在动画周期结束之后其属性值会一直保持,从而不能在运行时再次设置该属性的值(或者说可以进行设置但不起作用),对此查阅了MSDN的说法,其链接如为:
但微软官方的解释只是说明了FillBehavior的作用,我希望可以有更自由的方式控制动画,最后找到了这篇文章,原文如下:
在WPF的Animation中,有一个属性为FillBehavior,用于指定时间线在其活动周期结束后但其父时间线仍处于活动周期或填充周期时的行为方式。如果希望动画在活动周期结束时保留其值,则将动画FillBehavior 属性设置为HoldEnd(这也是其默认值)。如果动画的活动周期已结束且FillBehavior 的设置为HoldEnd,则说明动画进入填充周期。如果不希望动画在其活动周期结束时保留其值,则将其FillBehavior属性设置为Stop。因为处于填充周期的动画将继续重写其目标属性值,所以尝试通过其他方法设置目标属性的值似乎不起任何作用。下面演示如何在使用演示图板对属性进行动画处理后再设置该属性。
在某些情况下,在对属性进行动画处理之后,似乎无法更改该属性的值。
【示例】
在下面的示例中, Storyboard 用于对 SolidColorBrush 的颜色进行动画处理。当单击按钮时将触发演示图板。处理 Completed 事件以便在 ColorAnimation 完成时通知程序。
1 <Button 2 Content="Animate and Then Set Example 1"> 3 <Button.Background> 4 <SolidColorBrush x:Name="Button1BackgroundBrush" 5 Color="Red" /> 6 </Button.Background> 7 <Button.Triggers> 8 <EventTrigger RoutedEvent="Button.Click"> 9 <BeginStoryboard> 10 <Storyboard> 11 <ColorAnimation 12 Storyboard.TargetName="Button1BackgroundBrush" 13 Storyboard.TargetProperty="Color" 14 From="Red" To="Yellow" Duration="0:0:5" 15 FillBehavior="HoldEnd" 16 Completed="setButton1BackgroundBrushColor" /> 17 </Storyboard> 18 </BeginStoryboard> 19 </EventTrigger> 20 </Button.Triggers> 21 </Button>
在ColorAnimation完成之后,程序尝试将画笔的颜色改为蓝色。
private void setButton1BackgroundBrushColor(object sender, EventArgs e) { // Does not appear to have any effect: // the brush remains yellow. Button1BackgroundBrush.Color = Colors.Blue; }
上面的代码似乎未起任何作用:画笔仍然保持为黄色,即对画笔进行动画处理的 ColorAnimation 所提供的值。基础属性值(基值)实际上已改为蓝色。但是,因为 ColorAnimation 仍然在重写基值,所以有效值(或者说当前值)仍保持为黄色。如果需要将基值再次变为有效值,则必须禁止动画影响该属性。使用演示图板动画,可以有三种方法实现此目标:
1)将动画的 FillBehavior 属性设置为 Stop
2)移除整个演示图板。
3)从单个属性移除动画。
1、将动画的FillBehavior属性设置为Stop
通过将FillBehavior设置为Stop,即通知动画在到达其活动期末尾后停止影响其目标属性。
<Button Content="Animate and Then Set Example 2"> <Button.Background> <SolidColorBrush x:Name="Button2BackgroundBrush" Color="Red" /> </Button.Background> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard> <ColorAnimation Storyboard.TargetName="Button2BackgroundBrush" Storyboard.TargetProperty="Color" From="Red" To="Yellow" Duration="0:0:5" FillBehavior="Stop" Completed="setButton2BackgroundBrushColor" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button>
private void setButton2BackgroundBrushColor(object sender, EventArgs e) { // This appears to work: // the brush changes to blue. Button2BackgroundBrush.Color = Colors.Blue; }
2、移除整个演示图板
通过使用 RemoveStoryboard 触发器或 Storyboard的Remove实例方法,通知演示图板动画停止影响其目标属性。此方法与设置 FillBehavior 属性的不同之处在于:您可以在任何时候移除演示图板,而 FillBehavior 属性只有在动画到达其活动期末尾时才有效。
<Button Name="Button3" Content="Animate and Then Set Example 3"> <Button.Background> <SolidColorBrush x:Name="Button3BackgroundBrush" Color="Red" /> </Button.Background> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard Name="MyBeginStoryboard"> <Storyboard x:Name="MyStoryboard"> <ColorAnimation Storyboard.TargetName="Button3BackgroundBrush" Storyboard.TargetProperty="Color" From="Red" To="Yellow" Duration="0:0:5" FillBehavior="HoldEnd" Completed="setButton3BackgroundBrushColor" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button>
private void setButton3BackgroundBrushColor(object sender, EventArgs e) { // This appears to work: // the brush changes to blue. MyStoryboard.Remove(Button3); Button3BackgroundBrush.Color = Colors.Blue; }
3、从单个属性移除动画
禁止动画影响属性的另一种方法是使用正在进行动画处理的对象的 BeginAnimation(DependencyProperty, AnimationTimeline) 方法。将正进行动画处理的属性指定为第一个参数,将 null 指定为第二个参数。
<Button Name="Button4" Content="Animate and Then Set Example 4"> <Button.Background> <SolidColorBrush x:Name="Button4BackgroundBrush" Color="Red" /> </Button.Background> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard> <ColorAnimation Storyboard.TargetName="Button4BackgroundBrush" Storyboard.TargetProperty="Color" From="Red" To="Yellow" Duration="0:0:5" FillBehavior="HoldEnd" Completed="setButton4BackgroundBrushColor" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button>
private void setButton4BackgroundBrushColor(object sender, EventArgs e) { // This appears to work: // the brush changes to blue. Button4BackgroundBrush.BeginAnimation(SolidColorBrush.ColorProperty, null); Button4BackgroundBrush.Color = Colors.Blue; }
此方法对于非演示图板动画也有效。
在本次引导界面的制作中,我采用了第三种方法,现在把这些知识进行整理,留作日后巩固。