Skip to content

Instantly share code, notes, and snippets.

@ghuntley
Last active August 23, 2019 03:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ghuntley/4f31a4cf55426b545ce630d3f52fabbc to your computer and use it in GitHub Desktop.
Save ghuntley/4f31a4cf55426b545ce630d3f52fabbc to your computer and use it in GitHub Desktop.
SwipableItem for Uno. Code released under MIT
#if __ANDROID__
using Android.Views;
using System;
using System.Collections.Generic;
using System.Text;
using Uno.UI;
namespace Umbrella.View.Controls
{
public class OnSwipeListener : Java.Lang.Object, Android.Views.View.IOnTouchListener
{
public GestureDetector GestureDetector { get; private set; }
private Android.Views.View _attachedView;
public OnSwipeListener(Android.Views.View attachedView, Action<double, double> onPan)
{
_attachedView = attachedView;
var listener = new PanGestureListener(onPan);
GestureDetector = new GestureDetector(ContextHelper.Current, listener);
}
public bool OnTouch(Android.Views.View v, MotionEvent e)
{
return GestureDetector.OnTouchEvent(e);
}
}
}
#endif
#if __ANDROID__
using Android.Gestures;
using System;
using System.Collections.Generic;
using System.Text;
using Android.Views;
using Android.Graphics;
using nVentive.Umbrella.Extensions;
using Uno.Extensions;
namespace Umbrella.View.Controls
{
public class PanGestureListener : GestureDetector.SimpleOnGestureListener
{
Action<double, double> _onPan;
public PanGestureListener(Action<double, double> onPan)
{
_onPan = onPan;
}
public override bool OnDown(MotionEvent e)
{
return true;
}
public override bool OnScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
{
if (e2.Action == MotionEventActions.Move)
{
_onPan(e2.GetX(), e2.GetY());
return true;
}
return false;
}
}
}
#endif
#if __ANDROID__
using Android.Graphics;
using Android.Views;
using Android.Views.Animations;
using nVentive.Umbrella.Extensions;
using Uno.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Uno.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using System.Numerics;
namespace Umbrella.View.Controls
{
public partial class SwipableItem : ContentControl, Android.Views.View.IOnTouchListener
{
private const long _translationDuration = 100; // In miliseconds
private object _currentDataContext;
private bool _hasAppliedTemplate = false;
private bool _hasArrangedOnce = false;
private bool _hasInitializedSnapPoints = false;
private ContentPresenter _mainContainer;
public GestureDetector GestureDetector { get; private set; }
private double _translationWhenNotSwiped;
private double _translationWhenSnappedFar;
private double _translationWhenSnappedNear;
private GestureStart _gestureStart;
private bool _isSwiping = false;
// Distance in pixels a touch can wander before we think the user is scrolling
// https://developer.android.com/reference/android/view/ViewConfiguration.html#getScaledTouchSlop()
private static readonly float _touchSlop = ViewConfiguration.Get(ContextHelper.Current).ScaledTouchSlop;
private static readonly IInterpolator _interpolator = new AccelerateInterpolator(); // This is a EaseIn interpolator
public SwipableItem()
{
#if !__ANDROID__ && !__IOS__
this.DefaultStyleKey = typeof(SwipableItem);
#endif
var listener = new PanGestureListener(Pan);
GestureDetector = new GestureDetector(ContextHelper.Current, listener);
this.SetOnTouchListener(this);
IsTabStop = true; // We need to be able to take focus in order to lose it and reset ourselves.
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_mainContainer = this.GetTemplateChild("MainPresenter") as ContentPresenter;
_mainContainer.Validation().NotNull("MainPresenter is a required Template Part in SwipableItem");
_hasAppliedTemplate = true;
}
protected override void OnAfterArrange()
{
base.OnAfterArrange();
_hasArrangedOnce = true;
InitializeSnapPoints();
}
protected override void OnLostFocus(RoutedEventArgs e)
{
if (IsAutoReset)
{
ResetSwipeState();
}
}
/// <summary>
/// Indicates whether the DataContext is being applied.
/// We use it as a way to know whether the view is being recycled, in which case we disable swipe transitions.
/// </summary>
private bool IsDataContextChanging => !ReferenceEquals(_currentDataContext, DataContext);
protected override void OnDataContextChanged()
{
base.OnDataContextChanged();
if (IsAutoReset)
{
SwipeState = SwipingState.NotSwiped;
}
_currentDataContext = DataContext;
}
private void InitializeSnapPoints()
{
if (_hasAppliedTemplate && _hasArrangedOnce)
{
_translationWhenNotSwiped = 0;
_translationWhenSnappedFar = -(this.FarSnapWidth * ViewHelper.Scale);
_translationWhenSnappedNear = this.NearSnapWidth * ViewHelper.Scale;
_hasInitializedSnapPoints = true;
if (SwipeState.HasValue)
{
// Now that we have the snap points, make sure we align the main container accordingly.
OnStateChanged(SwipeState.Value, useTransitions: false);
}
}
}
private void OnStateChanged(SwipingState newState, bool useTransitions)
{
// no point moving the container if the snap points are not ready.
// OnStateChanged can be called before apply template in some cases.
if (_hasInitializedSnapPoints)
{
MoveMainContainer(newState, useTransitions);
}
}
private void Pan(double translationX, double translationY)
{
double translation = translationX - _gestureStart.TouchPosition.X + _gestureStart.ContainerPosition.X;
float xClamped = (float)translation.Clamp(_translationWhenSnappedFar, _translationWhenSnappedNear);
_mainContainer.SetX(xClamped);
}
private void ResetSwipeState()
{
if (SwipeState.HasValue)
{
if (SwipeState == SwipingState.NotSwiped)
{
// Force a OnStateChanged here if control is already in NotSwiped State.
//(changing a DP to the same value won't trigger the OnValueChanged)
OnStateChanged(SwipeState.Value, useTransitions: true);
}
else
{
// Reset to middle state;
SwipeState = SwipingState.NotSwiped;
}
}
}
private void OnSwipeEnded()
{
var x = _mainContainer.GetX();
// Check if centered first to avoid changing state to Far or Near
// if there are no near or farsnap points.
if (x == _translationWhenNotSwiped)
{
ResetSwipeState();
}
else if (x >= (_translationWhenSnappedNear * SwipeDecisionPoint))
{
if (SwipeState == SwipingState.SwipedNear)
{
// Force a OnStateChanged here if control is already in SwipedNear State.
//(changing a DP to the same value won't trigger the OnValueChanged)
OnStateChanged(SwipeState.Value, useTransitions: true);
}
else
{
SwipeState = SwipingState.SwipedNear;
}
}
else if (x <= (_translationWhenSnappedFar * SwipeDecisionPoint))
{
if (SwipeState == SwipingState.SwipedFar)
{
// Force a OnStateChanged here if control is already in SwipedFar State.
//(changing a DP to the same value won't trigger the OnValueChanged)
OnStateChanged(SwipeState.Value, useTransitions: true);
}
else
{
SwipeState = SwipingState.SwipedFar;
}
}
else
{
ResetSwipeState();
}
}
private void MoveMainContainer(SwipingState state, bool useTransitions)
{
double translation;
switch (state)
{
case SwipingState.SwipedFar:
translation = _translationWhenSnappedFar;
break;
case SwipingState.SwipedNear:
translation = _translationWhenSnappedNear;
break;
case SwipingState.NotSwiped:
default:
translation = _translationWhenNotSwiped;
break;
}
if (useTransitions)
{
// Do not set an interpolator. It can cause weird behaviors with parent scrollviewers.
_mainContainer.Animate()
.X((float)translation)
.SetDuration(_translationDuration)
.Start();
}
else
{
_mainContainer.SetX((float)translation);
}
}
#region UNO TOUCH OVERRIDES
protected override void OnPointerPressed(PointerRoutedEventArgs args)
{
// We handle PointerPressed event to make sure we continue to receive the Move events.
// In the android touch system, if you don't handle an event you will not receive the rest of the gesture.
args.Handled = true;
}
#endregion
#region ANDROID TOUCH OVERRIDES
public bool OnTouch(Android.Views.View view, MotionEvent e)
{
// CANCEL will come if our gesture is taken over by another control. i.e. if a parent scrollviewer detects a
// vertical scroll.
// UP will come when the last finger comes up
// When we receive either touch action, it means our gesture has ended
if (e.Action == MotionEventActions.Cancel || e.Action == MotionEventActions.Up)
{
if (_isSwiping)
{
OnSwipeEnded();
}
_isSwiping = false;
}
return GestureDetector.OnTouchEvent(e);
}
public override bool OnInterceptTouchEvent(MotionEvent e)
{
switch (e.Action)
{
case MotionEventActions.Down:
_gestureStart = new GestureStart(new PointF(e.GetX(), e.GetY()), new PointF(_mainContainer.GetX(), _mainContainer.GetY()));
return false;
case MotionEventActions.Move:
return ShouldInterceptMove(e);
default:
_isSwiping = false;
return base.OnInterceptTouchEvent(e); ;
}
}
private bool ShouldInterceptMove(MotionEvent e)
{
if (_isSwiping)
{
return true;
}
else
{
Vector2 cumulativeDistance = new Vector2(
Math.Abs(e.GetX() - _gestureStart.TouchPosition.X),
Math.Abs(e.GetY() - _gestureStart.TouchPosition.Y)
);
if (cumulativeDistance.X > _touchSlop && // touchSlop is the minimal distance for us to determine it is a scroll
cumulativeDistance.X > cumulativeDistance.Y) // Check the scroll is horizontal
{
_isSwiping = true;
//Take focus when we are swiped
Focus(FocusState.Pointer);
// Once we have intercepted the gesture, we prevent all parent ViewGroups from intercepting our gesture.
// For example, this prevents a parent listview from intercepting our gesture if the user starts scrolling
// vertically after starting horizontally.
this.GetParents()
.OfType<ViewGroup>()
.ForEach(view => view.RequestDisallowInterceptTouchEvent(disallowIntercept: true));
return true;
}
else
{
return false;
}
}
}
#endregion
private class GestureStart
{
public GestureStart(PointF touchPosition, PointF containerPosition)
{
TouchPosition = touchPosition;
ContainerPosition = containerPosition;
}
public PointF TouchPosition { get; private set; }
public PointF ContainerPosition { get; private set; }
}
}
}
#endif
#if __IOS__
using CoreGraphics;
using nVentive.Umbrella.Extensions;
using Uno.Extensions;
using System;
using UIKit;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace Umbrella.View.Controls
{
public partial class SwipableItem : ContentControl
{
private const double _translationDuration = 0.10; // In seconds
private object _currentDataContext;
private bool _hasAppliedTemplate = false;
private bool _hasArrangedOnce = false;
private bool _hasInitializedSnapPoints = false;
private double _actualWidth;
private UIElement _mainContainer;
private double _translationWhenNotSwiped;
private double _translationWhenSnappedFar;
private double _translationWhenSnappedNear;
private double _translation = 0;
private Lazy<UITapGestureRecognizer> _outsideTapRecognizer;
private Lazy<UIPanGestureRecognizer> _outsidePanRecognizer;
public SwipableItem()
{
#if !__ANDROID__ && !__IOS__
this.DefaultStyleKey = typeof(SwipableItem);
#endif
this.ClipsToBounds = true;
// Since we can potentially add/remove this recognizer a lot, we make it lazy.
_outsideTapRecognizer = new Lazy<UITapGestureRecognizer>(() =>
{
var recognizer = new UITapGestureRecognizer(OnOutsideTap);
recognizer.Delegate = new SwipableItemOutsideGestureDelegate(this);
recognizer.CancelsTouchesInView = false; // We only want to detect the tap we certainly don't want to cancel other gestures
return recognizer;
});
_outsidePanRecognizer = new Lazy<UIPanGestureRecognizer>(() =>
{
var recognizer = new UIPanGestureRecognizer(OnOutsidePan);
recognizer.Delegate = new SwipableItemOutsideGestureDelegate(this);
recognizer.CancelsTouchesInView = false; // We only want to detect the pan we certainly don't want to cancel other gestures
return recognizer;
});
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_mainContainer = this.GetTemplateChild("MainPresenter") as UIElement;
_mainContainer.Validation().NotNull("MainPresenter is a required Template Part in SwipableItem");
var panGesture = new UIPanGestureRecognizer(Pan);
panGesture.MaximumNumberOfTouches = 1;
panGesture.Delegate = new SwipableItemGestureDelegate();
_mainContainer.AddGestureRecognizer(panGesture);
// Add a tap gesture in order to synchronize Android and iOS behavior
var tapGesture = new UITapGestureRecognizer(Tap);
tapGesture.CancelsTouchesInView = false;
tapGesture.Delegate = new SwipableItemGestureDelegate();
this.AddGestureRecognizer(tapGesture);
_hasAppliedTemplate = true;
}
protected override void OnAfterArrange()
{
base.OnAfterArrange();
_hasArrangedOnce = true;
// Only calculate the snap points when the width changed because swipe is horizontal
if (_actualWidth != ActualWidth)
{
InitializeSnapPoints();
_actualWidth = ActualWidth;
}
}
/// <summary>
/// Indicates whether the DataContext is being applied.
/// We use it as a way to know whether the view is being recycled, in which case we disable swipe transitions.
/// </summary>
private bool IsDataContextChanging => !ReferenceEquals(_currentDataContext, DataContext);
protected override void OnDataContextChanged()
{
base.OnDataContextChanged();
if (IsAutoReset)
{
SwipeState = SwipingState.NotSwiped;
}
_currentDataContext = DataContext;
}
private void InitializeSnapPoints()
{
if (_hasAppliedTemplate && _hasArrangedOnce)
{
_translationWhenNotSwiped = 0;
_translationWhenSnappedNear = NearSnapWidth;
_translationWhenSnappedFar = -FarSnapWidth;
_hasInitializedSnapPoints = true;
if (SwipeState.HasValue)
{
// Now that we have the snap points, make sure we align the main container accordingly.
OnStateChanged(SwipeState.Value, useTransitions: false);
}
}
}
private void OnStateChanged(SwipingState newState, bool useTransitions)
{
// no point moving the container if the snap points are not ready.
// OnStateChanged can be called before apply template in some cases.
if (_hasInitializedSnapPoints)
{
MoveMainContainer(newState, useTransitions);
}
if (IsAutoReset)
{
UpdateOutsideRecognizers(newState);
}
}
private void UpdateOutsideRecognizers(SwipingState newState)
{
// We register recognizers to look for taps and horizontal pans outside of the control
// so we can reset this control when it happens
if (newState == SwipingState.NotSwiped)
{
UIApplication.SharedApplication.KeyWindow.RemoveGestureRecognizer(_outsideTapRecognizer.Value);
UIApplication.SharedApplication.KeyWindow.RemoveGestureRecognizer(_outsidePanRecognizer.Value);
}
else if (newState == SwipingState.SwipedFar || newState == SwipingState.SwipedNear)
{
UIApplication.SharedApplication.KeyWindow.AddGestureRecognizer(_outsideTapRecognizer.Value);
UIApplication.SharedApplication.KeyWindow.AddGestureRecognizer(_outsidePanRecognizer.Value);
}
}
private void Tap(UITapGestureRecognizer gestureRecognizer)
{
if(gestureRecognizer.State == UIGestureRecognizerState.Ended)
{
if (IsAutoReset)
{
ResetSwipeState();
}
}
}
private void Pan(UIPanGestureRecognizer gestureRecognizer)
{
// When moving, we slide the item
var presenter = gestureRecognizer.View;
if (gestureRecognizer.State == UIGestureRecognizerState.Began ||
gestureRecognizer.State == UIGestureRecognizerState.Changed ||
gestureRecognizer.State == UIGestureRecognizerState.Ended
)
{
var translation = gestureRecognizer.TranslationInView(this);
_translation += translation.X;
// Only translate horizontally
// Clamp the translation to make sure we never go outside the ranges
_translation = _translation.Clamp(_translationWhenSnappedFar, _translationWhenSnappedNear);
presenter.Transform = CGAffineTransform.MakeTranslation((float)_translation, 0);
// Reset the gesture recognizer's translation to {0, 0} - the next callback will get a delta from the current position.
gestureRecognizer.SetTranslation(CGPoint.Empty, presenter);
}
// When gesture ends, we change the state of the control
if (gestureRecognizer.State == UIGestureRecognizerState.Ended)
{
//If Centered first to avoid changing state to Far or Near is there are no near or farsnap points.
if (_translation == _translationWhenNotSwiped)
{
ResetSwipeState();
}
//We validate the Center of the gesture is passed 50%.
else if (_translation >= _translationWhenSnappedNear - (this.NearSnapWidth * (1 - SwipeDecisionPoint)))
{
if (SwipeState == SwipingState.SwipedNear)
{
// Force a OnStateChanged here if control is already in SwipedNear State.
//(changing a DP to the same value won't trigger the OnValueChanged)
OnStateChanged(SwipeState.Value, useTransitions: true);
}
else
{
SwipeState = SwipingState.SwipedNear;
}
}
//We validate the Center of the gesture is passed 50%.
else if (_translation <= _translationWhenSnappedFar + (this.FarSnapWidth * (1 - SwipeDecisionPoint)))
{
if (SwipeState == SwipingState.SwipedFar)
{
// Force a OnStateChanged here if control is already in SwipedFar State.
//(changing a DP to the same value won't trigger the OnValueChanged)
OnStateChanged(SwipeState.Value, useTransitions: true);
}
else
{
SwipeState = SwipingState.SwipedFar;
}
}
else
{
ResetSwipeState();
}
}
}
private void OnOutsideTap(UITapGestureRecognizer gestureRecognizer)
{
// When gesture ends, we change the state of the control
if (gestureRecognizer.State == UIGestureRecognizerState.Ended)
{
ResetSwipeState();
}
}
private void OnOutsidePan(UIPanGestureRecognizer gestureRecognizer)
{
// When gesture ends, we change the state of the control
if (gestureRecognizer.State == UIGestureRecognizerState.Began)
{
ResetSwipeState();
}
}
private void ResetSwipeState()
{
if (SwipeState.HasValue)
{
if (SwipeState == SwipingState.NotSwiped)
{
// Force a OnStateChanged here if control is already in NotSwiped State.
//(changing a DP to the same value won't trigger the OnValueChanged)
OnStateChanged(SwipeState.Value, useTransitions: true);
}
else
{
// Reset to middle state;
SwipeState = SwipingState.NotSwiped;
}
}
}
private void MoveMainContainer(SwipingState state, bool useTransitions)
{
Action animatedAction = null;
switch (state)
{
case SwipingState.SwipedFar:
animatedAction = () =>
{
_translation = _translationWhenSnappedFar;
_mainContainer.Transform = CGAffineTransform.MakeTranslation((float)_translation, 0);
};
break;
case SwipingState.SwipedNear:
animatedAction = () =>
{
_translation = _translationWhenSnappedNear;
_mainContainer.Transform = CGAffineTransform.MakeTranslation((float)_translation, 0);
};
break;
case SwipingState.NotSwiped:
default:
animatedAction = () =>
{
_translation = _translationWhenNotSwiped;
_mainContainer.Transform = CGAffineTransform.MakeTranslation((float)_translation, 0);
};
break;
}
if (useTransitions)
{
UIView.Animate(
duration: _translationDuration,
// Do it now
delay: 0,
options: UIViewAnimationOptions.CurveEaseIn,
animation: animatedAction,
// Force the position in case the animation did not execute properly.
// This happens when the animation is called while the control is offscreen
completion: animatedAction
);
}
else
{
animatedAction();
}
}
}
}
#endif
#if __ANDROID__ || __IOS__
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Text;
namespace Umbrella.View.Controls
{
public partial class SwipableItem
{
#region NEAR PROPERTIES
public DataTemplate NearContentTemplate
{
get { return (DataTemplate)GetValue(NearContentTemplateProperty); }
set { SetValue(NearContentTemplateProperty, value); }
}
public static readonly DependencyProperty NearContentTemplateProperty =
DependencyProperty.Register("NearContentTemplate", typeof(DataTemplate), typeof(SwipableItem), new PropertyMetadata(null));
public object NearContent
{
get { return (object)GetValue(NearContentProperty); }
set { SetValue(NearContentProperty, value); }
}
public static readonly DependencyProperty NearContentProperty =
DependencyProperty.Register("NearContent", typeof(object), typeof(SwipableItem), new PropertyMetadata(null));
public DataTemplateSelector NearContentTemplateSelector
{
get { return (DataTemplateSelector)GetValue(NearContentTemplateSelectorProperty); }
set { SetValue(NearContentTemplateSelectorProperty, value); }
}
public static readonly DependencyProperty NearContentTemplateSelectorProperty =
DependencyProperty.Register("NearContentTemplateSelector", typeof(DataTemplateSelector), typeof(SwipableItem), new PropertyMetadata(null));
#endregion
#region FAR PROPERTIES
public DataTemplate FarContentTemplate
{
get { return (DataTemplate)GetValue(FarContentTemplateProperty); }
set { SetValue(FarContentTemplateProperty, value); }
}
public static readonly DependencyProperty FarContentTemplateProperty =
DependencyProperty.Register("FarContentTemplate", typeof(DataTemplate), typeof(SwipableItem), new PropertyMetadata(null));
public object FarContent
{
get { return (object)GetValue(FarContentProperty); }
set { SetValue(FarContentProperty, value); }
}
public static readonly DependencyProperty FarContentProperty =
DependencyProperty.Register("FarContent", typeof(object), typeof(SwipableItem), new PropertyMetadata(null));
public DataTemplateSelector FarContentTemplateSelector
{
get { return (DataTemplateSelector)GetValue(FarContentTemplateSelectorProperty); }
set { SetValue(FarContentTemplateSelectorProperty, value); }
}
public static readonly DependencyProperty FarContentTemplateSelectorProperty =
DependencyProperty.Register("FarContentTemplateSelector", typeof(DataTemplateSelector), typeof(SwipableItem), new PropertyMetadata(null));
#endregion
#region RESET PROPERTIES
/// <summary>
/// Determines if we reset the control to NotSwiped when the datacontext changes (typically when recycling)
/// or when the user is interacting outside of the control.
/// Should set to false if we use binding on SwipeState
/// </summary>
public bool IsAutoReset
{
get { return (bool)GetValue(IsAutoResetProperty); }
set { SetValue(IsAutoResetProperty, value); }
}
public static readonly DependencyProperty IsAutoResetProperty =
DependencyProperty.Register("IsAutoReset", typeof(bool), typeof(SwipableItem), new PropertyMetadata(true));
#endregion
#region SNAP PROPERTIES
public double NearSnapWidth
{
get { return (double)GetValue(NearSnapWidthProperty); }
set { SetValue(NearSnapWidthProperty, value); }
}
public static readonly DependencyProperty NearSnapWidthProperty =
DependencyProperty.Register("NearSnapWidth", typeof(double), typeof(SwipableItem), new PropertyMetadata(0.0, OnSnapChanged));
public double FarSnapWidth
{
get { return (double)GetValue(FarSnapWidthProperty); }
set { SetValue(FarSnapWidthProperty, value); }
}
public static readonly DependencyProperty FarSnapWidthProperty =
DependencyProperty.Register("FarSnapWidth", typeof(double), typeof(SwipableItem), new PropertyMetadata(0.0, OnSnapChanged));
private static void OnSnapChanged(object o, DependencyPropertyChangedEventArgs e)
{
var item = (SwipableItem)o;
item.InitializeSnapPoints();
}
#endregion
#region SWIPESTATE PROPERTIES
public SwipingState? SwipeState
{
get { return (SwipingState?)GetValue(SwipeStateProperty); }
set { SetValue(SwipeStateProperty, value); }
}
public static readonly DependencyProperty SwipeStateProperty =
DependencyProperty.Register("SwipeState", typeof(SwipingState?), typeof(SwipableItem), new PropertyMetadata(null, OnSwipeStateChanged));
private static void OnSwipeStateChanged(object o, DependencyPropertyChangedEventArgs e)
{
var item = (SwipableItem)o;
var newState = e.NewValue as SwipingState?;
if (newState.HasValue)
{
item.OnStateChanged(newState.Value, useTransitions: !item.IsDataContextChanging);
}
}
#endregion
#region SWIPEDECISIONPOINT PROPERTIES
/// <summary>
/// Expected value between 0 and 1.
/// </summary>
public double SwipeDecisionPoint
{
get { return (double)GetValue(SwipeDecisionPointProperty); }
set { SetValue(SwipeDecisionPointProperty, value); }
}
public static readonly DependencyProperty SwipeDecisionPointProperty =
DependencyProperty.Register("SwipeDecisionPoint", typeof(double), typeof(SwipableItem), new PropertyMetadata(0.8d));
#endregion
}
}
#endif
#if __IOS__
//#define SWIPABLEITEM_LOGGING
using nVentive.Umbrella.Extensions;
using Uno.Extensions;
using Uno.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using UIKit;
namespace Umbrella.View.Controls
{
internal class SwipableItemGestureDelegate: UIGestureRecognizerDelegate
{
/// See the apple documentation
/// https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html#//apple_ref/doc/uid/TP40009541-CH2-SW2
/// for more details
/// <summary>
/// Determines if the recognizer should accept or not the touch.
/// </summary>
public override bool ShouldReceiveTouch(UIGestureRecognizer recognizer, UITouch touch)
{
// We always want to receive touches to possibly handle pan gestures.
// If we wanted to implement a way to disable the swiping functionality, we could return false here.
return true;
}
public override bool ShouldBegin(UIGestureRecognizer recognizer)
{
UIPanGestureRecognizer panRecognizer = recognizer as UIPanGestureRecognizer;
if (panRecognizer != null)
{
// Accept the pan gesture only if X is greater than Y
var translation = panRecognizer.TranslationInView(panRecognizer.View);
return Math.Abs(translation.X) > Math.Abs(translation.Y);
}
else
{
// if the recognizer is something else than a pan, just accept it. (for example tap or long press)
return true;
}
}
/// <summary>
/// Here we decide if multiple gestures can be recognized simultaneously
/// </summary>
public override bool ShouldRecognizeSimultaneously(UIGestureRecognizer gestureRecognizer, UIGestureRecognizer otherGestureRecognizer)
{
// if either of the gesture recognizers is a long press, don't recognize longpress and pan simultaneously
if (gestureRecognizer is UILongPressGestureRecognizer || otherGestureRecognizer is UILongPressGestureRecognizer)
{
return false;
}
// if both gestures are pan gestures we will assume the other recognizer comes from a parent scrollviewer (i.e. listview)
// Remark: Since we assume the parent scrollviewer is scrolling vertically, we only support this scenario.
var panGestureRecognizer = gestureRecognizer as UIPanGestureRecognizer;
var parentScrollViewGestureRecognizer = otherGestureRecognizer as UIPanGestureRecognizer;
if (panGestureRecognizer != null && parentScrollViewGestureRecognizer != null)
{
var translation = panGestureRecognizer.TranslationInView(panGestureRecognizer.View);
var velocity = panGestureRecognizer.VelocityInView(panGestureRecognizer.View);
#if SWIPABLEITEM_LOGGING
this.Log().InfoIfEnabled(() =>
string.Format("ShouldRecognize? Translation = {0},{1}, velocity {2},{3}",
translation.X,
translation.Y,
velocity.X,
velocity.Y
)
);
#endif
return Math.Abs(translation.X) > Math.Abs(translation.Y) // Pan should be horizontal
&& Math.Abs(velocity.Y) <= 0.25f; // Velocity should not be too high vertically, this is usually the case if someone is swiping very fast through the list
}
return true;
}
}
}
#endif
#if __IOS__
//#define SWIPABLEITEM_LOGGING
using nVentive.Umbrella.Extensions;
using Uno.Extensions;
using Uno.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using UIKit;
namespace Umbrella.View.Controls
{
internal class SwipableItemOutsideGestureDelegate : UIGestureRecognizerDelegate
{
SwipableItem _swipableItem;
public SwipableItemOutsideGestureDelegate(SwipableItem swipableItem)
{
_swipableItem = swipableItem;
}
/// <summary>
/// Determines if the recognizer should accept or not the touch.
/// </summary>
public override bool ShouldReceiveTouch(UIGestureRecognizer recognizer, UITouch touch)
{
// We always want to receive touches
return true;
}
public override bool ShouldBegin(UIGestureRecognizer recognizer)
{
UIPanGestureRecognizer panRecognizer = recognizer as UIPanGestureRecognizer;
if (panRecognizer != null)
{
// Accept the pan gesture only if X is greater than Y
var translation = panRecognizer.TranslationInView(_swipableItem);
if (Math.Abs(translation.X) <= Math.Abs(translation.Y))
{
// if pan is not horizontal, ignore it.
return false;
}
}
// Ignore gestures that occur within the SwipableItem
var tapLocation = recognizer.LocationInView(_swipableItem);
var bounds = _swipableItem.Bounds;
return !bounds.Contains(tapLocation);
}
/// <summary>
/// Here we decide if multiple gestures can be recognized simultaneously
/// </summary>
public override bool ShouldRecognizeSimultaneously(UIGestureRecognizer gestureRecognizer, UIGestureRecognizer otherGestureRecognizer)
{
// if either of the gesture recognizers is a long press, don't recognize longpress simultaneously
if (gestureRecognizer is UILongPressGestureRecognizer ||
otherGestureRecognizer is UILongPressGestureRecognizer)
{
return false;
}
return true;
}
}
}
#endif
using System;
using System.Collections.Generic;
using System.Text;
namespace Umbrella.View.Controls
{
public enum SwipingState
{
NotSwiped,
SwipingNear,
SwipedNear,
LongSwipingNear,
LongSwipedNear,
SwipingFar,
SwipedFar,
LongSwipingFar,
LongSwipedFar
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment