Avalonia 支持头和尾显示的 WrapPanel 控件

Avalonia 支持头和尾显示的 WrapPanel 控件

因为普通的 Avalonia 数据绑定很难实现这样的效果,于是就自己在源代码的基础上魔改了一个版本。

一、效果

使用例:

    <ItemsControl ItemsSource="{Binding Tags}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <fff:WrapPanelEx>

                    <fff:WrapPanelEx.Head>
                        <TextBlock Background="LightGreen" Text="hello" />
                    </fff:WrapPanelEx.Head>

                    <fff:WrapPanelEx.Tail>
                        <TextBlock Background="LightGreen" Text="world!" />
                    </fff:WrapPanelEx.Tail>

                </fff:WrapPanelEx>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>

二、代码

using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Utilities;
using System;
using static System.Math;

namespace FFFFControls
{
    public enum WrapPanelItemsAlignment
    {
        /// <summary>
        /// Items are laid out so the first one in each column/row touches the top/left of the panel.
        /// </summary>
        Start,

        /// <summary>
        /// Items are laid out so that each column/row is centred vertically/horizontally within the panel.
        /// </summary>
        Center,

        /// <summary>
        /// Items are laid out so the last one in each column/row touches the bottom/right of the panel.
        /// </summary>
        End,
    }

    public class WrapPanelEx : Panel, INavigableContainer
    {
        #region src

        /// <summary>
        /// Defines the <see cref="ItemSpacing"/> dependency property.
        /// </summary>
        public static readonly StyledProperty<double> ItemSpacingProperty =
            AvaloniaProperty.Register<WrapPanelEx, double>(nameof(ItemSpacing));

        /// <summary>
        /// Defines the <see cref="LineSpacing"/> dependency property.
        /// </summary>
        public static readonly StyledProperty<double> LineSpacingProperty =
            AvaloniaProperty.Register<WrapPanelEx, double>(nameof(LineSpacing));

        /// <summary>
        /// Defines the <see cref="Orientation"/> property.
        /// </summary>
        public static readonly StyledProperty<Orientation> OrientationProperty =
            AvaloniaProperty.Register<WrapPanelEx, Orientation>(nameof(Orientation), defaultValue: Orientation.Horizontal);

        /// <summary>
        /// Defines the <see cref="ItemsAlignment"/> property.
        /// </summary>
        public static readonly StyledProperty<WrapPanelItemsAlignment> ItemsAlignmentProperty =
            AvaloniaProperty.Register<WrapPanelEx, WrapPanelItemsAlignment>(nameof(ItemsAlignment), defaultValue: WrapPanelItemsAlignment.Start);

        /// <summary>
        /// Defines the <see cref="ItemWidth"/> property.
        /// </summary>
        public static readonly StyledProperty<double> ItemWidthProperty =
            AvaloniaProperty.Register<WrapPanelEx, double>(nameof(ItemWidth), double.NaN);

        /// <summary>
        /// Defines the <see cref="ItemHeight"/> property.
        /// </summary>
        public static readonly StyledProperty<double> ItemHeightProperty =
            AvaloniaProperty.Register<WrapPanelEx, double>(nameof(ItemHeight), double.NaN);

        /// <summary>
        /// Initializes static members of the <see cref="WrapPanelEx"/> class.
        /// </summary>
        static WrapPanelEx()
        {
            AffectsMeasure<WrapPanelEx>(ItemSpacingProperty, LineSpacingProperty, OrientationProperty, ItemWidthProperty, ItemHeightProperty, HeadProperty, TailProperty);
            AffectsArrange<WrapPanelEx>(ItemsAlignmentProperty);

            HeadProperty.Changed.AddClassHandler<WrapPanelEx>((x, e) => x.HeadChanged(e));
            TailProperty.Changed.AddClassHandler<WrapPanelEx>((x, e) => x.TailChanged(e));
        }

