最近在开发项目的首次使用引导界面时,遇到了问题,引导界面类似于安卓手机ViewPager那样的效果,希望通过左右滑动手指来实现切换不同页面,其间伴随动画。

实现思路:

1、界面布局:新建一个UserControl,最外层为Grid,两行一列,内嵌一个Canvas和StackPanel。Canvas中放一个StackPanel用于存放大图列表,外层的StackPanel用于存放RadioButton组,Xaml代码如下:

     <Grid x:Name="grid">
<Grid.RowDefinitions>
<RowDefinition Height="7*"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Canvas x:Name="canvas" Grid.Row="0" Grid.RowSpan="2" Background="#eaede6">
<StackPanel x:Name="imageStack" Orientation="Horizontal"></StackPanel>
</Canvas>
<StackPanel x:Name="buttonStack" Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Center" >
<RadioButton></RadioButton>
</StackPanel>
</Grid>

2、后台代码:定义三个依赖属性,分别为ActiveItemIndex,TotalItemsCount,ItemsListSource,分别表示当前处于活动状态的条目ID,总的条目数量,及条目源,这里的后台代码我的ItemsListSource的数据类型是IEnumerable<BitmapImage>,为了方便看效果我直接把每个页面作为一张图片,到项目集成的时候应该用Page或其他控件,当ActiveItemIndex改变时,执行相应的动画,C#代码如下:

 namespace UserInterface.UserControls
{
/// <summary>
/// IndicatorControl.xaml 的交互逻辑
/// </summary>
public partial class IndicatorControl : UserControl
{
#region 字段及属性
/// <summary>
/// 单张图片宽度
/// </summary>
private Double _width = ;
/// <summary>
/// 触摸起始点
/// </summary>
private TouchPoint _startTouchPoint;
/// <summary>
/// 触摸结束点
/// </summary>
private TouchPoint _endTouchPoint; // Using a DependencyProperty as the backing store for ActiveButtonIndex. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ActiveItemIndexProperty =
DependencyProperty.Register("ActiveItemIndex", typeof(Int32), typeof(IndicatorControl), new UIPropertyMetadata(-, new PropertyChangedCallback((sender, e) =>
{
IndicatorControl control = sender as IndicatorControl;
control.SetActiveItem();
}))); // Using a DependencyProperty as the backing store for TotalButtonCount. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TotalItemsCountProperty =
DependencyProperty.Register("TotalItemsCount", typeof(Int32), typeof(IndicatorControl), new UIPropertyMetadata(-, new PropertyChangedCallback((sender, e) =>
{
IndicatorControl control = sender as IndicatorControl;
control.SetItemsByTotalCount();
}))); // Using a DependencyProperty as the backing store for ImageListProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ItemsListSourceProperty =
DependencyProperty.Register("ItemsListSource", typeof(IEnumerable<BitmapImage>), typeof(IndicatorControl), new UIPropertyMetadata(null, new PropertyChangedCallback((sender, e) =>
{
IndicatorControl control = sender as IndicatorControl;
control.SetItemsList();
})));
/// <summary>
/// 当前处于激活状态的条目索引
/// </summary>
public Int32 ActiveItemIndex
{
get { return (Int32)GetValue(ActiveItemIndexProperty); }
set { SetValue(ActiveItemIndexProperty, value); }
}
/// <summary>
/// 总条目数量
/// </summary>
public Int32 TotalItemsCount
{
get { return (Int32)GetValue(TotalItemsCountProperty); }
set { SetValue(TotalItemsCountProperty, value); }
}
/// <summary>
/// 条目数据源
/// </summary>
public IEnumerable<BitmapImage> ItemsListSource
{
get { return (IEnumerable<BitmapImage>)GetValue(ItemsListSourceProperty); }
set { SetValue(ItemsListSourceProperty, value); }
}
#endregion #region 构造函数
public IndicatorControl()
{
InitializeComponent();
}
#endregion #region 方法
/// <summary>
/// 设置当前活动的Item项
/// </summary>
public void SetActiveItem()
{
for (int i = ; i < this.TotalItemsCount; i++)
{
if (i.Equals(this.ActiveItemIndex))
{
(this.buttonStack.Children[i] as RadioButton).IsChecked = true;
}
}
MoveAnimation(ActiveItemIndex);
}
/// <summary>
/// 设置Item的总数
/// </summary>
public void SetItemsByTotalCount()
{
this.buttonStack.Children.Clear();
for (Int32 i = ; i < this.TotalItemsCount; i++)
{
RadioButton r = new RadioButton();
r.IsEnabled = false;
r.GroupName = "Index";
r.Margin = new Thickness();
this.buttonStack.Children.Add(r);
}
}
/// <summary>
/// 设置Items数据源
/// </summary>
public void SetItemsList()
{
this.imageStack.Children.Clear();
for (Int32 i = ; i < ItemsListSource.Count(); i++)
{
Image image = new Image();
image.Source = ItemsListSource.ElementAt(i);
image.Width = _width;
image.Stretch = Stretch.Fill;
this.imageStack.Children.Add(image);
}
}
#endregion #region 事件
/// <summary>
/// 控件加载时执行一些操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
this.ActiveItemIndex = ;
this.imageStack.Width = _width * TotalItemsCount;
}
/// <summary>
/// 触摸按下
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void imageStack_TouchDown(object sender, TouchEventArgs e)
{
_startTouchPoint = e.GetTouchPoint(App.Current.MainWindow);
e.Handled = true;
}
/// <summary>
/// 长按并移动
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void imageStack_TouchMove(object sender, TouchEventArgs e)
{
TouchPoint tempPoint = e.GetTouchPoint(App.Current.MainWindow);
//得到前后两点X的平移距离
double distance = _startTouchPoint.Position.X - tempPoint.Position.X;
//计算相偏移量
Double offset = this._width * ActiveItemIndex + distance;
//释放属性,使其可以被设置
this.imageStack.BeginAnimation(Canvas.LeftProperty, null); Canvas.SetLeft(this.imageStack, -offset);
e.Handled = true;
}
/// <summary>
/// 触摸释放
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void imageStack_TouchUp(object sender, TouchEventArgs e)
{
_endTouchPoint = e.GetTouchPoint(App.Current.MainWindow);
double x_offset = _startTouchPoint.Position.X - _endTouchPoint.Position.X;
//当X轴偏移量向右大于100且当前Index小于页总数
if (x_offset > && ActiveItemIndex < TotalItemsCount - )
{
++ActiveItemIndex;
}
//当X轴偏移量向左偏移量大于100且当前Index大于1
else if (x_offset < - && ActiveItemIndex > )
{
--ActiveItemIndex;
}
else
{
MoveAnimation(ActiveItemIndex);
}
e.Handled = true;
}
#endregion #region 动画
/// <summary>
/// 动画
/// </summary>
/// <param name="index"></param>
private void MoveAnimation(Int32 index)
{
DoubleAnimation da = new DoubleAnimation();
da.Duration = new Duration(TimeSpan.FromMilliseconds());
da.DecelerationRatio = 0.2;
da.AccelerationRatio = 0.2;
da.From = Canvas.GetLeft(this.imageStack);
da.To = -(index * _width);
this.imageStack.BeginAnimation(Canvas.LeftProperty, da);
}
#endregion
}
}

C# Code

3、数据绑定:有了依赖属性,在客户端的任何一个窗口中调用该控件,都可以进行数据绑定了,以下是调用该控件的窗口XAML:

     <Grid>
<control:IndicatorControl ItemsListSource="{Binding ImageList}" TotalItemsCount="{Binding ImageList.Count}"></control:IndicatorControl>
</Grid>

Xaml Code

如果控件的当前激活条目需要绑定到其他地方,也可以设置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的说法,其链接如为:

http://msdn.microsoft.com/zh-cn/library/system.windows.media.animation.timeline.fillbehavior(VS.95).aspx

但微软官方的解释只是说明了FillBehavior的作用,我希望可以有更自由的方式控制动画,最后找到了这篇文章,原文如下:

在WPF的Animation中,有一个属性为FillBehavior,用于指定时间线在其活动周期结束后但其父时间线仍处于活动周期或填充周期时的行为方式。如果希望动画在活动周期结束时保留其值,则将动画FillBehavior 属性设置为HoldEnd(这也是其默认值)。如果动画的活动周期已结束且FillBehavior 的设置为HoldEnd,则说明动画进入填充周期。如果不希望动画在其活动周期结束时保留其值,则将其FillBehavior属性设置为Stop。因为处于填充周期的动画将继续重写其目标属性值,所以尝试通过其他方法设置目标属性的值似乎不起任何作用。下面演示如何在使用演示图板对属性进行动画处理后再设置该属性。

在某些情况下,在对属性进行动画处理之后,似乎无法更改该属性的值。

【示例】
在下面的示例中, Storyboard 用于对 SolidColorBrush 的颜色进行动画处理。当单击按钮时将触发演示图板。处理 Completed 事件以便在 ColorAnimation 完成时通知程序。

 <Button
Content="Animate and Then Set Example 1">
<Button.Background>
<SolidColorBrush x:Name="Button1BackgroundBrush"
Color="Red" />
</Button.Background>
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetName="Button1BackgroundBrush"
Storyboard.TargetProperty="Color"
From="Red" To="Yellow" Duration="0:0:5"
FillBehavior="HoldEnd"
Completed="setButton1BackgroundBrushColor" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>

Xaml Code

在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>

Xaml

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>

Xaml

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>

Xaml

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;
}

此方法对于非演示图板动画也有效。

在本次引导界面的制作中,我采用了第三种方法,现在把这些知识进行整理,留作日后巩固。

