Custom Renderer not Behaving on iOS as Expected - xamarin

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.

Related

Frame doesn't have shadow effect below Android Q

I've created a custom renderer for Frame to feel like CardView in Android it works fine on Android P but i've tested on API 21,22,23 it doesn't have any kind of effect. Here is my Android Renderer.
public class ShadowFrameRenderer : Xamarin.Forms.Platform.Android.AppCompat.FrameRenderer
{
public ShadowFrameRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Frame> e)
{
base.OnElementChanged(e);
if (e.NewElement != null && e.NewElement is ShadowFrame)
{
Elevation = 30.0f;
TranslationZ = 0.0f;
SetZ(30f);
//this.SetBackgroundResource(Resource.Drawable.shadow);
//GradientDrawable drawable = (GradientDrawable)this.Background;
//drawable.SetColor(Android.Graphics.Color.ParseColor("#F0F0F0"));
}
UpdateElevation();
}
private void UpdateElevation()
{
//var materialFrame = (ShadowFrame)Element;
// we need to reset the StateListAnimator to override the setting of Elevation on touch down and release.
if(Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Lollipop)
Control.StateListAnimator = new Android.Animation.StateListAnimator();
// set the elevation manually
ViewCompat.SetElevation(this, 10);
ViewCompat.SetElevation(Control, 10);
if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Lollipop)
{
Control.Elevation = 10;
Control.CardElevation = 10;
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
UpdateElevation();
//if (e.PropertyName == "Elevation")
//{
// UpdateElevation();
//}
}
}
Here is XAML.
<ContentPage.Content>
<controls:ShadowFrame Padding="10" Margin="10">
<Grid VerticalOptions="Start" HeightRequest="57" Margin="0,0,0,10" BackgroundColor="White">
<Grid ColumnSpacing="0" VerticalOptions="FillAndExpand">
<StackLayout BackgroundColor="{DynamicResource PMedium}"
x:Name="ListingLayoutB" VerticalOptions="FillAndExpand">
<Label Text="LISTING" HorizontalOptions="CenterAndExpand" TextColor="White" x:Name="ListingTxt"
FontSize="15" FontFamily="{StaticResource SBold}" VerticalOptions="CenterAndExpand"/>
</StackLayout>
<StackLayout Grid.Column="1" BackgroundColor="White" x:Name="DealsLayoutB" VerticalOptions="FillAndExpand">
<Label Text="DEALS" HorizontalOptions="CenterAndExpand" TextColor="{DynamicResource PMedium}" x:Name="DealsTxt"
FontSize="15" VerticalOptions="CenterAndExpand" FontFamily="{StaticResource SBold}"/>
</StackLayout>
</Grid>
</Grid>
</controls:ShadowFrame>
</ContentPage.Content>
And here is the result of above code. Screen shot taken from Android Emulator API level 23.
After setting BorderColor="White" the shadow is showing as expected.
Source: https://forums.xamarin.com/discussion/comment/416769#Comment_416769

On long press on entry goes behind the keyboard in Xamarin.Forms in iOS?

I have chat page. Page uses StackLayout as wrapper of ListView, Entry and Button.
ChatPage.xaml
<ContentPage.Resources>
<ResourceDictionary>
<local:MessageTemplateSelector x:Key="MessageTemplateSelector" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout>
<ListView x:Name="MessagesListView"
ItemTemplate="{StaticResource MessageTemplateSelector}"
ItemsSource="{Binding Messages}"
HasUnevenRows="True"
IsPullToRefreshEnabled="true"
IsRefreshing="{Binding IsRefreshing}"
RefreshCommand="{Binding RefreshCommand}"
SeparatorVisibility="None"
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToParent,Property=Height,Factor=1,Constant=0}"
/>
<Grid x:Name="MessageControls" RowSpacing="1" ColumnSpacing="2" Padding="5"
BackgroundColor="#EFEFF4"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Entry x:Name="txtMessage" Grid.Column="0" HeightRequest="40" Placeholder="Message" Text="{Binding OutGoingText}" TextChanged="EnableSend"/>
<Button x:Name="sendButton" Grid.Column="1" Text="Send" Command="{Binding SendCommand}"/>
</Grid>
</StackLayout>
</ContentPage.Content>
Issue: When I click on Entry, keyboard covers Entry. So, I have handled keyboard appear/disappear event to manage entry visibility as following code. It is working fine except this case. Whenever user long press on entry(most probable to cop/paste), entry goes down/behind the keyboard.
Can anybody please suggest me?
[assembly: ExportRenderer (typeof (ChatPage), typeof (KeyboardAdaptedPageRenderer))]
namespace Project.iOS
{
public class KeyboardAdaptedPageRenderer : ContentPageWithCustomBackButtonRenderer
{
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
Keyboard.WillShow += OnKeyboardWillShow;
Keyboard.WillHide += OnKeyboardWillHide;
}
public override void ViewDidDisappear(bool animated)
{
base.ViewDidDisappear(animated);
Keyboard.WillShow -= OnKeyboardWillShow;
Keyboard.WillHide -= OnKeyboardWillHide;
OnKeyboardWillHide(new KeyboardInfo()
{
AnimationDuration = 0,
AnimatonOptions = UIViewAnimationOptions.TransitionNone
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Keyboard.WillShow -= OnKeyboardWillShow;
Keyboard.WillHide -= OnKeyboardWillHide;
}
private void OnKeyboardWillShow(KeyboardInfo info)
{
if (info.SoftwareKeyboardIsVisible)
{
UIView.Animate(info.AnimationDuration, 0, info.AnimatonOptions, () =>
{
var bounds = View.Bounds;
bounds.Y = info.BeginRect.Top - info.EndRect.Top; // iphone 4 and others
View.Bounds = bounds;
}, null);
}
}
private void OnKeyboardWillHide(KeyboardInfo info)
{
UIView.Animate(info.AnimationDuration, 0, info.AnimatonOptions, () =>
{
var bounds = View.Bounds;
bounds.Y = 0;
View.Bounds = bounds;
}, null);
}
}
}

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;
}
}

