xamarin-samples
Xamarin code for posts published at dev.to/jefrypozo
This website collects cookies to deliver better user experience
TouchEffect
from the Xamarin Community Toolkit, for most use cases it should work well. ItemTemplate
to receive click events and according to this issue the TouchEffect doesn't work well on Android if your view has clickable children, so I had to roll my own. RoutingEffect
with some bindable properties to be used in the Android project. public class TouchRippleEffect : RoutingEffect
{
public TouchRippleEffect() : base("companyName.TouchRippleEffect")
{
}
}
public static readonly BindableProperty CommandProperty = BindableProperty.CreateAttached(
"Command",
typeof(ICommand),
typeof(TouchRippleEffect),
default(ICommand),
propertyChanged: OnCommandChanged
);
public static readonly BindableProperty CommandParameterProperty = BindableProperty.CreateAttached(
"CommandParameter",
typeof(object),
typeof(TouchRippleEffect),
null,
propertyChanged: OnCommandParameterChanged
);
public static readonly BindableProperty LongPressCommandProperty = BindableProperty.CreateAttached(
"LongPressCommand",
typeof(ICommand),
typeof(TouchRippleEffect),
default(ICommand),
propertyChanged: OnLongPressCommandChanged
);
public static readonly BindableProperty LongPressCommandParameterProperty = BindableProperty.CreateAttached(
"LongPressCommandParameter",
typeof(object),
typeof(TouchRippleEffect),
null,
propertyChanged: OnLongPressCommandParameterChanged
);
public static void SetCommand(BindableObject bindable, ICommand value)
{
bindable.SetValue(CommandProperty, value);
}
public static ICommand GetCommand(BindableObject bindable)
{
return (ICommand) bindable.GetValue(CommandProperty);
}
private static void OnCommandChanged(BindableObject bindable, object oldValue, object newValue)
{
AttachEffect(bindable);
}
public static void SetCommandParameter(BindableObject bindable, object value)
{
bindable.SetValue(CommandParameterProperty, value);
}
public static object GetCommandParameter(BindableObject bindable)
{
return bindable.GetValue(CommandParameterProperty);
}
private static void OnCommandParameterChanged(BindableObject bindable, object oldValue, object newValue)
{
AttachEffect(bindable);
}
public static void SetLongPressCommand(BindableObject bindable, ICommand value)
{
bindable.SetValue(LongPressCommandProperty, value);
}
public static ICommand GetLongPressCommand(BindableObject bindable)
{
return (ICommand) bindable.GetValue(LongPressCommandProperty);
}
private static void OnLongPressCommandChanged(BindableObject bindable, object oldValue, object newValue)
{
AttachEffect(bindable);
}
public static void SetLongPressCommandParameter(BindableObject bindable, object value)
{
bindable.SetValue(LongPressCommandParameterProperty, value);
}
public static object GetLongPressCommandParameter(BindableObject bindable)
{
return bindable.GetValue(LongPressCommandParameterProperty);
}
private static void OnLongPressCommandParameterChanged(BindableObject bindable, object oldValue,
object newValue)
{
AttachEffect(bindable);
}
public static readonly BindableProperty NormalColorProperty = BindableProperty.CreateAttached(
"NormalColor",
typeof(Color),
typeof(TouchRippleEffect),
Color.White,
propertyChanged: OnNormalColorChanged
);
public static void SetNormalColor(BindableObject bindable, Color value)
{
bindable.SetValue(NormalColorProperty, value);
}
public static Color GetNormalColor(BindableObject bindable)
{
return (Color) bindable.GetValue(NormalColorProperty);
}
static void OnNormalColorChanged(BindableObject bindable, object oldValue, object newValue)
{
AttachEffect(bindable);
}
public static readonly BindableProperty RippleColorProperty = BindableProperty.CreateAttached(
"RippleColor",
typeof(Color),
typeof(TouchRippleEffect),
Color.LightSlateGray,
propertyChanged: OnRippleColorChanged
);
public static void SetRippleColor(BindableObject bindable, Color value)
{
bindable.SetValue(RippleColorProperty, value);
}
public static Color GetRippleColor(BindableObject bindable)
{
return (Color) bindable.GetValue(RippleColorProperty);
}
static void OnRippleColorChanged(BindableObject bindable, object oldValue, object newValue)
{
AttachEffect(bindable);
}
public static readonly BindableProperty SelectedColorProperty = BindableProperty.CreateAttached(
"SelectedColor",
typeof(Color),
typeof(TouchRippleEffect),
Color.LightGreen,
propertyChanged: OnSelectedColorChanged
);
public static void SetSelectedColor(BindableObject bindable, Color value)
{
bindable.SetValue(SelectedColorProperty, value);
}
public static Color GetSelectedColor(BindableObject bindable)
{
return (Color) bindable.GetValue(SelectedColorProperty);
}
static void OnSelectedColorChanged(BindableObject bindable, object oldValue, object newValue)
{
AttachEffect(bindable);
}
static void AttachEffect(BindableObject bindable)
{
if (bindable is not VisualElement view || view.Effects.OfType<TouchRippleEffect>().Any())
return;
view.Effects.Add(new TouchRippleEffect());
}
PlatformEffect
:public class PlatformTouchRippleEffect : PlatformEffect
{
public bool IsDisposed => (Container as IVisualElementRenderer)?.Element == null;
}
[assembly: ExportEffect(typeof(PlatformTouchRippleEffect), nameof(TouchRippleEffect))]
ResolutionGroupName
:[assembly: ResolutionGroupName("companyName")]
OnTouch
and LongClick
events so we can execute the respective commands:protected override void OnAttached()
{
if (Container != null)
{
SetBackgroundDrawables();
Container.HapticFeedbackEnabled = false;
var command = TouchRippleEffect.GetCommand(Element);
if (command != null)
{
Container.Clickable = true;
Container.Focusable = false;
Container.Touch += OnTouch;
}
var longPressCommand = TouchRippleEffect.GetLongPressCommand(Element);
if (longPressCommand != null)
{
Container.LongClickable = true;
Container.LongClick += ContainerOnLongClick;
}
}
}
protected override void OnDetached()
{
if (IsDisposed) return;
Container.Touch -= OnTouch;
Container.LongClick -= ContainerOnLongClick;
}
StateListDrawable
:private void SetBackgroundDrawables()
{
var normalColor = TouchRippleEffect.GetNormalColor(Element).ToAndroid();
var rippleColor = TouchRippleEffect.GetRippleColor(Element).ToAndroid();
var selectedColor = TouchRippleEffect.GetSelectedColor(Element).ToAndroid();
var stateList = new StateListDrawable();
var normalDrawable = new GradientDrawable(GradientDrawable.Orientation.LeftRight,
new int[] {normalColor, normalColor});
var rippleDrawable = new RippleDrawable(ColorStateList.ValueOf(rippleColor), _normalDrawable, null);
var activatedDrawable = new GradientDrawable(GradientDrawable.Orientation.LeftRight,
new int[] {selectedColor, selectedColor});
stateList.AddState(new[] {Android.Resource.Attribute.Enabled}, normalDrawable);
stateList.AddState(new[] {Android.Resource.Attribute.StatePressed}, rippleDrawable);
stateList.AddState(new[] {Android.Resource.Attribute.StateActivated}, activatedDrawable);
Container.SetBackground(stateList);
}
private float _prevY;
private bool _hasMoved;
private bool _isLongPress;
private void OnTouch(object sender, AView.TouchEventArgs e)
{
e.Handled = false;
var currentY = e.Event.GetY();
var action = e.Event?.Action;
switch (action)
{
case MotionEventActions.Down:
_prevY = e.Event.GetY();
break;
}
}
case MotionEventActions.Up:
{
// The TouchEvent is called again after a long click
// here we ensure the ClickCommand is only called
// when not doing a long click
if (!_isLongPress)
{
if (_hasMoved) return;
if (Container.Activated) Container.Activated = false;
var command = TouchRippleEffect.GetCommand(Element);
var param = TouchRippleEffect.GetCommandParameter(Element);
command?.Execute(param);
}
// If a long click was fired, set the flag to false
// so we can correctly register a single click again
_isLongPress = false;
_hasMoved = false;
break;
}
case MotionEventActions.Cancel:
{
_isLongPress = false;
_hasMoved = false;
break;
}
Move
action. case MotionEventActions.Move:
{
var diffY = currentY - _prevY;
var absolute = Math.Abs(diffY);
if (absolute > 8) _hasMoved = true;
if (_hasMoved && Container.Background is StateListDrawable drawable)
{
// Keep the NormalColor on scroll
if (!Container.Activated)
{
drawable.SetState(new[] {Android.Resource.Attribute.Enabled});
drawable.JumpToCurrentState();
}
else
{
// Keep the SelectedColor when scrolling and the touched view is a selected item
drawable.SetState(new[] {Android.Resource.Attribute.StateActivated});
drawable.JumpToCurrentState();
}
}
break;
}
Container.Activated
state to see if the item was selected to handle each case. This flag is set in the long click event. _isLongPress
flag to true, since the Touch event fires again after the long click. We will also notify the user by performing the haptic feedback.private void ContainerOnLongClick(object sender, AView.LongClickEventArgs e)
{
// Notify to the user that the item was selected
(sender as AView)?.PerformHapticFeedback(FeedbackConstants.LongPress);
// If item is currently selected, disable long click
if (_hasMoved)
{
_hasMoved = false;
return;
}
var command = TouchRippleEffect.GetLongPressCommand(Element);
var param = TouchRippleEffect.GetLongPressCommandParameter(Element);
command?.Execute(param);
_isLongPress = true;
// Set the Container.Activated and the selected color drawable
// so we know the item is selected on a next touch
if (Container.Background is StateListDrawable drawable)
{
drawable.SetState(new[] {Android.Resource.Attribute.StateActivated});
Container.Activated = true;
}
// Bubble up the event to avoid touch errors
e.Handled = false;
_hasMoved = false;
}
public class TouchEffectModel
{
public TouchEffectModel(string labelText)
{
LabelText = labelText;
}
public string LabelText { get; set; }
}
private ICollection<TouchEffectModel> _collection;
public ICollection<TouchEffectModel> Collection
{
get => _collection;
set
{
_collection = value;
RaisePropertyChanged();
}
}
void InitializeCollection()
{
if (_collection is null) _collection = new List<TouchEffectModel>();
_collection.Add(new TouchEffectModel("I'm item 1"));
_collection.Add(new TouchEffectModel("I'm item 2"));
_collection.Add(new TouchEffectModel("I'm item 3"));
_collection.Add(new TouchEffectModel("I'm item 4"));
_collection.Add(new TouchEffectModel("I'm item 5"));
}
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:XamarinSamples.ViewModels;assembly=XamarinSamples"
xmlns:effects="clr-namespace:XamarinSamples.Effects;assembly=XamarinSamples"
x:Class="XamarinSamples.Views.TouchEffectView">
<ContentPage.BindingContext>
<viewModels:TouchEffectViewModel x:Key="ViewModel" />
</ContentPage.BindingContext>
<ContentPage.Content>
<CollectionView x:Name="CollectionView"
ItemsSource="{Binding Collection}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Orientation="Horizontal" Margin="10"
effects:TouchRippleEffect.Command="{Binding BindingContext.ClickCommand, Source={x:Reference CollectionView}}"
effects:TouchRippleEffect.LongPressCommand="{Binding BindingContext.LongClickCommand, Source={x:Reference CollectionView}}">
<Label HorizontalOptions="CenterAndExpand"
VerticalOptions="Center"
InputTransparent="True"
Text="{Binding LabelText}" FontSize="16"
Margin="{x:OnPlatform Android='5,0,0,-13', iOS='0,0,0,-15'}" />
<Switch InputTransparent="False"
HorizontalOptions="End" VerticalOptions="Center"
Margin="{x:OnPlatform Android='0,0,5,0', iOS='0,0,5,0'}" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage.Content>
</ContentPage>
Xamarin code for posts published at dev.to/jefrypozo