概述
UWP Community Toolkit 中有一个为图片或磁贴提供轮播效果的控件 - RotatorTile,本篇我们结合代码详细讲解 RotatorTile 的实现。
RotatorTile 提供了一种类似 Windows 10 磁贴的轮播方式,可以轮流播放开发者设置的内容序列,支持设置轮播方向,包括上下左右四个方向;接下来看看官方示例的截图:
Doc: https://docs.microsoft.com/zh-cn/windows/uwpcommunitytoolkit/controls/rotatortile
Namespace: Microsoft.Toolkit.Uwp.UI.Controls; Nuget: Microsoft.Toolkit.Uwp.UI.Controls;
开发过程
代码分析
RotatorTile 控件包括 RotatorTile.cs 和 RotatorTile.xaml,分别是控件的定义处理类和样式文件,分别来看一下:
1. RotatorTile.xaml
RotatorTile.xaml 是 RotatorTile 控件的样式文件,我们看 Template 部分,轮播效果的实现主要是靠 StackPanel 中排列的两个 Content,分别代表 current 和 next 内容,根据设置的轮播方向,设置 StackPanel 的排列方向;轮播时,使用 TranslateTransform 来实现轮播的元素切换动画;
<Style TargetType="controls:RotatorTile">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:RotatorTile">
<Grid Background="{TemplateBinding Background}">
<Canvas x:Name="Scroller"
DataContext="{x:Null}">
<StackPanel x:Name="Stack">
<StackPanel.RenderTransform>
<TranslateTransform x:Name="Translate" Y="0" />
</StackPanel.RenderTransform>
<ContentPresenter x:Name="Current"
Content="{Binding}"
ContentTemplate="{TemplateBinding ItemTemplate}"
DataContext="{x:Null}" />
<ContentPresenter x:Name="Next"
Content="{Binding}"
ContentTemplate="{TemplateBinding ItemTemplate}"
DataContext="{x:Null}" />
</StackPanel>
</Canvas>
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="RotationDelay" Value="0:0:5" />
<Setter Property="ExtraRandomDuration" Value="0:0:5" />
</Style>
2. RotatorTile.cs
RotatorTile 控件的定义和主要处理类,来看看类的结构:
首先看一下 OnApplyTemplate() 方法,他会获取控件的模板,根据当前轮播方向处理 StackPanel 容器,初始化并开始轮播动画;这也是 RotatorTile 控件的主要流程:使用 Timer,根据设置的间隔时间和轮播的方向,在 Tick 事件中不断按照某个方向去做平移动画,动画中不断更新当前显示元素为下一个元素,并不断相应中途的显示元素集合变化事件;
同时控件会响应 RotatorTile_SizeChanged 事件,根据新的尺寸去修改显示元素和容器的尺寸;响应 RotatorTile_Loaded 和 RotatorTile_Unloaded,处理 Timer 的开始和结束处理;
RotatorTile.cs 继承自 Control 类,先看一下它定义了哪些依赖属性:
首先来看 OnItemsSourcePropertyChanged 事件,它的主要逻辑在方法 Incc_CollectionChanged(s, e) 中:
private void Incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
{
if (e.OldItems?.Count > 0)
{
int endIndex = e.OldStartingIndex + e.OldItems.Count;
if (_currentIndex >= e.NewStartingIndex && _currentIndex < endIndex)
{
// Current item was removed. Replace with the next one
UpdateNextItem();
}
else if (_currentIndex > endIndex)
{
// Items were removed before the current item. Just update the changed index
_currentIndex -= (endIndex - e.NewStartingIndex) - 1;
}
else if (e.NewStartingIndex == _currentIndex + 1)
{
// Upcoming item was changed, so update the datacontext
_nextElement.DataContext = GetNext();
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Add)
{
int endIndex = e.NewStartingIndex + e.NewItems.Count;
if (e.NewItems?.Count > 0)
{
if (_currentIndex < 0)
{
// First item loaded. Start the rotator
Start();
}
else if (_currentIndex >= e.NewStartingIndex)
{
// Items were inserted before the current item. Update the index
_currentIndex += e.NewItems.Count;
}
else if (_currentIndex + 1 == e.NewStartingIndex)
{
// Upcoming item was changed, so update the datacontext
_nextElement.DataContext = GetNext();
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Replace)
{
int endIndex = e.OldStartingIndex + e.OldItems.Count;
if (_currentIndex >= e.OldStartingIndex && _currentIndex < endIndex + 1)
{
// Current item was removed. Replace with the next one
UpdateNextItem();
}
}
else if (e.Action == NotifyCollectionChangedAction.Move)
{
int endIndex = e.OldStartingIndex + e.OldItems.Count;
if (_currentIndex >= e.OldStartingIndex && _currentIndex < endIndex)
{
// The current item was moved. Get its new location
_currentIndex = GetIndexOf(CurrentItem);
}
}
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
// Significant change or clear. Restart.
Start();
}
}
接着来看 OnCurrentItemPropertyChanged(d, e) 方法的处理,主要处理逻辑在 RotateToNextItem() 中:
private void RotateToNextItem()
{
// Check if there's more than one item. if not, don't start animation
bool hasTwoOrMoreItems = false;
...
if (!hasTwoOrMoreItems) { return;}
var sb = new Storyboard();
if (_translate != null)
{
var anim = new DoubleAnimation
{
Duration = new Duration(TimeSpan.FromMilliseconds(500)),
From = 0
};
if (Direction == RotateDirection.Up)
{
anim.To = -ActualHeight;
}
else if (Direction == RotateDirection.Down) {...}
else if (Direction == RotateDirection.Right) {...}
else if (Direction == RotateDirection.Left) {...}
anim.FillBehavior = FillBehavior.HoldEnd;
anim.EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut };
Storyboard.SetTarget(anim, _translate);
if (Direction == RotateDirection.Up || Direction == RotateDirection.Down)
{
Storyboard.SetTargetProperty(anim, "Y");
}
else
{
Storyboard.SetTargetProperty(anim, "X");
}
sb.Children.Add(anim);
}
sb.Completed += async (a, b) =>
{
if (_currentElement != null)
{
_currentElement.DataContext = _nextElement.DataContext;
}
// make sure DataContext on _currentElement has had a chance to update the binding
// avoids flicker on rotation
await System.Threading.Tasks.Task.Delay(50);
// Reset back and swap images, getting the next image ready
sb.Stop();
if (_translate != null)
{
UpdateTranslateXY();
}
if (_nextElement != null)
{
_nextElement.DataContext = GetNext(); // Preload the next tile
}
};
sb.Begin();
}
我们看到有两个方法中都调用了 UpdateTranslateXY() 方法,来更新平移时的 X 或 Y:
对于 Left 和 Up,只需要充值 X 或 Y 为 0;对于 Right 和 Down,需要把对应的 X 或 Y 设置为 -1 × 对应的高度或宽度,让动画从负一倍尺寸平移到 0;
private void UpdateTranslateXY()
{
if (Direction == RotateDirection.Left || Direction == RotateDirection.Up)
{
_translate.X = _translate.Y = 0;
}
else if (Direction == RotateDirection.Right)
{
_translate.X = -1 * ActualWidth;
}
else if (Direction == RotateDirection.Down)
{
_translate.Y = -1 * ActualHeight;
}
}
调用示例
我们定义了一个 RotatorTile,动画间隔 1s,方向向上,来看一下 gif 图显示的运行结果:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<controls:RotatorTile x:Name="Tile1"
Height="200"
Background="LightGray"
RotationDelay="0:0:1"
ExtraRandomDuration="0:0:1"
Direction="Up"
ItemTemplate="{StaticResource PhotoTemplate}" />
</Grid>
总结
到这里我们就把 UWP Community Toolkit 中的 RotatorTile 控件的源代码实现过程和简单的调用示例讲解完成了,希望能对大家更好的理解和使用这个控件有所帮助。欢迎大家多多交流,谢谢!
最后,再跟大家安利一下 UWPCommunityToolkit 的官方微博:https://weibo.com/u/6506046490, 大家可以通过微博关注最新动态。
衷心感谢 UWPCommunityToolkit 的作者们杰出的工作,Thank you so much, UWPCommunityToolkit authors!!!