feat: add selection list.

This commit is contained in:
rabbitism
2024-02-08 18:17:08 +08:00
parent 31b25a4e41
commit da511c6078
10 changed files with 265 additions and 0 deletions

View File

@@ -22,6 +22,7 @@ public static class MenuKeys
public const string MenuKeyNumericUpDown = "NumericUpDown";
public const string MenuKeyPagination = "Pagination";
public const string MenuKeyRangeSlider = "RangeSlider";
public const string MenuKeySelectionList = "SelectionList";
public const string MenuKeyTagInput = "TagInput";
public const string MenuKeyTimeline = "Timeline";
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";

View File

@@ -0,0 +1,15 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
xmlns:vm="using:Ursa.Demo.ViewModels"
x:DataType="vm:SelectionListDemoViewModel"
x:CompileBindings="True"
mc:Ignorable="d" d:DesignWidth="800"
d:DesignHeight="450"
x:Class="Ursa.Demo.Pages.SelectionListDemo">
<StackPanel HorizontalAlignment="Left">
<u:SelectionList ItemsSource="{Binding Items}"></u:SelectionList>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Ursa.Demo.Pages;
public partial class SelectionListDemo : UserControl
{
public SelectionListDemo()
{
InitializeComponent();
}
}

View File

@@ -44,6 +44,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(),
MenuKeys.MenuKeySelectionList => new SelectionListDemoViewModel(),
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),

View File

@@ -31,6 +31,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" },
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"},
new() { MenuHeader = "Selection LIst", Key = MenuKeys.MenuKeySelectionList, Status = "New" },
new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput },
new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler },
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "Updated" },

View File

@@ -0,0 +1,17 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ursa.Demo.ViewModels;
public class SelectionListDemoViewModel: ObservableObject
{
public ObservableCollection<string> Items { get; set; }
public SelectionListDemoViewModel()
{
Items = new ObservableCollection<string>()
{
"Ding", "Otter", "Husky", "Mr. 17", "Cass"
};
}
}

View File

@@ -0,0 +1,52 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<ControlTheme x:Key="{x:Type u:SelectionList}" TargetType="u:SelectionList">
<Setter Property="Background" Value="LightBlue"></Setter>
<Setter Property="ListBox.Template">
<ControlTemplate TargetType="ListBox">
<Border
Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
ClipToBounds="{TemplateBinding ClipToBounds}"
CornerRadius="{TemplateBinding CornerRadius}">
<Panel>
<Border
Classes="Shadow"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Name="{x:Static u:SelectionList.PART_Indicator}"
Theme="{DynamicResource CardBorder}" />
<ItemsPresenter
Name="PART_ItemsPresenter"
Margin="{TemplateBinding Padding}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</Panel>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:SelectionListItem}" TargetType="u:SelectionListItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Padding" Value="8" />
<Setter Property="Template">
<ControlTemplate TargetType="u:SelectionListItem">
<ContentPresenter
Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}"
Foreground="{TemplateBinding Foreground}"
Background="{TemplateBinding Background}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</ControlTemplate>
</Setter>
<Style Selector="^:selected">
<Setter Property="Foreground" Value="Blue"></Setter>
</Style>
</ControlTheme>
</ResourceDictionary>

View File

@@ -21,6 +21,7 @@
<ResourceInclude Source="NumericUpDown.axaml" />
<ResourceInclude Source="Pagination.axaml" />
<ResourceInclude Source="RangeSlider.axaml" />
<ResourceInclude Source="SelectionList.axaml" />
<ResourceInclude Source="TagInput.axaml" />
<ResourceInclude Source="ThemeSelector.axaml" />
<ResourceInclude Source="Timeline.axaml" />

View File

