Last active
August 23, 2019 03:49
-
-
Save ghuntley/4f31a4cf55426b545ce630d3f52fabbc to your computer and use it in GitHub Desktop.
SwipableItem for Uno. Code released under MIT
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
#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 |
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
#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 |
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
#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 |
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
#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 |
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
#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 |
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
#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 |
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
#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 |
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
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