diff --git a/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json b/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json index 09954a0f..06e7a0ed 100644 --- a/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json +++ b/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json @@ -57,7 +57,8 @@ [ "Loading", "LoadingDemo", "Brush.Loading", "", "Feedback" ], [ "Tag", "TagDemo", "Brush.Tag", "", "DataDisplay" ], [ "Badge", "BadgeDemo", "Brush.CheckDot", "", "DataDisplay" ], - [ "Shield", "ShieldDemo", "Brush.SplitKeyValuePair", "", "DataDisplay" ] + [ "Shield", "ShieldDemo", "Brush.SplitKeyValuePair", "", "DataDisplay" ], + [ "ImageBlock", "ImageBlockDemo", "Brush.ImageStack", "", "DataDisplay" ] ] } ] diff --git a/src/Avalonia/HandyControlDemo_Avalonia/HandyControlDemo_Avalonia.csproj b/src/Avalonia/HandyControlDemo_Avalonia/HandyControlDemo_Avalonia.csproj index 87f431e9..b067b862 100644 --- a/src/Avalonia/HandyControlDemo_Avalonia/HandyControlDemo_Avalonia.csproj +++ b/src/Avalonia/HandyControlDemo_Avalonia/HandyControlDemo_Avalonia.csproj @@ -18,6 +18,7 @@ + @@ -26,6 +27,7 @@ + diff --git a/src/Avalonia/HandyControlDemo_Avalonia/Resources/Img/Dance.png b/src/Avalonia/HandyControlDemo_Avalonia/Resources/Img/Dance.png new file mode 100644 index 00000000..abf4bab1 Binary files /dev/null and b/src/Avalonia/HandyControlDemo_Avalonia/Resources/Img/Dance.png differ diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ImageBlockDemo.axaml b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ImageBlockDemo.axaml new file mode 100644 index 00000000..9682ad70 --- /dev/null +++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ImageBlockDemo.axaml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ImageBlockDemo.axaml.cs b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ImageBlockDemo.axaml.cs new file mode 100644 index 00000000..4630eb9d --- /dev/null +++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ImageBlockDemo.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace HandyControlDemo.UserControl; + +public partial class ImageBlockDemo : Avalonia.Controls.UserControl +{ + public ImageBlockDemo() + { + InitializeComponent(); + } +} diff --git a/src/Avalonia/HandyControl_Avalonia/Controls/Image/ImageBlock.cs b/src/Avalonia/HandyControl_Avalonia/Controls/Image/ImageBlock.cs new file mode 100644 index 00000000..1fba1b97 --- /dev/null +++ b/src/Avalonia/HandyControl_Avalonia/Controls/Image/ImageBlock.cs @@ -0,0 +1,244 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; + +namespace HandyControl.Controls; + +/// +/// A sprite-sheet frame animation player. +/// Slices the source image into a x grid and +/// plays back the frame range [StartRow,StartColumn] … [EndRow,EndColumn]. +/// +public class ImageBlock : Control +{ + private readonly DispatcherTimer _dispatcherTimer; + private IImage? _source; + private int _indexMax; + private int _indexMin; + private int _currentIndex; + private int _blockWidth; + private int _blockHeight; + private bool _isDisposed; + private int _columns = 1; + + public ImageBlock() + { + _dispatcherTimer = new DispatcherTimer + { + Interval = Interval + }; + _dispatcherTimer.Tick += DispatcherTimer_Tick; + } + + ~ImageBlock() => Dispose(); + + public void Dispose() + { + if (_isDisposed) return; + + _dispatcherTimer.Tick -= DispatcherTimer_Tick; + _dispatcherTimer.Stop(); + _isDisposed = true; + + GC.SuppressFinalize(this); + } + + private void UpdateDatas() + { + if (_source is null) return; + + _indexMin = StartRow * _columns + StartColumn; + _indexMax = EndRow * _columns + EndColumn; + _currentIndex = _indexMin; + + if (_source is Bitmap bitmap) + { + _blockWidth = bitmap.PixelSize.Width / _columns; + _blockHeight = bitmap.PixelSize.Height / Rows; + } + } + + private void DispatcherTimer_Tick(object? sender, EventArgs e) => InvalidateVisual(); + + // ── Dependency Properties ── + + public static readonly StyledProperty StartColumnProperty = + AvaloniaProperty.Register(nameof(StartColumn)); + + public int StartColumn + { + get => GetValue(StartColumnProperty); + set => SetValue(StartColumnProperty, value); + } + + public static readonly StyledProperty StartRowProperty = + AvaloniaProperty.Register(nameof(StartRow)); + + public int StartRow + { + get => GetValue(StartRowProperty); + set => SetValue(StartRowProperty, value); + } + + public static readonly StyledProperty EndColumnProperty = + AvaloniaProperty.Register(nameof(EndColumn)); + + public int EndColumn + { + get => GetValue(EndColumnProperty); + set => SetValue(EndColumnProperty, value); + } + + public static readonly StyledProperty EndRowProperty = + AvaloniaProperty.Register(nameof(EndRow)); + + public int EndRow + { + get => GetValue(EndRowProperty); + set => SetValue(EndRowProperty, value); + } + + public static readonly StyledProperty IsPlayingProperty = + AvaloniaProperty.Register(nameof(IsPlaying)); + + public bool IsPlaying + { + get => GetValue(IsPlayingProperty); + set => SetValue(IsPlayingProperty, value); + } + + public static readonly StyledProperty ColumnsProperty = + AvaloniaProperty.Register(nameof(Columns), defaultValue: 1, + validate: v => v >= 1); + + public int Columns + { + get => GetValue(ColumnsProperty); + set => SetValue(ColumnsProperty, value); + } + + public static readonly StyledProperty RowsProperty = + AvaloniaProperty.Register(nameof(Rows), defaultValue: 1, + validate: v => v >= 1); + + public int Rows + { + get => GetValue(RowsProperty); + set => SetValue(RowsProperty, value); + } + + public static readonly StyledProperty IntervalProperty = + AvaloniaProperty.Register(nameof(Interval), + defaultValue: TimeSpan.FromSeconds(1)); + + public TimeSpan Interval + { + get => GetValue(IntervalProperty); + set => SetValue(IntervalProperty, value); + } + + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); + + public IImage? Source + { + get => GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + // ── Static constructor ── + + static ImageBlock() + { + AffectsRender( + SourceProperty, StartColumnProperty, StartRowProperty, + EndColumnProperty, EndRowProperty, ColumnsProperty, RowsProperty, + IsPlayingProperty, IntervalProperty); + + SourceProperty.Changed.AddClassHandler((o, e) => o.OnSourceChanged(e)); + ColumnsProperty.Changed.AddClassHandler((o, e) => o.OnPositionsChanged(e)); + StartColumnProperty.Changed.AddClassHandler((o, e) => o.OnPositionsChanged(e)); + StartRowProperty.Changed.AddClassHandler((o, e) => o.OnPositionsChanged(e)); + EndColumnProperty.Changed.AddClassHandler((o, e) => o.OnPositionsChanged(e)); + EndRowProperty.Changed.AddClassHandler((o, e) => o.OnPositionsChanged(e)); + IsPlayingProperty.Changed.AddClassHandler((o, e) => o.OnIsPlayingChanged(e)); + IntervalProperty.Changed.AddClassHandler((o, e) => o.OnIntervalChanged(e)); + IsVisibleProperty.Changed.AddClassHandler((o, e) => o.OnIsVisibleChanged(e)); + } + + // ── Event handlers ── + + private void OnSourceChanged(AvaloniaPropertyChangedEventArgs e) + { + _source = e.GetNewValue(); + UpdateDatas(); + } + + private void OnPositionsChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == ColumnsProperty) + { + _columns = e.GetNewValue(); + } + + UpdateDatas(); + } + + private void OnIsPlayingChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.GetNewValue() && IsVisible) + { + _dispatcherTimer.Start(); + } + else + { + _dispatcherTimer.Stop(); + } + } + + private void OnIntervalChanged(AvaloniaPropertyChangedEventArgs e) + { + _dispatcherTimer.Interval = e.GetNewValue(); + } + + private void OnIsVisibleChanged(AvaloniaPropertyChangedEventArgs e) + { + if (IsVisible && IsPlaying) + { + _dispatcherTimer.Start(); + } + else + { + _dispatcherTimer.Stop(); + } + } + + // ── Render ── + + public override void Render(DrawingContext context) + { + if (_source is null) return; + + var rect = CalDisplayRect(); + var croppedBitmap = new CroppedBitmap(_source, rect); + context.DrawImage(croppedBitmap, new Rect(0, 0, Bounds.Width, Bounds.Height)); + } + + private PixelRect CalDisplayRect() + { + if (_currentIndex > _indexMax) + { + _currentIndex = _indexMin; + } + + var x = _currentIndex % _columns * _blockWidth; + var y = _currentIndex / _columns * _blockHeight; + + var rect = new PixelRect(x, y, _blockWidth, _blockHeight); + _currentIndex++; + return rect; + } +} diff --git a/src/Shared/HandyControl_Shared/Controls/Image/ImageBlock.cs b/src/Shared/HandyControl_Shared/Controls/Image/ImageBlock.cs index 22e55c1e..700e42a1 100644 --- a/src/Shared/HandyControl_Shared/Controls/Image/ImageBlock.cs +++ b/src/Shared/HandyControl_Shared/Controls/Image/ImageBlock.cs @@ -33,6 +33,7 @@ public class ImageBlock : FrameworkElement { Interval = Interval }; + _dispatcherTimer.Tick += DispatcherTimer_Tick; IsVisibleChanged += ImageBlock_IsVisibleChanged; } @@ -44,6 +45,7 @@ public class ImageBlock : FrameworkElement if (_isDisposed) return; IsVisibleChanged -= ImageBlock_IsVisibleChanged; + _dispatcherTimer.Tick -= DispatcherTimer_Tick; _dispatcherTimer.Stop(); _isDisposed = true; @@ -52,18 +54,13 @@ public class ImageBlock : FrameworkElement private void ImageBlock_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { - if (IsVisible) + if (IsVisible && IsPlaying) { - _dispatcherTimer.Tick += DispatcherTimer_Tick; - if (IsPlaying) - { - _dispatcherTimer.Start(); - } + _dispatcherTimer.Start(); } else { _dispatcherTimer.Stop(); - _dispatcherTimer.Tick -= DispatcherTimer_Tick; } } @@ -139,7 +136,7 @@ public class ImageBlock : FrameworkElement private static void OnIsPlayingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var ctl = (ImageBlock) d; - if ((bool) e.NewValue) + if ((bool) e.NewValue && ctl.IsVisible) { ctl._dispatcherTimer.Start(); }