WPF 应用 - 转盘及动画效果
1. 功能
上次在公众号看到一个转盘效果,觉得挺有意思,便也跟着实现并优化了一下。
具体功能:
- 将 n 个小圆沿着一个大圆的路径排列
- 能根据 n 的数量自适应各个小圆之间的间隔
- 定义一个大圆最多放 x 个小圆,当小圆的数量超出 x 个时,自适应沿着第二个大圆排序,大圆的半径依次递减,每个大圆上的数量也等比递减
- 可有用户 启动/暂定 转动、变色效果
2. 效果
3. 实现
3.1 布局控件
通过继承 Panel 实现一个子元素沿着圆排列的布局控件,从而实现功能点的前 3 点。
public class CirclePathPanel : System.Windows.Controls.Panel
{
public double ChildrenWidth
{
get { return (double)GetValue(ChildrenWidthProperty); }
set { SetValue(ChildrenWidthProperty, value); }
}
public readonly static DependencyProperty ChildrenWidthProperty =
DependencyProperty.Register("ChildrenWidth", typeof(double), typeof(CirclePathPanel), new PropertyMetadata(40.0));
double _marginBetweenEllipse = 80;//大圆之间的距离
List<int> _indexOfEllipse = new List<int>() { 32, 16, 8, 4 };//各个大圆上依次的小圆数量
/// <summary>
/// 获取第 n 个元素所在的环数
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
int GetLoopNum(int index)
{
int loopNum = 1;
foreach (int n in _indexOfEllipse)
{
index -= n;
if (index >= 0)
{
loopNum++;
}
}
return loopNum;
}
protected override Size ArrangeOverride(Size finalSize)
{
if (Children.Count > 0)
{
Point center = new Point(finalSize.Width / 2, finalSize.Height / 2);
double outsideRadius = Math.Min(finalSize.Width, finalSize.Height) / 2.0; //最大圆的半径
int flag = 32; //第一个环上的元素量,从第二环开始依次自动减半,16、8、4
int indexOfCurrent = 0; //current 元素的在整个大集合中的索引值
double radius; //元素所在环的半径
double angleIncrRadians; //元素之间相隔的角度
double angleInRadians = 0.0;//current 元素所在位置的角度
foreach (UIElement child in Children)
{
//确定当前索引的元素位于第几个圆
int loopNum = GetLoopNum(indexOfCurrent);
radius = outsideRadius + 40 - _marginBetweenEllipse * loopNum;
//各个大圆的半径
if (loopNum > 1)
{
angleIncrRadians = 2.0 * Math.PI / Math.Min(Children.Count - (_indexOfEllipse[0] * 2 - _indexOfEllipse[loopNum - 2]), _indexOfEllipse[loopNum - 1]);
}
else
{
angleIncrRadians = 2.0 * Math.PI / Math.Min(Children.Count, _indexOfEllipse[0]); ;
}
// 确定元素的位置
double x = radius * Math.Cos(angleInRadians) + center.X;
double y = radius * Math.Sin(angleInRadians) + center.Y;
Point childPosition = new Point(x, y);
childPosition.X -= ChildrenWidth / 2 ;
childPosition.Y -= ChildrenWidth / 2 ;
child.Arrange(new Rect(childPosition, new Size(ChildrenWidth, ChildrenWidth)));
angleInRadians += angleIncrRadians;
indexOfCurrent++;
if (indexOfCurrent > 59) { break; }
}
}
return finalSize;
}
}
3.2 封装集合控件
设置集合控件的布局面板为 CirclePathPanel。
<UserControl x:Class="WpfApp1.TurnTableUserControl"
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:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" x:Name="selfUserControl">
<UserControl.Resources>
<Style TargetType="ItemsControl">
<Setter Property="Width" Value="{Binding OutSideWidth, ElementName=selfUserControl}"/>
<Setter Property="Height" Value="{Binding OutSideWidth, ElementName=selfUserControl}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="LightBlue"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ItemsControl">
<Border
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True"
CornerRadius="{Binding OutSideRadius, ElementName=selfUserControl}">
<Grid>
<Ellipse Fill="#424242"
Width="{Binding InSideRadius, ElementName=selfUserControl}"
Height="{Binding InSideRadius, ElementName=selfUserControl}"/>
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"></ItemsPresenter>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Width="{Binding ChildrenWidth, ElementName=selfUserControl}"
Height="{Binding ChildrenWidth, ElementName=selfUserControl}"
Fill="{Binding Fill}" Stroke="DeepSkyBlue" StrokeThickness="1"></Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.RenderTransform>
<TransformGroup>
<RotateTransform />
</TransformGroup>
</ItemsControl.RenderTransform>
</ItemsControl>
</UserControl>
public partial class TurnTableUserControl : UserControl
{
/// <summary>
/// 最外圆直径
/// </summary>
public double OutSideWidth
{
get { return (double)GetValue(OutSideWidthProperty); }
set { SetValue(OutSideWidthProperty, value); }
}
public readonly static DependencyProperty OutSideWidthProperty =
DependencyProperty.Register("OutSideWidth", typeof(double), typeof(TurnTableUserControl), new PropertyMetadata(800.0));
/// <summary>
/// 最外圆半径
/// </summary>
public double OutSideRadius
{
get { return (double)GetValue(OutSideRadiusProperty); }
set { SetValue(OutSideRadiusProperty, value); }
}
public readonly static DependencyProperty OutSideRadiusProperty =
DependencyProperty.Register("OutSideRadius", typeof(double), typeof(TurnTableUserControl), new PropertyMetadata(400.0));
/// <summary>
/// 最内圆半径
/// </summary>
public double InSideRadius
{
get { return (double)GetValue(InSideRadiusProperty); }
set { SetValue(InSideRadiusProperty, value); }
}
public readonly static DependencyProperty InSideRadiusProperty =
DependencyProperty.Register("InSideRadius", typeof(double), typeof(TurnTableUserControl), new PropertyMetadata(60.0));
/// <summary>
/// 元素直径
/// </summary>
public double ChildrenWidth
{
get { return (double)GetValue(ChildrenWidthProperty); }
set { SetValue(ChildrenWidthProperty, value); }
}
public readonly static DependencyProperty ChildrenWidthProperty =
DependencyProperty.Register("ChildrenWidth", typeof(double), typeof(TurnTableUserControl), new PropertyMetadata(40.0));
public TurnTableUserControl()
{
InitializeComponent();
}
}
3.3 调用并添加小圆
<Window x:Class="WpfApp1.Window1"
...>
<Grid>
<local:TurnTableUserControl x:Name="turnTableUC"
DataContext="{Binding MyData}"
OutSideRadius="400" OutSideWidth="800"
InSideRadius="60" ChildrenWidth="50"/>
</Grid>
</Window>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
this.DataContext = new EllipseChildViewModel();
}
}
public class NotifyPropertyChanged : System.ComponentModel.INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnProperty(string propertyName)
{
PropertyChangedEventHandler propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class EllipseChild : NotifyPropertyChanged
{
Brush _fill;
public Brush Fill
{
get { return _fill; }
set
{
_fill = value;
OnProperty("Fill");
}
}
}
public class EllipseChildViewModel : NotifyPropertyChanged
{
ObservableCollection<EllipseChild> _myData;
public ObservableCollection<EllipseChild> MyData
{
get { return _myData; }
set
{
_myData = value;
OnProperty("MyData");
}
}
public EllipseChildViewModel()
{
MyData = GetData();
}
public ObservableCollection<EllipseChild> GetData()
{
ObservableCollection<EllipseChild> mydata = new ObservableCollection<EllipseChild>();
foreach (int _ in Enumerable.Range(0, 10))
{
mydata.Add(new EllipseChild() { Fill = new SolidColorBrush(Color.FromRgb(76, 175, 80)) });
mydata.Add(new EllipseChild() { Fill = new SolidColorBrush(Colors.Transparent) });
mydata.Add(new EllipseChild() { Fill = new SolidColorBrush(Color.FromRgb(68, 138, 254)) });
...
}
return mydata;
}
}
至此,就实现了前 3 个功能点:
3.4 实现转动、变色效果
<Window x:Class="WpfApp1.Window1"
...>
<Grid>
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10 0">
<Button Content="转" Click="Button_Click_rorate" Height="30" Width="100" Margin="0 10" Background="Transparent" BorderBrush="DarkSalmon"/>
<Button Content="变" Command="{Binding ChangeCommand}" Height="30" Width="100" Background="Transparent" BorderBrush="DarkSalmon"/>
</StackPanel>
<local:TurnTableUserControl x:Name="turnTableUC"
DataContext="{Binding MyData}"
OutSideRadius="400" OutSideWidth="800"
InSideRadius="60" ChildrenWidth="50"/>
</Grid>
</Window>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
this.DataContext = new EllipseChildViewModel();
}
private void Button_Click_rorate(object sender, RoutedEventArgs e)
{
turnTableUC.IsActionTurn = !turnTableUC.IsActionTurn;
}
}
public class EllipseChildViewModel : NotifyPropertyChanged
{
...
public DelegateCommand ChangeCommand { get; set; }
public EllipseChildViewModel()
{
ChangeCommand = new DelegateCommand() { ExecuteCommand = new Action<object>(ChangeColor) };
MyData = GetData();
}
CancellationTokenSource tokenSource;
CancellationToken token;
public void ChangeColor(object obj)
{
if (tokenSource == null || token.IsCancellationRequested)
{
tokenSource = new CancellationTokenSource();
token = tokenSource.Token;
Change(500);
}
else
{
tokenSource.Cancel();
}
}
/// <summary>
/// 启动/停止变色,当启动变色时,每隔 milliseconds 变一次色
/// </summary>
/// <param name="milliseconds">每次变色的时间间隔,单位毫秒</param>
public void Change(int milliseconds)
{
Task.Factory.StartNew(async () =>
{
try
{
while (true)
{
if (token.IsCancellationRequested) return;
App.Current.Dispatcher.Invoke(() =>
{
Random rd = new Random();
foreach (EllipseChild ec in MyData)
{
ec.Fill = new SolidColorBrush(Color.FromRgb((byte)rd.Next(0, 256), (byte)rd.Next(0, 256), (byte)rd.Next(0, 256)));
}
});
await Task.Delay(milliseconds);
}
}
catch (Exception ex) {}
});
}
}
public partial class TurnTableUserControl : UserControl
{
...
/// <summary>
/// 启动、暂停动画
/// </summary>
public bool IsActionTurn
{
get { return (bool)GetValue(IsActionTurnProperty); }
set { SetValue(IsActionTurnProperty, value); }
}
public readonly static DependencyProperty IsActionTurnProperty =
DependencyProperty.Register("IsActionTurn", typeof(bool), typeof(TurnTableUserControl),
new PropertyMetadata(false, new PropertyChangedCallback(ChangedActionStatus)));
/// <summary>
/// 是否首次启动动画
/// </summary>
static bool _isFirstTimeActionStoryboard = true;
/// <summary>
/// 控件动画
/// </summary>
static Storyboard _storyboard;
public TurnTableUserControl()
{
InitializeComponent();
_storyboard = itemsControl.FindResource("storyboard") as Storyboard;
}
static void ChangedActionStatus(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue == true)
{
if (_isFirstTimeActionStoryboard)
{//首次启动动画
_storyboard.Begin();
_isFirstTimeActionStoryboard = !_isFirstTimeActionStoryboard;
}
else
{//恢复动画
_storyboard.Resume();
}
}
else
{//暂停动画
_storyboard.Pause();
}
}
}