feat: Add ImageBlock styles and demos.
This commit is contained in:
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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user