using System.Linq; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Collections.Generic; using ElmSharp; using EColor = ElmSharp.Color; namespace Xamarin.Forms.Platform.Tizen { internal class GestureHandler { internal readonly IVisualElementRenderer _renderer; internal GestureLayer _gestureLayer; View _view => _renderer.Element as View; IPanGestureController _currentPanGestureController; int _currentPanGestureId; IPinchGestureController _currentPinchGestureController; Point _currentScalePoint; int _previousPinchRadius; double _originalPinchScale; Polygon _hitBox; public GestureHandler(IVisualElementRenderer renderer) { _renderer = renderer; // Whenever a GestureRecognizer is added to the View, it will be connected to GestureLayer (_view.GestureRecognizers as ObservableCollection).CollectionChanged += OnGestureRecognizersChanged; // handle GestureRecognizers which were already set by the time we got here if (_view.GestureRecognizers.Count > 0) { CreateGestureLayer(); foreach (var item in _view.GestureRecognizers) ToggleRecognizer(item, true); } } public void Clear() { // this will clear all callbacks in ElmSharp GestureLayer _gestureLayer.Unrealize(); (_view.GestureRecognizers as ObservableCollection).CollectionChanged -= OnGestureRecognizersChanged; if (_hitBox != null) { _hitBox.Unrealize(); _hitBox = null; } } public void UpdateHitBox() { if (_hitBox == null) return; // _hitBox has to be used because gestures do not work well with transformations (EvasMap) // so we create additional object which has the same shape as tranformed target, but does not have EvasMap on it EvasObject target = _renderer.NativeView; _hitBox.ClearPoints(); if (target.IsMapEnabled) { var map = target.EvasMap; Point3D point; for (var i = 0; i < 4; i++) { point = map.GetPointCoordinate(i); _hitBox.AddPoint(point.X, point.Y); } } else { var geometry = target.Geometry; if (geometry.Width == 0 || geometry.Height == 0) return; _hitBox.AddPoint(geometry.Left, geometry.Top); _hitBox.AddPoint(geometry.Right, geometry.Top); _hitBox.AddPoint(geometry.Right, geometry.Bottom); _hitBox.AddPoint(geometry.Left, geometry.Bottom); } } protected void ToggleRecognizer(IGestureRecognizer recognizer, bool enable) { TapGestureRecognizer tapRecognizer; PanGestureRecognizer panRecognizer; PinchGestureRecognizer pinchRecognizer; if ((tapRecognizer = recognizer as TapGestureRecognizer) != null) { ToggleTapRecognizer(tapRecognizer, enable); } else if ((panRecognizer = recognizer as PanGestureRecognizer) != null) { if (enable) AddPanRecognizer(panRecognizer); else RemovePanRecognizer(panRecognizer); } else if ((pinchRecognizer = recognizer as PinchGestureRecognizer) != null) { if (enable) AddPinchRecognizer(pinchRecognizer); else RemovePinchRecognizer(pinchRecognizer); } else { Log.Error("Unknown GestureRecognizer will be ignored: {0}", recognizer); } } void ToggleTapRecognizer(TapGestureRecognizer recognizer, bool enable) { GestureLayer.GestureType type; switch (recognizer.NumberOfTapsRequired) { case 1: type = GestureLayer.GestureType.Tap; break; case 2: type = GestureLayer.GestureType.DoubleTap; break; default: type = GestureLayer.GestureType.TripleTap; break; } if (enable) _gestureLayer.SetTapCallback(type, GestureLayer.GestureState.End, (data) => recognizer.SendTapped(_view)); else _gestureLayer.SetTapCallback(type, GestureLayer.GestureState.End, null); } void AddPanRecognizer(PanGestureRecognizer recognizer) { if (_currentPanGestureController != null) Log.Warn("More than one PanGestureRecognizer on {0}. Only the last one will work.", _view); EnsureHitBoxExists(); _currentPanGestureController = recognizer; _gestureLayer.SetLineCallback(GestureLayer.GestureState.Start, OnPanStarted); _gestureLayer.SetLineCallback(GestureLayer.GestureState.Move, OnPanMoved); _gestureLayer.SetLineCallback(GestureLayer.GestureState.End, OnPanCompleted); _gestureLayer.SetLineCallback(GestureLayer.GestureState.Abort, OnPanCancelled); } void RemovePanRecognizer(PanGestureRecognizer recognizer) { _gestureLayer.SetLineCallback(GestureLayer.GestureState.Start, null); _gestureLayer.SetLineCallback(GestureLayer.GestureState.Move, null); _gestureLayer.SetLineCallback(GestureLayer.GestureState.End, null); _gestureLayer.SetLineCallback(GestureLayer.GestureState.Abort, null); _currentPanGestureController = null; } void AddPinchRecognizer(PinchGestureRecognizer recognizer) { if (_currentPinchGestureController != null) Log.Warn("More than one PinchGestureRecognizer on {0}. Only the last one will work.", _view); EnsureHitBoxExists(); _currentPinchGestureController = recognizer; _gestureLayer.SetZoomCallback(GestureLayer.GestureState.Start, OnPinchStarted); _gestureLayer.SetZoomCallback(GestureLayer.GestureState.Move, OnPinchMoved); _gestureLayer.SetZoomCallback(GestureLayer.GestureState.End, OnPinchCompleted); _gestureLayer.SetZoomCallback(GestureLayer.GestureState.Abort, OnPinchCancelled); } void RemovePinchRecognizer(PinchGestureRecognizer recognizer) { _gestureLayer.SetZoomCallback(GestureLayer.GestureState.Start, null); _gestureLayer.SetZoomCallback(GestureLayer.GestureState.Move, null); _gestureLayer.SetZoomCallback(GestureLayer.GestureState.End, null); _gestureLayer.SetZoomCallback(GestureLayer.GestureState.Abort, null); _currentPinchGestureController = null; } void CreateGestureLayer() { _gestureLayer = new GestureLayer(_renderer.NativeView); _gestureLayer.Attach(_renderer.NativeView); } void EnsureHitBoxExists() { if (_hitBox == null) { Box parent = (Platform.GetRenderer(_renderer.Element.RealParent) as LayoutRenderer).Control; _hitBox = new Polygon(parent) { Color = EColor.Transparent }; _hitBox.Show(); UpdateHitBox(); parent.PackAfter(_hitBox, _renderer.NativeView); _gestureLayer.Attach(_hitBox); } } void AddAndRemoveRecognizers(IEnumerable removed, IEnumerable added) { if (_hitBox == null && added != null && added.Any(item => (item is IPanGestureController || item is IPinchGestureController))) { // at least one of the added recognizers requires _hitBot, which is not ready _gestureLayer.ClearCallbacks(); EnsureHitBoxExists(); // as _gestureLayer was reattached, register all callbacks, not only new ones removed = null; added = _view.GestureRecognizers; } if (removed != null) { foreach (var item in removed) ToggleRecognizer(item, false); } if (added != null) { foreach (var item in added) ToggleRecognizer(item, true); } } void OnGestureRecognizersChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { // Gestures will be registered/unregistered according to changes in the GestureRecognizers list switch (e.Action) { case NotifyCollectionChangedAction.Add: if (_gestureLayer == null) CreateGestureLayer(); AddAndRemoveRecognizers(null, e.NewItems.OfType()); break; case NotifyCollectionChangedAction.Replace: AddAndRemoveRecognizers(e.OldItems.OfType(), e.NewItems.OfType()); break; case NotifyCollectionChangedAction.Remove: AddAndRemoveRecognizers(e.OldItems.OfType(), null); break; case NotifyCollectionChangedAction.Reset: AddAndRemoveRecognizers(_view.GestureRecognizers, null); break; } } void OnPanStarted(GestureLayer.LineData data) { _currentPanGestureId++; _currentPanGestureController.SendPanStarted(_view, _currentPanGestureId); } void OnPanMoved(GestureLayer.LineData data) { _currentPanGestureController.SendPan(_view, data.X2 - data.X1, data.Y2 - data.Y1, _currentPanGestureId); } void OnPanCompleted(GestureLayer.LineData data) { _currentPanGestureController.SendPanCompleted(_view, _currentPanGestureId); } void OnPanCancelled(GestureLayer.LineData data) { // don't trust ElmSharp that the gesture has been aborted, report that it is completed _currentPanGestureController.SendPanCompleted(_view, _currentPanGestureId); } void OnPinchStarted(GestureLayer.ZoomData data) { var geometry = _renderer.NativeView.Geometry; _currentScalePoint = new Point((data.X - geometry.X) / (double)geometry.Width, (data.Y - geometry.Y) / (double)geometry.Height); _originalPinchScale = _view.Scale; _previousPinchRadius = data.Radius; _currentPinchGestureController.SendPinchStarted(_view, _currentScalePoint); } void OnPinchMoved(GestureLayer.ZoomData data) { if (_previousPinchRadius <= 0) _previousPinchRadius = 1; // functionality limitation: _currentScalePoint is not updated _currentPinchGestureController.SendPinch(_view, 1 + _originalPinchScale * (data.Radius - _previousPinchRadius) / _previousPinchRadius, _currentScalePoint ); _previousPinchRadius = data.Radius; } void OnPinchCompleted(GestureLayer.ZoomData data) { _currentPinchGestureController.SendPinchEnded(_view); } void OnPinchCancelled(GestureLayer.ZoomData data) { // ElmSharp says the gesture has been aborted really too often, report completion instead _currentPinchGestureController.SendPinchEnded(_view); } } }