Добавляем эффект нажатия в Xamarin.Forms

в 8:30, , рубрики: .net, C#, effects, xamarin, xamarin.forms, разработка мобильных приложений

Xamarin.Forms набирает обороты и, к сожалению, из коробки доступно совсем мало возможностей, все неоходимо добавлять через Dependency service или рендереры. На этой волне стало очень много различных библиотек, добавляющих зачастую базовый функционал.
Мое решение не исключение.

У меня стояла задача сделать небольшое расширение, позволяющее добавить эффект нажатия на почти любой элемент для iOS и Android.

Изначально у меня была мысль создать контейнер с эффектом нажатия и в него уже добавлять необходимые элементы. От этой идеи пришлось отказаться в виду дополнительной вложенности и некорректности выделения. То есть, положив в этот контейнер не прямоугольный элемент по типу CircleImage или Frame я получил бы выделение за пределами закругленной области.

Переписывать и расширять все контролы было бы глупо, поэтому я решил добавить статическое расширение.

Как это должно выглядеть

Для Android 5+ очевидно, надо использовать Ripple effect. Но для iOS и Android <5 это решение будет выглядеть неуместно. Для этих платформ я решил реализовать цветное анимированное выделение, срабатываемое при касании.

Реализация

PCL

Для начала в PCL проекте был создан статический класс TouchEffect с двумя BindableProperty:

  • bool On
  • Color Color

Первое отвечает за активность расширения для элемента, соответсвенно второе за цвет эффекта.

Android

Необходимо определить переменную, которая идентифицирует нужно использовать Ripple effect или нет в зависимости от версии Android:

public bool EnableRipple => Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop;

Реализация стандартных волн для андроида довольно проста:

private void AddRipple()
{
    if (Element is Layout)
    {
        _rippleOverlay = new FrameLayout(Container.Context)
        {
            LayoutParameters = new ViewGroup.LayoutParams(-1, -1)
        };

        _rippleListener = new ContainerOnLayoutChangeListener(_rippleOverlay);
        _view.AddOnLayoutChangeListener(_rippleListener);

        ((ViewGroup)_view).AddView(_rippleOverlay);

        _rippleOverlay.BringToFront();
        _rippleOverlay.Foreground = CreateRipple(Color.Accent.ToAndroid());
    }
    else
    {
        _orgDrawable = _view.Background;
        _view.Background = CreateRipple(Color.Accent.ToAndroid());
    }

    _ripple.SetColor(GetPressedColorSelector(_color));
}

private void RemoveRipple()
{
    if (Element is Layout)
    {
        var viewgrp = (ViewGroup)_view;
        viewgrp?.RemoveOnLayoutChangeListener(_rippleListener);
        viewgrp?.RemoveView(_rippleOverlay);

        _rippleListener?.Dispose();
        _rippleListener = null;

        _rippleOverlay?.Dispose();
        _rippleOverlay = null;
    }
    else
    {
        _view.Background = _orgDrawable;
        _orgDrawable?.Dispose();
        _orgDrawable = null;
    }
    _ripple?.Dispose();
    _ripple = null;
}

private RippleDrawable CreateRipple(Android.Graphics.Color color)
{
    if (Element is Layout)
    {
        var mask = new ColorDrawable(Android.Graphics.Color.White);
        return _ripple = new RippleDrawable(GetPressedColorSelector(color), null, mask);
    }

    var back = _view.Background;
    if (back == null)
    {
        var mask = new ColorDrawable(Android.Graphics.Color.White);
        return _ripple = new RippleDrawable(GetPressedColorSelector(color), null, mask);
    }
    else if (back is RippleDrawable)
    {
        _ripple = (RippleDrawable) back.GetConstantState().NewDrawable();
        _ripple.SetColor(GetPressedColorSelector(color));

        return _ripple;
    }
    else
    {
        return _ripple = new RippleDrawable(GetPressedColorSelector(color), back, null);
    }
}

У контрола берется задний фон и на него добавляется эффект.

Для более старых версий андроида я решил добавлять FrameLayout поверх элемента с анимацией Alpha канала заднего фона. К событию Touch элемента подписывается этот метод:

private void OnTouch(object sender, View.TouchEventArgs args)
{
    switch (args.Event.Action)
    {
        case MotionEventActions.Down:
            Container.RemoveView(_layer);
            Container.AddView(_layer);
            _layer.Top = 0;
            _layer.Left = 0;
            _layer.Right = _view.Width;
            _layer.Bottom = _view.Height;
            _layer.BringToFront();
            TapAnimation(250, 0, 65, false);
            break;
        case MotionEventActions.Up:
        case MotionEventActions.Cancel:
            TapAnimation(250, 65, 0);
            break;
    }
}

Который при нажатии добавляет в контейнер новый лэйаут с анимацией A-канала с 0 до 65, а при отпускании анимирует обратно от 65 до 0 и удаляет из контейнера.

Потом, в методе OnAttached определяем, что делать, создавать Ripple effect или подписываться на Touch:

if (EnableRipple)
    AddRipple();
else
    _view.Touch += OnTouch;

iOS

Для iOS подход схож с предыдущим шагом, добавляется UIView поверх основного элемента при нажатии и так же анимируется A-канал. Для этого создаются UITapGestureRecognizer и UILongPressGestureRecognizer и добавляются к элементу:

_tapGesture = new UITapGestureRecognizer(async (obj) => {
    await TapAnimation(0.3, _alpha, 0);
});

_longTapGesture = new UILongPressGestureRecognizer(async (obj) => {
    switch (obj.State)
    {
        case UIGestureRecognizerState.Began:
            await TapAnimation(0.5, 0, _alpha, false);
            break;
        case UIGestureRecognizerState.Ended:
        case UIGestureRecognizerState.Cancelled:
        case UIGestureRecognizerState.Failed:
            await TapAnimation(0.5, _alpha);
            break;
    }
});

_view.AddGestureRecognizer(_longTapGesture);
_view.AddGestureRecognizer(_tapGesture);

При долгом нажатии задается другое время анимации и, в отличие от простого нажатия, маска удаляется только после отпускания пальца.

Собственно все.

Использование

XAML:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamEffects.Sample"
             xmlns:xe="clr-namespace:XamEffects;assembly=XamEffects"
             x:Class="XamEffects.Sample.MainPage">
    <Grid HorizontalOptions="Center"
          VerticalOptions="Center"
          HeightRequest="100"
          WidthRequest="200"
          BackgroundColor="LightGray" 
          xe:TouchEffect.On="True"
          xe:TouchEffect.Color="Red">
        <Label Text="Test touch effect"
               HorizontalOptions="Center"
               VerticalOptions="Center"/>
    </Grid>
</ContentPage>

iOS Android API >=21 Android API < 21
Добавляем эффект нажатия в Xamarin.Forms - 1 Добавляем эффект нажатия в Xamarin.Forms - 2 Добавляем эффект нажатия в Xamarin.Forms - 3

Итоги

Я привел основую идею реализации эффекта касания, весь код, а так же Nuget пакеты доступны на GitHub.

P.S.: Опыт у меня в нативной разработке небольшой, буду рад советам, что можно улучшить/доработать.
P.P.S.: Habrastorage немного коряво преобразовал gif'ки.

Автор: mrxten

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js