I am manually positioning labels in an AbsoluteLayout.
To do this correctly I would like to know the label height prior to placing it on the UI.
I have found this solution, but not without actually placing a label:
public double MeasureLabelHeight(string text, double width, double fontSize, double lineHeight, string fontFamily)
{
Label label = new Label();
label.WidthRequest = width;
label.FontSize = fontSize;
label.LineHeight = lineHeight;
label.FontFamily = fontFamily;
label.LineBreakMode = LineBreakMode.WordWrap;
label.Text = text;
MyAbsoluteLayout.Children.Add(view: label, position: new Point(0, Height)); //place out of sight
var sizeRequest = label.Measure(widthConstraint: width, heightConstraint: double.MaxValue, flags: MeasureFlags.None);
var labelTextHeight = sizeRequest.Request.Height;
MyAbsoluteLayout.Children.Remove(label);
return labelTextHeight;
}
This solution works on UWP, I still have to test it on Android and iOS.
I would like to improve on it though.
I am unable to get a correct Height value without actually placing it in the AbsoluteLayout (out of view) and am a bit worried about the overhead this probably causes with extra redraws.
I have found an old piece of code that seemingly uses native code to do this without actually placing it in the UI for iOS and Android. I'm wondering if there is a solution available that has no need for platform specific code.
Let Xamarin Forms measure them for you. Then you move them into position.
Do this by subclassing AbsoluteLayout, and adding an Action that a page can set, to be called when your layout has done LayoutChildren.
MyAbsoluteLayout.cs:
using System;
using Xamarin.Forms;
namespace XFSOAnswers
{
public class MyAbsoluteLayout : AbsoluteLayout
{
public MyAbsoluteLayout()
{
}
// Containing page will set this, to act on children during LayoutChildren.
public Action CustomLayoutAction { get; set; }
private bool _busy;
protected override void LayoutChildren(double x, double y, double width, double height)
{
// Avoid recursed layout calls as CustomLayoutAction moves children.
if (_busy)
return;
// Xamarin measures the children.
base.LayoutChildren(x, y, width, height);
_busy = true;
try
{
CustomLayoutAction?.Invoke();
}
finally
{
_busy = false;
// Layout again, to position the children, based on adjusted (x,y)s.
base.LayoutChildren(x, y, width, height);
}
}
}
}
Example usage - MyAbsoluteLayoutPage.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:local="clr-namespace:XFSOAnswers"
x:Class="XFSOAnswers.MyAbsoluteLayoutPage">
<ContentPage.Content>
<local:MyAbsoluteLayout x:Name="TheLayout">
<!-- Layout positions start (0,0). Adjusted later in PositionLabels. -->
<Label x:Name="Label1" Text="Welcome" />
<Label x:Name="Label2" Text="to" />
<Label x:Name="Label3" Text="Xamarin" />
<Label x:Name="Label4" Text=".Forms!" />
</local:MyAbsoluteLayout>
</ContentPage.Content>
</ContentPage>
MyAbsoluteLayoutPage.xaml.cs:
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace XFSOAnswers
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MyAbsoluteLayoutPage : ContentPage
{
public MyAbsoluteLayoutPage()
{
InitializeComponent();
TheLayout.CustomLayoutAction = PositionLabels;
}
private void PositionLabels()
{
// Optional: Set breakpoint after these, to check that the bounds have values.
var bounds1 = Label1.Bounds;
var bounds2 = Label2.Bounds;
var bounds3 = Label3.Bounds;
var bounds4 = Label4.Bounds;
double x = 10;
double y = 20;
MoveAbsoluteChildTo(Label1, x, y);
x += Label1.Width;
y += Label1.Height;
MoveAbsoluteChildTo(Label2, x, y);
x += Label2.Width;
y += Label2.Height;
MoveAbsoluteChildTo(Label3, x, y);
x += Label3.Width;
y += Label3.Height;
MoveAbsoluteChildTo(Label4, x, y);
}
private static void MoveAbsoluteChildTo(View child, double x, double y)
{
AbsoluteLayout.SetLayoutBounds(child, new Rect(x, y, child.Width, child.Height));
}
}
}
Result:
See MyAbsoluteLayout and MyAbsoluteLayoutPage in ToolmakerSteve - repo XFormsSOAnswers.
I copied/wrote a class that inherits from Frame
public class Circle : Frame
{
//private double _radius;
public static readonly BindableProperty RadiusProperty = BindableProperty.Create(nameof(Radius), typeof(double), typeof(Circle), 126.0, BindingMode.TwoWay);
public double Radius
{
get => (double)GetValue(RadiusProperty); //_radius;
set
{
SetValue(RadiusProperty, value);
OnPropertyChanged();
AdjustSize();
}
}
private void AdjustSize()
{
HeightRequest = Radius;
WidthRequest = Radius;
Margin = new Thickness(0,0,0,0);
Padding = new Thickness(0, 0, 0, 0);
CornerRadius = (float) (Radius / 2);
}
public Circle()
{
HorizontalOptions = LayoutOptions.Center;
}
}
The consuming page defines these BinadableProperties
public static readonly BindableProperty InnerColorProperty = BindableProperty.Create("InnerColor", typeof(Color), typeof(CircleProgressView), defaultValue: Color.FromHex("#34495E"), BindingMode.TwoWay);
public Color InnerColor
{
get => (Color)GetValue(InnerColorProperty);
set => SetValue(InnerColorProperty, value);
}
public static readonly BindableProperty InnerRadiusProperty = BindableProperty.Create("InnerRadius", typeof(double), typeof(CircleProgressView), 126.0, BindingMode.TwoWay);
public double InnerRadius
{
get => (double)GetValue(InnerRadiusProperty);
set => SetValue(InnerRadiusProperty, value);
}
And uses the Circle like so
<components:Circle Grid.Row="0" BackgroundColor="{Binding InnerColor}" Radius="{Binding InnerRadius}" >
Alas, the bindable's setter, and hence AdjustSize(), is never called nor is the default value used. Instead of a circle I end up with a rectangle. The BackgroundColor, which is a property of Frame, binds and works fine.
If I remove the BindableProperty and leave behind a regular INotify property
public class Circle : Frame
{
private double _radius;
public double Radius
{
get => _radius;
set
{
_radius = value;
OnPropertyChanged();
AdjustSize();
}
}
private void AdjustSize()
{
HeightRequest = Radius;
WidthRequest = Radius;
Margin = new Thickness(0,0,0,0);
Padding = new Thickness(0, 0, 0, 0);
CornerRadius = (float) (Radius / 2);
}
public Circle()
{
HorizontalOptions = LayoutOptions.Center;
}
}
The compiler complains if I keep the InnerRadius binding
Severity Code Description Project File Line Suppression State
Error Position 17:92. No property, bindable property, or event found for 'Radius', or mismatching type between value and property. ...\Components\CircleProgressView.xaml 17
I can replace the Radius binding with a hardcoded value and it runs fine, a circle appears.
<components:Circle Grid.Row="0" BackgroundColor="{Binding InnerColor}" Radius="126" >
What's wrong with a BindableProperty in a regular C# class?
Firstly, we need to handle data in the property changed event of bindable property instead of the setter method of a normal property. So modify your Circle class like:
public static readonly BindableProperty RadiusProperty = BindableProperty.Create(nameof(Radius), typeof(double), typeof(Circle), 125.0, BindingMode.TwoWay, propertyChanged: RadiusChanged);
public double Radius
{
get => (double)GetValue(RadiusProperty); //_radius;
set => SetValue(RadiusProperty, value);
}
static void RadiusChanged(BindableObject bindableObject, object oldValue, object newValue)
{
Circle circle = bindableObject as Circle;
circle.HeightRequest = (double)newValue;
circle.WidthRequest = (double)newValue;
circle.CornerRadius = (float)((double)newValue / 2);
}
This is because we bind data in XAML we should manipulate the bindable property's changed event directly.
Secondly, I saw you bound the property using the parent page's bindable property. Normally, we won't do that. We will consume a view model as the page's binding context and then bind the property to the binding context. However, if you do want to consume the parent page's bindable property as the Circle's binding context, try this way:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Sample.SecondPage"
xmlns:components="clr-namespace:Sample"
x:Name="Page">
<ContentPage.Content>
<StackLayout>
<components:Circle BackgroundColor="{Binding InnerColor, Source={x:Reference Page}}" Radius="{Binding InnerRadius, Source={x:Reference Page}}"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
Name your parent page first and change the circle's source to that.
Here, I used a different default Radius value comparing to InnerRadius so the property changed event will be called at the initial time.
Is there a way to check if a touch of a pangesture is inside a certain element, like a Frame?
I added a PanGestureRecognizer to my screen. In my screen I have two Frame containing two Image. What I want is that when I move my finger on the screen I got noticed when my finger ends on one of that frames. I know how to get my finger coordinates (e.TotalX; e.TotalY;) and my thought was to check if they are inside the bounds of the frames, but I don't know how.
Any suggestion?
CODE
public MainPage()
{
InitializeComponent();
var panGesture = new PanGestureRecognizer();
panGesture.PanUpdated += PanUpdated;
mainLayout.GestureRecognizers.Add(panGesture);
dog = new Frame
{
BorderColor = Color.Blue,
Padding = 4,
Content = new Image
{
Source = ImageSource.FromResource("AccessibilityImagesSound.Immagini.dog.png"),
Aspect = Aspect.Fill,
},
};
AutomationProperties.SetIsInAccessibleTree(dog, true);
AutomationProperties.SetName(dog, "dog");
AbsoluteLayout.SetLayoutBounds(dog, new Rectangle(0.2, 0.5, 0.30, 0.30));
AbsoluteLayout.SetLayoutFlags(dog, AbsoluteLayoutFlags.All);
mainLayout.Children.Add(dog);
cat = new Frame
{
BorderColor = Color.Blue,
Padding = 4,
Content = new Image
{
Source = ImageSource.FromResource("AccessibilityImagesSound.Immagini.cat.png"),
Aspect = Aspect.Fill,
},
};
AutomationProperties.SetIsInAccessibleTree(cat, true);
AutomationProperties.SetName(cat, "cat");
AbsoluteLayout.SetLayoutBounds(cat, new Rectangle(0.8, 0.5, 0.30, 0.30));
AbsoluteLayout.SetLayoutFlags(cat, AbsoluteLayoutFlags.All);
mainLayout.Children.Add(cat);
}
private void PanUpdated(object sender, PanUpdatedEventArgs e)
{
switch (e.StatusType)
{
case GestureStatus.Started:
break;
case GestureStatus.Running:
//Here I think I have to check E.TotalX and e.TotalY
break;
case GestureStatus.Completed:
case GestureStatus.Canceled:
break;
}
}
As far as I know the only way to get location of Touch in xamarin is through native touch gestures.
Native gestures can be used using the Effects and this MSDocs link has full implementation of it.
Using the TouchEffect the exact location of the touch can be fetched easily. A sample to change color of the touched frame is given below.
XAML:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.Effects>
<touch:TouchEffect TouchAction="Grid_TouchAction"/>
</Grid.Effects>
<Frame
Grid.Row="1"
BackgroundColor="Green"
x:Name="leftFrame"/>
<Frame
Grid.Row="1"
Grid.Column="1"
BackgroundColor="Blue"
x:Name="rightFrame"/>
</Grid>
CS :
private void Grid_TouchAction(object sender, TouchTracking.TouchActionEventArgs args)
{
switch (args.Type)
{
case TouchActionType.Moved:
if (leftFrame.Bounds.Contains(args.Location))
{
leftFrame.BackgroundColor = Color.Red;
}
else
{
leftFrame.BackgroundColor = Color.Green;
}
if (rightFrame.Bounds.Contains(args.Location))
{
rightFrame.BackgroundColor = Color.Red;
}
else
{
rightFrame.BackgroundColor = Color.Blue;
}
break;
}
}
UI working:
Change the TouchAction event handler as per your requirement.
Say I have a list of items displayed as a Grid layout. Each item takes up a row and is made up of multiple items in a column. It's basically a table:
<Grid>
<Label Text="Item1" Grid.Row="0" Grid.Colum="0" />
<Image Src="something1" Grid.Row="0" Grid.Colum="1" />
<Label Text="Item2" Grid.Row="1" Grid.Colum="0" />
<Image Src="something2" Grid.Row="1" Grid.Colum="1" />
<Label Text="Item3" Grid.Row="2" Grid.Colum="0" />
<Image Src="something3" Grid.Row="2" Grid.Colum="1" />
</Grid>
Each Label/Image represents a row in my list of items to be displayed. I'm not worried about the databinding for the moment, I just want to move the Label/Image into a custom control so that I can use that custom control to add "Rows" into my Grid:
<Grid>
<customcontrol:MyCustomRowControl Text="Item1" Source="img1" Grid.Row="0"/>
<customcontrol:MyCustomRowControl Text="Item2" Source="img1" Grid.Row="1"/>
<customcontrol:MyCustomRowControl Text="Item3" Source="img1" Grid.Row="3"/>
</Grid>
I can probably set the Lable/Image/etc from my custom control to it's appropriate row/column from the code-behind.Where I get lost is what type of base class should I make this custom control? Because it is that class that will become the content of the Grid, not it's Labels and Images, therefore the Grid.Row and Grid.Column will not propagate correctly. I really hope I managed to explain this.
Can I create a custom control in Xamarin that I can add as a content to a Grid and have it's children respect the Grid's columns?
You can write about like this
DynamicGridView class
public class DynamicGridView : Grid
{
private int _rowCount;
private int _columnCount;
protected int _column;
protected int _starHeight = 0;
protected int _type;
protected int[] _starHeightList;
public DynamicGridEnum _dynamicGridEnum;
public DynamicGridView(DynamicGridEnum dynamicGridEnum, params int[] starHeightList)
{
_type = 2;
switch (dynamicGridEnum)
{
case DynamicGridEnum.Auto:
_column = starHeightList[0];
break;
case DynamicGridEnum.Star:
_column = starHeightList[0];
_starHeight = starHeightList[1];
_type = 1;
break;
case DynamicGridEnum.Custom:
_column = starHeightList.Length;
break;
default:
break;
}
_starHeightList = starHeightList;
_dynamicGridEnum = dynamicGridEnum;
_rowCount = 0;
_columnCount = 0;
Padding = 0;
Margin = 0;
ColumnSpacing = -1;
RowSpacing = -1;
}
public virtual void AddView(View view)
{
int countRow = _rowCount / _column;
if (RowDefinitions.Count <= countRow)
{
RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, (GridUnitType)_type) });
}
Children.Add(view, _columnCount, countRow);
_rowCount++;
_columnCount++;
_columnCount = _columnCount % _column;
}
}
DynamicGrid class
public class DynamicGrid : DynamicGridView
{
public DynamicGrid(DynamicGridEnum dynamicGridEnum, params int[] starHeightList) : base(dynamicGridEnum, starHeightList)
{
for (int i = 0; i < starHeightList.Length; i++) { starHeightList[i] = starHeightList[i] <= 0 ? 1 : starHeightList[i]; }
if (dynamicGridEnum == DynamicGridEnum.Custom)
{
StartCustomGrid();
}
else
StartGrid();
}
private void StartGrid()
{
int percent = 100 / _column;
for (int i = 0; i < _column; i++)
ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(percent, (GridUnitType)_type) });
}
private void StartCustomGrid()
{
foreach (var item in _starHeightList)
ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(item, GridUnitType.Star) });
}
}
And usage of dynamic grid (I defined an enum for type of grid. For
example if enum is auto it will be auto resize row/columns of grid.)
public partial class MainPage : ContentPage
{
public MainPage()
{
StackLayout sl = new StackLayout();
DynamicGrid dynamicGrid = new DynamicGrid(Enums.DynamicGridEnum.Custom, 20, 50, 20, 0);
dynamicGrid.AddView(new BoxView() { BackgroundColor = Color.AliceBlue });
dynamicGrid.AddView(new BoxView() { BackgroundColor = Color.Aqua });
dynamicGrid.AddView(new BoxView() { BackgroundColor = Color.AntiqueWhite });
dynamicGrid.AddView(new BoxView() { BackgroundColor = Color.Azure });
sl.Children.Add(new CardView(Color.Beige, Color.Bisque, 60, Color.Black, 90, 10));
sl.Children.Add(dynamicGrid);
Content = sl;
}
}
I'm looking to add a swipe function in my app that is pretty much identical to the unlock mechanic on (old?) iPhones (See pictures).
I am struggling with how this could be achieved on a crossplatform solution. My immediate thought would be to use a slider and a custom renderer but unsure how to create the function of snapping to start if the user lets go before finishing the slide. Would appreciate if anyone could either assist with that function or if they have a better suggestion on how achieve this.
Unless and until - you really need a particularly native look for every platform; you can pretty much write your own custom slider control using PanGestureRecognizer, and AbsoluteLayout (without any need for custom-renderers). For that snapping effect you can use Translation animation with Cubic easing effect.
For example, you can define a control as following; this sample control extends AbsoluteLayout while allowing you to define your own controls representing thumb and track-bar. It also creates an almost invisible top-most layer to act as pan-gesture listener. Once, gesture is completed, it checks to see if slide for complete (i.e entire width of track-bar) - and then raises SlideCompleted event.
public class SlideToActView : AbsoluteLayout
{
public static readonly BindableProperty ThumbProperty =
BindableProperty.Create(
"Thumb", typeof(View), typeof(SlideToActView),
defaultValue: default(View), propertyChanged: OnThumbChanged);
public View Thumb
{
get { return (View)GetValue(ThumbProperty); }
set { SetValue(ThumbProperty, value); }
}
private static void OnThumbChanged(BindableObject bindable, object oldValue, object newValue)
{
((SlideToActView)bindable).OnThumbChangedImpl((View)oldValue, (View)newValue);
}
protected virtual void OnThumbChangedImpl(View oldValue, View newValue)
{
OnSizeChanged(this, EventArgs.Empty);
}
public static readonly BindableProperty TrackBarProperty =
BindableProperty.Create(
"TrackBar", typeof(View), typeof(SlideToActView),
defaultValue: default(View), propertyChanged: OnTrackBarChanged);
public View TrackBar
{
get { return (View)GetValue(TrackBarProperty); }
set { SetValue(TrackBarProperty, value); }
}
private static void OnTrackBarChanged(BindableObject bindable, object oldValue, object newValue)
{
((SlideToActView)bindable).OnTrackBarChangedImpl((View)oldValue, (View)newValue);
}
protected virtual void OnTrackBarChangedImpl(View oldValue, View newValue)
{
OnSizeChanged(this, EventArgs.Empty);
}
private PanGestureRecognizer _panGesture = new PanGestureRecognizer();
private View _gestureListener;
public SlideToActView()
{
_panGesture.PanUpdated += OnPanGestureUpdated;
SizeChanged += OnSizeChanged;
_gestureListener = new ContentView { BackgroundColor = Color.White, Opacity = 0.05 };
_gestureListener.GestureRecognizers.Add(_panGesture);
}
public event EventHandler SlideCompleted;
private const double _fadeEffect = 0.5;
private const uint _animLength = 50;
async void OnPanGestureUpdated(object sender, PanUpdatedEventArgs e)
{
if (Thumb == null | TrackBar == null)
return;
switch (e.StatusType)
{
case GestureStatus.Started:
await TrackBar.FadeTo(_fadeEffect, _animLength);
break;
case GestureStatus.Running:
// Translate and ensure we don't pan beyond the wrapped user interface element bounds.
var x = Math.Max(0, e.TotalX);
if (x > (Width - Thumb.Width))
x = (Width - Thumb.Width);
if (e.TotalX < Thumb.TranslationX)
return;
Thumb.TranslationX = x;
break;
case GestureStatus.Completed:
var posX = Thumb.TranslationX;
// Reset translation applied during the pan (snap effect)
await TrackBar.FadeTo(1, _animLength);
await Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn);
if (posX >= (Width - Thumb.Width - 10/* keep some margin for error*/))
SlideCompleted?.Invoke(this, EventArgs.Empty);
break;
}
}
void OnSizeChanged(object sender, EventArgs e)
{
if (Width == 0 || Height == 0)
return;
if (Thumb == null || TrackBar == null)
return;
Children.Clear();
SetLayoutFlags(TrackBar, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(TrackBar, new Rectangle(0, 0, 1, 1));
Children.Add(TrackBar);
SetLayoutFlags(Thumb, AbsoluteLayoutFlags.None);
SetLayoutBounds(Thumb, new Rectangle(0, 0, this.Width/5, this.Height));
Children.Add(Thumb);
SetLayoutFlags(_gestureListener, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(_gestureListener, new Rectangle(0, 0, 1, 1));
Children.Add(_gestureListener);
}
}
Sample usage:
<StackLayout Margin="40">
<local:SlideToActView HeightRequest="50" SlideCompleted="Handle_SlideCompleted">
<local:SlideToActView.Thumb>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Silver" Padding="0">
<Image Source="icon.png" HorizontalOptions="Center" VerticalOptions="Center" HeightRequest="40" WidthRequest="40" />
</Frame>
</local:SlideToActView.Thumb>
<local:SlideToActView.TrackBar>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Gray" Padding="0">
<Label Text="Slide 'x' to cancel" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" />
</Frame>
</local:SlideToActView.TrackBar>
</local:SlideToActView>
<Label x:Name="MessageLbl" FontAttributes="Bold" TextColor="Green" />
</StackLayout>
Code-Behind
void Handle_SlideCompleted(object sender, System.EventArgs e)
{
MessageLbl.Text = "Success!!";
}
Update : 08/30
As #morten-j-petersen wanted support for a fill-bar like implementation; added support for that.
Updated control code
public class SlideToActView : AbsoluteLayout
{
public static readonly BindableProperty ThumbProperty =
BindableProperty.Create(
"Thumb", typeof(View), typeof(SlideToActView),
defaultValue: default(View));
public View Thumb
{
get { return (View)GetValue(ThumbProperty); }
set { SetValue(ThumbProperty, value); }
}
public static readonly BindableProperty TrackBarProperty =
BindableProperty.Create(
"TrackBar", typeof(View), typeof(SlideToActView),
defaultValue: default(View));
public View TrackBar
{
get { return (View)GetValue(TrackBarProperty); }
set { SetValue(TrackBarProperty, value); }
}
public static readonly BindableProperty FillBarProperty =
BindableProperty.Create(
"FillBar", typeof(View), typeof(SlideToActView),
defaultValue: default(View));
public View FillBar
{
get { return (View)GetValue(FillBarProperty); }
set { SetValue(FillBarProperty, value); }
}
private PanGestureRecognizer _panGesture = new PanGestureRecognizer();
private View _gestureListener;
public SlideToActView()
{
_panGesture.PanUpdated += OnPanGestureUpdated;
SizeChanged += OnSizeChanged;
_gestureListener = new ContentView { BackgroundColor = Color.White, Opacity = 0.05 };
_gestureListener.GestureRecognizers.Add(_panGesture);
}
public event EventHandler SlideCompleted;
private const double _fadeEffect = 0.5;
private const uint _animLength = 50;
async void OnPanGestureUpdated(object sender, PanUpdatedEventArgs e)
{
if (Thumb == null || TrackBar == null || FillBar == null)
return;
switch (e.StatusType)
{
case GestureStatus.Started:
await TrackBar.FadeTo(_fadeEffect, _animLength);
break;
case GestureStatus.Running:
// Translate and ensure we don't pan beyond the wrapped user interface element bounds.
var x = Math.Max(0, e.TotalX);
if (x > (Width - Thumb.Width))
x = (Width - Thumb.Width);
//Uncomment this if you want only forward dragging.
//if (e.TotalX < Thumb.TranslationX)
// return;
Thumb.TranslationX = x;
SetLayoutBounds(FillBar, new Rectangle(0, 0, x + Thumb.Width / 2, this.Height));
break;
case GestureStatus.Completed:
var posX = Thumb.TranslationX;
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
// Reset translation applied during the pan
await Task.WhenAll(new Task[]{
TrackBar.FadeTo(1, _animLength),
Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn),
});
if (posX >= (Width - Thumb.Width - 10/* keep some margin for error*/))
SlideCompleted?.Invoke(this, EventArgs.Empty);
break;
}
}
void OnSizeChanged(object sender, EventArgs e)
{
if (Width == 0 || Height == 0)
return;
if (Thumb == null || TrackBar == null || FillBar == null)
return;
Children.Clear();
SetLayoutFlags(TrackBar, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(TrackBar, new Rectangle(0, 0, 1, 1));
Children.Add(TrackBar);
SetLayoutFlags(FillBar, AbsoluteLayoutFlags.None);
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
Children.Add(FillBar);
SetLayoutFlags(Thumb, AbsoluteLayoutFlags.None);
SetLayoutBounds(Thumb, new Rectangle(0, 0, this.Width/5, this.Height));
Children.Add(Thumb);
SetLayoutFlags(_gestureListener, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(_gestureListener, new Rectangle(0, 0, 1, 1));
Children.Add(_gestureListener);
}
}
XAML Usage
<StackLayout Margin="40">
<local:SlideToActView HeightRequest="50" SlideCompleted="Handle_SlideCompleted">
<local:SlideToActView.Thumb>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Silver" Padding="0">
<Image Source="icon.png" HorizontalOptions="Center" VerticalOptions="Center" HeightRequest="40" WidthRequest="40" />
</Frame>
</local:SlideToActView.Thumb>
<local:SlideToActView.TrackBar>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Gray" Padding="0">
<Label Text="Slide 'x' to cancel" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" />
</Frame>
</local:SlideToActView.TrackBar>
<local:SlideToActView.FillBar>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Red" Padding="0" />
</local:SlideToActView.FillBar>
</local:SlideToActView>
<Label x:Name="MessageLbl" FontAttributes="Bold" TextColor="Green" />
</StackLayout>
using custom renders for xamarin forms so that you could define how the slider should look in each platform, In android SeekBars are commonly used for sliders and in iOS UiSlider
https://blog.xamarin.com/customizing-xamarin-forms-controls-with-effects/
https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/custom-renderer/
also since if you have decided to use custom render, you can use your own slider derived from android seek bar with animations
http://www.viralandroid.com/2015/11/android-custom-seekbar-example.html
also a custom UIslider for iOS
you can hold up your generic methods in a portable class , as you have explained the behavior which only have two states this might also be achievable using a custom switch widget
There is a bug in Android in which the Gesture Recognizer does not trigger the Started or Completed event! Here the link: https://bugzilla.xamarin.com/show_bug.cgi?id=39768
So, I implemented this workaround which checks if the pan is stopped every two seconds and restarts the position. It only runs the timer in Android as in iOS runs ok. Here the code:
public class SlideToOpenView : AbsoluteLayout
{
public static readonly BindableProperty ThumbProperty =
BindableProperty.Create(
"Thumb", typeof(View), typeof(SlideToOpenView),
defaultValue: default(View));
public View Thumb
{
get { return (View)GetValue(ThumbProperty); }
set { SetValue(ThumbProperty, value); }
}
public static readonly BindableProperty TrackBarProperty =
BindableProperty.Create(
"TrackBar", typeof(View), typeof(SlideToOpenView),
defaultValue: default(View));
public View TrackBar
{
get { return (View)GetValue(TrackBarProperty); }
set { SetValue(TrackBarProperty, value); }
}
public static readonly BindableProperty FillBarProperty =
BindableProperty.Create(
"FillBar", typeof(View), typeof(SlideToOpenView),
defaultValue: default(View));
public View FillBar
{
get { return (View)GetValue(FillBarProperty); }
set { SetValue(FillBarProperty, value); }
}
private PanGestureRecognizer _panGesture = new PanGestureRecognizer();
private View _gestureListener;
private bool _android = false;
public SlideToOpenView()
{
_panGesture.PanUpdated += OnPanGestureUpdated;
SizeChanged += OnSizeChanged;
_gestureListener = new ContentView { BackgroundColor = Color.White, Opacity = 0.05 };
_gestureListener.GestureRecognizers.Add(_panGesture);
if (Device.RuntimePlatform == Device.Android) {
_android = true;
}
}
public event EventHandler SlideCompleted;
private const double _fadeEffect = 0.5;
private const uint _animLength = 50;
//Variable that stores the last state in axis X
private double _lastX = -1;
private bool _panRunning = false;
async void OnPanGestureUpdated(object sender, PanUpdatedEventArgs e)
{
if (Thumb == null || TrackBar == null || FillBar == null)
return;
switch (e.StatusType)
{
case GestureStatus.Started:
Debug.WriteLine("GestureStatus.Started");
await TrackBar.FadeTo(_fadeEffect, _animLength);
break;
case GestureStatus.Running:
// Translate and ensure we don't pan beyond the wrapped user interface element bounds.
var x = Math.Max(0, e.TotalX);
if (x > (Width - Thumb.Width))
x = (Width - Thumb.Width);
//Uncomment this if you want only forward dragging.
//if (e.TotalX < Thumb.TranslationX)
// return;
Thumb.TranslationX = x;
SetLayoutBounds(FillBar, new Rectangle(0, 0, x + Thumb.Width / 2, this.Height));
if (_panRunning == false && _android == true)
{
Device.StartTimer(TimeSpan.FromMilliseconds(2000), TimerHandle);
_panRunning = true;
}
break;
case GestureStatus.Completed:
_panRunning = false;
var posX = Thumb.TranslationX;
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
// Reset translation applied during the pan
await Task.WhenAll(new Task[]{
TrackBar.FadeTo(1, _animLength),
Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn),
});
//await TrackBar.FadeTo(1, _animLength);
//await Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn);
if (posX >= (Width - Thumb.Width - 10/* keep some margin for error*/))
SlideCompleted?.Invoke(this, EventArgs.Empty);
break;
}
}
//Timer handle for Android Xamarin.Forms Gesture Bug
bool TimerHandle()
{
if (_lastX == 0) {
_lastX = -1;
return false;
}
if (Thumb.TranslationX == _lastX && _lastX != -1) {
_panRunning = false;
var posX = Thumb.TranslationX;
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
// Reset translation applied during the pan
TrackBar.FadeTo(1, _animLength);
Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn);
if (posX >= (Width - Thumb.Width - 10/* keep some margin for error*/))
SlideCompleted?.Invoke(this, EventArgs.Empty);
_lastX = -1;
return false;
}
_lastX = Thumb.TranslationX;
return true;
}
void OnSizeChanged(object sender, EventArgs e)
{
Debug.WriteLine("OnSizeChanged");
if (Width == 0 || Height == 0)
return;
if (Thumb == null || TrackBar == null || FillBar == null)
return;
Children.Clear();
SetLayoutFlags(TrackBar, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(TrackBar, new Rectangle(0, 0, 1, 1));
Children.Add(TrackBar);
SetLayoutFlags(FillBar, AbsoluteLayoutFlags.None);
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
Children.Add(FillBar);
SetLayoutFlags(Thumb, AbsoluteLayoutFlags.None);
SetLayoutBounds(Thumb, new Rectangle(0, 0, this.Width / 5, this.Height));
Children.Add(Thumb);
SetLayoutFlags(_gestureListener, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(_gestureListener, new Rectangle(0, 0, 1, 1));
Children.Add(_gestureListener);
}
}