        /// <summary>
        /// Gets or sets the spacing between lines.
        /// </summary>
        public double ItemSpacing
        {
            get => GetValue(ItemSpacingProperty);
            set => SetValue(ItemSpacingProperty, value);
        }

        /// <summary>
        /// Gets or sets the spacing between items.
        /// </summary>
        public double LineSpacing
        {
            get => GetValue(LineSpacingProperty);
            set => SetValue(LineSpacingProperty, value);
        }

        /// <summary>
        /// Gets or sets the orientation in which child controls will be laid out.
        /// </summary>
        public Orientation Orientation
        {
            get => GetValue(OrientationProperty);
            set => SetValue(OrientationProperty, value);
        }

        /// <summary>
        /// Gets or sets the alignment of items in the WrapPanel.
        /// </summary>
        public WrapPanelItemsAlignment ItemsAlignment
        {
            get => GetValue(ItemsAlignmentProperty);
            set => SetValue(ItemsAlignmentProperty, value);
        }

        /// <summary>
        /// Gets or sets the width of all items in the WrapPanel.
        /// </summary>
        public double ItemWidth
        {
            get => GetValue(ItemWidthProperty);
            set => SetValue(ItemWidthProperty, value);
        }

        /// <summary>
        /// Gets or sets the height of all items in the WrapPanel.
        /// </summary>
        public double ItemHeight
        {
            get => GetValue(ItemHeightProperty);
            set => SetValue(ItemHeightProperty, value);
        }

        /// <summary>
        /// Gets the next control in the specified direction.
        /// </summary>
        /// <param name="direction">The movement direction.</param>
        /// <param name="from">The control from which movement begins.</param>
        /// <param name="wrap">Whether to wrap around when the first or last item is reached.</param>
        /// <returns>The control.</returns>
        IInputElement? INavigableContainer.GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
        {
            var orientation = Orientation;
            var children = GetExChildren(); //Children;
            bool horiz = orientation == Orientation.Horizontal;
            int index = from is not null ? Children.IndexOf((Control)from) : -1;

            switch (direction)
            {
                case NavigationDirection.First:
                    index = 0;
                    break;
                case NavigationDirection.Last:
                    index = children.Count - 1;
                    break;
                case NavigationDirection.Next:
                    ++index;
                    break;
                case NavigationDirection.Previous:
                    --index;
                    break;
                case NavigationDirection.Left:
                    index = horiz ? index - 1 : -1;
                    break;
                case NavigationDirection.Right:
                    index = horiz ? index + 1 : -1;
                    break;
                case NavigationDirection.Up:
                    index = horiz ? -1 : index - 1;
                    break;
                case NavigationDirection.Down:
                    index = horiz ? -1 : index + 1;
                    break;
            }

            if (index >= 0 && index < children.Count)
            {
                return children[index];
            }
            else
            {
                return null;
            }
        }


        /// <inheritdoc/>
        protected override Size MeasureOverride(Size constraint)
        {
            double itemWidth = ItemWidth;
            double itemHeight = ItemHeight;
            double itemSpacing = ItemSpacing;
            double lineSpacing = LineSpacing;
            var orientation = Orientation;
            var children = GetExChildren(); //  Children;
            var curLineSize = new UVSize(orientation);
            var panelSize = new UVSize(orientation);
            var uvConstraint = new UVSize(orientation, constraint.Width, constraint.Height);
            bool itemWidthSet = !double.IsNaN(itemWidth);
            bool itemHeightSet = !double.IsNaN(itemHeight);
            bool itemExists = false;
            bool lineExists = false;

            var childConstraint = new Size(
                itemWidthSet ? itemWidth : constraint.Width,
                itemHeightSet ? itemHeight : constraint.Height);


            for (int i = 0, count = children.Count; i < count; ++i)
            {
                var child = children[i];
                // Flow passes its own constraint to children
                child.Measure(childConstraint);

                // This is the size of the child in UV space
                UVSize childSize = new UVSize(orientation,
                    itemWidthSet ? itemWidth : child.DesiredSize.Width,
                    itemHeightSet ? itemHeight : child.DesiredSize.Height);

                var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0;
                if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvConstraint.U)) // Need to switch to another line
                {
                    panelSize.U = Max(curLineSize.U, panelSize.U);
                    panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0);
                    curLineSize = childSize;

