Skip to content

Instantly share code, notes, and snippets.

@Shiggiddie
Forked from georgejecook/BaseGesturRecognizer.cs
Created November 11, 2016 19:22
Show Gist options
  • Save Shiggiddie/91a4d482be456ca75882df28589206f5 to your computer and use it in GitHub Desktop.
Save Shiggiddie/91a4d482be456ca75882df28589206f5 to your computer and use it in GitHub Desktop.
using System;
using Xamarin.Forms;
using System.Windows.Input;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.ComponentModel;
using System.Threading.Tasks;
[assembly:
InternalsVisibleTo ("TwinTechsLib.iOS"),
InternalsVisibleTo ("TwinTechsLib.Droid")]
namespace TwinTechs.Gestures
{
public enum GestureRecognizerState
{
Possible,
Began,
Changed,
Ended,
Cancelled,
Failed,
Recognized = 3
}
//TODO I would love to make this generic!
/// <summary>
/// Base gesture recognizer.
/// </summary>
public class BaseGestureRecognizer : BindableObject, IGestureRecognizer
{
/// <summary>
/// Delegate callback to check if this recognizer should proceed to begin state.
/// If false is returned, then the gesture will cancel.
/// If delayed touches is true, and false is returned, then the gesture will replay it's delayed touches
/// (implementaiton is slightly different on android/iOS; but the result should be more or less equal).
/// </summary>
/// <value>The view.</value>
public delegate bool GestureShouldBeginDelegate (BaseGestureRecognizer gestureRecognizer);
public GestureShouldBeginDelegate OnGestureShouldBeginDelegate;
public View View { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is consuming touches in parallel.
/// if true, then this gesture will register with the main touch dispatcher, and intercept touches as they occur at the system level
/// </summary>
/// <value><c>true</c> if this instance is consuming touches in parallel; otherwise, <c>false</c>.</value>
public bool IsConsumingTouchesInParallel { get; set; }
/// <summary>
/// Gets or sets the command.
/// </summary>
/// <value>The command.</value>
public ICommand Command {
get;
set;
}
/// <summary>
/// Gets or sets the command parameter.
/// </summary>
/// <value>The command parameter.</value>
public object CommandParameter {
get;
set;
}
/// <summary>
/// Gets or sets the OnAction callback. Made available in case your views need access to the gesture responses
/// </summary>
/// <value>The tapped callback.</value>
public event Action<BaseGestureRecognizer, GestureRecognizerState> OnAction;
bool _delaysTouches;
public bool DelaysTouches {
get{ return _delaysTouches; }
set {
_delaysTouches = value;
if (NativeGestureRecognizer != null) {
NativeGestureRecognizer.UpdateDelaysTouches (_delaysTouches);
}
}
}
bool _cancelsTouchesInView;
public bool CancelsTouchesInView {
get{ return _cancelsTouchesInView; }
set {
_cancelsTouchesInView = value;
if (NativeGestureRecognizer != null) {
NativeGestureRecognizer.UpdateCancelsTouchesInView (_cancelsTouchesInView);
}
}
}
public GestureRecognizerState State { get { return NativeGestureRecognizer == null ? GestureRecognizerState.Failed : NativeGestureRecognizer.State; } }
public int NumberOfTouches { get { return NativeGestureRecognizer == null ? 0 : NativeGestureRecognizer.NumberOfTouches; } }
public Point LocationInView (VisualElement view)
{
return NativeGestureRecognizer.LocationInView (view);
}
public Point LocationOfTouch (int touchIndex, VisualElement view)
{
return NativeGestureRecognizer.LocationOfTouch (touchIndex, view);
}
#region internal impl
internal void SendAction ()
{
if (Command != null) {
Command.Execute (CommandParameter);
}
if (OnAction != null) {
OnAction.Invoke (this, State);
}
}
/// <summary>
/// Sets the underlying gesture recognzier - used by the factory for adding/removal
/// </summary>
/// <value>The native gesture recognizer.</value>
internal INativeGestureRecognizer NativeGestureRecognizer { get; set; }
/// <summary>
/// Gets or sets the native gesture coordinator. - ONLY USED BY ANDROID
/// </summary>
/// <value>The native gesture coordinator.</value>
internal INativeGestureRecognizerCoordinator NativeGestureCoordinator { get; set; }
#endregion
public override string ToString ()
{
return string.Format ("[BaseGestureRecognizer: View={0}, State={1}]", View, State);
}
}
}
using System;
using UIKit;
using Xamarin.Forms.Platform.iOS;
using Xamarin.Forms;
using TwinTechs.Ios.Extensions;
namespace TwinTechs.Gestures
{
public interface IBaseNativeGestureRecognizerImpl : INativeGestureRecognizer
{
void AddRecognizer (BaseGestureRecognizer recognizer);
void RemoveRecognizer (BaseGestureRecognizer recognizer);
}
public abstract class BaseNativeGestureRecognizer<NativeGestureType,T> : INativeGestureRecognizer,IBaseNativeGestureRecognizerImpl
where NativeGestureType : UIGestureRecognizer
where T : BaseGestureRecognizer
{
protected T Recognizer { get; set; }
protected UIView NativeView { get; set; }
protected NativeGestureType NativeRecognizer { get; set; }
protected virtual void ConfigureNativeGestureRecognizer ()
{
NativeRecognizer.CancelsTouchesInView = Recognizer.CancelsTouchesInView;
NativeRecognizer.DelaysTouchesBegan = Recognizer.DelaysTouches;
// NativeRecognizer.DelaysTouchesEnded = Recognizer.DelaysTouchesEnded;
NativeRecognizer.ShouldRecognizeSimultaneously += _NativeRecognizer_ShouldRecognizeSimultaneously;
NativeRecognizer.ShouldBegin += _NativeRecognizer_ShouldBegin;
}
bool _NativeRecognizer_ShouldRecognizeSimultaneously (UIGestureRecognizer gestureRecognizer, UIGestureRecognizer otherGestureRecognizer)
{
var renderer = Recognizer.View.GetRenderer ();
return renderer != null && Recognizer.IsConsumingTouchesInParallel;
}
bool _NativeRecognizer_ShouldBegin (UIGestureRecognizer recognizer)
{
if (Recognizer.OnGestureShouldBeginDelegate != null) {
return Recognizer.OnGestureShouldBeginDelegate (Recognizer);
} else {
return true;
}
}
#region IBaseNativeGestureRecognizerImpl impl
public void AddRecognizer (BaseGestureRecognizer recognizer)
{
Recognizer = (T)recognizer;
var renderer = Recognizer.View.GetRenderer ();
if (renderer == null) {
Recognizer.View.PropertyChanged += Recognizer_View_PropertyChanged;
} else {
InitializeNativeRecognizer ();
}
}
void Recognizer_View_PropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "Renderer") {
var renderer = Recognizer.View.GetRenderer ();
if (renderer != null && NativeView == null) {
InitializeNativeRecognizer ();
} else if (renderer == null && NativeView != null && NativeRecognizer != null) {
RemoveRecognizer (Recognizer);
}
}
}
void InitializeNativeRecognizer ()
{
var renderer = Recognizer.View.GetRenderer ();
if (renderer == null) {
throw new InvalidOperationException ("attempted to initialize a native gesture recognizers for a view before it had created it's renderer");
}
NativeView = renderer.NativeView;
//workaroudn for irritating bugn which causes renderer to fail
if (typeof(NativeGestureType).Equals (typeof(UIPinchGestureRecognizer))) {
NativeRecognizer = new UIPinchGestureRecognizer (OnGesture) as NativeGestureType;
} else {
Action<NativeGestureType> action = OnGesture;
NativeRecognizer = (NativeGestureType)Activator.CreateInstance (typeof(NativeGestureType), action);
}
ConfigureNativeGestureRecognizer ();
NativeView.UserInteractionEnabled = true;
NativeView.AddGestureRecognizer (NativeRecognizer);
}
public void RemoveRecognizer (BaseGestureRecognizer recognizer)
{
NativeView.RemoveGestureRecognizer (NativeRecognizer);
NativeRecognizer.ShouldRecognizeSimultaneously -= _NativeRecognizer_ShouldRecognizeSimultaneously;
NativeRecognizer.ShouldBegin -= _NativeRecognizer_ShouldBegin;
NativeRecognizer = null;
recognizer.NativeGestureRecognizer = null;
}
#endregion
#region IBaseNativeGestureRecognizer impl
public void UpdateCancelsTouchesInView (bool _cancelsTouchesInView)
{
NativeRecognizer.CancelsTouchesInView = _cancelsTouchesInView;
}
public void UpdateDelaysTouches (bool _delaysTouches)
{
NativeRecognizer.DelaysTouchesBegan = _delaysTouches;
}
public GestureRecognizerState State {
get {
return (GestureRecognizerState)(NativeRecognizer != null ?
GetGestureRecognizerStateFromUIState (NativeRecognizer.State) : GestureRecognizerState.Failed);
}
}
public Point LocationInView (VisualElement view)
{
if (NativeRecognizer != null) {
var renderer = view.GetRenderer ();
return NativeRecognizer.LocationInView (renderer.NativeView).ToPoint ();
} else {
return Point.Zero;
}
}
public Point LocationOfTouch (int touchIndex, VisualElement view)
{
if (NativeRecognizer != null) {
var renderer = view.GetRenderer ();
return NativeRecognizer.LocationOfTouch (touchIndex, renderer.NativeView).ToPoint ();
} else {
return Point.Zero;
}
}
public int NumberOfTouches {
get {
return (int)NativeRecognizer.NumberOfTouches;
}
}
GestureRecognizerState GetGestureRecognizerStateFromUIState (UIGestureRecognizerState state)
{
switch (state) {
case UIGestureRecognizerState.Possible:
return GestureRecognizerState.Possible;
case UIGestureRecognizerState.Began:
return GestureRecognizerState.Began;
case UIGestureRecognizerState.Changed:
return GestureRecognizerState.Changed;
case UIGestureRecognizerState.Ended:
return GestureRecognizerState.Ended;
case UIGestureRecognizerState.Cancelled:
return GestureRecognizerState.Cancelled;
case UIGestureRecognizerState.Failed:
return GestureRecognizerState.Failed;
default:
throw new ArgumentOutOfRangeException ();
}
}
void OnGesture (UIGestureRecognizer recognizer)
{
Recognizer.SendAction ();
}
#endregion
}
}
public GestureRecognizerState State {
get {
return _state;
}
protected set {
var oldState = _state;
_state = value;
if (oldState == GestureRecognizerState.Possible && value == GestureRecognizerState.Began) {
if (Recognizer.OnGestureShouldBeginDelegate != null && !Recognizer.OnGestureShouldBeginDelegate (Recognizer)) {
_state = GestureRecognizerState.Failed;
}
}
if (_state == GestureRecognizerState.Cancelled || _state == GestureRecognizerState.Ended || _state == GestureRecognizerState.Failed) {
PointerId = -1;
}
//we track if the gesture had begun at some point in processing this gesture, so we can elect which continuous events to send
if (_state == GestureRecognizerState.Began) {
_gestureDidBegin = true;
}
if (_state == GestureRecognizerState.Recognized || (IsGestureCotinuous && _gestureDidBegin)) {
SendGestureEvent ();
}
if (GetIsFinishedState (_state)) {
_gestureDidBegin = false;
}
}
}
bool GetIsFinishedState (GestureRecognizerState state)
{
return state == GestureRecognizerState.Ended || state == GestureRecognizerState.Cancelled || state == GestureRecognizerState.Recognized ||
state == GestureRecognizerState.Failed;
}
var swipeGestureRecognizer = new SwipeGestureRecognizer(){
Direction = SwipeGestureDirection.Left,
NumberOfTouchesRequired = 2
};
swipeGestureRecongizer.OnAction += (gesturRecgonizer) => Debug.WriteLine(“did a swipe”);
var myView.AddGestureRecognizer(swipeGestureRecognizer);
_stackPanRecognizer = new PanGestureRecognizer ();
_stackPanRecognizer.OnAction += OnAction;
MyStack.AddGestureRecognizer (_stackPanRecognizer);
using System;
using Android.App;
using Android.Views;
using System.Collections.Generic;
namespace TwinTechs.Gestures
{
/// <summary>
/// Helper class which provides a mechanism for android apps to dispatch touches to
/// multiple views which facilitates composing gesture recognition,
/// without having to have custom subclasses, while providing better orchestration of touches to gestures.
///
/// The class hooks into an activity, which is expected to contain all of the view groups with views with touches
/// to coordinate
/// </summary>
public class GestureTouchDispatcher
{
Activity _activity { get; set; }
Dictionary<MotionEvent,GestureMotionEvent> _delayedMotionEvents = new Dictionary<MotionEvent,GestureMotionEvent> ();
public GestureTouchDispatcher (Activity activity)
{
_activity = activity;
}
public bool DispatchTouchEvent (MotionEvent ev)
{
bool wasDelayed = _delayedMotionEvents.ContainsKey (ev);
if (wasDelayed) {
Console.WriteLine ("was delayed event - processing now " + ev);
var gestureEvent = _delayedMotionEvents [ev];
_delayedMotionEvents.Remove (ev);
var restoredEvent = gestureEvent.GetCachedEvent ();
_activity.DispatchTouchEvent (restoredEvent);
var handled = _delayedMotionEvents.Remove (restoredEvent);
return handled;
}
var gestureMotionEvent = new GestureMotionEvent (ev);
//find if there's a view container with a gesture, which is currently on the screen.
foreach (var recognizer in NativeGestureCoordinator.GroupRecognizers) {
var nativeRecognizer = recognizer.NativeGestureRecognizer as BaseNativeGestureRecognizer;
// Console.WriteLine ("checkign gesture touch");
nativeRecognizer.ProcessGestureMotionEvent (gestureMotionEvent);
gestureMotionEvent.IsConsumed = GetIsConsumedState (nativeRecognizer.State);
wasDelayed = wasDelayed || gestureMotionEvent.IsMarkedForDelay;
}
if (gestureMotionEvent.IsConsumed && gestureMotionEvent.IsCancelled) {
ev.Action = MotionEventActions.Cancel;
}
if (gestureMotionEvent.IsMarkedForDelay) {
_delayedMotionEvents [ev] = gestureMotionEvent;
} else if (wasDelayed) {
//it's been released from being delayed
_activity.DispatchTouchEvent (ev);
}
return gestureMotionEvent.IsConsumed;
}
bool GetIsConsumedState (GestureRecognizerState state)
{
return state == GestureRecognizerState.Ended || state == GestureRecognizerState.Began ||
state == GestureRecognizerState.Recognized || state == GestureRecognizerState.Changed;
}
}
}
<Label
Text="Tap me twice"
x:Name="Label1"
BackgroundColor="Olive"
HeightRequest="90"
TextColor="White"
FontSize="20">
<Label.GestureRecognizers>
<gestures:TapGestureRecognizer
NumberOfTapsRequired="2"
OnAction="OnAction" />
</Label.GestureRecognizers>
</Label>
protected Xamarin.Forms.Point GetLocationInAncestorView (Xamarin.Forms.Point location, Xamarin.Forms.VisualElement view)
{
int[] nativeViewLocation = new int[2];
NativeView.GetLocationOnScreen (nativeViewLocation);
var nativeViewLocationOnScreen = new Xamarin.Forms.Point (nativeViewLocation [0], nativeViewLocation [1]);
var offsetLocation = new Xamarin.Forms.Point (location.X + nativeViewLocationOnScreen.X, location.Y + nativeViewLocationOnScreen.Y);
var targetViewRenderer = view.GetRenderer ();
var targetView = targetViewRenderer.ViewGroup;
int[] targetViewLocation = new int[2];
targetView.GetLocationOnScreen (targetViewLocation);
var nativeViewScreenLocation = new Xamarin.Forms.Point (targetViewLocation [0], targetViewLocation [1]);
var returnPoint = offsetLocation;
returnPoint.X -= nativeViewScreenLocation.X;
returnPoint.Y -= nativeViewScreenLocation.Y;
// Console.WriteLine ("offsetLocation {0} nativeViewLocationOnScreen {1} returnPoint", offsetLocation, nativeViewLocationOnScreen);
// Console.WriteLine ("location {0} parentViewLoc {1} returnPoint {2}", location, nativeViewScreenLocation, returnPoint);
return returnPoint;
}
using System;
using System.Collections.Generic;
namespace TwinTechs.Gestures
{
public class NativeGestureRecognizerFactory : INativeGestureRecognizerFactory
{
public NativeGestureRecognizerFactory ()
{
}
Dictionary<Type,Type> TypeDictionary = new Dictionary<Type, Type> () {
{ typeof(TapGestureRecognizer) , typeof(NativeTapGestureRecognizer) },
{ typeof(SwipeGestureRecognizer) , typeof(NativeSwipeGestureRecognizer) },
{ typeof(PanGestureRecognizer) ,typeof(NativePanGestureRecognizer) },
{ typeof(PinchGestureRecognizer), typeof(NativePinchGestureRecgonizer) },
{ typeof(LongPressGestureRecognizer) , typeof(NativeLongPressGestureRecognizer) },
};
#region INativeGestureRecognizerFactory implementation
public void AddNativeGestureRecognizerToRecgonizer<T> (T recognizer) where T : BaseGestureRecognizer
{
if (!TypeDictionary.ContainsKey (recognizer.GetType ())) {
throw new ArgumentException ("no native gesture recognizer for this forms recognizer " + recognizer.GetType ());
}
var targetType = TypeDictionary [recognizer.GetType ()];
var nativeRecongizer = (BaseNativeGestureRecognizer)Activator.CreateInstance (targetType);
nativeRecongizer.Recognizer = recognizer;
recognizer.NativeGestureRecognizer = nativeRecongizer;
if (recognizer.NativeGestureCoordinator == null) {
recognizer.NativeGestureCoordinator = new NativeGestureCoordinator (recognizer.View);
}
var coordinator = recognizer.NativeGestureCoordinator as NativeGestureCoordinator;
if (coordinator == null) {
throw new InvalidOperationException ("the recognizer's native gesture coordinator is null, or an invalid type");
}
coordinator.AddRecognizer (nativeRecongizer);
}
public void RemoveRecognizer (BaseGestureRecognizer recognizer)
{
if (recognizer.NativeGestureRecognizer != null) {
var coordinator = recognizer.NativeGestureCoordinator as NativeGestureCoordinator;
if (coordinator == null) {
throw new InvalidOperationException ("the recognizer's native gesture coordinator is null, or an invalid type");
}
coordinator.RemoveRecognizer ((BaseNativeGestureRecognizer)recognizer.NativeGestureRecognizer);
if (!coordinator.HasRecognizers) {
coordinator.Destroy ();
recognizer.NativeGestureCoordinator = null;
}
}
}
#endregion
}
}
using System;
using System.Collections.Generic;
namespace TwinTechs.Gestures
{
/// <summary>
/// Creates gesture recognizers
/// </summary>
public class NativeGestureRecognizerFactory : INativeGestureRecognizerFactory
{
#region INativeGestureRecognizerFactory implementation
Dictionary<Type,Type> TypeDictionary = new Dictionary<Type, Type> () {
{ typeof(SwipeGestureRecognizer) , typeof(NativeSwipeGestureRecognizer) },
{ typeof(PanGestureRecognizer) , typeof(NativePanGestureRecognizer) },
{ typeof(PinchGestureRecognizer) , typeof(NativePinchGestureRecognizer) },
{ typeof(LongPressGestureRecognizer) , typeof(NativeLongPressGestureRecgonizer) },
{ typeof(TwinTechs.Gestures.TapGestureRecognizer) , typeof(NativeTapPressGestureRecgonizer) },
};
public void AddNativeGestureRecognizerToRecgonizer<T> (T recognizer) where T : BaseGestureRecognizer
{
if (!TypeDictionary.ContainsKey (recognizer.GetType ())) {
throw new ArgumentException ("no native gesture recognizer for this forms recognizer " + recognizer.GetType ());
}
var targetType = TypeDictionary [recognizer.GetType ()];
var nativeRecongizer = (IBaseNativeGestureRecognizerImpl)Activator.CreateInstance (targetType);
nativeRecongizer.AddRecognizer (recognizer);
recognizer.NativeGestureRecognizer = nativeRecongizer;
}
public INativeGestureRecognizerCoordinator CreateNativeGestureCoordinator ()
{
throw new InvalidOperationException ("iOS does not use the native gesture coordinator.");
}
public void RemoveRecognizer (BaseGestureRecognizer recognizer)
{
var nativeRecognizer = recognizer.NativeGestureRecognizer as IBaseNativeGestureRecognizerImpl;
nativeRecognizer.RemoveRecognizer (recognizer);
}
#endregion
}
}
using System;
using Android.Views;
using Android.Graphics;
using TwinTechs.Droid.Extensions;
using Xamarin.Forms;
namespace TwinTechs.Gestures
{
public class NativeLongPressGestureRecognizer : BaseNativeGestureRecognizer
{
public NativeLongPressGestureRecognizer ()
{
}
LongPressGestureRecognizer LongPressGestureRecognizer { get { return Recognizer as LongPressGestureRecognizer; } }
System.Timers.Timer _longPressTimer;
#region implemented abstract members of BaseNativeGestureRecognizer
internal override void ProcessMotionEvent (GestureMotionEvent e)
{
if (e.ActionMasked == MotionEventActions.Down && PointerId == -1) {
OnDown (e);
// e.IsConsumed = true;
e.IsCancelled = Recognizer.CancelsTouchesInView;
} else if (State == GestureRecognizerState.Cancelled || State == GestureRecognizerState.Ended || State == GestureRecognizerState.Failed) {
return;
} else {
var xMovement = Math.Abs (e.GetX (0) - FirstTouchPoint.X);
var yMovement = Math.Abs (e.GetY (0) - FirstTouchPoint.Y);
var isMovedBeyondMaxDistance = xMovement > LongPressGestureRecognizer.MaxDistanceTolerance || yMovement > LongPressGestureRecognizer.MaxDistanceTolerance;
Console.WriteLine ("isMovedBeyondMaxDistance {0} xd {1} yd{2}", isMovedBeyondMaxDistance, xMovement, yMovement);
if (e.ActionMasked == MotionEventActions.Cancel || isMovedBeyondMaxDistance) {
State = GestureRecognizerState.Cancelled;
Console.WriteLine ("LONG PRESS CANCELLED");
} else if (e.ActionMasked == MotionEventActions.Up) {
OnUp (e);
// e.IsConsumed = true;
}
}
}
#endregion
void OnDown (GestureMotionEvent e)
{
//TODO - should really be possible until all taps/fingers are satisfied.
if (e.PointerCount == LongPressGestureRecognizer.NumberOfTouchesRequired) {
State = GestureRecognizerState.Began;
PointerId = e.GetPointerId (0);
FirstTouchPoint = new Xamarin.Forms.Point (e.GetX (0), e.GetY (0));
ResetLongPressTimer (true);
} else {
State = GestureRecognizerState.Failed;
}
}
void OnUp (GestureMotionEvent e)
{
ResetLongPressTimer (false);
//TODO track the correct fingers
if (State == GestureRecognizerState.Began) {
State = GestureRecognizerState.Failed;
Console.WriteLine ("LONG PRESS CANCELLED");
}
}
void _longPressTapTimer_Elapsed (object sender, System.Timers.ElapsedEventArgs e)
{
Console.WriteLine ("LONG PRESS RECOGNIZED");
ResetLongPressTimer (false);
State = GestureRecognizerState.Recognized;
}
void ResetLongPressTimer (bool isActive)
{
if (_longPressTimer != null) {
_longPressTimer.Elapsed -= _longPressTapTimer_Elapsed;
_longPressTimer.Stop ();
}
if (isActive) {
State = GestureRecognizerState.Possible;
_longPressTimer = new System.Timers.Timer ();
_longPressTimer.AutoReset = false;
_longPressTimer.Interval = LongPressGestureRecognizer.MinimumPressDuration * 1000;
_longPressTimer.Elapsed += _longPressTapTimer_Elapsed;
_longPressTimer.Start ();
}
}
}
}
using System;
using UIKit;
using System.Drawing;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using TwinTechs.Ios.Extensions;
using CoreGraphics;
namespace TwinTechs.Gestures
{
public class NativePanGestureRecognizer : BaseNativeGestureRecognizer<UIPanGestureRecognizer,PanGestureRecognizer>
, INativePanGestureRecognizer
{
public NativePanGestureRecognizer ()
{
}
#region overridden
protected override void ConfigureNativeGestureRecognizer ()
{
base.ConfigureNativeGestureRecognizer ();
NativeRecognizer.MinimumNumberOfTouches = (nuint)Recognizer.MinimumNumberOfTouches;
NativeRecognizer.MaximumNumberOfTouches = (nuint)Recognizer.MaximumNumberOfTouches;
}
#endregion
#region INativePanGestureRecognizer impl
public Xamarin.Forms.Point GetVelocityInView (VisualElement view)
{
var renderer = view.GetRenderer ();
return NativeRecognizer.VelocityInView (renderer.NativeView).ToPoint ();
}
public Xamarin.Forms.Point GetTranslationInView (VisualElement view)
{
var renderer = view.GetRenderer ();
if (renderer == null || renderer.NativeView == null) {
//TODO -not sure why this isn't working on iOS. very weird.
return new Xamarin.Forms.Point (0, 0);
}
return NativeRecognizer.TranslationInView (renderer.NativeView).ToPoint ();
}
public void SetTranslationInView (Xamarin.Forms.Point translation, VisualElement view)
{
var renderer = view.GetRenderer ();
NativeRecognizer.SetTranslation (new CGPoint (translation.X, translation.Y), renderer.NativeView);
}
#endregion
}
}
using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using UIKit;
using CoreImage;
using Foundation;
namespace TwinTechs.Gestures
{
public class NativeSwipeGestureRecognizer : BaseNativeGestureRecognizer<UISwipeGestureRecognizer,SwipeGestureRecognizer>
{
public NativeSwipeGestureRecognizer ()
{
}
#region abstract impl
protected override void ConfigureNativeGestureRecognizer ()
{
base.ConfigureNativeGestureRecognizer ();
NativeRecognizer.Direction = (UISwipeGestureRecognizerDirection)Recognizer.Direction;
NativeRecognizer.NumberOfTouchesRequired = (nuint)Recognizer.NumberOfTouchesRequired;
}
#endregion
}
}
using System;
using Android.Views;
using Xamarin.Forms;
namespace TwinTechs.Gestures
{
public class NativeSwipeGestureRecognizer : BaseNativeGestureRecognizer
{
const int MinimumSwipeDistance = 5;
const int MaxSwipeDuration = 1000;
DateTime _startTime;
public NativeSwipeGestureRecognizer ()
{
}
SwipeGestureRecognizer SwipeGestureRecognizer { get { return Recognizer as SwipeGestureRecognizer; } }
#region implemented abstract members of BaseNativeGestureRecognizer
internal override void ProcessMotionEvent (GestureMotionEvent e)
{
if (e.Action == MotionEventActions.Down && PointerId == -1) {
OnDown (e);
//TODO - this should probably be possible at this point?
if (State == GestureRecognizerState.Began) {
//TODO track all pointers that are down.
PointerId = e.GetPointerId (0);
// e.IsConsumed = true;
e.IsCancelled = Recognizer.CancelsTouchesInView;
}
} else if (State == GestureRecognizerState.Cancelled || State == GestureRecognizerState.Ended || State == GestureRecognizerState.Failed) {
return;
} else if (e.ActionMasked == MotionEventActions.Cancel) {
State = GestureRecognizerState.Cancelled;
Console.WriteLine ("GESTURE CANCELLED");
} else if (e.ActionMasked == MotionEventActions.Up) {
OnUp (e);
// e.IsConsumed = State != GestureRecognizerState.Failed;
}
}
#endregion
void OnDown (GestureMotionEvent e)
{
//TODO - should really be possible until all taps/fingers are satisfied.
State = GestureRecognizerState.Began;
// State = (e.PointerCount == SwipeGestureRecognizer.NumberOfTouchesRequired) ? GestureRecognizerState.Began : GestureRecognizerState.Failed;
FirstTouchPoint = new Xamarin.Forms.Point (e.GetX (0), e.GetY (0));
_startTime = DateTime.Now;
}
void OnUp (GestureMotionEvent e)
{
NumberOfTouches = e.PointerCount;
var tookTooLong = (DateTime.Now - _startTime).Milliseconds > MaxSwipeDuration;
var wrongAmountOfTouches = NumberOfTouches < SwipeGestureRecognizer.NumberOfTouchesRequired;
if (tookTooLong || wrongAmountOfTouches) {
State = GestureRecognizerState.Failed;
return;
}
var endTouchPoint = new Xamarin.Forms.Point (e.GetX (0), e.GetY (0));
double velocityX = endTouchPoint.X - FirstTouchPoint.X;
double velocityY = endTouchPoint.Y - FirstTouchPoint.Y;
var direction = GetSwipeDirection (velocityX, velocityY);
var expectedDirection = (Recognizer as SwipeGestureRecognizer).Direction;
if (direction == expectedDirection) {
State = GestureRecognizerState.Recognized;
} else {
State = GestureRecognizerState.Failed;
Console.WriteLine ("failed gesture was expecting {0} got {1}", expectedDirection, direction);
}
}
SwipeGestureRecognizerDirection GetSwipeDirection (double velocityX, double velocityY)
{
var isHorizontalSwipe = Math.Abs (velocityX) > Math.Abs (velocityY);
if (isHorizontalSwipe) {
return velocityX > 0 ? SwipeGestureRecognizerDirection.Right : SwipeGestureRecognizerDirection.Left;
} else {
return velocityY > 0 ? SwipeGestureRecognizerDirection.Down : SwipeGestureRecognizerDirection.Up;
}
}
}
}
using System;
using Xamarin.Forms;
namespace TwinTechs.Gestures
{
public interface INativePanGestureRecognizer : INativeGestureRecognizer
{
Point GetVelocityInView (VisualElement view);
void SetTranslationInView (Point translation, VisualElement view);
Point GetTranslationInView (VisualElement view);
}
public class PanGestureRecognizer : BaseGestureRecognizer
{
public int MinimumNumberOfTouches { get; set; }
public int MaximumNumberOfTouches { get; set; }
public Point GetVelocityInView (VisualElement view)
{
return (NativeGestureRecognizer as INativePanGestureRecognizer).GetVelocityInView (view);
}
public void SetTranslationInView (Point translation, VisualElement view)
{
(NativeGestureRecognizer as INativePanGestureRecognizer).SetTranslationInView (translation, view);
}
public Point GetTranslationInView (VisualElement view)
{
return (NativeGestureRecognizer as INativePanGestureRecognizer).GetTranslationInView (view);
}
public PanGestureRecognizer ()
{
MinimumNumberOfTouches = 1;
MaximumNumberOfTouches = 1;
}
public override string ToString ()
{
return string.Format ("[PanGestureRecognizer: MinimumNumberOfTouches={0}, MaximumNumberOfTouches={1}, State={2}]", MinimumNumberOfTouches, MaximumNumberOfTouches, State);
}
}
}
/**
* We have an issue with Android that makes it pretty much impossible to compose gestures.
* Below is my stab at picking up gestures from the activity - the idea is that the main activity would pass all touches to this method
* the system will work, and will work very richly (with delay touch, cancel touches, delegate callback for should
* recognize in parallel, etc)
* However, my time is limited, so this is crude for now.
*/
public void ProcessGestureMotionEvent (GestureMotionEvent gestureEvent)
{
var ev = gestureEvent.MotionEvent;
var nativeViewScreenLocation = Recognizer.View.GetNativeScreenPosition ();
var offset = Xamarin.Forms.Point.Zero;
var touchPoint = new Xamarin.Forms.Point (ev.GetX (), ev.GetY ());
var mainPointerId = ev.GetPointerId (0);
//1. is it inside the view?
// Console.WriteLine ("touch point {0} vlocs {1} vlocw {2}", touchPoint.PrettyPrint (), nativeViewScreenLocation.PrettyPrint (), nativeViewWindowLocation.PrettyPrint ());
// Console.WriteLine ("touch point {0} view bounds {1} size {2},{3}", touchPoint, nativeViewScreenLocation, NativeView.Width, NativeView.Height);
var isInsideOfView = touchPoint.X >= nativeViewScreenLocation.X && touchPoint.Y >= nativeViewScreenLocation.Y &&
touchPoint.X <= (NativeView.Width + nativeViewScreenLocation.X) && touchPoint.Y <= (NativeView.Height + nativeViewScreenLocation.Y);
//2. report touches inside, or outside but tracked? (so cancels can occur)
//TODO track more touches
if (isInsideOfView || PointerId == mainPointerId) {
//if letting the view know, translate the coords into local view coords (apply the offsets to the touch)
offset.X = -nativeViewScreenLocation.X;
offset.Y = -nativeViewScreenLocation.Y;
ev.OffsetLocation ((float)offset.X, (float)offset.Y);
var offsetLocation = new Xamarin.Forms.Point (ev.GetX (), ev.GetY ());
if (isInsideOfView) {
// Console.WriteLine ("INSIDE " + ev.Action + " offset " + offset.PrettyPrint () + " results in " + offsetLocation.PrettyPrint ());
} else {
// Console.WriteLine ("touch outside view, but was tracked " + offset);
}
//TODO - ask the view if it's happy to process this touch at the same time as another gesture - I see no way to make it work for views.. (without
//an entire Touch dispatching mechanism:/)
//that will be done by 2 parses - one to discover all *gestures* that want the touch, then another parse to go back through and either cancel
//or pass the touches long
//that's not implemented yet though (time)
ProcessMotionEvent (gestureEvent);
//remove the offset
ev.OffsetLocation ((float)-offset.X, (float)-offset.Y);
}
// Console.WriteLine ("location " + ev.GetX () + ", " + ev.GetY () + " offset " + offset);
}
using System;
using Xamarin.Forms;
namespace TwinTechs.Gestures
{
public interface ITapGestureRecognizer : INativeGestureRecognizer
{
}
public class TapGestureRecognizer : BaseGestureRecognizer
{
public TapGestureRecognizer ()
{
NumberOfTapsRequired = 1;
NumberOfTouchesRequired = 1;
}
public int NumberOfTapsRequired { get; set; }
public int NumberOfTouchesRequired { get; set; }
public override string ToString ()
{
return string.Format ("[TapGestureRecognizer: NumberOfTapsRequired={0}, NumberOfTouchesRequired={1}, State={2}]", NumberOfTapsRequired, NumberOfTouchesRequired, State);
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TwinTechs.Example.Gestures.GestureYoutubeLikeExample"
xmlns:gestures="clr-namespace:TwinTechs.Gestures;assembly=TwinTechsForms"
xmlns:cells="clr-namespace:TwinTechs.Example.Gestures.Cells;assembly=TwinTechsFormsExample"
xmlns:local="clr-namespace:TwinTechs.Example.Gestures;assembly=TwinTechsFormsExample"
xmlns:controls="clr-namespace:TwinTechs.Controls;assembly=TwinTechsForms"
BackgroundColor="Silver"
Title="SwipeExample">
<local:SimpleLayout
BackgroundColor="Silver"
x:Name="MainLayout"
IsHandlingLayoutManually="true">
<ListView
BackgroundColor="Silver"
x:Name="MediaItemsListView"
SeparatorVisibility="None"
ItemSelected="OnItemSelected"
RowHeight="100">
<ListView.ItemTemplate>
<DataTemplate>
<cells:VideoCell />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<controls:PageViewContainer
BackgroundColor="Transparent"
x:Name="PageContainer" />
</local:SimpleLayout>
</ContentPage>
using System;
using System.Collections.Generic;
using Xamarin.Forms;
using TwinTechs.Gestures;
using System.Diagnostics;
namespace TwinTechs.Example.Gestures
{
public partial class GestureYoutubeLikeExample : ContentPage
{
Rectangle _contentBounds = new Rectangle (100, 200, 150, 150);
YoutubeStyleContentPage _contentPage;
PanGestureRecognizer _panGesture;
bool _didLayoutContainer;
public GestureYoutubeLikeExample ()
{
InitializeComponent ();
MainLayout.OnLayoutChildren += MainLayout_OnLayoutChildren;
MediaItemsListView.ItemsSource = DataProvider.GetMediaItems ();
}
protected override void LayoutChildren (double x, double y, double width, double height)
{
base.LayoutChildren (x, y, width, height);
if (_contentPage != null) {
_contentPage.ParentHeight = height;
}
}
void MainLayout_OnLayoutChildren (double x, double y, double width, double height)
{
MediaItemsListView.Layout (new Rectangle (0, 0, Width, Height));
if (!_didLayoutContainer) {
_contentBounds.Y = height - 100;
_contentBounds.X = width - 160;
_contentBounds.Width = 160;
_contentBounds.Height = 100;
_didLayoutContainer = true;
}
PageContainer.Layout (_contentBounds);
}
Rectangle _startBounds;
void Gesture_OnAction (BaseGestureRecognizer recgonizer, GestureRecognizerState state)
{
if (recgonizer.View != _contentPage.VideoPlayerView) {
return;
}
var panGesture = recgonizer as PanGestureRecognizer;
Point translation = panGesture.GetTranslationInView (MainLayout);
Point velocity = panGesture.GetVelocityInView (MainLayout);
panGesture.SetTranslationInView (new Point (0, 0), MainLayout);
switch (panGesture.State) {
case GestureRecognizerState.Began:
break;
case GestureRecognizerState.Changed:
var newY = _contentBounds.Y + translation.Y;
if (newY > 0 && newY < Height - _contentPage.MinimumHeightRequest) {
var minHeight = _contentPage.MinimumHeightRequest;
var minWidth = _contentPage.MinimumWidthRequest;
_contentBounds.Y = newY;
var complete = Math.Min (1, (Height - (_contentBounds.Y + minHeight)) / Height);
// Debug.WriteLine ("complete {0} newY {1} h{2}", complete, newY, Height);
var inverseCompletion = 1 - complete;
_contentBounds.X = (Width - minWidth) * inverseCompletion;
_contentBounds.Width = (minWidth) + ((Width - minWidth) * complete);
_contentBounds.Height = Math.Max (minHeight, (Height + minHeight) * complete);
PageContainer.Layout (_contentBounds);
}
break;
case GestureRecognizerState.Cancelled:
case GestureRecognizerState.Ended:
case GestureRecognizerState.Failed:
var isShowing = _contentBounds.Y < 200;
ToggleShowing (isShowing, true);
break;
default:
break;
}
}
protected override void OnDisappearing ()
{
base.OnDisappearing ();
if (_contentPage != null) {
_contentPage.VideoPlayerView.RemoveAllGestureRecognizers ();
}
}
void OnItemSelected (object sender, SelectedItemChangedEventArgs e)
{
ToggleShowing (true, true);
}
void ToggleShowing (bool isShowing, bool animated)
{
if (_contentPage == null) {
_contentPage = new YoutubeStyleContentPage ();
_contentPage.ParentHeight = Height;
PageContainer.Content = _contentPage;
_panGesture = new PanGestureRecognizer ();
_panGesture.OnAction += Gesture_OnAction;
_panGesture.IsConsumingTouchesInParallel = true;
_contentPage.VideoPlayerView.AddGestureRecognizer (_panGesture);
}
var minHeight = _contentPage.MinimumHeightRequest;
var minWidth = _contentPage.MinimumWidthRequest;
_contentBounds.Y = isShowing ? 0 : Height - minHeight;
_contentBounds.X = isShowing ? 0 : Width - minWidth;
_contentBounds.Width = isShowing ? Width : minWidth;
_contentBounds.Height = isShowing ? Height : minHeight;
if (MediaItemsListView.SelectedItem != null) {
_contentPage.Item = MediaItemsListView.SelectedItem as MediaItem;
}
if (animated) {
PageContainer.LayoutTo (_contentBounds);
} else {
PageContainer.Layout (_contentBounds);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment