470 lines
12 KiB
C#
470 lines
12 KiB
C#
// Licensed to the .NET Foundation under one or more agreements.
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
using SkiaSharp;
|
|
|
|
namespace Microsoft.Maui.Platform;
|
|
|
|
/// <summary>
|
|
/// A view that supports swipe gestures to reveal actions.
|
|
/// </summary>
|
|
public class SkiaSwipeView : SkiaLayoutView
|
|
{
|
|
private SkiaView? _content;
|
|
private readonly List<SwipeItem> _leftItems = new();
|
|
private readonly List<SwipeItem> _rightItems = new();
|
|
private readonly List<SwipeItem> _topItems = new();
|
|
private readonly List<SwipeItem> _bottomItems = new();
|
|
|
|
private float _swipeOffset = 0f;
|
|
private SwipeDirection _activeDirection = SwipeDirection.None;
|
|
private bool _isSwiping = false;
|
|
private float _swipeStartX;
|
|
private float _swipeStartY;
|
|
private float _swipeStartOffset;
|
|
private bool _isOpen = false;
|
|
|
|
private const float SwipeThreshold = 60f;
|
|
private const float VelocityThreshold = 500f;
|
|
private float _velocity;
|
|
private DateTime _lastMoveTime;
|
|
private float _lastMovePosition;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the content view.
|
|
/// </summary>
|
|
public SkiaView? Content
|
|
{
|
|
get => _content;
|
|
set
|
|
{
|
|
if (_content != value)
|
|
{
|
|
if (_content != null)
|
|
{
|
|
RemoveChild(_content);
|
|
}
|
|
|
|
_content = value;
|
|
|
|
if (_content != null)
|
|
{
|
|
AddChild(_content);
|
|
}
|
|
|
|
InvalidateMeasure();
|
|
Invalidate();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the left swipe items.
|
|
/// </summary>
|
|
public IList<SwipeItem> LeftItems => _leftItems;
|
|
|
|
/// <summary>
|
|
/// Gets the right swipe items.
|
|
/// </summary>
|
|
public IList<SwipeItem> RightItems => _rightItems;
|
|
|
|
/// <summary>
|
|
/// Gets the top swipe items.
|
|
/// </summary>
|
|
public IList<SwipeItem> TopItems => _topItems;
|
|
|
|
/// <summary>
|
|
/// Gets the bottom swipe items.
|
|
/// </summary>
|
|
public IList<SwipeItem> BottomItems => _bottomItems;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the swipe mode.
|
|
/// </summary>
|
|
public SwipeMode Mode { get; set; } = SwipeMode.Reveal;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the left swipe threshold.
|
|
/// </summary>
|
|
public float LeftSwipeThreshold { get; set; } = 100f;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the right swipe threshold.
|
|
/// </summary>
|
|
public float RightSwipeThreshold { get; set; } = 100f;
|
|
|
|
/// <summary>
|
|
/// Event raised when swipe is started.
|
|
/// </summary>
|
|
public event EventHandler<SwipeStartedEventArgs>? SwipeStarted;
|
|
|
|
/// <summary>
|
|
/// Event raised when swipe ends.
|
|
/// </summary>
|
|
public event EventHandler<SwipeEndedEventArgs>? SwipeEnded;
|
|
|
|
/// <summary>
|
|
/// Opens the swipe view in the specified direction.
|
|
/// </summary>
|
|
public void Open(SwipeDirection direction)
|
|
{
|
|
_activeDirection = direction;
|
|
_isOpen = true;
|
|
|
|
float targetOffset = direction switch
|
|
{
|
|
SwipeDirection.Left => -RightSwipeThreshold,
|
|
SwipeDirection.Right => LeftSwipeThreshold,
|
|
_ => 0
|
|
};
|
|
|
|
AnimateTo(targetOffset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the swipe view.
|
|
/// </summary>
|
|
public void Close()
|
|
{
|
|
_isOpen = false;
|
|
AnimateTo(0);
|
|
}
|
|
|
|
private void AnimateTo(float target)
|
|
{
|
|
// Simple animation - in production would use proper animation
|
|
_swipeOffset = target;
|
|
Invalidate();
|
|
}
|
|
|
|
protected override SKSize MeasureOverride(SKSize availableSize)
|
|
{
|
|
if (_content != null)
|
|
{
|
|
_content.Measure(availableSize);
|
|
}
|
|
return availableSize;
|
|
}
|
|
|
|
protected override SKRect ArrangeOverride(SKRect bounds)
|
|
{
|
|
if (_content != null)
|
|
{
|
|
var contentBounds = new SKRect(
|
|
bounds.Left + _swipeOffset,
|
|
bounds.Top,
|
|
bounds.Right + _swipeOffset,
|
|
bounds.Bottom);
|
|
_content.Arrange(contentBounds);
|
|
}
|
|
return bounds;
|
|
}
|
|
|
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
|
{
|
|
canvas.Save();
|
|
canvas.ClipRect(bounds);
|
|
|
|
// Draw swipe items behind content
|
|
if (_swipeOffset > 0)
|
|
{
|
|
DrawSwipeItems(canvas, bounds, _leftItems, true);
|
|
}
|
|
else if (_swipeOffset < 0)
|
|
{
|
|
DrawSwipeItems(canvas, bounds, _rightItems, false);
|
|
}
|
|
|
|
// Draw content
|
|
_content?.Draw(canvas);
|
|
|
|
canvas.Restore();
|
|
}
|
|
|
|
private void DrawSwipeItems(SKCanvas canvas, SKRect bounds, List<SwipeItem> items, bool isLeft)
|
|
{
|
|
if (items.Count == 0) return;
|
|
|
|
float revealWidth = Math.Abs(_swipeOffset);
|
|
float itemWidth = revealWidth / items.Count;
|
|
|
|
for (int i = 0; i < items.Count; i++)
|
|
{
|
|
var item = items[i];
|
|
float x = isLeft ? bounds.Left + i * itemWidth : bounds.Right - (items.Count - i) * itemWidth;
|
|
|
|
var itemBounds = new SKRect(
|
|
x,
|
|
bounds.Top,
|
|
x + itemWidth,
|
|
bounds.Bottom);
|
|
|
|
// Draw background
|
|
using var bgPaint = new SKPaint
|
|
{
|
|
Color = item.BackgroundColor,
|
|
Style = SKPaintStyle.Fill
|
|
};
|
|
canvas.DrawRect(itemBounds, bgPaint);
|
|
|
|
// Draw icon or text
|
|
if (!string.IsNullOrEmpty(item.Text))
|
|
{
|
|
using var textPaint = new SKPaint
|
|
{
|
|
Color = item.TextColor,
|
|
TextSize = 14f,
|
|
IsAntialias = true,
|
|
TextAlign = SKTextAlign.Center
|
|
};
|
|
|
|
float textY = itemBounds.MidY + 5;
|
|
canvas.DrawText(item.Text, itemBounds.MidX, textY, textPaint);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override SkiaView? HitTest(float x, float y)
|
|
{
|
|
if (!IsVisible || !Bounds.Contains(x, y)) return null;
|
|
|
|
// Check if hit is on swipe items
|
|
if (_isOpen)
|
|
{
|
|
if (_swipeOffset > 0 && x < Bounds.Left + _swipeOffset)
|
|
{
|
|
return this; // Hit on left items
|
|
}
|
|
else if (_swipeOffset < 0 && x > Bounds.Right + _swipeOffset)
|
|
{
|
|
return this; // Hit on right items
|
|
}
|
|
}
|
|
|
|
if (_content != null)
|
|
{
|
|
var hit = _content.HitTest(x, y);
|
|
if (hit != null) return hit;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
public override void OnPointerPressed(PointerEventArgs e)
|
|
{
|
|
if (!IsEnabled) return;
|
|
|
|
// Check for swipe item tap when open
|
|
if (_isOpen)
|
|
{
|
|
SwipeItem? tappedItem = null;
|
|
|
|
if (_swipeOffset > 0)
|
|
{
|
|
int index = (int)((e.X - Bounds.Left) / (_swipeOffset / _leftItems.Count));
|
|
if (index >= 0 && index < _leftItems.Count)
|
|
{
|
|
tappedItem = _leftItems[index];
|
|
}
|
|
}
|
|
else if (_swipeOffset < 0)
|
|
{
|
|
float itemWidth = Math.Abs(_swipeOffset) / _rightItems.Count;
|
|
int index = (int)((e.X - (Bounds.Right + _swipeOffset)) / itemWidth);
|
|
if (index >= 0 && index < _rightItems.Count)
|
|
{
|
|
tappedItem = _rightItems[index];
|
|
}
|
|
}
|
|
|
|
if (tappedItem != null)
|
|
{
|
|
tappedItem.OnInvoked();
|
|
Close();
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
_isSwiping = true;
|
|
_swipeStartX = e.X;
|
|
_swipeStartY = e.Y;
|
|
_swipeStartOffset = _swipeOffset;
|
|
_lastMovePosition = e.X;
|
|
_lastMoveTime = DateTime.UtcNow;
|
|
_velocity = 0;
|
|
|
|
base.OnPointerPressed(e);
|
|
}
|
|
|
|
public override void OnPointerMoved(PointerEventArgs e)
|
|
{
|
|
if (!_isSwiping) return;
|
|
|
|
float deltaX = e.X - _swipeStartX;
|
|
float deltaY = e.Y - _swipeStartY;
|
|
|
|
// Determine swipe direction
|
|
if (_activeDirection == SwipeDirection.None)
|
|
{
|
|
if (Math.Abs(deltaX) > 10)
|
|
{
|
|
_activeDirection = deltaX > 0 ? SwipeDirection.Right : SwipeDirection.Left;
|
|
SwipeStarted?.Invoke(this, new SwipeStartedEventArgs(_activeDirection));
|
|
}
|
|
}
|
|
|
|
if (_activeDirection == SwipeDirection.Right || _activeDirection == SwipeDirection.Left)
|
|
{
|
|
_swipeOffset = _swipeStartOffset + deltaX;
|
|
|
|
// Clamp offset based on available items
|
|
float maxRight = _leftItems.Count > 0 ? LeftSwipeThreshold : 0;
|
|
float maxLeft = _rightItems.Count > 0 ? -RightSwipeThreshold : 0;
|
|
_swipeOffset = Math.Clamp(_swipeOffset, maxLeft, maxRight);
|
|
|
|
// Calculate velocity
|
|
var now = DateTime.UtcNow;
|
|
float timeDelta = (float)(now - _lastMoveTime).TotalSeconds;
|
|
if (timeDelta > 0)
|
|
{
|
|
_velocity = (e.X - _lastMovePosition) / timeDelta;
|
|
}
|
|
_lastMovePosition = e.X;
|
|
_lastMoveTime = now;
|
|
|
|
Invalidate();
|
|
e.Handled = true;
|
|
}
|
|
|
|
base.OnPointerMoved(e);
|
|
}
|
|
|
|
public override void OnPointerReleased(PointerEventArgs e)
|
|
{
|
|
if (!_isSwiping) return;
|
|
|
|
_isSwiping = false;
|
|
|
|
// Determine final state
|
|
bool shouldOpen = false;
|
|
|
|
if (Math.Abs(_velocity) > VelocityThreshold)
|
|
{
|
|
// Use velocity
|
|
shouldOpen = (_velocity > 0 && _leftItems.Count > 0) || (_velocity < 0 && _rightItems.Count > 0);
|
|
}
|
|
else
|
|
{
|
|
// Use threshold
|
|
shouldOpen = Math.Abs(_swipeOffset) > SwipeThreshold;
|
|
}
|
|
|
|
if (shouldOpen)
|
|
{
|
|
if (_swipeOffset > 0)
|
|
{
|
|
Open(SwipeDirection.Right);
|
|
}
|
|
else
|
|
{
|
|
Open(SwipeDirection.Left);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Close();
|
|
}
|
|
|
|
SwipeEnded?.Invoke(this, new SwipeEndedEventArgs(_activeDirection, _isOpen));
|
|
_activeDirection = SwipeDirection.None;
|
|
|
|
base.OnPointerReleased(e);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a swipe action item.
|
|
/// </summary>
|
|
public class SwipeItem
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the text.
|
|
/// </summary>
|
|
public string Text { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the icon source.
|
|
/// </summary>
|
|
public string? IconSource { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the background color.
|
|
/// </summary>
|
|
public SKColor BackgroundColor { get; set; } = new SKColor(33, 150, 243);
|
|
|
|
/// <summary>
|
|
/// Gets or sets the text color.
|
|
/// </summary>
|
|
public SKColor TextColor { get; set; } = SKColors.White;
|
|
|
|
/// <summary>
|
|
/// Event raised when the item is invoked.
|
|
/// </summary>
|
|
public event EventHandler? Invoked;
|
|
|
|
internal void OnInvoked()
|
|
{
|
|
Invoked?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Swipe direction.
|
|
/// </summary>
|
|
public enum SwipeDirection
|
|
{
|
|
None,
|
|
Left,
|
|
Right,
|
|
Up,
|
|
Down
|
|
}
|
|
|
|
/// <summary>
|
|
/// Swipe mode.
|
|
/// </summary>
|
|
public enum SwipeMode
|
|
{
|
|
Reveal,
|
|
Execute
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event args for swipe started.
|
|
/// </summary>
|
|
public class SwipeStartedEventArgs : EventArgs
|
|
{
|
|
public SwipeDirection Direction { get; }
|
|
|
|
public SwipeStartedEventArgs(SwipeDirection direction)
|
|
{
|
|
Direction = direction;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event args for swipe ended.
|
|
/// </summary>
|
|
public class SwipeEndedEventArgs : EventArgs
|
|
{
|
|
public SwipeDirection Direction { get; }
|
|
public bool IsOpen { get; }
|
|
|
|
public SwipeEndedEventArgs(SwipeDirection direction, bool isOpen)
|
|
{
|
|
Direction = direction;
|
|
IsOpen = isOpen;
|
|
}
|
|
}
|