123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- using Avalonia;
- using Avalonia.Controls;
- using Avalonia.Controls.Metadata;
- using Avalonia.Controls.Primitives;
- using Avalonia.Input;
- using Avalonia.Interactivity;
- using Avalonia.Layout;
- using Avalonia.Markup.Xaml;
- using Avalonia.Media;
- using Avalonia.Metadata;
- using System;
- namespace InABox.Avalonia.Components;
- /// <summary>
- /// Presents a control within a panel in which to zoom in and out and pan. The <see cref="Content"/> must be a <see cref="Layoutable"/>,
- /// and its <see cref="Layoutable.Width"/> and <see cref="Layoutable.Height"/> <b>must</b> be set.
- /// </summary>
- [TemplatePart("PART_ZoomContent", typeof(ContentControl))]
- [TemplatePart("PART_ZoomCanvas", typeof(Canvas))]
- [TemplatePart("PART_ZoomContentBorder", typeof(Border))]
- public partial class ZoomPanel : TemplatedControl
- {
- public static readonly StyledProperty<Layoutable?> ContentProperty =
- AvaloniaProperty.Register<ZoomPanel, Layoutable?>(nameof(Content));
- [Content]
- public Layoutable? Content
- {
- get => GetValue(ContentProperty);
- set => SetValue(ContentProperty, value);
- }
- private double ContentWidth => Content?.Width ?? 1;
- private double ContentHeight => Content?.Height ?? 1;
- private Canvas OuterCanvas = null!;
- private ContentControl ZoomContent = null!;
- private Border ZoomContentBorder = null!;
- private double ScaleFactor = 1.0;
- private double _originalScaleFactor = 1.0;
- private const double _wheelSpeed = 0.1;
- private const double _panSpeed = 30;
- // Center of the image.
- private Point ContentCentre = new();
- public ZoomPanel()
- {
- this.GetPropertyChangedObservable(ContentProperty).Subscribe(ContentChanged);
- }
- private void ContentChanged(AvaloniaPropertyChangedEventArgs args)
- {
- if(Content is null) return;
- void Update(AvaloniaPropertyChangedEventArgs? args = null)
- {
- if(OuterCanvas is not null)
- {
- PositionContent();
- }
- }
- Update();
- Content.GetPropertyChangedObservable(Layoutable.WidthProperty).Subscribe(Update);
- Content.GetPropertyChangedObservable(Layoutable.HeightProperty).Subscribe(Update);
- }
- protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
- {
- base.OnApplyTemplate(e);
- OuterCanvas = e.NameScope.Get<Canvas>("PART_ZoomCanvas");
- ZoomContent = e.NameScope.Get<ContentControl>("PART_ZoomContent");
- ZoomContentBorder = e.NameScope.Get<Border>("PART_ZoomContentBorder");
- OuterCanvas.LayoutUpdated += OuterCanvas_LayoutUpdated;
- OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEndedEvent, OuterCanvas_PinchEnded);
- OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEvent, OuterCanvas_Pinch);
- OuterCanvas.PointerWheelChanged += OuterCanvas_PointerWheelChanged;
- }
- private void OuterCanvas_PinchEnded(object? sender, PanAndZoomEndedEventArgs e)
- {
- _originalScaleFactor = ScaleFactor;
- }
- private void OuterCanvas_Pinch(object? sender, PanAndZoomEventArgs e)
- {
- Zoom(e.ScaleOrigin - e.Pan, e.ScaleOrigin, _originalScaleFactor * e.Scale);
- }
- private void Zoom(Point originalOrigin, Point newOrigin, double newScaleFactor)
- {
- // Convert Scale Origin to image coordinates (relative to center).
- // Work out where this position will move to under the new scaling.
- // Adjust so that these are the same.
- var pos = originalOrigin - ContentCentre;
- var contentMPos = pos / ScaleFactor;
- ScaleFactor = newScaleFactor;
- var scaledPos = ContentCentre + contentMPos * ScaleFactor;
- var offset = scaledPos - newOrigin;
- ContentCentre -= offset;
- UpdateCanvasPosition();
- }
- private void Pan(double x, double y)
- {
- ContentCentre += new Vector(x, y);
- UpdateCanvasPosition();
- }
- private void OuterCanvas_PointerWheelChanged(object? sender, PointerWheelEventArgs e)
- {
- if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
- {
- var pos = e.GetPosition(OuterCanvas);
- var wheelSpeed = _wheelSpeed;
- Zoom(pos, pos, e.Delta.Y > 0 ? ScaleFactor * (1 + e.Delta.Y * wheelSpeed) : ScaleFactor / (1 + (-e.Delta.Y) * wheelSpeed));
- }
- else if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
- {
- Pan(e.Delta.Y * _panSpeed, e.Delta.X * _panSpeed);
- }
- else
- {
- Pan(e.Delta.X * _panSpeed, e.Delta.Y * _panSpeed);
- }
- }
- private void OuterCanvas_LayoutUpdated(object? sender, EventArgs e)
- {
- // PositionImage();
- }
- protected override void OnLoaded(RoutedEventArgs e)
- {
- base.OnLoaded(e);
- PositionContent();
- }
- private void PositionContent()
- {
- var canvasWidth = OuterCanvas.Bounds.Width;
- var canvasHeight = OuterCanvas.Bounds.Height;
- var scaleFactor = Math.Min(canvasWidth / ContentWidth, canvasHeight / ContentHeight);
- ScaleFactor = scaleFactor;
- _originalScaleFactor = ScaleFactor;
- ContentCentre = new Point(
- OuterCanvas.Bounds.Width / 2,
- OuterCanvas.Bounds.Height / 2);
- UpdateCanvasPosition();
- }
- private void UpdateCanvasPosition()
- {
- var imageWidth = ContentWidth * ScaleFactor;
- var imageHeight = ContentHeight * ScaleFactor;
- ZoomContentBorder.Width = imageWidth;
- ZoomContentBorder.Height = imageHeight;
- Canvas.SetLeft(ZoomContentBorder, ContentCentre.X - imageWidth / 2);
- Canvas.SetTop(ZoomContentBorder, ContentCentre.Y - imageHeight / 2);
- ZoomContent.RenderTransform = new ScaleTransform(ScaleFactor, ScaleFactor);
- ZoomContent.Width = ContentWidth;
- ZoomContent.Height = ContentHeight;
- }
- }
|