301 lines
13 KiB
C#
301 lines
13 KiB
C#
using System.Diagnostics;
|
||
using System.Linq;
|
||
|
||
namespace NotesDataAnalyst
|
||
{
|
||
public partial class GraphPage : ContentPage
|
||
{
|
||
private Point _initialPanPosition;
|
||
private PointF _initialNodePosition;
|
||
private PointF _initialTouchPosition;
|
||
private SizeF _initialNodeSize;
|
||
private readonly List<GraphNode> _nodes = new();
|
||
private readonly List<GraphConnection> _connections = new();
|
||
private GraphNode _selectedNode;
|
||
private bool _isScrollingBlocked = false;
|
||
|
||
private double _scale = 1.0;
|
||
private double _initialScale = 1.0;
|
||
private bool _isNodeBeingMoved = false;
|
||
private const double SmoothFactor = 0.85; // Коэффициент сглаживания
|
||
private Point _lastTranslation;
|
||
// Переменная для хранения центральной точки жеста (между двумя пальцами при старте масштабирования)
|
||
private Point _gestureCenter;
|
||
|
||
private Point _canvasTranslation = new Point(0, 0);
|
||
|
||
double currentScale = 1;
|
||
double startScale = 1;
|
||
double xOffset = 0;
|
||
double yOffset = 0;
|
||
|
||
public GraphPage()
|
||
{
|
||
InitializeComponent();
|
||
|
||
var graphDrawable = new GraphDrawable(_nodes, _connections);
|
||
canvasView.Drawable = graphDrawable;
|
||
|
||
var panGesture = new PanGestureRecognizer();
|
||
panGesture.PanUpdated += OnPanUpdated;
|
||
canvasView.GestureRecognizers.Add(panGesture);
|
||
|
||
var tapGesture = new TapGestureRecognizer();
|
||
tapGesture.Tapped += OnCanvasTapped;
|
||
canvasView.GestureRecognizers.Add(tapGesture);
|
||
|
||
// Добавляем Pinch только один раз
|
||
if (!canvasView.GestureRecognizers.OfType<PinchGestureRecognizer>().Any())
|
||
{
|
||
var pinchGesture = new PinchGestureRecognizer();
|
||
pinchGesture.PinchUpdated += OnPinchUpdated;
|
||
canvasView.GestureRecognizers.Add(pinchGesture);
|
||
}
|
||
|
||
|
||
}
|
||
|
||
|
||
// Обработчик жеста перемещения (Pan) для перемещения канвы или ноды
|
||
private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
|
||
{
|
||
if (_selectedNode != null)
|
||
{
|
||
// Начало перетаскивания
|
||
if (e.StatusType == GestureStatus.Started)
|
||
{
|
||
// Запоминаем начальную позицию касания и ноды
|
||
_initialTouchPosition = new Point(e.TotalX, e.TotalY);
|
||
_initialNodePosition = _selectedNode.Position;
|
||
}
|
||
// Перемещение
|
||
else if (e.StatusType == GestureStatus.Running)
|
||
{
|
||
var deltaX = e.TotalX - _initialTouchPosition.X; // Разница с начальной точкой
|
||
var deltaY = e.TotalY - _initialTouchPosition.Y;
|
||
|
||
// Перемещаем ноду относительно её начальной позиции
|
||
_selectedNode.Position = new Point(
|
||
_initialNodePosition.X + deltaX,
|
||
_initialNodePosition.Y + deltaY
|
||
);
|
||
|
||
canvasView.Invalidate(); // Перерисовываем канву
|
||
}
|
||
// Завершение перетаскивания
|
||
else if (e.StatusType == GestureStatus.Completed)
|
||
{
|
||
// Можно добавить логику на завершение перетаскивания, например, включить прокрутку
|
||
//scrollView.IsEnabled = true;
|
||
}
|
||
}
|
||
else // Перемещение канвы при снятии выделения с ноды
|
||
{
|
||
if (e.StatusType == GestureStatus.Started)
|
||
{
|
||
_initialPanPosition = new Point(canvasView.TranslationX - e.TotalX, canvasView.TranslationY - e.TotalY);
|
||
}
|
||
else if (e.StatusType == GestureStatus.Running)
|
||
{
|
||
var deltaX = e.TotalX;
|
||
var deltaY = e.TotalY;
|
||
|
||
// Применяем сглаживание к движению
|
||
double smoothedX = _lastTranslation.X + SmoothFactor * (deltaX - _lastTranslation.X);
|
||
double smoothedY = _lastTranslation.Y + SmoothFactor * (deltaY - _lastTranslation.Y);
|
||
|
||
// Обновляем положение канвы
|
||
canvasView.TranslationX = _initialPanPosition.X + smoothedX;
|
||
canvasView.TranslationY = _initialPanPosition.Y + smoothedY;
|
||
|
||
// Сохраняем последнюю смягченную позицию для следующего цикла
|
||
_lastTranslation = new Point(smoothedX, smoothedY);
|
||
}
|
||
else if (e.StatusType == GestureStatus.Completed)
|
||
{
|
||
_initialPanPosition = new Point(canvasView.TranslationX, canvasView.TranslationY);
|
||
canvasView.Invalidate();
|
||
}
|
||
|
||
}
|
||
}
|
||
void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
|
||
{
|
||
if (e.Status == GestureStatus.Started)
|
||
{
|
||
// Store the current scale factor applied to the wrapped user interface element,
|
||
// and zero the components for the center point of the translate transform.
|
||
startScale = Content.Scale;
|
||
Content.AnchorX = 0;
|
||
Content.AnchorY = 0;
|
||
}
|
||
if (e.Status == GestureStatus.Running)
|
||
{
|
||
// Calculate the scale factor to be applied.
|
||
currentScale += (e.Scale - 1) * startScale;
|
||
currentScale = Math.Max(1, currentScale);
|
||
|
||
// The ScaleOrigin is in relative coordinates to the wrapped user interface element,
|
||
// so get the X pixel coordinate.
|
||
double renderedX = Content.X + xOffset;
|
||
double deltaX = renderedX / Width;
|
||
double deltaWidth = Width / (Content.Width * startScale);
|
||
double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth;
|
||
|
||
// The ScaleOrigin is in relative coordinates to the wrapped user interface element,
|
||
// so get the Y pixel coordinate.
|
||
double renderedY = Content.Y + yOffset;
|
||
double deltaY = renderedY / Height;
|
||
double deltaHeight = Height / (Content.Height * startScale);
|
||
double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;
|
||
|
||
// Calculate the transformed element pixel coordinates.
|
||
double targetX = xOffset - (originX * Content.Width) * (currentScale - startScale);
|
||
double targetY = yOffset - (originY * Content.Height) * (currentScale - startScale);
|
||
|
||
// Apply translation based on the change in origin.
|
||
Content.TranslationX = Math.Clamp(targetX, -Content.Width * (currentScale - 1), 0);
|
||
Content.TranslationY = Math.Clamp(targetY, -Content.Height * (currentScale - 1), 0);
|
||
|
||
// Apply scale factor
|
||
Content.Scale = currentScale;
|
||
}
|
||
if (e.Status == GestureStatus.Completed)
|
||
{
|
||
// Store the translation delta's of the wrapped user interface element.
|
||
xOffset = Content.TranslationX;
|
||
yOffset = Content.TranslationY;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private async void OnEditTextClicked(object sender, EventArgs e)
|
||
{
|
||
// Проверяем, выбрана ли нода
|
||
if (_selectedNode == null)
|
||
{
|
||
await DisplayAlert("Ошибка", "Сначала выберите ноду", "OK");
|
||
return;
|
||
}
|
||
|
||
// Показываем поле для редактирования текста и заполняем его текущим текстом ноды
|
||
textEditor.IsVisible = true;
|
||
textEditor.Text = _selectedNode.Text;
|
||
|
||
// Ставим фокус на текстовое поле для ввода
|
||
textEditor.Focus();
|
||
}
|
||
|
||
// Обработчик завершения редактирования текста
|
||
private void OnTextEditorCompleted(object sender, EventArgs e)
|
||
{
|
||
if (_selectedNode != null)
|
||
{
|
||
// Обновляем текст ноды с введенным значением
|
||
_selectedNode.Text = textEditor.Text;
|
||
|
||
// Скрываем поле для редактирования текста
|
||
textEditor.IsVisible = false;
|
||
|
||
// Перерисовываем канву для обновления отображения текста
|
||
canvasView.Invalidate();
|
||
}
|
||
}
|
||
|
||
private async void OnResizeNodeClicked(object sender, EventArgs e)
|
||
{
|
||
// Проверяем, выбрана ли нода
|
||
if (_selectedNode == null)
|
||
{
|
||
await DisplayAlert("Ошибка", "Сначала выберите ноду", "OK");
|
||
return;
|
||
}
|
||
|
||
// Открываем диалог для ввода новых размеров
|
||
var result = await DisplayPromptAsync("Изменить размер", "Введите ширину и высоту",
|
||
initialValue: $"{_selectedNode.Size.Width} x {_selectedNode.Size.Height}",
|
||
keyboard: Keyboard.Numeric);
|
||
|
||
if (string.IsNullOrEmpty(result)) return;
|
||
|
||
// Разбиваем введенное значение на ширину и высоту
|
||
var dimensions = result.Split('x');
|
||
if (dimensions.Length == 2 &&
|
||
double.TryParse(dimensions[0].Trim(), out double width) &&
|
||
double.TryParse(dimensions[1].Trim(), out double height))
|
||
{
|
||
// Обновляем размеры ноды
|
||
_selectedNode.Size = new Size(width, height);
|
||
canvasView.Invalidate(); // Перерисовываем канву
|
||
}
|
||
else
|
||
{
|
||
await DisplayAlert("Ошибка", "Неверный формат. Введите значения в формате ширина x высота.", "OK");
|
||
}
|
||
}
|
||
|
||
private async void OnAddNodeClicked(object sender, EventArgs e)
|
||
{
|
||
var newNode = new GraphNode
|
||
{
|
||
Text = "Новая нода",
|
||
Position = new Point(100, 200),
|
||
Size = new Size(100, 50),
|
||
};
|
||
|
||
_nodes.Add(newNode);
|
||
canvasView.Invalidate();
|
||
}
|
||
|
||
private void OnChildClicked(object sender, EventArgs e)
|
||
{
|
||
if (_selectedNode != null)
|
||
{
|
||
var childNode = new GraphNode
|
||
{
|
||
Text = "Child Node",
|
||
Position = new Point(_selectedNode.Position.X + 100, _selectedNode.Position.Y + 70),
|
||
Size = _selectedNode.Size,
|
||
BackgroundColor = Colors.Gray,
|
||
};
|
||
_nodes.Add(childNode);
|
||
_connections.Add(new GraphConnection { FromNode = _selectedNode, ToNode = childNode });
|
||
canvasView.Invalidate();
|
||
_selectedNode = childNode;
|
||
}
|
||
else
|
||
{
|
||
Debug.WriteLine("Сначала выберите ноду");
|
||
}
|
||
}
|
||
|
||
private void OnCanvasTapped(object sender, TappedEventArgs e)
|
||
{
|
||
var tapPosition = e.GetPosition((View)sender);
|
||
_selectedNode = null;
|
||
|
||
if (tapPosition.HasValue)
|
||
{
|
||
var touchPoint = new PointF((float)tapPosition.Value.X, (float)tapPosition.Value.Y);
|
||
_selectedNode = _nodes.FirstOrDefault(node => IsPointInsideNode(touchPoint, node));
|
||
|
||
foreach (var node in _nodes)
|
||
node.IsActive = node == _selectedNode;
|
||
|
||
canvasView.Invalidate();
|
||
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private bool IsPointInsideNode(PointF point, GraphNode node)
|
||
{
|
||
return point.X >= node.Position.X &&
|
||
point.X <= node.Position.X + node.Size.Width &&
|
||
point.Y >= node.Position.Y &&
|
||
point.Y <= node.Position.Y + node.Size.Height;
|
||
}
|
||
}
|
||
}
|