                    itemExists = child.IsVisible;
                    lineExists = true;
                }
                else // Continue to accumulate a line
                {
                    curLineSize.U += childSize.U + nextSpacing;
                    curLineSize.V = Max(childSize.V, curLineSize.V);

                    itemExists |= child.IsVisible; // keep true
                }
            }

            // The last line size, if any should be added
            panelSize.U = Max(curLineSize.U, panelSize.U);
            panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0);

            // Go from UV space to W/H space
            return new Size(panelSize.Width, panelSize.Height);
        }

        /// <inheritdoc/>
        protected override Size ArrangeOverride(Size finalSize)
        {
            double itemWidth = ItemWidth;
            double itemHeight = ItemHeight;
            double itemSpacing = ItemSpacing;
            double lineSpacing = LineSpacing;
            var orientation = Orientation;
            bool isHorizontal = orientation == Orientation.Horizontal;
            var children = GetExChildren();//; Children;
            int firstInLine = 0;
            double accumulatedV = 0;
            double itemU = isHorizontal ? itemWidth : itemHeight;
            var curLineSize = new UVSize(orientation);
            var uvFinalSize = new UVSize(orientation, finalSize.Width, finalSize.Height);
            bool itemWidthSet = !double.IsNaN(itemWidth);
            bool itemHeightSet = !double.IsNaN(itemHeight);
            bool itemExists = false;
            bool lineExists = false;

            for (int i = 0; i < children.Count; ++i)
            {
                var child = children[i];
                var childSize = new UVSize(orientation,
                    itemWidthSet ? itemWidth : child.DesiredSize.Width,
                    itemHeightSet ? itemHeight : child.DesiredSize.Height);

                var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0;
                if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvFinalSize.U)) // Need to switch to another line
                {
                    accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first
                    ArrangeLine(curLineSize.V, firstInLine, i);
                    accumulatedV += curLineSize.V; // add the height of the line just arranged
                    curLineSize = childSize;

                    firstInLine = i;

                    itemExists = child.IsVisible;
                    lineExists = true;
                }
                else // Continue to accumulate a line
                {
                    curLineSize.U += childSize.U + nextSpacing;
                    curLineSize.V = Max(childSize.V, curLineSize.V);

                    itemExists |= child.IsVisible; // keep true
                }
            }

            // Arrange the last line, if any
            if (firstInLine < children.Count)
            {
                accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first
                ArrangeLine(curLineSize.V, firstInLine, children.Count);
            }

            return finalSize;

            void ArrangeLine(double lineV, int start, int end)
            {
                bool useItemU = isHorizontal ? itemWidthSet : itemHeightSet;
                double u = 0;
                if (ItemsAlignment != WrapPanelItemsAlignment.Start)
                {
                    double totalU = -itemSpacing;
                    for (int i = start; i < end; ++i)
                    {
                        totalU += GetChildU(i) + (!children[i].IsVisible ? 0 : itemSpacing);
                    }

                    u = ItemsAlignment switch
                    {
                        WrapPanelItemsAlignment.Center => (uvFinalSize.U - totalU) / 2,
                        WrapPanelItemsAlignment.End => uvFinalSize.U - totalU,
                        WrapPanelItemsAlignment.Start => 0,
                        _ => throw new ArgumentOutOfRangeException(nameof(ItemsAlignment), ItemsAlignment, null),
                    };
                }

                for (int i = start; i < end; ++i)
                {
                    double layoutSlotU = GetChildU(i);
                    children[i].Arrange(isHorizontal ? new(u, accumulatedV, layoutSlotU, lineV) : new(accumulatedV, u, lineV, layoutSlotU));
                    u += layoutSlotU + (!children[i].IsVisible ? 0 : itemSpacing);
                }

                return;
                double GetChildU(int i) => useItemU ? itemU :
                    isHorizontal ? children[i].DesiredSize.Width : children[i].DesiredSize.Height;
            }
        }

        private struct UVSize
        {
            internal UVSize(Orientation orientation, double width, double height)
            {
                U = V = 0d;
                _orientation = orientation;
                Width = width;
                Height = height;
            }

            internal UVSize(Orientation orientation)
            {
                U = V = 0d;
                _orientation = orientation;
            }

            internal double U;
            internal double V;
            private Orientation _orientation;

            internal double Width
            {
                get => _orientation == Orientation.Horizontal ? U : V;
                set { if (_orientation == Orientation.Horizontal) U = value; else V = value; }
            }
            internal double Height
            {
                get => _orientation == Orientation.Horizontal ? V : U;
                set { if (_orientation == Orientation.Horizontal) V = value; else U = value; }
            }


        }
        #endregion

        #region customs


        /// <summary>
        /// Head StyledProperty definition
        /// </summary>
        public static readonly StyledProperty<object> HeadProperty =
            AvaloniaProperty.Register<WrapPanelEx, object>(nameof(Head));

        /// <summary>
        /// Gets or sets the Head property. This StyledProperty
        /// indicates ....
        /// </summary>
        public object Head
        {
            get => this.GetValue(HeadProperty);
            set => SetValue(HeadProperty, value);
        }



        /// <summary>
        /// Tail StyledProperty definition
        /// </summary>
        public static readonly StyledProperty<object> TailProperty =
            AvaloniaProperty.Register<WrapPanelEx, object>(nameof(Tail));

        /// <summary>
        /// Gets or sets the Tail property. This StyledProperty
        /// indicates ....
        /// </summary>
        public object Tail
        {
            get => this.GetValue(TailProperty);
            set => SetValue(TailProperty, value);
        }

        /// <summary>
        /// Called when the <see cref="Child"/> property changes.
        /// </summary>
        /// <param name="e">The event args.</param>
        private void HeadChanged(AvaloniaPropertyChangedEventArgs e)
        {
            var oldChild = (Control?)e.OldValue;
            var newChild = (Control?)e.NewValue;

            if (oldChild != null)
            {
                ((ISetLogicalParent)oldChild).SetParent(null);
                LogicalChildren.Clear();
                VisualChildren.Remove(oldChild);
            }

            if (newChild != null)
            {
                ((ISetLogicalParent)newChild).SetParent(this);
                VisualChildren.Add(newChild);
                LogicalChildren.Add(newChild);
            }
        }

        /// <summary>
        /// Called when the <see cref="Child"/> property changes.
        /// </summary>
        /// <param name="e">The event args.</param>
        private void TailChanged(AvaloniaPropertyChangedEventArgs e)
        {
            var oldChild = (Control?)e.OldValue;
            var newChild = (Control?)e.NewValue;

            if (oldChild != null)
            {
                ((ISetLogicalParent)oldChild).SetParent(null);
                LogicalChildren.Clear();
                VisualChildren.Remove(oldChild);
            }

            if (newChild != null)
            {
                ((ISetLogicalParent)newChild).SetParent(this);
                VisualChildren.Add(newChild);
                LogicalChildren.Add(newChild);
            }
        }

        private Controls GetExChildren()
        {
            var children = Children;
            var controls = new Controls();
            if (Head is Control headControl)
                controls.Add(headControl);

            controls.AddRange(children);

            if (Tail is Control tailControl)
                controls.Add(tailControl);
            return controls;
        }

        #endregion
    }
}

posted @ 2025-04-08 14:17  fanbal  阅读(155)  评论(0)    收藏  举报