ZoomPanel.cs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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.SizeChanged += OuterCanvas_SizeChanged;
  67. OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEndedEvent, OuterCanvas_PinchEnded);
  68. OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEvent, OuterCanvas_Pinch);
  69. OuterCanvas.PointerWheelChanged += OuterCanvas_PointerWheelChanged;
  70. if (IsLoaded)
  71. {
  72. PositionContent();
  73. }
  74. }
  75. private void OuterCanvas_SizeChanged(object? sender, SizeChangedEventArgs e)
  76. {
  77. if (IsLoaded)
  78. {
  79. PositionContent();
  80. }
  81. }
  82. private void OuterCanvas_PinchEnded(object? sender, PanAndZoomEndedEventArgs e)
  83. {
  84. _originalScaleFactor = ScaleFactor;
  85. }
  86. private void OuterCanvas_Pinch(object? sender, PanAndZoomEventArgs e)
  87. {
  88. Zoom(e.ScaleOrigin - e.Pan, e.ScaleOrigin, _originalScaleFactor * e.Scale);
  89. }
  90. private void Zoom(Point originalOrigin, Point newOrigin, double newScaleFactor)
  91. {
  92. // Convert Scale Origin to image coordinates (relative to center).
  93. // Work out where this position will move to under the new scaling.
  94. // Adjust so that these are the same.
  95. var pos = originalOrigin - ContentCentre;
  96. var contentMPos = pos / ScaleFactor;
  97. ScaleFactor = newScaleFactor;
  98. var scaledPos = ContentCentre + contentMPos * ScaleFactor;
  99. var offset = scaledPos - newOrigin;
  100. ContentCentre -= offset;
  101. UpdateCanvasPosition();
  102. }
  103. private void Pan(double x, double y)
  104. {
  105. ContentCentre += new Vector(x, y);
  106. UpdateCanvasPosition();
  107. }
  108. private void OuterCanvas_PointerWheelChanged(object? sender, PointerWheelEventArgs e)
  109. {
  110. if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
  111. {
  112. var pos = e.GetPosition(OuterCanvas);
  113. var wheelSpeed = _wheelSpeed;
  114. Zoom(pos, pos, e.Delta.Y > 0 ? ScaleFactor * (1 + e.Delta.Y * wheelSpeed) : ScaleFactor / (1 + (-e.Delta.Y) * wheelSpeed));
  115. }
  116. else if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
  117. {
  118. Pan(e.Delta.Y * _panSpeed, e.Delta.X * _panSpeed);
  119. }
  120. else
  121. {
  122. Pan(e.Delta.X * _panSpeed, e.Delta.Y * _panSpeed);
  123. }
  124. }
  125. private void OuterCanvas_LayoutUpdated(object? sender, EventArgs e)
  126. {
  127. // PositionImage();
  128. }
  129. protected override void OnLoaded(RoutedEventArgs e)
  130. {
  131. base.OnLoaded(e);
  132. if(OuterCanvas is not null)
  133. {
  134. PositionContent();
  135. }
  136. }
  137. private void PositionContent()
  138. {
  139. var canvasWidth = OuterCanvas.Bounds.Width;
  140. var canvasHeight = OuterCanvas.Bounds.Height;
  141. var scaleFactor = Math.Min(canvasWidth / ContentWidth, canvasHeight / ContentHeight);
  142. ScaleFactor = scaleFactor;
  143. _originalScaleFactor = ScaleFactor;
  144. ContentCentre = new Point(
  145. OuterCanvas.Bounds.Width / 2,
  146. OuterCanvas.Bounds.Height / 2);
  147. UpdateCanvasPosition();
  148. }
  149. private void UpdateCanvasPosition()
  150. {
  151. var imageWidth = ContentWidth * ScaleFactor;
  152. var imageHeight = ContentHeight * ScaleFactor;
  153. ZoomContentBorder.Width = imageWidth;
  154. ZoomContentBorder.Height = imageHeight;
  155. Canvas.SetLeft(ZoomContentBorder, ContentCentre.X - imageWidth / 2);
  156. Canvas.SetTop(ZoomContentBorder, ContentCentre.Y - imageHeight / 2);
  157. ZoomContent.RenderTransform = new ScaleTransform(ScaleFactor, ScaleFactor);
  158. ZoomContent.Width = ContentWidth;
  159. ZoomContent.Height = ContentHeight;
  160. }
  161. }