扩展已经更新

https://www.cnblogs.com/dalgleish/p/18972924

由于Avalonia没有内置逻辑滚动,自己实现一个。

LogicalScrollControl类

    public class LogicalScrollControl : ContentControl, ILogicalScrollable
    {
        private Vector offset;
        private Size extent;
        private Size viewport;
        private Size scrollSize = new Size(100, 100);
        private Size pageScrollSize = new Size(300, 300);
        private bool canHorizontallyScroll;
        private bool canVerticallyScroll;
        private EventHandler? scrollInvalidated;

        public LogicalScrollControl()
        {
            Focusable = true;
            AddHandler(PointerWheelChangedEvent, OnPointerWheelChanged, RoutingStrategies.Tunnel);
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            if (Content is Control contentControl)
            {
                contentControl.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
                extent = contentControl.DesiredSize;
                // 动态获取第一个子控件尺寸作为 scrollSize
                if (contentControl is Panel panel && panel.Children.Count > 0)
                {
                    var firstChild = panel.Children[0];
                    firstChild.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
                    var firstSize = firstChild.DesiredSize;

                    scrollSize = new Size(firstSize.Width, firstSize.Height);
                    pageScrollSize = new Size(scrollSize.Width * 3, scrollSize.Height * 3);
                }
                else
                {
                    // 如果不是Panel或者没有子元素,fallback
                    scrollSize = new Size(100, 100);
                    pageScrollSize = new Size(300, 300);
                }
            }
            else
            {
                extent = new Size(0, 0);
            }

            viewport = availableSize;
            RaiseScrollInvalidated(EventArgs.Empty);

            canHorizontallyScroll = Extent.Width > Viewport.Width;
            canVerticallyScroll = Extent.Height > Viewport.Height;
            return base.MeasureOverride(availableSize);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            if (Content is Control contentControl)
            {
                var scrollX = offset.X;
                var scrollY = offset.Y;

                var width = Math.Max(contentControl.DesiredSize.Width, finalSize.Width);
                var height = Math.Max(contentControl.DesiredSize.Height, finalSize.Height);

                // 移动内容,模拟滚动视口
                var rect = new Rect(-scrollX, -scrollY, width, height);
                contentControl.Arrange(rect);
            }

            return finalSize;
        }

        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);

            switch (e.Key)
            {
                case Key.Down:
                    Offset += new Vector(0, scrollSize.Height);
                    e.Handled = true;
                    break;
                case Key.Up:
                    Offset -= new Vector(0, scrollSize.Height);
                    e.Handled = true;
                    break;
                case Key.Right:
                    Offset += new Vector(scrollSize.Width, 0);
                    e.Handled = true;
                    break;
                case Key.Left:
                    Offset -= new Vector(scrollSize.Width, 0);
                    e.Handled = true;
                    break;
                case Key.PageDown:
                    Offset += new Vector(0, pageScrollSize.Height);
                    e.Handled = true;
                    break;
                case Key.PageUp:
                    Offset -= new Vector(0, pageScrollSize.Height);
                    e.Handled = true;
                    break;
            }
        }

        private void OnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
        {
            if (CanVerticallyScroll)
            {
                // 垂直滚动: Delta.Y 为正表示向上滚,负表示向下滚
                // 这里乘以 scrollSize.Height 是滚动一个逻辑单位
                Offset += new Vector(0, -e.Delta.Y * scrollSize.Height);
                e.Handled = true;
            }
            else if (CanHorizontallyScroll)
            {
                // 水平滚动: Delta.Y 为正表示向左滚,负表示向右滚
                Offset += new Vector(-e.Delta.Y * scrollSize.Width, 0);
                e.Handled = true;
            }
            else
            {
                // 如果内容未溢出,则不滚动
                e.Handled = false;
            }
        }

        private Vector CoerceOffset(Vector value)
        {
            double maxX = Math.Max(Extent.Width - Viewport.Width, 0);
            double maxY = Math.Max(Extent.Height - Viewport.Height, 0);

            double x = Math.Max(0, Math.Min(value.X, maxX));
            double y = Math.Max(0, Math.Min(value.Y, maxY));

            return new Vector(x, y);
        }

        public void RaiseScrollInvalidated(EventArgs e)
        {
            scrollInvalidated?.Invoke(this, e);
        }

        public Size Extent => extent;

        public Size Viewport => viewport;

        public Vector Offset
        {
            get => offset;
            set
            {
                var coerced = CoerceOffset(value);
                if (!coerced.Equals(offset))
                {
                    offset = coerced;
                    InvalidateArrange();
                    RaiseScrollInvalidated(EventArgs.Empty);
                }
            }
        }

        public bool CanHorizontallyScroll
        {
            get => canHorizontallyScroll;
            set => canHorizontallyScroll = value;
        }

        public bool CanVerticallyScroll
        {
            get => canVerticallyScroll;
            set => canVerticallyScroll = value;
        }

        public bool IsLogicalScrollEnabled => true;

        public Size ScrollSize => scrollSize;

        public Size PageScrollSize => pageScrollSize;

        public event EventHandler? ScrollInvalidated
        {
            add => scrollInvalidated += value;
            remove => scrollInvalidated -= value;
        }

        public bool BringIntoView(Control target, Rect targetRect = default)
        {
            if (target == null || !IsLogicalScrollEnabled || Content == null)
                return false;

            var targetBounds = AvaloniaExtensions.GetMinBoundingMatrix(target, this);

            Vector newOffset = offset;

            if (targetBounds.Top < 0)
                newOffset = new Vector(newOffset.X, offset.Y + targetBounds.Top);
            else if (targetBounds.Bottom > viewport.Height)
                newOffset = new Vector(newOffset.X, offset.Y + (targetBounds.Bottom - viewport.Height));

            if (targetBounds.Left < 0)
                newOffset = new Vector(offset.X + targetBounds.Left, newOffset.Y);
            else if (targetBounds.Right > viewport.Width)
                newOffset = new Vector(offset.X + (targetBounds.Right - viewport.Width), newOffset.Y);

            Offset = newOffset;
            return true;
        }

        public Control? GetControlInDirection(NavigationDirection direction, Control? from)
        {
            if (from == null || !IsLogicalScrollEnabled || Content == null)
                return null;

            Control? root = this.Content as Control;
            // 计算起点控件视觉矩形
            Rect fromRect = AvaloniaExtensions.GetMinBoundingMatrix(from, this);
            var fromCenter = fromRect.Center;

            var candidates = root?.GetVisualDescendants()
                .OfType<Control>()
                .Where(c => c != from && c.Focusable && c.IsEffectivelyVisible && c is Visual)
                .Select(c =>
                {
                    var rect = AvaloniaExtensions.GetMinBoundingMatrix(c, this);

                    return new
                    {
                        Control = c,
                        Rect = rect,
                        Center = rect.Center
                    };
                })
                .Where(x => x != null)
                .ToList()!;

            Control? best = null;
            double bestScore = double.MaxValue;

            foreach (var candidate in candidates)
            {
                var targetRect = candidate!.Rect;
                var targetCenter = candidate.Center;

                bool isInDirection = direction switch
                {
                    NavigationDirection.Left => targetRect.Right <= fromRect.Left,
                    NavigationDirection.Right => targetRect.Left >= fromRect.Right,
                    NavigationDirection.Up => targetRect.Bottom <= fromRect.Top,
                    NavigationDirection.Down => targetRect.Top >= fromRect.Bottom,
                    _ => false
                };

                if (!isInDirection)
                    continue;

                var dx = targetCenter.X - fromCenter.X;
                var dy = targetCenter.Y - fromCenter.Y;
                var distance = Math.Sqrt(dx * dx + dy * dy);


                if (distance < bestScore)
                {
                    bestScore = distance;
                    best = candidate.Control;
                }
            }

            return best;
        }
    }

LogicalScrolling.axaml代码

<Window 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"
        Height="400" Width="400"
        x:Class="AvaloniaUI.LogicalScrolling"
        Title="LogicalScrolling">
    <LogicalScrollControl Name="logical">
        <StackPanel Orientation="Horizontal" Name="panel">
            <Button Name="A1" Width="150">1</Button>
            <Button Name="A2" Width="150">2</Button>
            <Button Name="A3" Width="150">3</Button>
            <Button Name="A4" Width="150">4</Button>
        </StackPanel>
    </LogicalScrollControl>
</Window>

LogicalScrolling.axaml.cs代码

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Avalonia.VisualTree;
using System;

namespace AvaloniaUI;

public partial class LogicalScrolling : Window
{
    public LogicalScrolling()
    {
        InitializeComponent();
        Dispatcher.UIThread.Post(() =>
        {
            var target = panel.Children[3];
            bool res = logical.BringIntoView(target);
            Console.WriteLine($"{res}");
            var to = logical.GetControlInDirection(NavigationDirection.Right, panel.Children[0]);
            Console.WriteLine($"{to?.Name}");
        }
        );
    }
}

运行效果

 

posted on 2025-07-22 20:23  dalgleish  阅读(13)  评论(0)    收藏  举报