I've created a custom renderer for Android and implemented it for a custom Slider control. I've got 2 solutions. In both solutions an Android App is build and started on an Android Phone. The test program shows the slider fine and I can drag the thumb up and down without any problems. The second solution is a bigger one where the slider should be implemented. If I run that solution the slider is displayed but I cannot move the thumb up and down. The OnTouchEvent is never fired. Below is all the code needed for the Slider. I know it's a lot but I hope someone sees the my problem.
My guess is that the ExportRenderer attribute at on the namespace somehow isn't found or triggered. The 'OnElementChanged' override is called but further nothing.
This is the custom renderer for the DraggableView (the thumb of the slider) in the Android project
using Android.Content;
using Android.Views;
using TGB.Xamarin.Forms.TestApp.Droid.Renderers.Views;
using TGB.Xamarin.Forms.Views;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using static TGB.Xamarin.Forms.Views.DraggableView;
using xam = global::Xamarin.Forms;
[assembly: ExportRenderer(typeof(DraggableView), typeof(DraggableViewRenderer))]
namespace TGB.Xamarin.Forms.TestApp.Droid.Renderers.Views
{
public class DraggableViewRenderer : VisualElementRenderer<xam.View>
{
float originalX;
float originalY;
float dX;
float dY;
bool firstTime = true;
bool touchedDown = false;
public DraggableViewRenderer(Context context) : base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<xam.View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
LongClick -= HandleLongClick;
}
if (e.NewElement != null)
{
LongClick += HandleLongClick;
var dragView = Element as DraggableView;
dragView.RestorePositionCommand = new Command(() =>
{
if (!firstTime)
{
SetX(originalX);
SetY(originalY);
}
});
}
}
private void HandleLongClick(object sender, LongClickEventArgs e)
{
var dragView = Element as DraggableView;
if (firstTime)
{
originalX = GetX();
originalY = GetY();
firstTime = false;
}
dragView.DragStarted();
touchedDown = true;
}
public override bool OnTouchEvent(MotionEvent e)
{
float x = e.RawX;
float y = e.RawY;
var dragView = Element as DraggableView;
var parent = dragView.Parent as xam.View;
switch (e.Action)
{
case MotionEventActions.Down:
if (dragView.DragMode == DragModes.Touch)
{
if (!touchedDown)
{
if (firstTime)
{
originalX = GetX();
originalY = GetY();
firstTime = false;
}
dragView.DragStarted();
}
touchedDown = true;
}
dX = x - this.GetX();
dY = y - this.GetY();
break;
case MotionEventActions.Move:
if (touchedDown)
{
var density = global::Xamarin.Essentials.DeviceDisplay.MainDisplayInfo.Density;
if (dragView.DragDirection == DragDirectionTypes.All || dragView.DragDirection == DragDirectionTypes.Horizontal)
{
var newX = x - dX;
if (parent != null)
{
if (newX + Width > parent.Width * density) newX = (float)(parent.Width * density - Width);
if (newX < 0) newX = 0;
}
SetX(newX);
}
if (dragView.DragDirection == DragDirectionTypes.All || dragView.DragDirection == DragDirectionTypes.Vertical)
{
var newY = y - dY;
if (parent != null)
{
if (newY + Height > parent.Height * density) newY = (float)(parent.Height * density - Height);
if (newY < 0) newY = 0;
}
SetY(newY);
}
}
break;
case MotionEventActions.Up:
touchedDown = false;
DraggableViewDragEndedEventArgs args = new DraggableViewDragEndedEventArgs
{
X = GetX(),
Y = GetY()
};
dragView.DragEnded(args);
break;
case MotionEventActions.Cancel:
touchedDown = false;
break;
}
return base.OnTouchEvent(e);
}
public override bool OnInterceptTouchEvent(MotionEvent e)
{
BringToFront();
return true;
}
}
}
This is the DraggableView in the .Net standard 2.0 project.
using System;
using System.Windows.Input;
using Xamarin.Forms;
namespace TGB.Xamarin.Forms.Views
{
public partial class DraggableView : ContentView
{
public event EventHandler DragStart = delegate { };
public delegate void DragEndEventHandler(object sender, DraggableViewDragEndedEventArgs args);
public event DragEndEventHandler DragEnd;
public enum DragDirectionTypes
{
All,
Vertical,
Horizontal
}
public enum DragModes
{
Touch,
LongPress
}
public static readonly BindableProperty DragDirectionProperty = BindableProperty.Create(
propertyName: "DragDirection",
returnType: typeof(DragDirectionTypes),
declaringType: typeof(DraggableView),
defaultValue: DragDirectionTypes.All,
defaultBindingMode: BindingMode.TwoWay);
public DragDirectionTypes DragDirection
{
get { return (DragDirectionTypes)GetValue(DragDirectionProperty); }
set { SetValue(DragDirectionProperty, value); }
}
public static readonly BindableProperty DragModeProperty = BindableProperty.Create(
propertyName: "DragMode",
returnType: typeof(DragModes),
declaringType: typeof(DraggableView),
defaultValue: DragModes.Touch,
defaultBindingMode: BindingMode.TwoWay);
public DragModes DragMode
{
get { return (DragModes)GetValue(DragModeProperty); }
set { SetValue(DragModeProperty, value); }
}
public static readonly BindableProperty IsDraggingProperty = BindableProperty.Create(
propertyName: "IsDragging",
returnType: typeof(bool),
declaringType: typeof(DraggableView),
defaultValue: false,
defaultBindingMode: BindingMode.TwoWay);
public bool IsDragging
{
get { return (bool)GetValue(IsDraggingProperty); }
set { SetValue(IsDraggingProperty, value); }
}
public static readonly BindableProperty RestorePositionCommandProperty = BindableProperty.Create(nameof(RestorePositionCommand), typeof(ICommand), typeof(DraggableView), default(ICommand), BindingMode.TwoWay, null, OnRestorePositionCommandPropertyChanged);
static void OnRestorePositionCommandPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var source = bindable as DraggableView;
if (source == null)
{
return;
}
source.OnRestorePositionCommandChanged();
}
private void OnRestorePositionCommandChanged()
{
OnPropertyChanged("RestorePositionCommand");
}
public ICommand RestorePositionCommand
{
get
{
return (ICommand)GetValue(RestorePositionCommandProperty);
}
set
{
SetValue(RestorePositionCommandProperty, value);
}
}
public void DragStarted()
{
DragStart(this, default);
IsDragging = true;
}
public void DragEnded(DraggableViewDragEndedEventArgs args)
{
IsDragging = false;
DragEnd(this, args);
}
}
}
The EventArgs class used at EndDrag
using System;
using System.Collections.Generic;
using System.Text;
namespace TGB.Xamarin.Forms.Views
{
public class DraggableViewDragEndedEventArgs : EventArgs
{
public double X;
public double Y;
}
}
This is the Slider class:
using System;
using TGB.Xamarin.Forms.Views;
using Xamarin.Forms;
namespace TGB.Xamarin.Forms.Controls
{
public class Slider : AbsoluteLayout
{
private DraggableView m_Thumb;
public static readonly BindableProperty MinProperty = BindableProperty.Create(
"Min", typeof(int), typeof(Slider), 0, propertyChanged: (bindable, oldvalue, newvalue) => { ((Slider)bindable).InvalidateLayout(); });
/// <summary>
/// The minimum for the slider
/// </summary>
public int Min
{
set { SetValue(MinProperty, value); }
get { return (int)GetValue(MinProperty); }
}
public static readonly BindableProperty MaxProperty = BindableProperty.Create(
"Max", typeof(int), typeof(Slider), 100, propertyChanged: (bindable, oldvalue, newvalue) => { ((Slider)bindable).InvalidateLayout(); });
/// <summary>
/// The maximum for the slider
/// </summary>
public int Max
{
set { SetValue(MaxProperty, value); }
get { return (int)GetValue(MaxProperty); }
}
public static readonly BindableProperty ValueProperty = BindableProperty.Create(
"Value", typeof(int), typeof(Slider), 100, propertyChanged: (bindable, oldvalue, newvalue) => { ((Slider)bindable).InvalidateLayout(); });
/// <summary>
/// The value for the slider
/// </summary>
public int Value
{
set { SetValue(ValueProperty, value); }
get { return (int)GetValue(ValueProperty); }
}
public Slider()
{
BackgroundColor = System.Drawing.Color.Green;
Init();
}
private void Init()
{
m_Thumb = new DraggableView();
m_Thumb.DragEnd += Draggable_DragEnd;
m_Thumb.HorizontalOptions = new LayoutOptions { Alignment = LayoutAlignment.Fill, Expands = true };
AbsoluteLayout.SetLayoutBounds(m_Thumb, new Rectangle(0, 0, 1, 0.2));
AbsoluteLayout.SetLayoutFlags(m_Thumb, AbsoluteLayoutFlags.All);
m_Thumb.BackgroundColor = System.Drawing.Color.Orange;
this.Children.Add(m_Thumb);
}
private void Draggable_DragEnd(object sender, DraggableViewDragEndedEventArgs e)
{
var scope = Max - Min;
var regionSize = this.Height - m_Thumb.Height;
var perPixel = scope / regionSize;
Value = Min + Convert.ToInt32(e.Y * perPixel);
}
}
}
Here's the implementation in the MainPage.xaml:
<?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:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tgbc="clr-namespace:TGB.Xamarin.Forms.Controls;assembly=TGB.Xamarin.Forms"
mc:Ignorable="d"
x:Name="Page"
x:Class="ElectroSpit.MainPage">
<ContentPage.Resources>
<StyleSheet Source="Styles\Styles.css" />
</ContentPage.Resources>
<StackLayout x:Name="Main"
Orientation="Vertical"
BackgroundColor="Transparent"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<ContentView x:Name="Sidebar"
VerticalOptions="Start"
HorizontalOptions="Start"
StyleClass="Sidebar">
<ContentView.Content>
<Grid VerticalOptions="FillAndExpand" StyleClass="SidebarSlider">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackLayout Grid.Row="0"
Grid.Column="0"
Orientation="Vertical"
VerticalOptions="FillAndExpand"
StyleClass="SidebarSlider" >
<!-- HERE ARE THE SLIDERS -->
<tgbc:Slider x:Name="Pitch" VerticalOptions="FillAndExpand" StyleClass="SidebarSlider" />
<tgbc:Slider x:Name="Tremolo" VerticalOptions="FillAndExpand" StyleClass="SidebarSlider" />
</StackLayout>
<StackLayout Grid.Row="0"
Grid.Column="1"
Orientation="Vertical">
<Label Text="Trans" StyleClass="SidebarLabel"/>
<StackLayout Orientation="Horizontal">
<Button x:Name="TransposeUp" Text="+" StyleClass="SidebarButton"/>
<Button x:Name="TransposeDown" Text="{Binding Transposition}" StyleClass="SidebarButton" />
</StackLayout>
<Label Text="Brightness" StyleClass="SidebarLabel"/>
<Stepper x:Name="Brichtness" Minimum="0" Maximum="100" StyleClass="SidebarStepper"/>
<Button x:Name="Settings" Text="Opt" StyleClass="SidebarButton"/>
<Label Text="Glide" StyleClass="SidebarLabel"/>
<Stepper x:Name="Glide" Minimum="0" Maximum="100" StyleClass="SidebarStepper"/>
<Label Text="Octave" StyleClass="SidebarLabel"/>
<StackLayout Orientation="Horizontal">
<Button x:Name="OctaveUp" Text="+" StyleClass="SidebarButton"/>
<Button x:Name="OctaveDown" Text="{Binding Octave}" StyleClass="SidebarButton"/>
</StackLayout>
</StackLayout>
<StackLayout>
<ContentView x:Name="Schema"
BackgroundColor="Transparent"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
</ContentView>
</StackLayout>
</Grid>
</ContentView.Content>
</ContentView>
<ContentView x:Name="Notes"
BackgroundColor="Transparent"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
</ContentView>
</StackLayout>
</ContentPage>
== EDIT ==
There is a difference in messages on the working and non-working app. This is what I get on the working app:
05-22 10:29:42.178 D/Mono ( 6676): DllImport searching in: '__Internal' ('(null)').
05-22 10:29:42.178 D/Mono ( 6676): Searching for 'java_interop_jnienv_call_float_method_a'.
05-22 10:29:42.178 D/Mono ( 6676): Probing 'java_interop_jnienv_call_float_method_a'.
05-22 10:29:42.178 D/Mono ( 6676): Found as 'java_interop_jnienv_call_float_method_a'.
05-22 10:29:42.201 D/Mono ( 6676): DllImport searching in: '__Internal' ('(null)').
05-22 10:29:42.201 D/Mono ( 6676): Searching for 'java_interop_jnienv_call_long_method_a'.
05-22 10:29:42.201 D/Mono ( 6676): Probing 'java_interop_jnienv_call_long_method_a'.
05-22 10:29:42.201 D/Mono ( 6676): Found as 'java_interop_jnienv_call_long_method_a'.
05-22 10:29:42.260 D/Mono ( 6676): Requesting loading reference 6 (of 7) of TGB.Xamarin.Forms.Android.dll
05-22 10:29:42.260 D/Mono ( 6676): Loading reference 6 of TGB.Xamarin.Forms.Android.dll asmctx DEFAULT, looking for Xamarin.Essentials, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
05-22 10:29:42.260 D/Mono ( 6676): Assembly Ref addref TGB.Xamarin.Forms.Android[0xd57b8680] -> Xamarin.Essentials[0xebf7fd00]: 3
And this on the non-working App:
05-22 09:46:52.254 D/Mono (14307): DllImport searching in: '__Internal' ('(null)').
05-22 09:46:52.254 D/Mono (14307): Searching for 'java_interop_jnienv_call_float_method_a'.
05-22 09:46:52.254 D/Mono (14307): Probing 'java_interop_jnienv_call_float_method_a'.
05-22 09:46:52.254 D/Mono (14307): Found as 'java_interop_jnienv_call_float_method_a'.
Ok, found it. The control was already working and my mistake was elsewhere. If you watch closely at the XAML you will see that the last StackLayout has no Grid.Row and Grid.Column attributes causing it to be on top of both Sliders. So the touch doesn't go to the sliders but to the ContentView that's inside the StackLayout giving me the impression it's not working.