Ripple effect in Xamarin.Android
Tags: xamarin, xamarinforms, android, csharp
Published: 2021-07-04 12:29:40

If you want your controls to have ripple effect in Xamarin.Android you can use the `TouchEffect` from the [Xamarin Community Toolkit] (https://github.com/xamarin/XamarinCommunityToolkit), for most use cases it should work well. 

But I wanted to have ripple effect in a CollectionView and I need a Switch in the `ItemTemplate` to receive click events and according to [this issue] (https://github.com/xamarin/XamarinCommunityToolkit/issues/852) the TouchEffect doesn't work well on Android if your view has clickable children, so I had to roll my own. 


## Creating a ripple effect in Xamarin.Android
To achieve our goal, we're going to need to handle the touch events ourselves in order to fire the ripple effect. But first, let's create a `RoutingEffect` with some bindable properties to be used in the Android project. 

### Creating a RoutingEffect
We are going to add some bindable properties to our Effect: 
* Command
* CommandParameter
* LongPressCommand
* LongPressCommandParameter
* NormalColor
* RippleColor
* SelectedColor.

First, let's create a new class for our RoutingEffect.
Always remember to add the company name to avoid clashes with other effects. 
```csharp
public class TouchRippleEffect : RoutingEffect
{
        public TouchRippleEffect() : base("companyName.TouchRippleEffect")
        {
        }
}
```

Now, let's add the bindable properties:
```csharp
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());
}
```

Notice we're are attaching the Effect to the view if one of the properties change, this is so we can use the Effect directly on the control without having to add it to the <Effects> collection of the control.

### Adding the PlatformEffect in the Android project
Now that the Effect is ready, let's go to our Android project and add a `PlatformEffect`:

```csharp
public class PlatformTouchRippleEffect : PlatformEffect
{
  public bool IsDisposed => (Container as IVisualElementRenderer)?.Element == null;
}
```

Remember to export the effect:
```csharp
[assembly: ExportEffect(typeof(PlatformTouchRippleEffect), nameof(TouchRippleEffect))]
```

If this is the first effect in your solution, then you must also add the `ResolutionGroupName`:
```csharp
[assembly: ResolutionGroupName("companyName")]
```

#### OnAttached logic
When the effect is attached, we simply need to subscribe to the `OnTouch` and `LongClick` events so we can execute the respective commands:

```csharp
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;
        }
    }
}
```

#### OnDetached
We simply unsubscribe from the events to prevent memory leaks:
```csharp
protected override void OnDetached()
{
    if (IsDisposed) return;
    
    Container.Touch -= OnTouch;
    Container.LongClick -= ContainerOnLongClick;
}
```

#### Adding background drawables
In Android, in order to change the color applied when the view state changes, you must set the background (or foreground) with a drawable that will be used on certain state. 

We are going to add a drawable to the background for each of the states of our properties (Normal, Pressed (ripple) and Selected) with a `StateListDrawable`:

```csharp
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);
}
```

### Handling user touch

Since we are going to handle the touch interactions ourselves, let's add some flags to know the current state of the touch event:
```csharp
private float _prevY;
private bool _hasMoved;
private bool _isLongPress;
```
### Handling click event
The first thing we need is to save the current action and position when the touch interaction starts:

#### Touch start
```csharp
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;
    }
}
```

#### Touch finished
Now, we need to handle each case when the user finishes the touch, moves the finger, or the touch event is cancelled. First let's see the finish and cancel cases:
```csharp
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;
}
```

#### Touch move
Now, the tricky part is the `Move` action. 
It happened that sometimes the current view would get the selected color if you touched it and then scrolled, and would lose the state if it was selected.
```csharp
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;
}
``` 
I had to check the `Container.Activated` state to see if the item was selected to handle each case. This flag is set in the long click event. 

### Handling long click
In the long click event, we will set the `_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.
```csharp
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;
}
``` 

## Using the ripple effect
With all that in place, now it's time to put our effect to good use so let's create a simple app to see it in action.

The **model**:
```csharp
public class TouchEffectModel
    {
        public TouchEffectModel(string labelText)
        {
            LabelText = labelText;
        }

        public string LabelText { get; set; }
    }
```
The **ViewModel** is a simple ViewModel with two command and a collection, I'll omit the Commands for brevity:
```csharp
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"));
}
```

The **View**:
```xml
<?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>
```

Now we can see our effect in action:
![Ripple effect in action](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hxewgfk5jb6jkqnrinw0.gif)

## Repository
The code for the article is at this repo.

{% github jpozo20/xamarin-samples %}


## Conclusions
Though in general Xamarin works really well out of the box, more often than not I've found that if you want niceties in Xamarin.Forms you have to put your hands at the native platforms for certain features. 

In the next entry I'll be talking about adding a contextual menu for multiple selection in the toolbar when using a CollectionView, like the menu that Whatsapp or Telegram display when you select a chat or a message.