ZoomPanel.cs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. using Avalonia;
  2. using Avalonia.Controls;
  3. using Avalonia.Controls.Metadata;
  4. using Avalonia.Controls.Primitives;
  5. using Avalonia.Input;
  6. using Avalonia.Interactivity;
  7. using Avalonia.Layout;
  8. using Avalonia.Markup.Xaml;
  9. using Avalonia.Media;
  10. using Avalonia.Metadata;
  11. using System;
  12. namespace InABox.Avalonia.Components;
  13. /// <summary>
  14. /// 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"/>,
  15. /// and its <see cref="Layoutable.Width"/> and <see cref="Layoutable.Height"/> <b>must</b> be set.
  16. /// </summary>
  17. [TemplatePart("PART_ZoomContent", typeof(ContentControl))]
  18. [TemplatePart("PART_ZoomCanvas", typeof(Canvas))]
  19. [TemplatePart("PART_ZoomContentBorder", typeof(Border))]
  20. public partial class ZoomPanel : TemplatedControl
  21. {
  22. public static readonly StyledProperty<Layoutable?> ContentProperty =
  23. AvaloniaProperty.Register<ZoomPanel, Layoutable?>(nameof(Content));
  24. [Content]
  25. public Layoutable? Content
  26. {
  27. get => GetValue(ContentProperty);
  28. set => SetValue(ContentProperty, value);
  29. }
  30. private double ContentWidth => Content?.Width ?? 1;
  31. private double ContentHeight => Content?.Height ?? 1;
  32. private Canvas OuterCanvas = null!;
  33. private ContentControl ZoomContent = null!;
  34. private Border ZoomContentBorder = null!;
  35. private double ScaleFactor = 1.0;
  36. private double _originalScaleFactor = 1.0;
  37. private const double _wheelSpeed = 0.1;
  38. private const double _panSpeed = 30;
  39. // Center of the image.
  40. private Point ContentCentre = new();
  41. public ZoomPanel()
  42. {
  43. this.GetPropertyChangedObservable(ContentProperty).Subscribe(ContentChanged);
  44. }
  45. private void ContentChanged(AvaloniaPropertyChangedEventArgs args)
  46. {
  47. if(Content is null) return;
  48. void Update(AvaloniaPropertyChangedEventArgs? args = null)
  49. {
  50. if(OuterCanvas is not null)
  51. {
  52. PositionContent();
  53. }
  54. }
  55. Update();
  56. Content.GetPropertyChangedObservable(Layoutable.WidthProperty).Subscribe(Update);
  57. Content.GetPropertyChangedObservable(Layoutable.HeightProperty).Subscribe(Update);
  58. }
  59. protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
  60. {
  61. base.OnApplyTemplate(e);
  62. OuterCanvas = e.NameScope.Get<Canvas>("PART_ZoomCanvas");
  63. ZoomContent = e.NameScope.Get<ContentControl>("PART_ZoomContent");
  64. ZoomContentBorder = e.NameScope.Get<Border>("PART_ZoomContentBorder");
  65. OuterCanvas.LayoutUpdated += OuterCanvas_LayoutUpdated;
  66. OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEndedEvent, OuterCanvas_PinchEnded);
  67. OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEvent, OuterCanvas_Pinch);
  68. OuterCanvas.PointerWheelChanged += OuterCanvas_PointerWheelChanged;
  69. }
  70. private void OuterCanvas_PinchEnded(object? sender, PanAndZoomEndedEventArgs e)
  71. {
  72. _originalScaleFactor = ScaleFactor;
  73. }
  74. private void OuterCanvas_Pinch(object? sender, PanAndZoomEventArgs e)
  75. {
  76. Zoom(e.ScaleOrigin - e.Pan, e.ScaleOrigin, _originalScaleFactor * e.Scale);
  77. }
  78. private void Zoom(Point originalOrigin, Point newOrigin, double newScaleFactor)
  79. {
  80. // Convert Scale Origin to image coordinates (relative to center).
  81. // Work out where this position will move to under the new scaling.
  82. // Adjust so that these are the same.
  83. var pos = originalOrigin - ContentCentre;
  84. var contentMPos = pos / ScaleFactor;
  85. ScaleFactor = newScaleFactor;
  86. var scaledPos = ContentCentre + contentMPos * ScaleFactor;
  87. var offset = scaledPos - newOrigin;
  88. ContentCentre -= offset;
  89. UpdateCanvasPosition();
  90. }
  91. private void Pan(double x, double y)
  92. {
  93. ContentCentre += new Vector(x, y);
  94. UpdateCanvasPosition();
  95. }
  96. private void OuterCanvas_PointerWheelChanged(object? sender, PointerWheelEventArgs e)
  97. {
  98. if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
  99. {
  100. var pos = e.GetPosition(OuterCanvas);
  101. var wheelSpeed = _wheelSpeed;
  102. Zoom(pos, pos, e.Delta.Y > 0 ? ScaleFactor * (1 + e.Delta.Y * wheelSpeed) : ScaleFactor / (1 + (-e.Delta.Y) * wheelSpeed));
  103. }
  104. else if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
  105. {
  106. Pan(e.Delta.Y * _panSpeed, e.Delta.X * _panSpeed);
  107. }
  108. else
  109. {
  110. Pan(e.Delta.X * _panSpeed, e.Delta.Y * _panSpeed);
  111. }
  112. }
  113. private void OuterCanvas_LayoutUpdated(object? sender, EventArgs e)
  114. {
  115. // PositionImage();
  116. }
  117. protected override void OnLoaded(RoutedEventArgs e)
  118. {
  119. base.OnLoaded(e);
  120. PositionContent();
  121. }
  122. private void PositionContent()
  123. {
  124. var canvasWidth = OuterCanvas.Bounds.Width;
  125. var canvasHeight = OuterCanvas.Bounds.Height;
  126. var scaleFactor = Math.Min(canvasWidth / ContentWidth, canvasHeight / ContentHeight);
  127. ScaleFactor = scaleFactor;
  128. _originalScaleFactor = ScaleFactor;
  129. ContentCentre = new Point(
  130. OuterCanvas.Bounds.Width / 2,
  131. OuterCanvas.Bounds.Height / 2);
  132. UpdateCanvasPosition();
  133. }
  134. private void UpdateCanvasPosition()
  135. {
  136. var imageWidth = ContentWidth * ScaleFactor;
  137. var imageHeight = ContentHeight * ScaleFactor;
  138. ZoomContentBorder.Width = imageWidth;
  139. ZoomContentBorder.Height = imageHeight;
  140. Canvas.SetLeft(ZoomContentBorder, ContentCentre.X - imageWidth / 2);
  141. Canvas.SetTop(ZoomContentBorder, ContentCentre.Y - imageHeight / 2);
  142. ZoomContent.RenderTransform = new ScaleTransform(ScaleFactor, ScaleFactor);
  143. ZoomContent.Width = ContentWidth;
  144. ZoomContent.Height = ContentHeight;
  145. }
  146. }