Skip to content

Commit 8bcc042

Browse files
piersdeseillignyAndreiMisiukevich
authored andcommitted
Add support for native touch feedback animations (#34)
* Add NativeAnimation * style fixes * code style fixes * Update README.md * Update NativeAnimation API * code cleanup
1 parent c9533ec commit 8bcc042

File tree

9 files changed

+366
-24
lines changed

9 files changed

+366
-24
lines changed

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# TouchView control for Xamarin Forms (based on Xamarin Forms AbsoluteLayout)
2-
This plugin provides opportunity to create views with touch effects without using TapGestureRecognizer
2+
This plugin provides opportunity to create views with touch effects without using TapGestureRecognizer. It makes it possible to change the appearance of any control in response to touch events, either directly via xaml or with your custom logic hooked up to the events which this plugin exposes.
3+
4+
With this plugin it is also possible to respond to hover events (if the platform exposes them) and to display native touch feedback events (Tilt on UWP, Ripple on Android).
35

46
### Build Status
57
* Azure DevOps: [![Build status](https://dev.azure.com/andreimisiukevich/TouchView/_apis/build/status/TouchView-nuget-CI)](https://dev.azure.com/andreimisiukevich/TouchView/_build/latest?definitionId=1)
@@ -37,9 +39,9 @@ This plugin provides opportunity to create views with touch effects without usin
3739
## Samples
3840
The samples you can find here https://github.com/AndreiMisiukevich/TouchEffect/tree/master/TouchEffectSample
3941

40-
**XAML:** use TouchEff for achieving responisve UI (Changing background image or/and background color or/and opacity or/and scale).
42+
**XAML:** use TouchEff for achieving repsonsive UI (Changing background image or/and background color or/and opacity or/and scale).
4143

42-
Add TouchEff to element's Effects collection and use TouchEff attached propertis for setting up touc visual effect.
44+
Add TouchEff to element's Effects collection and use TouchEff attached properties for setting up touch visual effect.
4345

4446
```xaml
4547
...
@@ -138,12 +140,17 @@ RegularAnimationDuration | `int` | 0 | The duration of animation by applying Reg
138140
RegularAnimationEasing | `Easing` | null | The easing of animation by applying RegularOpacity and/or RegularBackgroundColor and/or RegularScale
139141
RippleCount | `int` | 0 | This property allows to set ripple of animation (Pressed/Regular animation loop). '**0**: disabled'; '**-1**: infinite loop'; '**1, 2, 3 ... n**: Ripple's interations'
140142
IsToggled | `bool?` | null | This property allows to achieve "switch" behavior. **null** means that feature is disabled and view will return to inital state after touch releasing
143+
NativeAnimation | `bool` | false | If native platform touch feedback animations are present (Tilt on UWP, Ripple on Android)
144+
NativeAnimationColor | `Color` | Color.Default | The color used for the native touch feedback animation
145+
NativeAnimationRadius | `int` | -1 | The radius of the native ripple animation on Android
141146

142147
### TouchEff Attached events
143148
Event | Type | Default | Description
144149
--- | --- | --- | ---
145150
StatusChanged | `TEffectStatusChangedHandler` | null | Touch status changed
146151
StateChanged | `TEffectStateChangedHandler` | null | Touch state changed
152+
HoverStatusChanged | `TEffectHoverStatusChangedHandler` | null | Hover status changed
153+
HoverStateChanged | `TEffectHoverStateChangedHandler` | null | Hover state changed
147154
Completed | `TEffectCompletedHandler` | null | User tapped
148155
AnimationStarted | `AnimationStartedHandler` | null | Animation started
149156

TouchEffect.Droid/PlatformTouchEff.cs

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
using Android.Views;
99
using AView = Android.Views.View;
1010
using System;
11+
using Android.Graphics.Drawables;
12+
using Android.Widget;
13+
using Android.Animation;
14+
using Android.Graphics;
15+
using Color = Android.Graphics.Color;
16+
using Android.Content.Res;
1117

1218
[assembly: ResolutionGroupName(nameof(TouchEffect))]
1319
[assembly: ExportEffect(typeof(PlatformTouchEff), nameof(TouchEff))]
@@ -18,8 +24,12 @@ public class PlatformTouchEff : PlatformEffect
1824
{
1925
public static void Preserve() { }
2026

27+
2128
private TouchEff _effect;
2229
private bool _isHoverSupported;
30+
private RippleDrawable _ripple;
31+
private FrameLayout _viewOverlay;
32+
private AView View => Control ?? Container;
2333

2434
protected override void OnAttached()
2535
{
@@ -35,6 +45,26 @@ protected override void OnAttached()
3545
{
3646
Control.Touch += OnTouch;
3747
}
48+
49+
if(_effect.NativeAnimation)
50+
{
51+
View.Clickable = true;
52+
View.LongClickable = true;
53+
_viewOverlay = new FrameLayout(Container.Context)
54+
{
55+
LayoutParameters = new ViewGroup.LayoutParams(-1, -1),
56+
Clickable = false,
57+
Focusable = false,
58+
};
59+
View.LayoutChange += LayoutChange;
60+
61+
_ripple = CreateRipple();
62+
_ripple.Radius = _effect.NativeAnimationRadius;
63+
_viewOverlay.Background = _ripple;
64+
65+
Container.AddView(_viewOverlay);
66+
_viewOverlay.BringToFront();
67+
}
3868
}
3969

4070
protected override void OnDetached()
@@ -51,6 +81,15 @@ protected override void OnDetached()
5181
{
5282
Control.Touch -= OnTouch;
5383
}
84+
Container.LayoutChange -= LayoutChange;
85+
if (_viewOverlay != null)
86+
{
87+
Container.RemoveView(_viewOverlay);
88+
_viewOverlay.Pressed = false;
89+
_viewOverlay.Foreground = null;
90+
_viewOverlay.Dispose();
91+
_ripple?.Dispose();
92+
}
5493
}
5594
catch (ObjectDisposedException)
5695
{
@@ -66,16 +105,19 @@ private void OnTouch(object sender, AView.TouchEventArgs e)
66105
{
67106
case MotionEventActions.Down:
68107
Element.GetTouchEff().HandleTouch(TouchStatus.Started);
108+
StartRipple(e.Event.GetX(), e.Event.GetY());
69109
break;
70110
case MotionEventActions.Up:
71111
Element.GetTouchEff().HandleTouch(Element.GetTouchEff().Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled);
112+
EndRipple();
72113
break;
73114
case MotionEventActions.Cancel:
74115
Element.GetTouchEff().HandleTouch(TouchStatus.Canceled);
116+
EndRipple();
75117
break;
76118
case MotionEventActions.Move:
77119
var view = sender as AView;
78-
var screenPointerCoords = new Point(view.Left + e.Event.GetX(), view.Top + e.Event.GetY());
120+
var screenPointerCoords = new Xamarin.Forms.Point(view.Left + e.Event.GetX(), view.Top + e.Event.GetY());
79121
var viewRect = new Rectangle(view.Left, view.Top, view.Right - view.Left, view.Bottom - view.Top);
80122
var status = viewRect.Contains(screenPointerCoords) ? TouchStatus.Started : TouchStatus.Canceled;
81123

@@ -88,6 +130,10 @@ private void OnTouch(object sender, AView.TouchEventArgs e)
88130
if (Element.GetTouchEff().Status != status)
89131
{
90132
Element.GetTouchEff().HandleTouch(status);
133+
if(status == TouchStatus.Started)
134+
StartRipple(e.Event.GetX(), e.Event.GetY());
135+
if (status == TouchStatus.Canceled)
136+
EndRipple();
91137
}
92138
break;
93139
case MotionEventActions.HoverEnter:
@@ -103,5 +149,73 @@ private void OnTouch(object sender, AView.TouchEventArgs e)
103149
break;
104150
}
105151
}
152+
153+
public bool StartRipple(float x, float y)
154+
{
155+
if (_effect.NativeAnimation && _viewOverlay.Background is RippleDrawable)
156+
{
157+
_viewOverlay.BringToFront();
158+
_ripple.SetHotspot(x, y);
159+
_viewOverlay.Pressed = true;
160+
return true;
161+
}
162+
return false;
163+
}
164+
165+
public bool EndRipple()
166+
{
167+
if (_viewOverlay != null && _viewOverlay.Pressed)
168+
{
169+
_viewOverlay.Pressed = false;
170+
return true;
171+
}
172+
return false;
173+
}
174+
175+
private RippleDrawable CreateRipple()
176+
{
177+
if (Element is Layout)
178+
{
179+
var mask = new ColorDrawable(Color.White);
180+
return new RippleDrawable(GetColorStateList(), null, mask);
181+
}
182+
183+
var background = (Control ?? Container).Background;
184+
if (background == null)
185+
{
186+
var mask = new ColorDrawable(Color.White);
187+
return new RippleDrawable(GetColorStateList(), null, mask);
188+
}
189+
190+
if (background is RippleDrawable)
191+
{
192+
var ripple = (RippleDrawable)background.GetConstantState().NewDrawable();
193+
ripple.SetColor(GetColorStateList());
194+
return ripple;
195+
}
196+
return new RippleDrawable(GetColorStateList(), background, null);
197+
}
198+
199+
private ColorStateList GetColorStateList()
200+
{
201+
int color;
202+
var defaultcolor = TouchEff.GetNativeAnimationColor(Element);
203+
if (defaultcolor != Xamarin.Forms.Color.Default)
204+
color = defaultcolor.ToAndroid();
205+
else
206+
color = Color.Argb(31, 0, 0, 0);
207+
208+
return new ColorStateList(
209+
new[] { new int[] { } },
210+
new[] { color, });
211+
}
212+
213+
private void LayoutChange(object sender, AView.LayoutChangeEventArgs e)
214+
{
215+
var group = (ViewGroup)sender;
216+
if (group == null || (Container as IVisualElementRenderer)?.Element == null) return;
217+
_viewOverlay.Right = group.Width;
218+
_viewOverlay.Bottom = group.Height;
219+
}
106220
}
107221
}

TouchEffect.UWP/PlatformTouchEff.cs

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
using Xamarin.Forms;
77
using Xamarin.Forms.Internals;
88
using Xamarin.Forms.Platform.UWP;
9+
using Windows.UI.Xaml.Media.Animation;
10+
using System;
11+
using System.Collections.Generic;
912

1013
[assembly: ResolutionGroupName(nameof(TouchEffect))]
1114
[assembly: ExportEffect(typeof(PlatformTouchEff), nameof(TouchEff))]
@@ -24,11 +27,48 @@ public static void Preserve()
2427
private bool _pressed;
2528
private bool _intentionalCaptureLoss;
2629

30+
private Storyboard _pointerDownStoryboard;
31+
private Storyboard _pointerUpStoryboard;
32+
2733
protected override void OnAttached()
2834
{
2935
_effect = Element.GetTouchEff();
3036
_effect.Control = Element as VisualElement;
3137
_effect.ForceUpdateState(false);
38+
if (_effect.NativeAnimation)
39+
{
40+
var nativeControl = Container;
41+
if (String.IsNullOrEmpty(nativeControl.Name))
42+
{
43+
nativeControl.Name = Guid.NewGuid().ToString();
44+
}
45+
46+
if (nativeControl.Resources.ContainsKey("PointerDownAnimation"))
47+
{
48+
_pointerDownStoryboard = (Storyboard)nativeControl.Resources["PointerDownAnimation"];
49+
}
50+
else
51+
{
52+
_pointerDownStoryboard = new Storyboard();
53+
var downThemeAnimation = new PointerDownThemeAnimation();
54+
Storyboard.SetTargetName(downThemeAnimation, nativeControl.Name);
55+
_pointerDownStoryboard.Children.Add(downThemeAnimation);
56+
nativeControl.Resources.Add(new KeyValuePair<object, object>("PointerDownAnimation", _pointerDownStoryboard));
57+
}
58+
59+
if (nativeControl.Resources.ContainsKey("PointerUpAnimation"))
60+
{
61+
_pointerUpStoryboard = (Storyboard)nativeControl.Resources["PointerUpAnimation"];
62+
}
63+
else
64+
{
65+
_pointerUpStoryboard = new Storyboard();
66+
var upThemeAnimation = new PointerUpThemeAnimation();
67+
Storyboard.SetTargetName(upThemeAnimation, nativeControl.Name);
68+
_pointerUpStoryboard.Children.Add(upThemeAnimation);
69+
nativeControl.Resources.Add(new KeyValuePair<object, object>("PointerUpAnimation", _pointerUpStoryboard));
70+
}
71+
}
3272

3373
if (Container != null)
3474
{
@@ -60,52 +100,62 @@ protected override void OnDetached()
60100

61101
private void OnPointerEntered(object sender, PointerRoutedEventArgs e)
62102
{
63-
_effect.HandleHover(HoverStatus.Entered);
64-
if (_pressed)
103+
Element.GetTouchEff().HandleHover(HoverStatus.Entered);
104+
if (_pressed)
65105
{
66-
_effect.HandleTouch(TouchStatus.Started);
106+
Element.GetTouchEff().HandleTouch(TouchStatus.Started);
107+
AnimateTilt(_pointerDownStoryboard);
67108
}
68109
}
69110

70111
private void OnPointerExited(object sender, PointerRoutedEventArgs e)
71112
{
72-
_effect.HandleHover(HoverStatus.Exited);
73-
if (_pressed)
113+
if (_pressed)
74114
{
75-
_effect.HandleTouch(TouchStatus.Canceled);
115+
Element.GetTouchEff().HandleTouch(TouchStatus.Canceled);
116+
AnimateTilt(_pointerUpStoryboard);
76117
}
118+
Element.GetTouchEff().HandleHover(HoverStatus.Exited);
77119
}
78120

79121
private void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
80122
{
81123
_pressed = false;
82-
_effect.HandleHover(HoverStatus.Exited);
83-
_effect.HandleTouch(TouchStatus.Canceled);
124+
Element.GetTouchEff().HandleTouch(TouchStatus.Canceled);
125+
Element.GetTouchEff().HandleHover(HoverStatus.Exited);
126+
AnimateTilt(_pointerUpStoryboard);
84127
}
85128

86129
private void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
87130
{
88131
if (_intentionalCaptureLoss) return;
89132
_pressed = false;
90-
91-
if (_effect.HoverStatus != HoverStatus.Exited)
133+
if (_effect.Status != TouchStatus.Canceled)
92134
{
93-
_effect.HandleHover(HoverStatus.Exited);
135+
Element.GetTouchEff().HandleTouch(TouchStatus.Canceled);
94136
}
95-
96-
if (_effect.Status != TouchStatus.Canceled)
137+
if (_effect.HoverStatus != HoverStatus.Exited)
97138
{
98-
_effect.HandleTouch(TouchStatus.Canceled);
139+
Element.GetTouchEff().HandleHover(HoverStatus.Exited);
99140
}
141+
142+
AnimateTilt(_pointerUpStoryboard);
100143
}
101144

102145
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
103146
{
104147

105-
if(_pressed && (_effect.HoverStatus == HoverStatus.Entered))
106-
_effect.HandleTouch(TouchStatus.Completed);
107-
else if(_effect.HoverStatus != HoverStatus.Exited)
108-
_effect.HandleTouch(TouchStatus.Canceled);
148+
if(_pressed && (Element.GetTouchEff().HoverStatus == HoverStatus.Entered))
149+
{
150+
Element.GetTouchEff().HandleTouch(TouchStatus.Completed);
151+
AnimateTilt(_pointerUpStoryboard);
152+
}
153+
else if(Element.GetTouchEff().HoverStatus != HoverStatus.Exited)
154+
{
155+
Element.GetTouchEff().HandleTouch(TouchStatus.Canceled);
156+
AnimateTilt(_pointerUpStoryboard);
157+
}
158+
109159
_pressed = false;
110160
_intentionalCaptureLoss = true;
111161
}
@@ -114,8 +164,17 @@ private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
114164
{
115165
_pressed = true;
116166
Container.CapturePointer(e.Pointer);
117-
_effect.HandleTouch(TouchStatus.Started);
167+
Element.GetTouchEff().HandleTouch(TouchStatus.Started);
168+
AnimateTilt(_pointerDownStoryboard);
118169
_intentionalCaptureLoss = false;
119170
}
171+
172+
private void AnimateTilt(Storyboard storyboard)
173+
{
174+
if (_effect.NativeAnimation && storyboard != null) {
175+
storyboard.Stop();
176+
storyboard.Begin();
177+
}
178+
}
120179
}
121-
}
180+
}

TouchEffect/Interfaces/ITouchEff.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ public interface ITouchEff
9696
[EditorBrowsable(EditorBrowsableState.Never)]
9797
bool IsCompletedSet { get; }
9898

99+
bool NativeAnimation { get; }
100+
101+
Color NativeAnimationColor { get; }
102+
103+
int NativeAnimationRadius { get; }
104+
99105
VisualElement Control { get; set; }
100106

101107
[EditorBrowsable(EditorBrowsableState.Never)]

0 commit comments

Comments
 (0)