【笔记】WPF实现ViewPager引导界面效果及问题汇总的更多相关文章

  1. ViewPager引导页效果实例源码

    首先大家先找到本地的sdk,然后找到Google提供的API,具体查找方法如下:sdk——>docs——>index.html——>develop——>training——&g ...

  2. 安卓开发笔记——ViewPager组件(仿微信引导界面)

    这2天事情比较多,都没时间更新博客,趁周末,继续继续~ 今天来讲个比较新潮的组件——ViewPager 什么是ViewPager? ViewPager是安卓3.0之后提供的新特性,继承自ViewGro ...

  3. 转-ViewPager组件(仿微信引导界面)

    http://www.cnblogs.com/lichenwei/p/3970053.html 这2天事情比较多,都没时间更新博客,趁周末,继续继续~ 今天来讲个比较新潮的组件——ViewPager ...

  4. 【Android 界面效果21】Android ViewPager使用详解

    这是谷歌官方给我们提供的一个兼容低版本安卓设备的软件包,里面包囊了只有在安卓3.0以上可以使用的api.而viewpager就是其中之一利用它,我们可以做很多事情,从最简单的导航,到页面菜单等等.那如 ...

  5. 【Android UI设计与开发】第03期:引导界面(三)仿微信引导界面以及动画效果

    基于前两篇比较简单的实例做铺垫之后,这一篇我们来实现一个稍微复杂一点的引导界面的效果,当然也只是稍微复杂了一点,对于会的人来说当然还是so easy!正所谓会者不难,难者不会,大概说的就是这个意思了吧 ...

  6. 【Android UI设计与开发】第02期:引导界面(二)使用ViewPager实现欢迎引导页面

    本系列文章都会以一个程序的实例开发为主线来进行讲解,以求达到一个循序渐进的学习效果,这样更能加深大家对于程序为什么要这样写的用意,理论加上实际的应用才能达到事半功倍的效果,不是吗? 最下方有源码的下载 ...

  7. 【Android UI设计与开发】第01期:引导界面(一)ViewPager介绍和使用详解

    做Android开发加起来差不多也有一年多的时间了,总是想写点自己在开发中的心得体会与大家一起交流分享.共同进步,刚开始写也不知该如何下手,仔细想了一下,既然是刚开始写,那就从一个软件给人最直观的感受 ...

  8. 用ViewPager实现一个程序引导界面

    下面使用ViewPager来实现一个程序引导的demo: 一般来说,引导界面是出现第一次运行时出现的,之后不会再出现.所以需要记录是否是第一次使用程序,办法有很多,最容易想到的就是使用SharedPr ...

  9. Android控件-ViewPager(仿微信引导界面)

    什么是ViewPager? ViewPager是安卓3.0之后提供的新特性,继承自ViewGroup,专门用以实现左右滑动切换View的效果. 如果想向下兼容就必须要android-support-v ...

随机推荐

  1. 剑指Offer:面试题5——从尾到头打印链表(java实现)

    问题描述:输入一个链表的头结点,从尾巴到头反过来打印出每个结点的值. 首先定义链表结点 public class ListNode { int val; ListNode next = null; L ...

  2. windows 创建服务提示失败 5 拒绝 访问拒绝

    1.桌面创建文本,输入 sc create .....echo. & pause 保存,重命名为   .bat 2.右键该文件,管理员运行

  3. 站点发布到 IIS 后,System.Data.SqlLite.dll 末找到

    近来在部署一个站点到客户的服务器 IIS 上时,打开后却出现一个错误的页面,系统提示System.Data.SqlLite.dll 末找到,在站点部署到客户的服务器之前时,在本地测试,却没有发现什么异 ...

  4. mysql 二进制安装文件 下载

    在linuex环境下安装mysql,二进制安装包是最合适的方式,下载下来不用编译就可用了. 官方说明文档:http://dev.mysql.com/doc/refman/5.1/en/binary-i ...

  5. 在Web中实现C/S模式的Tab

    在探讨C/S模式的Tab之前,我们先总结一下B/S模式的Tab通常是什么样的.web中常见的tab设计通常是用于分节展示大量信息以提高页面空间的利用率,而且这些信息通常是静态的,或者交互比较简单.通过 ...

  6. javascript代码复用模式(二)

    前面说到,javascript的代码复用模式,可分为类式继承和非类式继承(现代继承).这篇就继续类式继承. 类式继承模式-借用构造函数 使用借用构造函数的方法,可以从子构造函数得到父构造函数传任意数量 ...

  7. Bootstrap <第一篇>

    一.使用Bootstrap要引用的文件 要使用Bootstrap,基本架构要引用如下文件: <link href="bootstrap.min.css" rel=" ...

  8. Linux之磁盘管理

    本章重点提示: 1):理解基础命令,df,fdisk. 2):磁盘分区的理论基础. 1:查看当前系统分区与挂载情况: [root@localhost ~]# df Filesystem 1K-bloc ...

  9. Android 中断线程的处理

    我现在对一个用户注册的功能1.用ProgressDialog将当前页面设成不可操作(保留返回键 退出ProgressDialog)2.用一个线程clientThread执行数据的提交和返回 问题:考虑 ...

  10. mysql数据库去重语句和不同表之间列的复制语句

    1.去重语句:DELETE FROM `v_klg_item` WHERE id NOT IN (SELECT * FROM (SELECT MAX(id) FROM `v_klg_item` GRO ...