简易的DragDropCarousel 拖拽轮播控件
上一篇文章有写到 自动轮播的控件 简易的AutoPlayCarousel 轮播控件 - 黄高林 - 博客园 (cnblogs.com)
本章是基于自动轮播的一种衍生,通过拖拽鼠标进切换
直接上代码
DragDropCarousel核心代码
[ContentProperty(nameof(ItemSources))] [TemplatePart(Name = "PART_StackPanel", Type = typeof(StackPanel))] public class DragDropCarousel : Control { #region 字段 private StackPanel _stkMain; /// <summary> /// 标记是否点击 /// </summary> private bool _isMouseDown; /// <summary> /// 标记是否动画中,动画过程中不允许操作 /// </summary> private bool _isAnimating; /// <summary> /// 最后一次点击的位置 /// </summary> private Point _lastDownPoint; /// <summary> /// 移动中 /// </summary> private bool _isMoving; #endregion #region 构造 static DragDropCarousel() { DefaultStyleKeyProperty.OverrideMetadata(typeof(DragDropCarousel), new FrameworkPropertyMetadata(typeof(DragDropCarousel))); } public DragDropCarousel() { Loaded += Carousel_Loaded; SizeChanged += Carousel_SizeChanged; } #endregion #region 属性 /// <summary> /// 子控件集合 /// </summary> public ObservableCollection<FrameworkElement> ItemSources { get => (ObservableCollection<FrameworkElement>)GetValue(ItemSourcesProperty); private set => SetValue(ItemSourcesProperty, value); } public static readonly DependencyProperty ItemSourcesProperty = DependencyProperty.Register("ItemSources", typeof(ObservableCollection<FrameworkElement>), typeof(DragDropCarousel), new PropertyMetadata(new ObservableCollection<FrameworkElement>())); /// <summary> /// 方向 /// </summary> public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(DragDropCarousel), new PropertyMetadata(Orientation.Horizontal)); /// <summary> /// 下标 /// </summary> public int Index { get => (int)GetValue(IndexProperty); set => SetValue(IndexProperty, value); } public static readonly DependencyProperty IndexProperty = DependencyProperty.Register("Index", typeof(int), typeof(DragDropCarousel), new PropertyMetadata(0)); /// <summary> /// 抬起手时,滑动切页动画持续时间 /// </summary> public TimeSpan AnimateDuration { get => (TimeSpan)GetValue(AnimateDurationProperty); set => SetValue(AnimateDurationProperty, value); } public static readonly DependencyProperty AnimateDurationProperty = DependencyProperty.Register("AnimateDuration", typeof(TimeSpan), typeof(DragDropCarousel), new PropertyMetadata(TimeSpan.FromSeconds(0.5))); /// <summary> /// 移动阈值 /// </summary> public double MoveThreshold { get => (double)GetValue(MoveThresholdProperty); set => SetValue(MoveThresholdProperty, value); } // Using a DependencyProperty as the backing store for MoveThreshold. This enables animation, styling, binding, etc... public static readonly DependencyProperty MoveThresholdProperty = DependencyProperty.Register("MoveThreshold", typeof(double), typeof(DragDropCarousel), new PropertyMetadata(10d)); #endregion #region 对外方法 public override void OnApplyTemplate() { base.OnApplyTemplate(); _stkMain = GetTemplateChild("PART_StackPanel") as StackPanel; } #endregion #region Event Handler private void Carousel_SizeChanged(object sender, SizeChangedEventArgs e) { foreach (FrameworkElement children in ItemSources) { children.Width = ActualWidth; children.Height = ActualHeight; UnRegisterMouseEvent(children); RegisterMouseEvent(children); } } private void UnRegisterMouseEvent(FrameworkElement children) { children.MouseDown -= Children_MouseDown; children.MouseMove -= Children_MouseMove; children.MouseUp -= Children_MouseUp; } private void RegisterMouseEvent(FrameworkElement children) { children.MouseDown += Children_MouseDown; children.MouseMove += Children_MouseMove; children.MouseUp += Children_MouseUp; } private void Children_MouseUp(object sender, MouseButtonEventArgs e) { if (!_isMouseDown || _isAnimating||!_isMoving) {
_isMoving = false;
_isMouseDown = false;
return;
}
_isMoving=false; _isMouseDown = false; var targetIndex = GetTargetIndex(e, _lastDownPoint); Index = targetIndex; var newThickness = new Thickness(-1 * ActualWidth * targetIndex, 0, 0, 0); var oldThickness = _stkMain.Margin; var thicknessAnimation = new ThicknessAnimation() { From = oldThickness, To = newThickness, Duration = AnimateDuration, EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut }, FillBehavior = FillBehavior.Stop, }; thicknessAnimation.Completed += (s, args) => { _isAnimating = false; _stkMain.Margin = newThickness; }; _isAnimating = true; _stkMain.BeginAnimation(MarginProperty, thicknessAnimation); } private int GetTargetIndex(MouseButtonEventArgs e, Point lastDownPoint) { var position = e.GetPosition(this); var vector = position - lastDownPoint; int targetIndex; if (vector.X < 0) { targetIndex = Index + 1 > (ItemSources.Count - 1) ? Index : Index + 1; } else { targetIndex = Index - 1 >= 0 ? Index - 1 : 0; } return targetIndex; } private void Children_MouseMove(object sender, MouseEventArgs e) { var position = e.GetPosition(this); if (!IsCanMove(_lastDownPoint, position)) { _stkMain.Margin = new Thickness((-1 * ActualWidth * Index), 0, 0, 0); return; } var moveLength = _lastDownPoint.X - position.X; _stkMain.Margin = new Thickness((-1 * ActualWidth * Index - moveLength), 0, 0, 0); } private bool IsCanMove(Point lastPoint, Point currentPoint) { if (!_isMouseDown) { return false; } //以下控制不允许超出边界,想要回弹效果,可以去掉这部分代码 var vector = currentPoint - lastPoint; if (!_isMoving && Math.Abs(vector.X) <= MoveThreshold) { return false; } _isMoving = true; if (vector.X < 0) { return Index != ItemSources.Count - 1; } return Index != 0; } private void Children_MouseDown(object sender, MouseButtonEventArgs e) { if (!(MoveElementAttach.GetAllowMove(e.Source as UIElement) ?? false) || _isAnimating) { _isMouseDown = false; return; } _isMouseDown = true; _lastDownPoint = e.GetPosition(this); } private void Carousel_Loaded(object sender, RoutedEventArgs e) { if (ItemSources == null) return; Loaded -= Carousel_Loaded; foreach (FrameworkElement child in ItemSources) { child.Width = ActualWidth; child.Height = ActualHeight; } } #endregion }
一些辅助的代码
public class IndexChangedEventArgs : RoutedEventArgs { public IndexChangedEventArgs(int currentIndex, RoutedEvent routedEvent) : base(routedEvent) { CurrentIndex = currentIndex; } public int CurrentIndex { get; set; } } public delegate void IndexChangedEventHandler(object sender, IndexChangedEventArgs e);
/// <summary> /// 移动附加属性 /// </summary> public class MoveElementAttach : DependencyObject { /// <summary>允许操作</summary> public static readonly DependencyProperty AllowMoveProperty = DependencyProperty.RegisterAttached("AllowMove", typeof(bool), typeof(MoveElementAttach), new PropertyMetadata((PropertyChangedCallback)null)); public static bool? GetAllowMove(DependencyObject obj) => (bool?)obj?.GetValue(MoveElementAttach.AllowMoveProperty); public static void SetAllowMove(DependencyObject obj, bool? value) => obj.SetValue(MoveElementAttach.AllowMoveProperty, (object)value); }
默认样式
<sys:Double x:Key="DefaultFontSize">14</sys:Double> <sys:Boolean x:Key="DefaultSnapsToDevicePixels">false</sys:Boolean> <Style TargetType="{x:Type local:DragDropCarousel}"> <Setter Property="SnapsToDevicePixels" Value="{StaticResource DefaultSnapsToDevicePixels}" /> <Setter Property="FontSize" Value="{StaticResource DefaultFontSize}" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:DragDropCarousel}"> <StackPanel x:Name="PART_StackPanel" Orientation="Horizontal"> <ItemsControl x:Name="PART_ItemsControl" ItemsSource="{TemplateBinding ItemSources}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Width="{TemplateBinding Width}" Height="{TemplateBinding Height}" Orientation="{Binding Orientation,RelativeSource={RelativeSource AncestorType=local:DragDropCarousel}}"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> </StackPanel> </ControlTemplate> </Setter.Value> </Setter> </Style>
界面的使用
<customControl:DragDropCarousel x:Name="Carousel" Height="1080"> <Grid Background="Red" customControl:MoveElementAttach.AllowMove="True" > <Grid HorizontalAlignment="Center" VerticalAlignment="Center" Height="500" Width="500" Background="Black"> <TextBlock Text="1" FontSize="20" Foreground="Wheat" HorizontalAlignment="Center" VerticalAlignment="Center"></TextBlock> </Grid> </Grid> <Grid Background="Green" customControl:MoveElementAttach.AllowMove="True" > <Grid HorizontalAlignment="Center" VerticalAlignment="Center" Height="500" Width="500" Background="Black"> <TextBlock Text="2" FontSize="20" Foreground="Wheat" HorizontalAlignment="Center" VerticalAlignment="Center"></TextBlock> </Grid> </Grid> <Grid Background="Yellow" customControl:MoveElementAttach.AllowMove="True" > <Grid HorizontalAlignment="Center" VerticalAlignment="Center" Height="500" Width="500" Background="Black"> <TextBlock Text="3" FontSize="20" Foreground="Wheat" HorizontalAlignment="Center" VerticalAlignment="Center"></TextBlock> </Grid> </Grid> <Grid Background="Blue" customControl:MoveElementAttach.AllowMove="True" > <Grid HorizontalAlignment="Center" VerticalAlignment="Center" Height="500" Width="500" Background="Black"> <TextBlock Text="4" FontSize="20" Foreground="Wheat" HorizontalAlignment="Center" VerticalAlignment="Center"></TextBlock> </Grid> </Grid> </customControl:DragDropCarousel>
通过 customControl:MoveElementAttach.AllowMove="True" 来标记那些控件支持拖拽,哪些不支持拖拽,这种只是临时解决方案,其实还有其他的解决方案。后续我如果要用到再持续更新