Created
December 31, 2021 09:38
-
-
Save bbenetskyy/b271666a6824f8dfbf299d3badf4e9f5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class DraggableView : ContentView, IDisposable | |
{ | |
public static readonly BindableProperty NewXProperty = BindableProperty.Create( | |
propertyName: nameof(NewX), | |
returnType: typeof(double), | |
declaringType: typeof(DraggableView)); | |
public double NewX | |
{ | |
get => (double)GetValue(NewXProperty); | |
set => SetValue(NewXProperty, value); | |
} | |
public static readonly BindableProperty NewYProperty = BindableProperty.Create( | |
propertyName: nameof(NewY), | |
returnType: typeof(double), | |
declaringType: typeof(DraggableView)); | |
public double NewY | |
{ | |
get => (double)GetValue(NewYProperty); | |
set => SetValue(NewYProperty, value); | |
} | |
public static readonly BindableProperty CommandProperty = BindableProperty.Create( | |
nameof(Command), | |
typeof(ICommand), | |
typeof(DraggableView)); | |
public ICommand Command | |
{ | |
get => (ICommand)GetValue(CommandProperty); | |
set => SetValue(CommandProperty, value); | |
} | |
public static readonly BindableProperty CommandParamProperty = BindableProperty.Create( | |
nameof(CommandParam), | |
typeof(object), | |
typeof(DraggableView)); | |
public object CommandParam | |
{ | |
get => GetValue(CommandParamProperty); | |
set => SetValue(CommandParamProperty, value); | |
} | |
public DraggableArea LimitArea { get; set; } | |
protected override void OnParentSet() | |
{ | |
base.OnParentSet(); | |
if (Parent is View parent) | |
{ | |
parent.SizeChanged += Parent_SizeChanged; | |
} | |
} | |
private void Parent_SizeChanged(object sender, EventArgs e) | |
{ | |
if (sender is View {Height: > 0, Width: > 0} parent) | |
{ | |
LimitArea = new DraggableArea(parent.X, parent.Y, | |
parent.Width, | |
parent.Height); | |
} | |
} | |
public void Dispose() | |
{ | |
if (Parent is View parent) | |
{ | |
parent.SizeChanged -= Parent_SizeChanged; | |
} | |
} | |
} | |
public class DraggableArea | |
{ | |
public DraggableArea(double x, | |
double y, | |
double width, | |
double height) | |
{ | |
X = x; | |
Y = y; | |
Width = width; | |
Height = height; | |
} | |
public double X { get; } | |
public double Y { get; } | |
public double Width { get; } | |
public double Height { get; } | |
} | |
//Android | |
[assembly: ExportRenderer(typeof(DraggableView), typeof(DraggableViewRenderer))] | |
public class DraggableViewRenderer : VisualElementRenderer<Xamarin.Forms.View> | |
{ | |
#region Fields | |
private CancellationTokenSource _throttleCts = new(); | |
private float _deltaX; | |
private float _deltaY; | |
private bool _touchedDown; | |
private DraggableView _draggableView; | |
#endregion Fields | |
#region Constructors | |
public DraggableViewRenderer(Context context) : base(context) | |
{ | |
_touchedDown = false; | |
} | |
#endregion Constructors | |
#region Protected Methods | |
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.View> e) | |
{ | |
base.OnElementChanged(e); | |
if (e.OldElement != null) | |
{ | |
LongClick -= HandleLongClick; | |
Click -= HandleOnClick; | |
_draggableView = null; | |
} | |
if (e.NewElement != null) | |
{ | |
LongClick += HandleLongClick; | |
if(e.NewElement is DraggableView draggableView) | |
{ | |
_draggableView = draggableView; | |
Click += HandleOnClick; | |
} | |
} | |
} | |
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) | |
{ | |
base.OnElementPropertyChanged(sender, e); | |
if (e.PropertyName == Xamarin.Forms.VisualElement.XProperty.PropertyName || e.PropertyName == Xamarin.Forms.VisualElement.YProperty.PropertyName) | |
{ | |
CancellationToken(() => | |
{ | |
var draggableView = Element as DraggableView; | |
if(draggableView is null) | |
return; | |
Element.TranslationX = draggableView.NewX; | |
Element.TranslationY = draggableView.NewY; | |
}); | |
} | |
} | |
#endregion Protected Methods | |
#region Public Methods | |
public override bool OnTouchEvent(MotionEvent e) | |
{ | |
var draggView = Element as DraggableView; | |
float x = e.RawX; | |
float y = e.RawY; | |
switch (e.Action) | |
{ | |
case MotionEventActions.Down: | |
_deltaX = x - GetX(); | |
_deltaY = y - GetY(); | |
break; | |
case MotionEventActions.Move: | |
if (_touchedDown) | |
{ | |
var newX = x - _deltaX; | |
var newY = y - _deltaY; | |
var limitArea = draggView.LimitArea; | |
var density = Xamarin.Essentials.DeviceDisplay.MainDisplayInfo.Density; | |
if (limitArea == null) | |
{ | |
SetX(newX); | |
SetY(newY); | |
} | |
else | |
{ | |
var width = Element.Width * density; | |
var height = Element.Height * density; | |
var limitWidth = limitArea.Width * density; | |
var limitHeight = limitArea.Height * density; | |
var limitX = limitArea.X * density; | |
var limitY = limitArea.Y * density; | |
var resX = newX; | |
var resY = newY; | |
if (Math.Max(limitX, resX) > limitX | |
&& Math.Min(limitX + limitWidth, resX + width) < limitX + limitWidth) | |
SetX(resX); | |
if (Math.Max(limitY, resY) > limitY | |
&& Math.Min(limitY + limitHeight, resY + height) < limitY + limitHeight) | |
SetY(newY); | |
} | |
draggView.NewX = newX / density; | |
draggView.NewY = newY / density; | |
} | |
break; | |
case MotionEventActions.Up: | |
_touchedDown = false; | |
break; | |
case MotionEventActions.Cancel: | |
_touchedDown = false; | |
break; | |
} | |
return base.OnTouchEvent(e); | |
} | |
#endregion Public Methods | |
#region Private Methods | |
private void HandleLongClick(object sender, LongClickEventArgs e) | |
{ | |
_touchedDown = true; | |
} | |
private void HandleOnClick(object sender, EventArgs e) | |
{ | |
_draggableView?.Command?.Execute(_draggableView?.CommandParam); | |
} | |
private void CancellationToken(Action action) | |
{ | |
if (action != null) | |
{ | |
Interlocked.Exchange(ref _throttleCts, new CancellationTokenSource()).Cancel(); | |
Task.Delay(TimeSpan.FromMilliseconds(100), _throttleCts.Token) // throttle time | |
.ContinueWith( | |
delegate | |
{ | |
action(); | |
}, | |
System.Threading.CancellationToken.None, | |
TaskContinuationOptions.OnlyOnRanToCompletion, | |
TaskScheduler.FromCurrentSynchronizationContext()); | |
} | |
} | |
#endregion Private Methods | |
} | |
//iOS | |
[assembly: ExportRenderer(typeof(DraggableView), typeof(DraggableViewRenderer))] | |
public class DraggableViewRenderer : VisualElementRenderer<View> | |
{ | |
#region Fields | |
private CancellationTokenSource _throttleCts = new(); | |
private bool _longPress; | |
private bool _firstTime = true; | |
private double _lastTimeStamp; | |
private double _translatedXPos; | |
private double _translatedYPos; | |
private UIPanGestureRecognizer _panGesture; | |
private UIGestureRecognizer.Token _panGestureToken; | |
private DraggableView _draggableView; | |
private UITapGestureRecognizer _tapGesture; | |
private UIGestureRecognizer.Token _tapGestureToken; | |
#endregion Fields | |
#region Public Methods | |
public override void TouchesBegan(NSSet touches, UIEvent evt) | |
{ | |
base.TouchesBegan(touches, evt); | |
_lastTimeStamp = evt.Timestamp; | |
Superview.BringSubviewToFront(this); | |
} | |
public override void TouchesMoved(NSSet touches, UIEvent evt) | |
{ | |
if (evt.Timestamp - _lastTimeStamp >= 0.5) | |
{ | |
_longPress = true; | |
} | |
base.TouchesMoved(touches, evt); | |
} | |
#endregion Public Methods | |
#region Protected Methods | |
protected override void OnElementChanged(ElementChangedEventArgs<View> e) | |
{ | |
base.OnElementChanged(e); | |
if (e.OldElement != null) | |
{ | |
RemoveGestureRecognizer(_panGesture); | |
_panGesture.RemoveTarget(_panGestureToken); | |
RemoveGestureRecognizer(_tapGesture); | |
_tapGesture.RemoveTarget(_tapGestureToken); | |
_draggableView = null; | |
} | |
if (e.NewElement != null) | |
{ | |
_draggableView = Element as DraggableView; | |
_panGesture = new UIPanGestureRecognizer(); | |
_panGestureToken = _panGesture.AddTarget(DetectPan); | |
AddGestureRecognizer(_panGesture); | |
_tapGesture = new UITapGestureRecognizer(); | |
_tapGestureToken = _tapGesture.AddTarget(DetectTap); | |
AddGestureRecognizer(_tapGesture); | |
} | |
} | |
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) | |
{ | |
base.OnElementPropertyChanged(sender, e); | |
if (e.PropertyName == VisualElement.XProperty.PropertyName || e.PropertyName == VisualElement.YProperty.PropertyName) | |
{ | |
CancellationToken(() => | |
{ | |
if (_draggableView is null) | |
return; | |
Element.TranslationX = _draggableView.NewX; | |
Element.TranslationY = _draggableView.NewY; | |
}); | |
} | |
} | |
#endregion Protected Methods | |
#region Private Methods | |
private void DetectPan() | |
{ | |
if (_longPress) | |
{ | |
if (_panGesture.State == UIGestureRecognizerState.Began) | |
{ | |
_panGesture.Reset(); | |
if (_firstTime) | |
{ | |
_firstTime = false; | |
} | |
// The Pan has just started, but we need the relative translation | |
// The view currently has applied | |
_translatedXPos = Element.TranslationX; | |
_translatedYPos = Element.TranslationY; | |
} | |
var translation = _panGesture.TranslationInView(Superview); | |
// Set the total translation based on the original state and the current translation | |
var dragView = Element as DraggableView; | |
var limitArea = dragView.LimitArea; | |
var translationX = _translatedXPos + translation.X; | |
var translationY = _translatedYPos + translation.Y; | |
if (limitArea == null) | |
{ | |
Element.TranslationX = translationX; | |
Element.TranslationY = translationY; | |
} | |
else | |
{ | |
var resX = Element.X + translationX; | |
var resY = Element.Y + translationY; | |
if (Math.Max(limitArea.X, resX) > limitArea.X | |
&& Math.Min(limitArea.X + limitArea.Width, resX + Element.Width) < limitArea.X + limitArea.Width) | |
Element.TranslationX = translationX; | |
if (Math.Max(limitArea.Y, resY) > limitArea.Y | |
&& Math.Min(limitArea.Y + limitArea.Height, resY + Element.Height) < limitArea.Y + limitArea.Height) | |
Element.TranslationY = translationY; | |
} | |
if (_panGesture.State == UIGestureRecognizerState.Ended) | |
{ | |
_longPress = false; | |
var draggableView = Element as DraggableView; | |
if (draggableView is null) | |
return; | |
draggableView.NewX = Element.TranslationX; | |
draggableView.NewY = Element.TranslationY; | |
} | |
} | |
} | |
private void DetectTap() | |
{ | |
if (!_longPress) | |
{ | |
_draggableView?.Command?.Execute(_draggableView?.CommandParam); | |
} | |
} | |
private void CancellationToken(Action action) | |
{ | |
if (action != null) | |
{ | |
Interlocked.Exchange(ref _throttleCts, new CancellationTokenSource()).Cancel(); | |
Task.Delay(TimeSpan.FromMilliseconds(100), _throttleCts.Token) // throttle time | |
.ContinueWith( | |
delegate | |
{ | |
action(); | |
}, | |
System.Threading.CancellationToken.None, | |
TaskContinuationOptions.OnlyOnRanToCompletion, | |
TaskScheduler.FromCurrentSynchronizationContext()); | |
} | |
} | |
#endregion Private Methods | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment