Xamarin | Custom control - xamarin

I'm pretty new in Xamarin development.
I spent about a week building my custom Button control.
But unfortually I can do something good;
This is have look like:
I need a gradient backgound (I already have a simple button with a gradient background, so I know (more or less) how to achieve this)
I tried to use a frame - but cannot get a good ellipse
I trried to call "NeedToDisplay" on iOS custom renderer, but the background is not changed to the new gradient
I dont know how to add the circle inside the ellipse and how to set to circle image (And I need be change the color of the image - I have PNG icons)
I'd be very grateful if you could help me to build this control.

Instead of adding a custom button , you can achieve the same by building a button like component in xamarin.forms.
You can build it using stack layout and image and label.
<StackLayout>
<Frame CornerRadius="30" BackgroundColor="Coral" HorizontalOptions="CenterAndExpand" VerticalOptions="End" Margin="0,0,0,20" HeightRequest="50" WidthRequest="300" OutlineColor="Red" HasShadow="True" Padding="8" >
<Grid VerticalOptions="FillAndExpand" RowSpacing="0" BackgroundColor="Coral">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" BackgroundColor="Transparent">
<local:GradientColor StartColor="#97ef97" EndColor="#3e663e">
<StackLayout Orientation="Horizontal" Padding="0" BackgroundColor="Transparent">
<Image Source="Sound.png" Margin="5,0,0,0" WidthRequest="40" HeightRequest="40" HorizontalOptions="Center" VerticalOptions="Center"></Image>
<Label Text="Button" FontSize="20" TextColor="Black" HorizontalOptions="End"
VerticalOptions="CenterAndExpand" />
<Frame.GestureRecognizers>
<TapGestureRecognizer Tapped="OnButtonClicked"/>
</Frame.GestureRecognizers>
</StackLayout>
</local:GradientColor>
</Grid>
</Grid>
</Frame>
</StackLayout>
and for the gradient color you can use custom renderer
Custom Renderer within core
public class GradientColor : StackLayout
{
public Color StartColor
{
get;
set;
}
public Color EndColor
{
get;
set;
}
}
Custom Renderer in Android where you can paint the gradient color and give it a rounded corner
public class GradientColorRenderer : VisualElementRenderer<StackLayout>
{
private Color StartColor
{
get;
set;
}
private Color EndColor
{
get;
set;
}
protected override void DispatchDraw(global::Android.Graphics.Canvas canvas)
{
#region for Vertical Gradient
//var gradient = new Android.Graphics.LinearGradient(0, 0, 0, Height,
#endregion
#region for Horizontal Gradient
var gradient = new LinearGradient(0, 0, Width, 0,
#endregion
this.StartColor.ToAndroid(),
this.EndColor.ToAndroid(),
Android.Graphics.Shader.TileMode.Mirror);
var paint = new Android.Graphics.Paint()
{
Dither = true,
};
paint.SetShader(gradient);
RectF rect = new RectF(0, 0, 450,68);
canvas.DrawRoundRect(rect, 30, 30, paint);
base.DispatchDraw(canvas);
}
protected override void OnElementChanged(ElementChangedEventArgs<StackLayout> e)
{
base.OnElementChanged(e);
if (e.OldElement != null || Element == null)
{
return;
}
try
{
var frame = e.NewElement as GradientColor;
this.StartColor = frame.StartColor;
this.EndColor = frame.EndColor;
}
catch (Exception ex)
{
}
}
}

Related

Custom Renderer not Behaving on iOS as Expected

I have an Xamarin application. One of the pages I want to display an Image in a circle, rather than square. To do this I have created a custom rendered for each of the platforms following some online guidance. The classes are below; first in the (portable) project I have
public class CircleImage : Image
{
public static readonly BindableProperty BorderThicknessProperty =
BindableProperty.Create(propertyName: nameof(BorderThickness),
returnType: typeof(float),
declaringType: typeof(CircleImage),
defaultValue: 0F);
public float BorderThickness
{
get { return (float)GetValue(BorderThicknessProperty); }
set { SetValue(BorderThicknessProperty, value); }
}
public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create(propertyName: nameof(BorderColor),
returnType: typeof(Color),
declaringType: typeof(CircleImage),
defaultValue: Color.White);
public Color BorderColor
{
get { return (Color)GetValue(BorderColorProperty); }
set { SetValue(BorderColorProperty, value); }
}
public static readonly BindableProperty FillColorProperty =
BindableProperty.Create(propertyName: nameof(FillColor),
returnType: typeof(Color),
declaringType: typeof(CircleImage),
defaultValue: Color.Transparent);
public Color FillColor
{
get { return (Color)GetValue(FillColorProperty); }
set { SetValue(FillColorProperty, value); }
}
}
Then for Android, I have the renderer
[assembly: ExportRenderer(typeof(CircleImage), typeof(CircleImageRenderer))]
namespace GL.Droid.Renderer
{
[Preserve(AllMembers = true)]
public class CircleImageRenderer : ImageRenderer
{
#pragma warning disable CS0618 // Type or member is obsolete.
public CircleImageRenderer() : base()
#pragma warning restore CS0618 // Type or member is obsolete.
{
}
public CircleImageRenderer(Context context) : base(context) { }
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously.
public async static void Init()
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
var temp = DateTime.Now;
}
protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
// Only enable hardware accelleration on lollipop.
if ((int)Build.VERSION.SdkInt < 21)
{
SetLayerType(LayerType.Software, null);
}
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == CircleImage.BorderColorProperty.PropertyName ||
e.PropertyName == CircleImage.BorderThicknessProperty.PropertyName ||
e.PropertyName == CircleImage.FillColorProperty.PropertyName)
{
Invalidate();
}
}
protected override bool DrawChild(Canvas canvas, Android.Views.View child, long drawingTime)
{
try
{
var radius = (float)Math.Min(Width, Height) / 2f;
var borderThickness = ((CircleImage)Element).BorderThickness;
var strokeWidth = 0f;
if (borderThickness > 0)
{
var logicalDensity = Android.App.Application.Context.Resources.DisplayMetrics.Density;
strokeWidth = (float)Math.Ceiling(borderThickness * logicalDensity + .5f);
}
radius -= strokeWidth / 2f;
var path = new Path();
path.AddCircle(Width / 2.0f, Height / 2.0f, radius, Path.Direction.Ccw);
canvas.Save();
canvas.ClipPath(path);
var paint = new Paint
{
AntiAlias = true
};
paint.SetStyle(Paint.Style.Fill);
paint.Color = ((CircleImage)Element).FillColor.ToAndroid();
canvas.DrawPath(path, paint);
paint.Dispose();
var result = base.DrawChild(canvas, child, drawingTime);
path.Dispose();
canvas.Restore();
path = new Path();
path.AddCircle(Width / 2f, Height / 2f, radius, Path.Direction.Ccw);
if (strokeWidth > 0.0f)
{
paint = new Paint
{
AntiAlias = true,
StrokeWidth = strokeWidth
};
paint.SetStyle(Paint.Style.Stroke);
paint.Color = ((CircleImage)Element).BorderColor.ToAndroid();
canvas.DrawPath(path, paint);
paint.Dispose();
}
path.Dispose();
return result;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Unable to create circle image: " + ex);
}
return base.DrawChild(canvas, child, drawingTime);
}
}
}
This works great and gives me the following looking layout
Now for the iOS and where the problem lies, we have the following, this (as far as I can tell), matches the Android implementation which is below
[assembly: ExportRenderer(typeof(CircleImage), typeof(CircleImageRenderer))]
namespace GL.iOS.Renderer
{
[Preserve(AllMembers = true)]
public class CircleImageRenderer : ImageRenderer
{
#pragma warning disable CS0108 // Member hides inherited member; missing new keyword
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
public async static void Init()
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
#pragma warning restore CS0108 // Member hides inherited member; missing new keyword
{
var temp = DateTime.Now;
}
protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
{
base.OnElementChanged(e);
if (Element == null)
return;
CreateCircle();
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == VisualElement.HeightProperty.PropertyName ||
e.PropertyName == VisualElement.WidthProperty.PropertyName ||
e.PropertyName == CircleImage.BorderColorProperty.PropertyName ||
e.PropertyName == CircleImage.BorderThicknessProperty.PropertyName ||
e.PropertyName == CircleImage.FillColorProperty.PropertyName)
{
CreateCircle();
}
}
private void CreateCircle()
{
try
{
var min = Math.Min(Element.Width, Element.Height);
Control.Layer.CornerRadius = (nfloat)(min / 2.0);
Control.Layer.MasksToBounds = false;
Control.BackgroundColor = ((CircleImage)Element).FillColor.ToUIColor();
Control.ClipsToBounds = true;
var borderThickness = ((CircleImage)Element).BorderThickness;
// Remove previously added layers.
var tempLayer = Control.Layer.Sublayers?
.Where(p => p.Name == borderName)
.FirstOrDefault();
tempLayer?.RemoveFromSuperLayer();
var externalBorder = new CALayer();
externalBorder.Name = borderName;
externalBorder.CornerRadius = Control.Layer.CornerRadius;
externalBorder.Frame = new CGRect(-.5, -.5, min + 1, min + 1);
externalBorder.BorderColor = ((CircleImage)Element).BorderColor.ToCGColor();
externalBorder.BorderWidth = ((CircleImage)Element).BorderThickness;
Control.Layer.AddSublayer(externalBorder);
}
catch (Exception ex)
{
Debug.WriteLine("Unable to create circle image: " + ex);
}
}
const string borderName = "borderLayerName";
}
}
But this gives me the rendered output of
My XAML is
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage x:Class="GL.ProfilePage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Title="Generation London"
xmlns:local="clr-namespace:GL;assembly=GL"
xmlns:Controls="clr-namespace:GL.Controls"
xmlns:Converters="clr-namespace:GL.Converters"
BackgroundColor="White">
<ContentPage.Resources>
<ResourceDictionary>
<Converters:ResizingImageConverter x:Key="ResizingImageConverter"/>
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<ScrollView>
<Grid ColumnSpacing="0" RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Aspect="AspectFill"
Source="login_background.jpg" />
<Image Aspect="Fill"
Margin="0,-1,0,-1"
Source="curved_mask.png"
VerticalOptions="End" />
<Controls:CircleImage BorderThickness="2"
BorderColor="{x:Static local:Settings.LightPurple}"
WidthRequest="100"
HeightRequest="100"
TranslationY="50"
HorizontalOptions="FillAndExpand"
VerticalOptions="End"
Source="{Binding ProfilePicture, Converter={StaticResource ResizingImageConverter}}">
<!--<Image.Source>
<UriImageSource Uri="{Binding ProfilePicture}" CacheValidity="90"/>
</Image.Source>-->
</Controls:CircleImage>
<StackLayout Grid.Row="1" Padding="0,50,0,00" HorizontalOptions="Center">
<Label x:Name="fullName" Style="{StaticResource MainLabel}"/>
<Label Margin="0,-5" Style="{StaticResource SubLabel}" Text="{Binding Occupation}" />
</StackLayout>
<Grid Grid.Row="2" Margin="0,30" ColumnSpacing="0" RowSpacing="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackLayout>
<Label Style="{StaticResource ValueLabel}" Text="{Binding DateOfBirth, StringFormat='{0:dd/MM/yyyy}'}"/>
<Label Style="{StaticResource CaptionLabel}" Text="DOB"/>
</StackLayout>
<StackLayout Grid.Column="1">
<Label x:Name="workTubeStation" Style="{StaticResource ValueLabel}"/>
<Label Style="{StaticResource CaptionLabel}" Text="Nearest Tube"/>
</StackLayout>
<StackLayout Grid.Column="2">
<Label x:Name="gender" Style="{StaticResource ValueLabel}"/>
<Label Style="{StaticResource CaptionLabel}" Text="Gender"/>
</StackLayout>
</Grid>
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="1"
Margin="0,-5"
Text="Interests"
Style="{StaticResource MainLabel}"/>
</Grid>
<ContentView Grid.Row="4" Padding="5">
<ListView x:Name="userInterests"
RowHeight="35"
ItemsSource="{Binding Interests}"
ItemTapped="NoOpInterestSelected"
HorizontalOptions="Center"
SeparatorVisibility="None">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<Label Text="{Binding .}"
Style="{StaticResource ValueLabel}"
HorizontalTextAlignment="Center"
YAlign="Center" />
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentView>
<Button Grid.Row="5"
Margin="20"
Style="{StaticResource EditButton}"
Clicked="OnEditProfile"
Text="Edit"/>
</Grid>
</ScrollView>
</ContentPage.Content>
</ContentPage>
Q. Why is the circle container not being rendered correctly?
Thanks for your time.
You haven't shown your XAML, but based on your renderer and your output it seems that the Image is covering not just the photo part, but rather the whole screen width, which makes your code work (corner radius and drawing the ellipse) to appear on the unexpected parts and eventually results in what you have shown. The renderer code expects that the Image control has no transparent parts (e.g. that it uses AspectFill)
If you want to set the Rounded Corner for the control ,Refer the following code
...
Control.Layer.MasksToBounds = true;
Control.Layer.CornerRadius = (nfloat)(min / 2.0);
Control.Layer.BorderColor = ((CircleImage)Element).BorderColor.ToCGColor();
Control.Layer.BorderWidth = ((CircleImage)Element).BorderThickness;;
...
You don't need to add a new sublayer on layer.If you do want to do it.Refer to this similar issue.

How to show label to appear off the page when using StackLayout

I have a gesture to swipe left and I would like the label to appear like it is swiped off the screen.
How does one achieve this with xamarin forms?
For example in the picture below the item selected was "Check the trains schedule London - Paris"
When the item is selected the text appears off the screen. I would like to achieve the same
Create a lovely grid with 2 columns.
EDIT This grid is going to sit inside a horizontal scrollview. This is what makes the trick to go out of the screen possible.
Now if you want to prohibit user from manually scrolling your ScrollView use a custom renderer. But this is not needed in case you want the user to be able to swipe the control left-right, it then acts rather naturally. So in XAML code below you can just use ScrollView instead of controls:DisabledScrollView.
1st column is what you have on the screen.
2nd column is what is hidden out of the screen.
First col width = ScreenSize. Define in code as it can change dynamically on screen rotation.
Second column width - at will. In your case it's small for just this TrashCan icon.
I'm managing it in the OnSize changed event for the grid, the invoked OnSizeChanged_TitleBarMain from XAML:
cNavBarSlider.ColumnDefinitions.Clear();
cNavBarSlider.ColumnDefinitions.Add(
new ColumnDefinition { Width = new GridLength(ScreenWidth, GridUnitType.Absolute) }
);
cNavBarSlider.ColumnDefinitions.Add(
new ColumnDefinition { Width = new GridLength(ScreenWidth - popupSearchOffset, GridUnitType.Absolute) }
);
//reposition scroll if needed (on screen rotation)
if (IsPopupSearchVisible)
{
await cNavBarSlider.TranslateTo(-ScreenWidth + popupSearchOffset, 0, 0, null);
}
Slide this table left-right with TranslateTo, in my case it was:
await cNavBarSlider.TranslateTo(-cTitleBarMain.Width + popupSearchOffset, 0, PopupOptionsTimeIn, Easing.CubicInOut);
Both IconSearch and Cancel hotspots (using hotspots and i want larger area to respond to touch, instead of user trying to tap some small icon or small word) invoke same method:
private bool _tapped;
//-------------------------------------------------------------
private async void OnTapped_SearchIcon(object sender, EventArgs e)
//-------------------------------------------------------------
{
if (_tapped) return;
_tapped = true;
if (!IsPopupSearchVisible) await PopupSearchShow();
else await PopupSearchHide();
_tapped = false;
}
//-------------------------------------------------------------------
public async Task PopupSearchShow()
//-------------------------------------------------------------------
{
await PopupSearchInit();
await cNavBarSlider.TranslateTo(-cTitleBarMain.Width + popupSearchOffset, 0, PopupOptionsTimeIn, Easing.CubicInOut);
IsPopupSearchVisible = true;
ControlSearchEntry.Focus();
}
//-------------------------------------------------------------------
public async Task PopupSearchHide(bool animate = true)
//-------------------------------------------------------------------
{
uint d = PopupOptionsTimeOut;
if (!animate) d = 0;
await cNavBarSlider.TranslateTo(0, 0, d, Easing.CubicInOut);
IsPopupSearchVisible = false;
}
The XAML scheme to play with:
<!-- SWIPE CONTAINER -->
<controls:DisabledScrollView Orientation="Horizontal" VerticalOptions="FillAndExpand">
<Grid
x:Name="cNavBarSlider"
ColumnSpacing="0"
HorizontalOptions="Fill">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- this in on screen, going to swipe -->
<Grid
x:Name="cTitleBarMain"
Grid.Column="0"
SizeChanged="OnSizeChanged_TitleBarMain">
<!-- put ur stuff here -->
</Grid>
<StackLayout
x:Name="cTitleBarSearch"
Grid.Column="1"
Orientation="Horizontal">
<!-- your hidden content to appear put here --!>
<StackLayout>
</Grid>
</controls:DisabledScrollView>
EDIT
Here's your code in list (Android emulator) note you can implement swipes to limit the scrolling coordinates, to make it sticky to bounds:
Listview:
<ListView
x:Name="MainList"
ItemsSource="{Binding Items}"
BackgroundColor="{StaticResource ColorListView}"
HasUnevenRows="False"
RowHeight="40"
HorizontalOptions="FillAndExpand"
IsVisible="{Binding IsOffline, Converter={StaticResource not}}"
ItemSelected="MainList_OnItemSelected"
RefreshCommand="{Binding ForceRefreshCommand}">
<ListView.SeparatorColor>
<OnPlatform
x:TypeArguments="Color"
WinPhone="{StaticResource ListSeparator}"
iOS="{StaticResource ListSeparator}" />
</ListView.SeparatorColor>
<ListView.ItemTemplate>
<DataTemplate>
<appoMobi:CellJessica/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Cell XAML:
<?xml version="1.0" encoding="utf-8" ?>
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="AppoMobi.CellJessica"
x:Name="MyViewCell">
<!-- SWIPE CONTAINER -->
<StackLayout SizeChanged="OnSizeChanged_TitleBarMain" x:Name="cCell" HorizontalOptions="FillAndExpand">
<ScrollView x:Name="scrollView"
Orientation="Horizontal"
VerticalOptions="FillAndExpand">
<Grid x:Name="cNavBarSlider"
ColumnSpacing="0"
HorizontalOptions="Fill">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- this in on screen, going to swipe -->
<Grid
x:Name="cTitleBarMain"
Grid.Column="0">
<!-- put ur stuff here -->
<Label x:Name="txtLabel" TextColor="Black" />
</Grid>
<StackLayout
x:Name="cTitleBarSearch"
Grid.Column="1"
Orientation="Horizontal">
<Image Source="cake" HeightRequest="35" WidthRequest="35" VerticalOptions="Center" HorizontalOptions="Start" />
</StackLayout>
</Grid>
</ScrollView>
</StackLayout>
</ViewCell>
Cell Code:
public partial class CellJessica
{
public CellJessica()
{
InitializeComponent();
}
//-------------------------------------------------------------
protected override void OnBindingContextChanged()
//-------------------------------------------------------------
{
SetupCell();
base.OnBindingContextChanged();
}
private bool first_setup = true;
//-------------------------------------------------------------
public void SetupCell()
//-------------------------------------------------------------
{
var item = BindingContext as CSalonListItemEx;
if (item == null) return;
txtLabel.Text = item.Name;
}
private bool _titlebar_changingsize = false;
private double popupSearchOffset = 0;
//-------------------------------------------------------------------
private async void OnSizeChanged_TitleBarMain(object sender, EventArgs e)
//-------------------------------------------------------------------
{
if (_titlebar_changingsize) return;
_titlebar_changingsize = true;
cNavBarSlider.ColumnDefinitions.Clear();
cNavBarSlider.ColumnDefinitions.Add(
new ColumnDefinition { Width = new GridLength(cCell.Width, GridUnitType.Absolute) }
);
cNavBarSlider.ColumnDefinitions.Add(
//new ColumnDefinition { Width = new GridLength(cCell.Width - popupSearchOffset, GridUnitType.Absolute) }
new ColumnDefinition { Width = new GridLength(40, GridUnitType.Absolute) }
);
//todo
//reposition scroll if rotated when hidden barea is shown
//if (IsPopupSearchVisible)
//{
// await cNavBarSlider.TranslateTo(-cCell.Width + 40, 0, 0, null);
//}
_titlebar_changingsize = false;
}
}

How can I stop the clicking on a ViewCell from changing the background color for a brief time?

I have this XAML code:
<TableView x:Name="tableView" Intent="Settings" HasUnevenRows="True">
<TableSection>
<TableSection.Title>
Card Selection
</TableSection.Title>
<ViewCell Height="50">
<Grid>
<Grid x:Name="deselectGridLink" VerticalOptions="CenterAndExpand" Padding="20, 0">
<Label TextColor="Blue" Style="{DynamicResource ListItemTextStyle}" x:Name="deselectLink" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All" />
</Grid>
<Grid x:Name="deselectGridLabel" VerticalOptions="CenterAndExpand" Padding="20, 0">
<Label TextColor="Silver" Style="{DynamicResource ListItemTextStyle}" x:Name="deselectLabel" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All" />
</Grid>
</Grid>
</ViewCell>
<ViewCell Height="50">
<Grid x:Name="selectGridLink" VerticalOptions="CenterAndExpand" Padding="20, 0">
<Label TextColor="Blue" Style="{DynamicResource ListItemTextStyle}" x:Name="selectLink" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Select All" />
</Grid>
</ViewCell>
</TableSection>
</TableView>
When other parts of my code call: SetPageDetails() then the label in the grid is changed to a link or the link is changed to a label. So for this when it is a label I would like to have no background flash event and no action called.
I attach a tap gesture recognizer like this. Note it's all on one line but covers two lines so it's more visible here in the SO question:
deselectGridLink.GestureRecognizers
.Add(NewTapGestureForUpdateCategories(false));
private TapGestureRecognizer NewTapGestureForUpdateCategories(bool val)
{
return new TapGestureRecognizer()
{
Command = new Command(() =>
{
App.DB.UpdateAllCategoryGroups(val);
App.DB.UpdateAllCategories(val);
GetPageData();
RemoveTableViewClickSection();
tableView.Root.Add(CreateTableSection());
})
};
}
When the user clicks the row when deselectGridLink grid is visible then:
The deselectGridLink visibility is set to false
The deselectGridLabel visibility is set to true
private void SetPageDetails()
{
Title = App.cardCountForSelectedCategories + (App.cardCountForSelectedCategories == 1 ? " Card Selected" : " Cards Selected");
if (App.cardCountForSelectedCategories == 0)
{
deselectGridLink.IsVisible = false;
deselectGridLabel.IsVisible = true;
}
else
{
deselectGridLink.IsVisible = true;
deselectGridLabel.IsVisible = false;
}
}
The effect of this is that the grid link text will change to silver and the link becomes a label.
However even though it's a gray color label when the label is clicked there is still a brief background row color change from white to a dark color when the label is clicked. I assume it's just the way a view cell works.
Is there a way to suppress this from happening?
EDIT 1 - Updated answer as per updates to question. i.e. add support for switching between highlight enabled/disabled mode.
EDIT 2 - Restructure answer and add more details.
Option-1: Enable/disable view-cell through IsEnabled
The simplest option would be to use the IsEnabled property, which in turn enables/disables the background flash behavior. The only downside to this approach is that it will also disable the taps on child controls, i.e. tap events/gesture recognizer(s) will not be triggered if parent view-cell's IsEnabled is false.
For example:
XAML
<!-- Add name attribute to view-cell -->
<ViewCell x:Name="deselectCell" ..>
<Grid>
<Grid x:Name="deselectGridLink" ..
....
</ViewCell>
Code-behind
private void SetPageDetails()
{
if (App.cardCountForSelectedCategories == 0)
{
deselectCell.IsEnabled = false; //disable background flash
...
}
else
{
deselectCell.IsEnabled = true;
...
}
}
Recommendation 1 - Use data-binding and triggers
Instead of controlling visibility for each label in code-behind, you can use triggers and data-binding as follows (view-model will have a IsDeselectEnabled property):
<ViewCell IsEnabled="{Binding IsDeselectEnabled}" Height="50">
<Label Margin="20,0,20,0" Style="{DynamicResource ListItemTextStyle}" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All">
<Label.Triggers>
<DataTrigger TargetType="Label" Binding="{Binding IsDeselectEnabled}" Value="true">
<Setter Property="TextColor" Value="Blue" />
</DataTrigger>
<DataTrigger TargetType="Label" Binding="{Binding IsDeselectEnabled}" Value="false">
<Setter Property="TextColor" Value="Silver" />
</DataTrigger>
</Label.Triggers>
</Label>
</ViewCell>
Recommendation 2 - Use triggers with view as source
<ViewCell x:Name="deselectCell" Height="50">
<Label Margin="20,0,20,0" Style="{DynamicResource ListItemTextStyle}" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All">
<Label.Triggers>
<DataTrigger TargetType="Label" Binding="{Binding IsEnabled, Source={x:Reference deselectCell}}" Value="true">
<Setter Property="TextColor" Value="Blue" />
</DataTrigger>
<DataTrigger TargetType="Label" Binding="{Binding IsEnabled, Source={x:Reference deselectCell}}" Value="false">
<Setter Property="TextColor" Value="Silver" />
</DataTrigger>
</Label.Triggers>
</Label>
</ViewCell>
Option-2: Enable/disable highlight, but allow taps
To allow taps while toggling ViewCell's background-highlight behavior, we will need to implement platform-renderer(s).
In case of iOS, we can use SelectionStyle to toggle this behavior, while in case of android, we can use Clickable property.
Shared control:
public class CustomViewCell : ViewCell
{
public static readonly BindableProperty AllowHighlightProperty =
BindableProperty.Create(
"AllowHighlight", typeof(bool), typeof(CustomViewCell),
defaultValue: true);
public bool AllowHighlight
{
get { return (bool)GetValue(AllowHighlightProperty); }
set { SetValue(AllowHighlightProperty, value); }
}
}
iOS renderer:
[assembly: ExportRenderer(typeof(CustomViewCell), typeof(CustomViewCellRenderer))]
namespace SampleApp.iOS
{
public class CustomViewCellRenderer : ViewCellRenderer
{
UITableViewCell _nativeCell;
//get access to the associated forms-element and subscribe to property-changed
public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
{
_nativeCell = base.GetCell(item, reusableCell, tv);
var formsCell = item as CustomViewCell;
if (formsCell != null)
{
formsCell.PropertyChanged -= OnPropertyChanged;
formsCell.PropertyChanged += OnPropertyChanged;
}
//and, update the style
SetStyle(formsCell);
return _nativeCell;
}
void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var formsCell = sender as CustomViewCell;
if (formsCell == null)
return;
//TODO: Trying to find a nicer and more robust way to dispose and unsubscribe :(
if (_nativeCell == null)
formsCell.PropertyChanged -= OnPropertyChanged;
if (e.PropertyName == CustomViewCell.AllowHighlightProperty.PropertyName)
{
SetStyle(formsCell);
}
}
private void SetStyle(CustomViewCell formsCell)
{
//added this code as sometimes on tap, the separator disappears, if style is updated before tap animation finishes
//https://stackoverflow.com/questions/25613117/how-do-you-prevent-uitableviewcellselectionstylenone-from-removing-cell-separato
Device.StartTimer(TimeSpan.FromMilliseconds(50), () => {
Device.BeginInvokeOnMainThread(() =>
{
if (formsCell.AllowHighlight)
_nativeCell.SelectionStyle = UITableViewCellSelectionStyle.Default;
else
_nativeCell.SelectionStyle = UITableViewCellSelectionStyle.None;
});
return false;
});
}
}
}
Android renderer:
[assembly: ExportRenderer(typeof(CustomViewCell), typeof(CustomViewCellRenderer))]
namespace SampleApp.Droid
{
public class CustomViewCellRenderer : ViewCellRenderer
{
Android.Views.View _nativeCell;
protected override Android.Views.View GetCellCore(Cell item, Android.Views.View convertView, Android.Views.ViewGroup parent, Android.Content.Context context)
{
_nativeCell = base.GetCellCore(item, convertView, parent, context);
SetStyle();
return _nativeCell;
}
// this one is simpler as the base class has a nice override-able method for our purpose - so we don't need to subscribe
protected override void OnCellPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnCellPropertyChanged(sender, e);
if(e.PropertyName == CustomViewCell.AllowHighlightProperty.PropertyName)
{
SetStyle();
}
}
private void SetStyle()
{
var formsCell = Cell as CustomViewCell;
if (formsCell == null)
return;
_nativeCell.Clickable = !formsCell.AllowHighlight;
}
}
}
Sample usage 1 - Through data-binding
<local:CustomViewCell AllowHighlight="{Binding IsHighlightEnabled}" ..>
<Grid>
<Grid x:Name="deselectGridLink" ..
...
</local:CustomViewCell>
Sample usage 2 - Through code-behind
XAML
<!-- Add name attribute to view-cell -->
<local:CustomViewCell x:Name="deselectCell" ..>
<Grid>
<Grid x:Name="deselectGridLink" ..
...
</local:CustomViewCell>
Code-behind
private void SetPageDetails()
{
if (App.cardCountForSelectedCategories == 0)
{
deselectCell.AllowHighlight= false; //disable background flash
...
}
else
{
deselectCell.AllowHighlight= true;
...
}
}
Option-3: Disable highlight, selection for all items
This particularly applies to ListView. The updated question now specifies that the cells are part of TableView, so this option is no longer valid in current question context.
You will need to implement platform renderers to disable highlight colors, and add ItemTapped handler to ListView to disable selection by setting SelectedItem as null always. References used:
Disable highlight item
Disable selection
Code
To get started, create a custom view-cell:
public class NoSelectViewCell : ViewCell { }
Implement iOS renderer as:
[assembly: ExportRenderer(typeof(NoSelectViewCell), typeof(NoSelectViewCellRenderer))]
namespace SampleApp.iOS
{
public class NoSelectViewCellRenderer : ViewCellRenderer
{
public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
{
var nativeCell = base.GetCell(item, reusableCell, tv);
nativeCell.SelectionStyle = UITableViewCellSelectionStyle.None;
return nativeCell;
}
}
}
Implement android renderer as:
[assembly: ExportRenderer(typeof(NoSelectViewCell), typeof(NoSelectViewCellRenderer))]
namespace SampleApp.Droid
{
public class NoSelectViewCellRenderer : ViewCellRenderer
{
protected override Android.Views.View GetCellCore(Cell item, Android.Views.View convertView, Android.Views.ViewGroup parent, Android.Content.Context context)
{
var cell = base.GetCellCore(item, convertView, parent, context);
cell.Focusable = false;
cell.FocusableInTouchMode = false;
var listView = parent as Android.Widget.ListView;
if (listView != null)
{
listView.SetSelector(Android.Resource.Color.Transparent);
listView.CacheColorHint = Xamarin.Forms.Color.Transparent.ToAndroid();
}
return cell;
}
}
}
Sample Usage:
XAML
<ListView ItemTapped="Handle_ItemTapped">
<ListView.ItemTemplate>
<DataTemplate>
<local:NoSelectViewCell Height="50">
<Grid>
<Grid x:Name="deselectGridLink" VerticalOptions="CenterAndExpand" Padding="20, 0">
<Label TextColor="Blue" Style="{DynamicResource ListItemTextStyle}" x:Name="deselectLink" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All" />
</Grid>
<Grid x:Name="deselectGridLabel" VerticalOptions="CenterAndExpand" Padding="20, 0">
<Label TextColor="Silver" Style="{DynamicResource ListItemTextStyle}" x:Name="deselectLabel" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All" />
</Grid>
</Grid>
</local:NoSelectViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Code-behind
void Handle_ItemTapped(object sender, Xamarin.Forms.ItemTappedEventArgs e)
{
// don't do anything if we just de-selected the row
if (e.Item == null) return;
// do something with e.SelectedItem
((ListView)sender).SelectedItem = null; // de-select the row
}
What G.Sharada proposes is very nicely working for iOS, but on Android I still had blinks on click.
Adding this line to the styles file solved the problem.
<item name="android:colorActivatedHighlight">#android:color/transparent</item>

Xamarin Forms XAML Label Rotation

I've got a problem with Xamarin.Forms and Label.
I'm trying to set a label on a grid column.
The first image here shows the expected result, which is written in AXML on Android.
The second image here is written in XAML in Xamarin.Forms.
The code in the XAML file is as follows:
<Grid
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400*"/>
<ColumnDefinition Width="75*"/>
</Grid.ColumnDefinitions>
<WebView Source="{Binding ContentSource}" />
<!--<ProgressBar IsVisible="{Binding IsLoading}"
Progress="{Binding Progress}"/>-->
<Grid Grid.Column="1"
BackgroundColor="#EE7F00"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<Label
Text="{Binding DocumentIndex}"
LineBreakMode="NoWrap"
HorizontalOptions="Center"
Rotation="-90"
VerticalOptions="Center" />
</Grid>
</Grid>
How can I expand the height or width of the label to equal to the text length?
Thank you so far
Remove the Grid container for label and place a Box view instead, and set the Grid Row and Column same for both the box view and label. like this
<Grid
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80*"/>
<ColumnDefinition Width="20*"/>
</Grid.ColumnDefinitions>
<WebView Grid.Row="0" Grid.Column="0" Source="{Binding ContentSource}" />
<!--<ProgressBar IsVisible="{Binding IsLoading}"
Progress="{Binding Progress}"/>-->
<BoxView Grid.Row="0" Grid.Column="1" BackgroundColor="#EE7F00" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"/>
<Label Grid.Row="0" Grid.Column="1" Text="{Binding DocumentIndex}"
LineBreakMode="NoWrap"
HorizontalOptions="Center"
Rotation="-90"
VerticalOptions="Center" />
</Grid>
I hope this will solve your label length problem after rotation.
I solved this with a custom renderer. In your Forms project:
public class RotatedText : View
{
public static BindableProperty TitleValueProperty = BindableProperty.Create(nameof(TitleValue), typeof(string), typeof(string), null, BindingMode.TwoWay, null,
(bindable, oldValue, newValue) =>
{
});
public string TitleValue
{
get => (string)GetValue(TitleValueProperty);
set => SetValue(TitleValueProperty, value);
}
}
And in your Android project:
[assembly: ExportRenderer(typeof(RotatedText), typeof(RotatedTextRenderer))]
namespace MyNamespace
{
public class RotatedTextRenderer : ViewRenderer
{
private Context _context;
public RotatedTextRenderer(Context c) : base(c)
{
_context = c;
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.View> e)
{
base.OnElementChanged(e);
if (e.NewElement is RotatedText)
{
string title = ((RotatedText)e.NewElement).TitleValue;
SetNativeControl(new RotatedTextView(_context, title));
}
}
}
public class RotatedTextView : Android.Views.View
{
private int DEFAULT_TEXT_SIZE = 30;
private string _text;
private TextPaint _textPaint;
public RotatedTextView(Context c, string title) : base(c)
{
_text = title;
initLabelView();
}
private void initLabelView()
{
this._textPaint = new TextPaint();
this._textPaint.AntiAlias = true;
this._textPaint.TextAlign = Paint.Align.Center;
this._textPaint.TextSize = DEFAULT_TEXT_SIZE;
this._textPaint.Color = new Android.Graphics.Color(0, 0, 0);
}
public override void Draw(Canvas canvas)
{
base.Draw(canvas);
if (!string.IsNullOrEmpty(this._text))
{
float x = (Width / 2) - DEFAULT_TEXT_SIZE/3;
float y = (Height / 2);
canvas.Rotate(90);
canvas.DrawText(this._text, y, -x, this._textPaint);
}
}
}
}
Then just set TitleValue where ever you use RotatedText. It's a little ugly but I couldn't find a better way.

