feat: Add ImageBlock styles and demos.

This commit is contained in:
NaBian committed 2026-06-17 23:15:57 +08:00
1 parent 7b0e9ba108
commit c19e3870fa
7 files changed
+346 -9

No files matched your search

@@ -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" ]
]
}
]
@@ -18,6 +18,7 @@
<ItemGroup>
<None Remove="Data\DemoInfo.json" />
<None Remove="Resources\Img\cloud.png" />
<None Remove="Resources\Img\Dance.png" />
<None Remove="Resources\Img\icon.ico" />
<None Remove="Resources\Img\Avatar\*.png" />
<None Remove="Resources\Img\Album\*.jpg" />
@@ -26,6 +27,7 @@
<ItemGroup>
<EmbeddedResource Include="Data\DemoInfo.json" />
<AvaloniaResource Include="Resources\Img\cloud.png" />
<AvaloniaResource Include="Resources\Img\Dance.png" />
<AvaloniaResource Include="Resources\Img\icon.ico" />
<AvaloniaResource Include="Resources\Img\Avatar\*.png" />
<AvaloniaResource Include="Resources\Img\Album\*.jpg" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

@@ -0,0 +1,82 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:cv="clr-namespace:HandyControlDemo.Tools.Converter"
x:Class="HandyControlDemo.UserControl.ImageBlockDemo"
Background="{DynamicResource RegionBrush}">
<UserControl.Resources>
<cv:StringToBitmapConverter x:Key="StringToBitmap" />
</UserControl.Resources>
<TransitioningContentControl>
<ScrollViewer>
<hc:UniformSpacingPanel Margin="32"
Spacing="32"
ChildWrapping="Wrap"
VerticalAlignment="Center"
HorizontalAlignment="Center"
ItemWidth="110"
ItemHeight="128">
<hc:ImageBlock Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Dance.png', Converter={StaticResource StringToBitmap}}"
Interval="0:0:0.125"
Columns="8" Rows="10"
StartColumn="0" StartRow="0"
EndColumn="7" EndRow="0"
IsPlaying="True" />
<hc:ImageBlock Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Dance.png', Converter={StaticResource StringToBitmap}}"
Interval="0:0:0.125"
Columns="8" Rows="10"
StartColumn="0" StartRow="1"
EndColumn="7" EndRow="1"
IsPlaying="True" />
<hc:ImageBlock Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Dance.png', Converter={StaticResource StringToBitmap}}"
Interval="0:0:0.125"
Columns="8" Rows="10"
StartColumn="0" StartRow="2"
EndColumn="7" EndRow="2"
IsPlaying="{Binding IsPointerOver, RelativeSource={RelativeSource Self}}" />
<hc:ImageBlock Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Dance.png', Converter={StaticResource StringToBitmap}}"
Interval="0:0:0.125"
Columns="8" Rows="10"
StartColumn="0" StartRow="3"
EndColumn="7" EndRow="3"
IsPlaying="{Binding IsPointerOver, RelativeSource={RelativeSource Self}}" />
<hc:ImageBlock Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Dance.png', Converter={StaticResource StringToBitmap}}"
Interval="0:0:0.1"
Columns="8" Rows="10"
StartColumn="0" StartRow="4"
EndColumn="7" EndRow="4"
IsPlaying="True" />
<hc:ImageBlock Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Dance.png', Converter={StaticResource StringToBitmap}}"
Interval="0:0:0.125"
Columns="8" Rows="10"
StartColumn="0" StartRow="5"
EndColumn="7" EndRow="5"
IsPlaying="True" />
<hc:ImageBlock Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Dance.png', Converter={StaticResource StringToBitmap}}"
Interval="0:0:0.125"
Columns="8" Rows="10"
StartColumn="0" StartRow="6"
EndColumn="7" EndRow="6"
IsPlaying="{Binding IsPointerOver, RelativeSource={RelativeSource Self}}" />
<hc:ImageBlock Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Dance.png', Converter={StaticResource StringToBitmap}}"
Interval="0:0:0.1"
Columns="8" Rows="10"
StartColumn="0" StartRow="7"
EndColumn="7" EndRow="7"
IsPlaying="True" />
<hc:ImageBlock Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Dance.png', Converter={StaticResource StringToBitmap}}"
Interval="0:0:0.125"
Columns="8" Rows="10"
StartColumn="0" StartRow="8"
EndColumn="7" EndRow="8"
IsPlaying="{Binding IsPointerOver, RelativeSource={RelativeSource Self}}" />
<hc:ImageBlock Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Dance.png', Converter={StaticResource StringToBitmap}}"
Interval="0:0:0.125"
Columns="8" Rows="10"
StartColumn="0" StartRow="9"
EndColumn="7" EndRow="9"
IsPlaying="{Binding IsPointerOver, RelativeSource={RelativeSource Self}}" />
</hc:UniformSpacingPanel>
</ScrollViewer>
</TransitioningContentControl>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace HandyControlDemo.UserControl;
public partial class ImageBlockDemo : Avalonia.Controls.UserControl
{
public ImageBlockDemo()
{
InitializeComponent();
}
}
@@ -0,0 +1,244 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
namespace HandyControl.Controls;
/// <summary>
/// A sprite-sheet frame animation player.
/// Slices the source image into a <see cref="Columns"/> x <see cref="Rows"/> grid and
/// plays back the frame range [StartRow,StartColumn] … [EndRow,EndColumn].
/// </summary>
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<int> StartColumnProperty =
AvaloniaProperty.Register<ImageBlock, int>(nameof(StartColumn));
public int StartColumn
{
get => GetValue(StartColumnProperty);
set => SetValue(StartColumnProperty, value);
}
public static readonly StyledProperty<int> StartRowProperty =
AvaloniaProperty.Register<ImageBlock, int>(nameof(StartRow));
public int StartRow
{
get => GetValue(StartRowProperty);
set => SetValue(StartRowProperty, value);
}
public static readonly StyledProperty<int> EndColumnProperty =
AvaloniaProperty.Register<ImageBlock, int>(nameof(EndColumn));
public int EndColumn
{
get => GetValue(EndColumnProperty);
set => SetValue(EndColumnProperty, value);
}
public static readonly StyledProperty<int> EndRowProperty =
AvaloniaProperty.Register<ImageBlock, int>(nameof(EndRow));
public int EndRow
{
get => GetValue(EndRowProperty);
set => SetValue(EndRowProperty, value);
}
public static readonly StyledProperty<bool> IsPlayingProperty =
AvaloniaProperty.Register<ImageBlock, bool>(nameof(IsPlaying));
public bool IsPlaying
{
get => GetValue(IsPlayingProperty);
set => SetValue(IsPlayingProperty, value);
}
public static readonly StyledProperty<int> ColumnsProperty =
AvaloniaProperty.Register<ImageBlock, int>(nameof(Columns), defaultValue: 1,
validate: v => v >= 1);
public int Columns
{
get => GetValue(ColumnsProperty);
set => SetValue(ColumnsProperty, value);
}
public static readonly StyledProperty<int> RowsProperty =
AvaloniaProperty.Register<ImageBlock, int>(nameof(Rows), defaultValue: 1,
validate: v => v >= 1);
public int Rows
{
get => GetValue(RowsProperty);
set => SetValue(RowsProperty, value);
}
public static readonly StyledProperty<TimeSpan> IntervalProperty =
AvaloniaProperty.Register<ImageBlock, TimeSpan>(nameof(Interval),
defaultValue: TimeSpan.FromSeconds(1));
public TimeSpan Interval
{
get => GetValue(IntervalProperty);
set => SetValue(IntervalProperty, value);
}
public static readonly StyledProperty<IImage?> SourceProperty =
AvaloniaProperty.Register<ImageBlock, IImage?>(nameof(Source));
public IImage? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
// ── Static constructor ──
static ImageBlock()
{
AffectsRender<ImageBlock>(
SourceProperty, StartColumnProperty, StartRowProperty,
EndColumnProperty, EndRowProperty, ColumnsProperty, RowsProperty,
IsPlayingProperty, IntervalProperty);
SourceProperty.Changed.AddClassHandler<ImageBlock>((o, e) => o.OnSourceChanged(e));
ColumnsProperty.Changed.AddClassHandler<ImageBlock>((o, e) => o.OnPositionsChanged(e));
StartColumnProperty.Changed.AddClassHandler<ImageBlock>((o, e) => o.OnPositionsChanged(e));
StartRowProperty.Changed.AddClassHandler<ImageBlock>((o, e) => o.OnPositionsChanged(e));
EndColumnProperty.Changed.AddClassHandler<ImageBlock>((o, e) => o.OnPositionsChanged(e));
EndRowProperty.Changed.AddClassHandler<ImageBlock>((o, e) => o.OnPositionsChanged(e));
IsPlayingProperty.Changed.AddClassHandler<ImageBlock>((o, e) => o.OnIsPlayingChanged(e));
IntervalProperty.Changed.AddClassHandler<ImageBlock>((o, e) => o.OnIntervalChanged(e));
IsVisibleProperty.Changed.AddClassHandler<ImageBlock>((o, e) => o.OnIsVisibleChanged(e));
}
// ── Event handlers ──
private void OnSourceChanged(AvaloniaPropertyChangedEventArgs e)
{
_source = e.GetNewValue<IImage?>();
UpdateDatas();
}
private void OnPositionsChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ColumnsProperty)
{
_columns = e.GetNewValue<int>();
}
UpdateDatas();
}
private void OnIsPlayingChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.GetNewValue<bool>() && IsVisible)
{
_dispatcherTimer.Start();
}
else
{
_dispatcherTimer.Stop();
}
}
private void OnIntervalChanged(AvaloniaPropertyChangedEventArgs e)
{
_dispatcherTimer.Interval = e.GetNewValue<TimeSpan>();
}
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;
}
}
@@ -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();
}