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();
        }
    }
}
posted @ 2021-03-15 22:48  鑫茂  阅读(188)  评论(0编辑  收藏  举报