Xamarin.Forms набирает обороты и, к сожалению, из коробки доступно совсем мало возможностей, все неоходимо добавлять через Dependency service или рендереры. На этой волне стало очень много различных библиотек, добавляющих зачастую базовый функционал.
Мое решение не исключение.
У меня стояла задача сделать небольшое расширение, позволяющее добавить эффект нажатия на почти любой элемент для iOS и Android.
Изначально у меня была мысль создать контейнер с эффектом нажатия и в него уже добавлять необходимые элементы. От этой идеи пришлось отказаться в виду дополнительной вложенности и некорректности выделения. То есть, положив в этот контейнер не прямоугольный элемент по типу CircleImage или Frame я получил бы выделение за пределами закругленной области.
Переписывать и расширять все контролы было бы глупо, поэтому я решил добавить статическое расширение.
Как это должно выглядеть
Для Android 5+ очевидно, надо использовать Ripple effect. Но для iOS и Android <5 это решение будет выглядеть неуместно. Для этих платформ я решил реализовать цветное анимированное выделение, срабатываемое при касании.
Реализация
PCL
Для начала в PCL проекте был создан статический класс TouchEffect с BindableProperty, отвечающее за цвет эффекта.
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.Color="Red"> <Label Text="Test touch effect" HorizontalOptions="Center" VerticalOptions="Center"/> </Grid> </ContentPage>
| iOS | Android API >=21 | Android API < 21 |
|---|---|---|
![]() |
![]() |
![]() |
Итоги
Я привел основую идею реализации эффекта касания, весь код, а так же Nuget пакеты доступны на GitHub.
P.S.: Опыт у меня в нативной разработке небольшой, буду рад советам, что можно улучшить/доработать.
P.P.S.: Habrastorage немного коряво преобразовал gif'ки.