Aligning Image with Entry in Xamarin Forms

In the following Xamarin Forms code, I am trying to align Image with Entry to create visual appearance like a bootstrap input-group as explained in Bootstrap input group addon alignment problems
But it has following shortcomings:
The image takes more width and height than specified HeightRequest and WidthRequest
There is unwanted space between Image and Entry
How to fix this?
XAML
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyHomeScreen2;assembly=MyHomeScreen2"
x:Class="MyHomeScreen2.InputFormTest"
NavigationPage.HasNavigationBar="False">
<ContentPage.Content>
<Grid x:Name="inputGrid" Grid.Row="1" ColumnSpacing="0" RowSpacing="0" Padding="0" BackgroundColor="#606060">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label x:Name="lblReading" TextColor="White" Text="READING" Grid.Row="0" Margin="15"></Label>
<StackLayout Grid.Row="1" Orientation="Horizontal">
<Image Source="homea.png" Aspect="AspectFit"
HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
HeightRequest="10" WidthRequest="20"
BackgroundColor="Silver" ></Image>
<Entry x:Name="myEntry" TextColor="Black" Text="1" Keyboard="Numeric" BackgroundColor="White"
Opacity="0.9" HeightRequest="20">
</Entry>
</StackLayout>
</Grid>
</ContentPage.Content>
</ContentPage>
First things first:
XAML:
<Grid Grid.Row="1" ColumnSpacing="0"
RowSpacing="0"
Padding="0"
BackgroundColor="#606060">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0"
x:Name="lblReading"
TextColor="White"
Text="READING"
Margin="15"/>
<StackLayout Grid.Row="1"
Orientation="Horizontal"
Spacing="1"
Margin="5,0">
<Image Source="lan_connect_white_36dp"
Aspect="AspectFit"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand"
HeightRequest="40"
WidthRequest="40"
BackgroundColor="Silver"/>
<Entry x:Name="myEntry"
TextColor="Black"
Text="1"
Keyboard="Numeric"
BackgroundColor="White"
HorizontalOptions="FillAndExpand"
VerticalOptions="CenterAndExpand"
Opacity="0.9">
</Entry>
</StackLayout>
</Grid>
Result:
Explanation
The image takes more width and height than specified HeightRequest and WidthRequest
When you use a asterisk to set the Width of ColumnDefinition or the Height of RowDefinition you are saying that it should take all available space of it, and the others columns/rows will just use enough space to hold the inner view on it.
There is unwanted space between Image and Entry
Some layout containers have a default spacing between views. It is the case for GridLayout, that has a default ColumnSpacing and RowSpacing of 6.
I have done this using this blog.
In PCL: Create a class named ImageEntry
using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms;
namespace ImageEntry
{
public class ImageEntry1 : Entry
{
public ImageEntry1()
{
this.HeightRequest = 50;
}
public static readonly BindableProperty ImageProperty =
BindableProperty.Create(nameof(Image), typeof(string), typeof(ImageEntry1), string.Empty);
public static readonly BindableProperty LineColorProperty =
BindableProperty.Create(nameof(LineColor), typeof(Xamarin.Forms.Color), typeof(ImageEntry1), Color.White);
public static readonly BindableProperty ImageHeightProperty =
BindableProperty.Create(nameof(ImageHeight), typeof(int), typeof(ImageEntry1), 40);
public static readonly BindableProperty ImageWidthProperty =
BindableProperty.Create(nameof(ImageWidth), typeof(int), typeof(ImageEntry1), 40);
public static readonly BindableProperty ImageAlignmentProperty =
BindableProperty.Create(nameof(ImageAlignment), typeof(ImageAlignment), typeof(ImageEntry1), ImageAlignment.Left);
public Color LineColor
{
get { return (Color)GetValue(LineColorProperty); }
set { SetValue(LineColorProperty, value); }
}
public int ImageWidth
{
get { return (int)GetValue(ImageWidthProperty); }
set { SetValue(ImageWidthProperty, value); }
}
public int ImageHeight
{
get { return (int)GetValue(ImageHeightProperty); }
set { SetValue(ImageHeightProperty, value); }
}
public string Image
{
get { return (string)GetValue(ImageProperty); }
set { SetValue(ImageProperty, value); }
}
public ImageAlignment ImageAlignment
{
get { return (ImageAlignment)GetValue(ImageAlignmentProperty); }
set { SetValue(ImageAlignmentProperty, value); }
}
}
public enum ImageAlignment
{
Left,
Right
}
}
In android create a class named ImageEntryRenderer
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using Android.Support.V4.Content;
using Android.Views;
using Android.Widget;
using ImageEntry;
using ImageEntry.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(ImageEntry1), typeof(ImageEntryRenderer))]
namespace ImageEntry.Droid
{
public class ImageEntryRenderer : EntryRenderer
{
ImageEntry1 element;
public ImageEntryRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (e.OldElement != null || e.NewElement == null)
return;
element = (ImageEntry1)this.Element;
var editText = this.Control;
if (!string.IsNullOrEmpty(element.Image))
{
switch (element.ImageAlignment)
{
case ImageAlignment.Left:
editText.SetCompoundDrawablesWithIntrinsicBounds(GetDrawable(element.Image), null, null, null);
break;
case ImageAlignment.Right:
editText.SetCompoundDrawablesWithIntrinsicBounds(null, null, GetDrawable(element.Image), null);
break;
}
}
editText.CompoundDrawablePadding = 25;
Control.Background.SetColorFilter(element.LineColor.ToAndroid(), PorterDuff.Mode.SrcAtop);
}
private BitmapDrawable GetDrawable(string imageEntryImage)
{
int resID = Resources.GetIdentifier(imageEntryImage, "drawable", this.Context.PackageName);
var drawable = ContextCompat.GetDrawable(this.Context, resID);
var bitmap = ((BitmapDrawable)drawable).Bitmap;
return new BitmapDrawable(Resources, Bitmap.CreateScaledBitmap(bitmap, element.ImageWidth * 2, element.ImageHeight * 2, true));
}
}
}
In IOS create a class named ImageEntryRenderer
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using CoreAnimation;
using CoreGraphics;
using Foundation;
using ImageEntry;
using ImageEntry.iOS;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(ImageEntry1), typeof(ImageEntryRenderer))]
namespace ImageEntry.iOS
{
public class ImageEntryRenderer : EntryRenderer
{
ImageEntry1 element;
public ImageEntryRenderer()
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (e.OldElement != null || e.NewElement == null)
return;
var element = (ImageEntry1)this.Element;
var textField = this.Control;
if (!string.IsNullOrEmpty(element.Image))
{
switch (element.ImageAlignment)
{
case ImageAlignment.Left:
textField.LeftViewMode = UITextFieldViewMode.Always;
textField.LeftView = GetImageView(element.Image, element.ImageHeight, element.ImageWidth);
break;
case ImageAlignment.Right:
textField.RightViewMode = UITextFieldViewMode.Always;
textField.RightView = GetImageView(element.Image, element.ImageHeight, element.ImageWidth);
break;
}
}
textField.BorderStyle = UITextBorderStyle.None;
CALayer bottomBorder = new CALayer
{
Frame = new CGRect(0.0f, element.HeightRequest - 1, this.Frame.Width, 1.0f),
BorderWidth = 2.0f,
BorderColor = element.LineColor.ToCGColor()
};
textField.Layer.AddSublayer(bottomBorder);
textField.Layer.MasksToBounds = true;
}
private UIView GetImageView(string imagePath, int height, int width)
{
var uiImageView = new UIImageView(UIImage.FromBundle(imagePath))
{
Frame = new RectangleF(0, 0, width, height)
};
UIView objLeftView = new UIView(new System.Drawing.Rectangle(0, 0, width + 10, height));
objLeftView.AddSubview(uiImageView);
return objLeftView;
}
}
}
In XAML added the entry code: 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:local="clr-namespace:ImageEntry"
BackgroundColor="#2f4259"
x:Class="ImageEntry.MainPage">
<ContentPage.Content>
<ScrollView>
<StackLayout Padding="40" Spacing="10">
<local:ImageEntry1 TextColor="White"
Image="user"
Placeholder="Emil"
HorizontalOptions="FillAndExpand"/>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
Attached a sample project on here, happy coding :)

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.

Resources