@@ -0,0 +1,126 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Animations;
using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls;
[TemplatePart(PART_Indicator, typeof(Control))]
public class SelectionList: SelectingItemsControl
{
public const string PART_Indicator = "PART_Indicator";
private static readonly FuncTemplate<Panel?> DefaultPanel = new(() => new StackPanel());
private Control? _indicator;
private ImplicitAnimationCollection? _implicitAnimations;
static SelectionList()
{
SelectionModeProperty.OverrideMetadata<SelectionList>(
new StyledPropertyMetadata<SelectionMode>(
defaultValue: SelectionMode.Single,
coerce: (o, mode) => SelectionMode.Single)
);
SelectedItemProperty.Changed.AddClassHandler<SelectionList, object?>((list, args) =>
list.OnSelectedItemChanged(args));
}
private void OnSelectedItemChanged(AvaloniaPropertyChangedEventArgs<object?> args)
{
var newValue = args.NewValue.Value;
if (newValue is null)
{
OpacityProperty.SetValue(0d, _indicator);
return;
}
var container = ContainerFromItem(newValue);
if (container is null)
{
OpacityProperty.SetValue(0d, _indicator);
return;
}
OpacityProperty.SetValue(1d, _indicator);
InvalidateMeasure();
InvalidateArrange();
}
protected override Size ArrangeOverride(Size finalSize)
{
var size = base.ArrangeOverride(finalSize);
if(_indicator is not null && SelectedItem is not null)
{
var container = ContainerFromItem(SelectedItem);
if (container is null) return size;
_indicator.Arrange(container.Bounds);
}
return size;
}
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<SelectionListItem>(item, out recycleKey);
}
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new SelectionListItem();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_indicator = e.NameScope.Find<Control>(PART_Indicator);
_indicator?.Arrange(new Rect());
if (_indicator is not null)
{
_indicator.Opacity = 0;
SetUpAnimation();
if (ElementComposition.GetElementVisual(_indicator) is { } v)
{
v.ImplicitAnimations = _implicitAnimations;
}
_indicator.SizeChanged += OnIndicatorSizeChanged;
}
}
private void OnIndicatorSizeChanged(object sender, SizeChangedEventArgs e)
{
}
internal void SelectByIndex(int index)
{
using var operation = Selection.BatchUpdate();
Selection.Clear();
Selection.Select(index);
}
private void SetUpAnimation()
{
var compositor = ElementComposition.GetElementVisual(this)!.Compositor;
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromSeconds(0.3);
var sizeAnimation = compositor.CreateVector2KeyFrameAnimation();
sizeAnimation.Target = "Size";
sizeAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
sizeAnimation.Duration = TimeSpan.FromSeconds(0.3);
var opacityAnimation = compositor.CreateScalarKeyFrameAnimation();
opacityAnimation.Target = nameof(CompositionVisual.Opacity);
opacityAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
opacityAnimation.Duration = TimeSpan.FromSeconds(0.3);
_implicitAnimations = compositor.CreateImplicitAnimationCollection();
_implicitAnimations["Offset"] = offsetAnimation;
_implicitAnimations["Size"] = sizeAnimation;
_implicitAnimations["Opacity"] = opacityAnimation;
}
}

View File

@@ -0,0 +1,38 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
namespace Ursa.Controls;
public class SelectionListItem: ContentControl, ISelectable
{
static SelectionListItem()
{
SelectableMixin.Attach<SelectionListItem>(IsSelectedProperty);
PressedMixin.Attach<SelectionListItem>();
FocusableProperty.OverrideDefaultValue<SelectionListItem>(true);
}
private static readonly Point s_invalidPoint = new Point(double.NaN, double.NaN);
private Point _pointerDownPoint = s_invalidPoint;
public static readonly StyledProperty<bool> IsSelectedProperty = SelectingItemsControl.IsSelectedProperty.AddOwner<ListBoxItem>();
public bool IsSelected
{
get => GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (ItemsControl.ItemsControlFromItemContaner(this) is SelectionList list)
{
int index = list.IndexFromContainer(this);
list.SelectByIndex(index);
}
}
}