Xamarin.Forms Listview with 2 line label?

I'm converting a MonoTouch.Dialog app to Xamarin.Forms.
I have a Cell in a ListView and that has a "Detail" line that should be 2 lines long, and should truncate the tail after that.
var lblDetail = new Label
{
LineBreakMode = LineBreakMode.TailTruncation,
Text = "a really long string that should show 2 lines then ..."
};
How do I set something like, "Lines = 2"
I solved this with a custom renderer
This is my xaml (in my pcl project)
<ListView ItemsSource="{Binding Cards}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid Padding="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Source="{Binding Icon}"/>
<customRenderers:MultiLineLabel Text="{Binding Summary}"
Grid.Column="1"/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
This is my MultiLineLabel class
public class MultiLineLabel : Label
{
}
This is the renderer for iOS:
[assembly: ExportRenderer(typeof(MultiLineLabel), typeof(MultiLineLabelRenderer))]
namespace NameSpace.iOS.Renderers
{
public class MultiLineLabelRenderer : LabelRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.LineBreakMode = UILineBreakMode.TailTruncation;
Control.Lines = 3;
}
}
}
}
the following code working fine for me :)
UserNotesListView = new ListView() {
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand,
HasUnevenRows=true,
ItemsSource=//upon your bussiess
};
DataTemplate dt=new DataTemplate(()=>
{
var LabelText = new Label();
LabelText.SetBinding(Label.TextProperty, new Binding("note"));
return new ViewCell { View = LabelText };
});
UserNotesListView.ItemTemplate = dt;
Content = new StackLayout
{
HorizontalOptions=LayoutOptions.FillAndExpand,
Children=
{
UserNotesListView
}
};
You might want to customize ListView:
<ListView x:Name="ListMenu" ItemsSource="{Binding _YourItems_}" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<StackLayout Orientation="Horizontal">
<Label Text="{Binding _YourText_}" TextColor="Black" />
</StackLayout>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
even if text not fit one line, it will wrap automatically.

Resources