Capturing a hand-drawn signature with Xamarin Forms (PCL) - xamarin

I am trying to figure out how I can ask the user for a signature area so they can sign with their finger then save that signature to a file and I am reaching a dead end. I have looked at Kimserey's blog, the CrossGraphics library and SkiaSharp but these all seem to be geared around making the image through code vs a user drawing with their finger. The solution needs to be used in a pcl project and will be deployed to Android, iOS, and UWP. Does anyone have an idea?

As #Jason commented, use SignaturePad.
The NuGet can be found here: https://www.nuget.org/packages/Xamarin.Controls.SignaturePad.Forms
It works on basically any platform.
Source: https://github.com/xamarin/SignaturePad
An example page might be this:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:signature="clr-namespace:SignaturePad.Forms;assembly=SignaturePad.Forms"
x:Class="Samples.Views.SignatureXamlView">
<Grid>
<signature:SignaturePadView />
</Grid>
</ContentPage>

SignaturePadDemoPage.xaml
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
BackgroundColor="Gray"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-
namespace:SignaturePadDemo;assembly:SignaturePadDemo"
x:Class="SignaturePadDemo.SignaturePadDemoPage">
<Grid HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" Padding="30">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Text="SignaturePad Demo" Grid.Row="0" VerticalOptions="Start" HorizontalOptions="Center" TextColor="White" FontSize="25"/>
<local:ImageWithTouch x:Name="imgSiganturePad" Grid.Row="1" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" BackgroundColor="White" CurrentLineColor="Fuchsia"/>
<Grid Grid.Row="2" VerticalOptions="EndAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Button x:Name="btnSave" Text="Save the Image" Grid.Row="0" HorizontalOptions="FillAndExpand" TextColor="Blue" BackgroundColor="White" Clicked="btnSaveImage_Click"/>
<Button x:Name="btnSee" Text="See the Image" Grid.Row="1" HorizontalOptions="FillAndExpand" TextColor="Blue" BackgroundColor="White" Clicked="btnSeeImage_Click"/>
<Button x:Name="btnClear" Text="Clear" Grid.Row="2" HorizontalOptions="FillAndExpand" TextColor="Blue" BackgroundColor="White" Clicked="btnClear_Click"/>
</Grid>
</Grid>
SignaturePadDemoPage.xaml.cs
using System;
using Xamarin.Forms;
namespace SignaturePadDemo
{
public partial class SignaturePadDemoPage : ContentPage
{
public SignaturePadDemoPage()
{
InitializeComponent();
}
private void btnSaveImage_Click(object sender, EventArgs e)
{
var imgPath = DependencyService.Get<ISign>().Sign();
imgSiganturePad.SavedImagePath = imgPath;
btnSee.IsEnabled = true;
DisplayAlert("SignaturePadDemo", "Your siganture saved succesfully", "Ok");
}
private void btnSeeImage_Click(object sender, EventArgs e)
{
Navigation.PushModalAsync(new
SignatureDetailPage(imgSiganturePad.SavedImagePath));
imgSiganturePad.ClearPath = !imgSiganturePad.ClearPath;
}
private void btnClear_Click(object sender, EventArgs e)
{
imgSiganturePad.ClearPath = !imgSiganturePad.ClearPath;
DisplayAlert("SignaturePadDemo", "Siganture was clear", "Ok");
}
}
}
CustomRenderer for Image
PCL:
ImageWithTouch.cs
using System;
using Xamarin.Forms;
namespace SignaturePadDemo
{
public class ImageWithTouch : Image
{
public static readonly BindableProperty CurrentLineColorProperty =
BindableProperty.Create((ImageWithTouch w) => w.CurrentLineColor, Color.Default);
public static readonly BindableProperty CurrentLineWidthProperty =
BindableProperty.Create((ImageWithTouch w) => w.CurrentLineWidth, 1);
public static readonly BindableProperty CurrentImageProperty =
BindableProperty.Create((ImageWithTouch w) => w.CurrentImagePath, "");
public static readonly BindableProperty ClearImagePathProperty =
BindableProperty.Create((ImageWithTouch w) => w.ClearPath, false);
public static readonly BindableProperty SavedImagePathProperty =
BindableProperty.Create((ImageWithTouch w) => w.SavedImagePath, "");
public Color CurrentLineColor
{
get
{
return (Color)GetValue(CurrentLineColorProperty);
}
set
{
SetValue(CurrentLineColorProperty, value);
}
}
public int CurrentLineWidth
{
get
{
return (int)GetValue(CurrentLineWidthProperty);
}
set
{
SetValue(CurrentLineWidthProperty, value);
}
}
public string CurrentImagePath
{
get
{
return (string)GetValue(CurrentImageProperty);
}
set
{
SetValue(CurrentImageProperty, value);
}
}
public bool ClearPath
{
get
{
return (bool)GetValue(ClearImagePathProperty);
}
set
{
SetValue(ClearImagePathProperty, value);
}
}
public string SavedImagePath
{
get
{
return (string)GetValue(SavedImagePathProperty);
}
set
{
SetValue(SavedImagePathProperty, value);
}
}
}
}
Xamarin.Android:
ImageWithTouchRenderer.cs
using System;
using System.ComponentModel;
using Xamarin.Forms.Platform.Android;
using Xamarin.Forms;
using Android.Graphics;
using Java.IO;
using System.IO;
using SignaturePadDemo;
using SignaturePadDemo.Droid;
[assembly: ExportRenderer(typeof(ImageWithTouch),
typeof(ImageWithTouchRenderer))]
namespace SignaturePadDemo.Droid
{
public class ImageWithTouchRenderer : ViewRenderer<ImageWithTouch, DrawView>
{
protected override void OnElementChanged(ElementChangedEventArgs<ImageWithTouch> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
SetNativeControl(new DrawView(Context));
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == ImageWithTouch.ClearImagePathProperty.PropertyName)
{
Control.Clear();
}
else if (e.PropertyName == ImageWithTouch.SavedImagePathProperty.PropertyName)
{
Bitmap curDrawingImage = Control.GetImageFromView();
Byte[] imgBytes = ImageHelper.BitmapToBytes(curDrawingImage);
Java.IO.File f = new Java.IO.File(Element.SavedImagePath);
f.CreateNewFile();
FileOutputStream fo = new FileOutputStream(f);
fo.Write(imgBytes);
fo.Close();
}
else
{
UpdateControl(true);
}
}
private void UpdateControl(bool bDisplayFlag)
{
Control.CurrentLineColor = Element.CurrentLineColor.ToAndroid();
Control.PenWidth = Element.CurrentLineWidth * 3;
Control.ImageFilePath = Element.CurrentImagePath;
if (bDisplayFlag)
{
Control.LoadImageFromFile();
Control.Invalidate();
}
}
}
}
Xamarin.iOS:
ImageWithTouchRenderer.cs
using System.Drawing;
using Xamarin.Forms.Platform.iOS;
using Xamarin.Forms;
using System.ComponentModel;
using UIKit;
using Foundation;
using SignaturePadDemo;
using SignaturePadDemo.iOS;
[assembly: ExportRenderer(typeof(ImageWithTouch), typeof(ImageWithTouchRenderer))]
namespace SignaturePadDemo.iOS
{
public class ImageWithTouchRenderer : ViewRenderer<ImageWithTouch, DrawView>
{
protected override void OnElementChanged(ElementChangedEventArgs<ImageWithTouch> e)
{
base.OnElementChanged(e);
SetNativeControl(new DrawView(RectangleF.Empty));
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == ImageWithTouch.ClearImagePathProperty.PropertyName)
{
Control.Clear();
}
else if (e.PropertyName == ImageWithTouch.SavedImagePathProperty.PropertyName)
{
UIImage curDrawingImage = Control.GetImageFromView();
NSData data = curDrawingImage.AsJPEG();
NSError error = new NSError();
bool bSuccess = data.Save(Element.SavedImagePath, true, out error);
}
else
{
UpdateControl(e.PropertyName == ImageWithTouch.CurrentLineColorProperty.PropertyName ||
e.PropertyName == ImageWithTouch.CurrentImageProperty.PropertyName ||
e.PropertyName == ImageWithTouch.CurrentLineWidthProperty.PropertyName);
}
}
private void UpdateControl(bool bDisplayFlag)
{
Control.CurrentLineColor = Element.CurrentLineColor.ToUIColor();
Control.PenWidth = Element.CurrentLineWidth;
if (Control.ImageFilePath != Element.CurrentImagePath)
{
Control.ImageFilePath = Element.CurrentImagePath;
Control.LoadImageFromFile();
}
if (bDisplayFlag)
{
Control.SetNeedsDisplay();
}
}
}
}
DependencyService to get path to store Image in Local storage
ISign.cs
using System;
namespace SignaturePadDemo
{
public interface ISign
{
string Sign();
}
}
Xamarin.Android:
ISignService.cs
using System;
using SignaturePadDemo.Droid;
using Xamarin.Forms;
[assembly: Dependency(typeof(ISignService))]
namespace SignaturePadDemo.Droid
{
public class ISignService : ISign
{
public string Sign()
{
string savedFileName = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/temp_" + DateTime.Now.ToString("yyyy_mm_dd_hh_mm_ss") + ".jpg";
return savedFileName;
}
}
}
DrawView.cs
using Android.Views;
using Android.Graphics;
using Android.Content;
using System;
namespace SignaturePadDemo.Droid
{
public class DrawView : View
{
public DrawView(Context context)
: base(context)
{
Start();
}
public Color CurrentLineColor { get; set; }
public String ImageFilePath { get; set; }
public float PenWidth { get; set; }
private Path DrawPath;
private Paint DrawPaint;
private Paint CanvasPaint;
private Canvas DrawCanvas;
private Bitmap CanvasBitmap;
private int w, h;
private Bitmap _image;
private void Start()
{
CurrentLineColor = Color.Black;
PenWidth = 5.0f;
DrawPath = new Path();
DrawPaint = new Paint
{
Color = CurrentLineColor,
AntiAlias = true,
StrokeWidth = PenWidth
};
DrawPaint.SetStyle(Paint.Style.Stroke);
DrawPaint.StrokeJoin = Paint.Join.Round;
DrawPaint.StrokeCap = Paint.Cap.Round;
CanvasPaint = new Paint
{
Dither = true
};
}
public void Clear()
{
try
{
DrawPath = new Path();
CanvasBitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888);
DrawCanvas = new Canvas(CanvasBitmap);
DrawCanvas.DrawColor(Color.White, PorterDuff.Mode.Multiply);
CanvasBitmap.EraseColor(Color.Transparent);
DrawPaint = new Paint
{
Color = CurrentLineColor,
AntiAlias = true,
StrokeWidth = PenWidth
};
DrawPaint.SetStyle(Paint.Style.Stroke);
DrawPaint.StrokeJoin = Paint.Join.Round;
DrawPaint.StrokeCap = Paint.Cap.Round;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Invalidate();
}
protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
{
base.OnSizeChanged(w, h, oldw, oldh);
if (w > 0 && h > 0)
{
try
{
CanvasBitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888);
DrawCanvas = new Canvas(CanvasBitmap);
this.w = w;
this.h = h;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
protected override void OnDraw(Canvas canvas)
{
base.OnDraw(canvas);
DrawPaint.Color = CurrentLineColor;
DrawPaint.StrokeWidth = PenWidth;
canvas.DrawBitmap(CanvasBitmap, 0, 0, CanvasPaint);
canvas.DrawPath(DrawPath, DrawPaint);
}
public override bool OnTouchEvent(MotionEvent e)
{
var touchX = e.GetX();
var touchY = e.GetY();
switch (e.Action)
{
case MotionEventActions.Down:
DrawPath.MoveTo(touchX, touchY);
break;
case MotionEventActions.Move:
DrawPath.LineTo(touchX, touchY);
break;
case MotionEventActions.Up:
DrawCanvas.DrawPath(DrawPath, DrawPaint);
DrawPath.Reset();
break;
default:
return false;
}
Invalidate();
return true;
}
public void LoadImageFromFile()
{
if (ImageFilePath != null && ImageFilePath != "")
{
_image = BitmapFactory.DecodeFile(ImageFilePath);
}
}
public Bitmap GetImageFromView()
{
Bitmap tempBitmap = null;
try
{
tempBitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888);
DrawCanvas = new Canvas(tempBitmap);
if (_image != null)
{
DrawPaint.SetStyle(Paint.Style.Fill);
DrawPaint.Color = Color.White;
DrawCanvas.DrawRect(new Rect(0, 0, w, h), DrawPaint);
float scaleX = (float)_image.Width / w;
float scaleY = (float)_image.Height / h;
Rect outRect = new Rect();
int outWidth, outHeight;
if (scaleX > scaleY)
{
outWidth = w;
outHeight = (int)(_image.Height / scaleX);
}
else
{
outWidth = (int)(_image.Width / scaleY);
outHeight = h;
}
outRect.Left = w / 2 - outWidth / 2;
outRect.Top = h / 2 - outHeight / 2;
outRect.Right = w / 2 + outWidth / 2;
outRect.Bottom = h / 2 + outHeight / 2;
DrawCanvas.DrawBitmap(_image, new Rect(0, 0, _image.Width, _image.Height), outRect, DrawPaint);
}
else
{
DrawPaint.SetStyle(Paint.Style.Fill);
DrawPaint.Color = Color.White;
DrawCanvas.DrawRect(new Rect(0, 0, w, h), DrawPaint);
}
DrawPaint.Color = CurrentLineColor;
DrawCanvas.DrawBitmap(CanvasBitmap, 0, 0, CanvasPaint);
DrawCanvas.DrawPath(DrawPath, DrawPaint);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return tempBitmap;
}
}
}
ImageHelper.cs
using Android.Views;
using Android.Graphics;
using Android.Content;
using System;
namespace SignaturePadDemo.Droid
{
public class DrawView : View
{
public DrawView(Context context)
: base(context)
{
Start();
}
public Color CurrentLineColor { get; set; }
public String ImageFilePath { get; set; }
public float PenWidth { get; set; }
private Path DrawPath;
private Paint DrawPaint;
private Paint CanvasPaint;
private Canvas DrawCanvas;
private Bitmap CanvasBitmap;
private int w, h;
private Bitmap _image;
private void Start()
{
CurrentLineColor = Color.Black;
PenWidth = 5.0f;
DrawPath = new Path();
DrawPaint = new Paint
{
Color = CurrentLineColor,
AntiAlias = true,
StrokeWidth = PenWidth
};
DrawPaint.SetStyle(Paint.Style.Stroke);
DrawPaint.StrokeJoin = Paint.Join.Round;
DrawPaint.StrokeCap = Paint.Cap.Round;
CanvasPaint = new Paint
{
Dither = true
};
}
public void Clear()
{
try
{
DrawPath = new Path();
CanvasBitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888);
DrawCanvas = new Canvas(CanvasBitmap);
DrawCanvas.DrawColor(Color.White, PorterDuff.Mode.Multiply);
CanvasBitmap.EraseColor(Color.Transparent);
DrawPaint = new Paint
{
Color = CurrentLineColor,
AntiAlias = true,
StrokeWidth = PenWidth
};
DrawPaint.SetStyle(Paint.Style.Stroke);
DrawPaint.StrokeJoin = Paint.Join.Round;
DrawPaint.StrokeCap = Paint.Cap.Round;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Invalidate();
}
protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
{
base.OnSizeChanged(w, h, oldw, oldh);
if (w > 0 && h > 0)
{
try
{
CanvasBitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888);
DrawCanvas = new Canvas(CanvasBitmap);
this.w = w;
this.h = h;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
protected override void OnDraw(Canvas canvas)
{
base.OnDraw(canvas);
DrawPaint.Color = CurrentLineColor;
DrawPaint.StrokeWidth = PenWidth;
canvas.DrawBitmap(CanvasBitmap, 0, 0, CanvasPaint);
canvas.DrawPath(DrawPath, DrawPaint);
}
public override bool OnTouchEvent(MotionEvent e)
{
var touchX = e.GetX();
var touchY = e.GetY();
switch (e.Action)
{
case MotionEventActions.Down:
DrawPath.MoveTo(touchX, touchY);
break;
case MotionEventActions.Move:
DrawPath.LineTo(touchX, touchY);
break;
case MotionEventActions.Up:
DrawCanvas.DrawPath(DrawPath, DrawPaint);
DrawPath.Reset();
break;
default:
return false;
}
Invalidate();
return true;
}
public void LoadImageFromFile()
{
if (ImageFilePath != null && ImageFilePath != "")
{
_image = BitmapFactory.DecodeFile(ImageFilePath);
}
}
public Bitmap GetImageFromView()
{
Bitmap tempBitmap = null;
try
{
tempBitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888);
DrawCanvas = new Canvas(tempBitmap);
if (_image != null)
{
DrawPaint.SetStyle(Paint.Style.Fill);
DrawPaint.Color = Color.White;
DrawCanvas.DrawRect(new Rect(0, 0, w, h), DrawPaint);
float scaleX = (float)_image.Width / w;
float scaleY = (float)_image.Height / h;
Rect outRect = new Rect();
int outWidth, outHeight;
if (scaleX > scaleY)
{
outWidth = w;
outHeight = (int)(_image.Height / scaleX);
}
else
{
outWidth = (int)(_image.Width / scaleY);
outHeight = h;
}
outRect.Left = w / 2 - outWidth / 2;
outRect.Top = h / 2 - outHeight / 2;
outRect.Right = w / 2 + outWidth / 2;
outRect.Bottom = h / 2 + outHeight / 2;
DrawCanvas.DrawBitmap(_image, new Rect(0, 0, _image.Width, _image.Height), outRect, DrawPaint);
}
else
{
DrawPaint.SetStyle(Paint.Style.Fill);
DrawPaint.Color = Color.White;
DrawCanvas.DrawRect(new Rect(0, 0, w, h), DrawPaint);
}
DrawPaint.Color = CurrentLineColor;
DrawCanvas.DrawBitmap(CanvasBitmap, 0, 0, CanvasPaint);
DrawCanvas.DrawPath(DrawPath, DrawPaint);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return tempBitmap;
}
}
}
Xamarin.iOS:
ISignService.cs
using System;
using SignaturePadDemo.iOS;
using Xamarin.Forms;
[assembly: Dependency(typeof(ISignService))]
namespace SignaturePadDemo.iOS
{
public class ISignService : ISign
{
public string Sign()
{
string savedFileName = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/temp_" + DateTime.Now.ToString("yyyy_mm_dd_hh_mm_ss") + ".jpg";
return savedFileName;
}
}
}
DrawView.cs
using System;
using UIKit;
using System.Drawing;
using CoreGraphics;
using System.Collections.Generic;
using Foundation;
namespace SignaturePadDemo.iOS
{
public class DrawView : UIView
{
public DrawView(RectangleF frame) : base(frame)
{
DrawPath = new CGPath();
CurrentLineColor = UIColor.Black;
PenWidth = 3.0f;
Lines = new List<VESLine>();
}
private PointF PreviousPoint;
private CGPath DrawPath;
private byte IndexCount;
private UIBezierPath CurrentPath;
private List<VESLine> Lines;
public UIColor CurrentLineColor { get; set; }
public String ImageFilePath { get; set; }
public float PenWidth { get; set; }
private UIImage _image = null;
public void Clear()
{
Lines.Clear();
DrawPath.Dispose();
DrawPath = new CGPath();
SetNeedsDisplay();
}
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
IndexCount++;
var path = new UIBezierPath
{
LineWidth = PenWidth
};
var touch = (UITouch)touches.AnyObject;
PreviousPoint = (PointF)touch.PreviousLocationInView(this);
var newPoint = touch.LocationInView(this);
path.MoveTo(newPoint);
InvokeOnMainThread(SetNeedsDisplay);
CurrentPath = path;
var line = new VESLine
{
Path = CurrentPath,
Color = CurrentLineColor,
Index = IndexCount
};
Lines.Add(line);
}
public override void TouchesMoved(NSSet touches, UIEvent evt)
{
var touch = (UITouch)touches.AnyObject;
var currentPoint = touch.LocationInView(this);
if (Math.Abs(currentPoint.X - PreviousPoint.X) >= 4 ||
Math.Abs(currentPoint.Y - PreviousPoint.Y) >= 4)
{
var newPoint = new PointF((float)(currentPoint.X + PreviousPoint.X) / 2, (float)(currentPoint.Y + PreviousPoint.Y) / 2);
CurrentPath.AddQuadCurveToPoint(newPoint, PreviousPoint);
PreviousPoint = (PointF)currentPoint;
}
else
{
CurrentPath.AddLineTo(currentPoint);
}
InvokeOnMainThread(SetNeedsDisplay);
}
public override void TouchesEnded(NSSet touches, UIEvent evt)
{
InvokeOnMainThread(SetNeedsDisplay);
}
public override void TouchesCancelled(NSSet touches, UIEvent evt)
{
InvokeOnMainThread(SetNeedsDisplay);
}
public override void Draw(CGRect rect)
{
foreach (VESLine p in Lines)
{
p.Color.SetStroke();
p.Path.Stroke();
}
}
public void LoadImageFromFile()
{
if (ImageFilePath != null && ImageFilePath != "")
{
_image = ImageHelper.GetRotateImage(ImageFilePath);
}
}
public UIImage GetImageFromView()
{
RectangleF rect;
rect = (RectangleF)Frame;
UIGraphics.BeginImageContext(rect.Size);
CGContext context = UIGraphics.GetCurrentContext();
if (_image != null)
context.DrawImage(Frame, _image.CGImage);
this.Layer.RenderInContext(context);
UIImage image = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
return image;
}
}
}
ImageHelper.cs
using System;
using System.Drawing;
using CoreGraphics;
using Foundation;
using UIKit;
namespace SignaturePadDemo.iOS
{
public class ImageHelper
{
public static UIImage GetRotateImage(String imagePath)
{
UIImage image = UIImage.FromFile(imagePath);
CGImage imgRef = image.CGImage;
float width = imgRef.Width;
float height = imgRef.Height;
CGAffineTransform transform = CGAffineTransform.MakeIdentity();
RectangleF bounds = new RectangleF(0, 0, width, height);
SizeF imageSize = new SizeF(width, height);
float boundHeight;
UIImageOrientation orient = image.Orientation;
switch (orient)
{
case UIImageOrientation.Up:
transform = CGAffineTransform.MakeIdentity();
break;
case UIImageOrientation.UpMirrored:
transform = CGAffineTransform.MakeTranslation(imageSize.Width, 0.0f);
transform.Scale(-1.0f, 1.0f);
break;
case UIImageOrientation.Down:
transform.Rotate((float)Math.PI);
transform.Translate(imageSize.Width, imageSize.Height);
break;
case UIImageOrientation.DownMirrored:
transform = CGAffineTransform.MakeTranslation(0.0f, imageSize.Height);
transform.Scale(1.0f, -1.0f);
break;
case UIImageOrientation.LeftMirrored:
boundHeight = bounds.Size.Height;
bounds.Height = bounds.Size.Width;
bounds.Width = boundHeight;
transform.Scale(-1.0f, 1.0f);
transform.Rotate((float)Math.PI / 2.0f);
break;
case UIImageOrientation.Left:
boundHeight = bounds.Size.Height;
bounds.Height = bounds.Size.Width;
bounds.Width = boundHeight;
transform = CGAffineTransform.MakeRotation((float)Math.PI / 2.0f);
transform.Translate(imageSize.Height, 0.0f);
break;
case UIImageOrientation.RightMirrored:
boundHeight = bounds.Size.Height;
bounds.Height = bounds.Size.Width;
bounds.Width = boundHeight;
transform = CGAffineTransform.MakeTranslation(imageSize.Height, imageSize.Width);
transform.Scale(-1.0f, 1.0f);
transform.Rotate(3.0f * (float)Math.PI / 2.0f);
break;
case UIImageOrientation.Right:
boundHeight = bounds.Size.Height;
bounds.Height = bounds.Size.Width;
bounds.Width = boundHeight;
transform = CGAffineTransform.MakeRotation(-(float)Math.PI / 2.0f);
transform.Translate(0.0f, imageSize.Width);
break;
default:
break;
}
UIGraphics.BeginImageContext(bounds.Size);
CGContext context = UIGraphics.GetCurrentContext();
context.ConcatCTM(transform);
context = UIGraphics.GetCurrentContext();
context.DrawImage(new RectangleF(0, 0, width, height), imgRef);
UIImage imageCopy = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
return imageCopy;
}
public static bool SaveRotateImage(String imagePath)
{
UIImage imageCopy = GetRotateImage(imagePath);
NSData data = imageCopy.AsJPEG();
NSError error = new NSError();
bool bSuccess = data.Save(imagePath, true, out error);
return bSuccess;
}
}
}
VESLine.cs
using System;
using UIKit;
namespace SignaturePadDemo.iOS
{
public class VESLine
{
public UIBezierPath Path
{
get;
set;
}
public UIColor Color
{
get;
set;
}
public byte Index
{
get;
set;
}
}
}
You can download from here

Related

How to add dot icon for TabbedPage strong Xamarin

I'm trying to add a red dot as shown below for TabbedPage. I have searched the internet but it doesn't look like anything. It looks like Badges but it's not. I want to permanently assign it at any time. I also don't know what keywords to search for exactly. For Xamarin iOS and Android.
MainView.xaml
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:views="clr-namespace:Appssss.Views"
x:Class="Appssss.Views.Mainview">
<!--Pages can be added as references or inline-->
<views:AboutPage Title="Page 1" IconImageSource="homeicon" BackgroundColor="#fff"/>
<views:LoginPage Title="Page 2" IconImageSource="feeds" BackgroundColor="#fff"/>
<views:AboutPage Title="Page 3" IconImageSource="moneys" BackgroundColor="#fbfbfb" />
<views:AboutPage Title="Page 4" IconImageSource="chats" BackgroundColor="#fff" />
<views:AboutPage Title="Page 5" IconImageSource="usericon" BackgroundColor="#fff"/>
</TabbedPage>
Ask for help from everyone. Thank you
Update
MyTabDotRenderer.cs
[assembly: ExportRenderer(typeof(TabbedPage), typeof(MyTabDotRenderer))]
namespace XXXX.iOS
{
internal class MyTabDotRenderer : TabbedRenderer
{
private bool _initialized;
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
//TabBar.ClipsToBounds = true;
TabBar.TintColor = UIColor.Gray;
TabBar.BarTintColor = UIColor.White;
TabBar.BackgroundColor = UIColor.White;
MessagingCenter.Subscribe<object, int>(this, "Add", (obj, index) => {
TabBar.addItemBadge(index);
});
MessagingCenter.Subscribe<object, int>(this, "Remove", (obj, index) => {
TabBar.removeItemBadge(index);
});
}
public override void ViewWillAppear(bool animated)
{
if (!_initialized)
{
if (TabBar?.Items == null)
return;
foreach (var item in TabBar.Items)
{
item.Image = ScalingImageToSize(item.Image, new CGSize(20, 20)); // set the size here as you want
}
var tabs = Element as TabbedPage;
if (tabs != null)
{
for (int i = 0; i < TabBar.Items.Length; i++)
{
UpdateItem(TabBar.Items[i], tabs.Children[i].Icon, tabs.Children[i].StyleId);
}
}
_initialized = true;
}
base.ViewWillAppear(animated);
}
private void UpdateItem(UITabBarItem item, string icon, string badgeValue)
{
if (item == null) return;
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
{
//change icon select
if (icon.EndsWith(".png"))
icon = icon.Replace(".png", "_selected.png");
else
icon += "_selected";
item.SelectedImage = UIImage.FromBundle(icon);
item.SelectedImage.AccessibilityIdentifier = icon;
item.SelectedImage = ScalingImageToSize(item.SelectedImage, new CGSize(20, 20)); // set the size here as you want
//change icon select
UITabBarAppearance app = new UITabBarAppearance();
app.ConfigureWithOpaqueBackground();
app.BackgroundColor = UIColor.Clear;
app.StackedLayoutAppearance.Normal.TitleTextAttributes = new UIStringAttributes() { Font = UIFont.FromName("Roboto Medium", 12), ForegroundColor = Color.FromHex("#808080").ToUIColor() };
app.StackedLayoutAppearance.Selected.TitleTextAttributes = new UIStringAttributes() { Font = UIFont.FromName("Roboto Medium", 13), ForegroundColor = Color.FromHex("#00AA13").ToUIColor() };
item.StandardAppearance = app;
if (UIDevice.CurrentDevice.CheckSystemVersion(15, 0))
{
item.ScrollEdgeAppearance = item.StandardAppearance;
}
}
}
public UIImage ScalingImageToSize(UIImage sourceImage, CGSize newSize)
{
if (UIScreen.MainScreen.Scale == 2.0) //#2x iPhone 6 7 8
{
UIGraphics.BeginImageContextWithOptions(newSize, false, 2.0f);
}
else if (UIScreen.MainScreen.Scale == 3.0) //#3x iPhone 6p 7p 8p...
{
UIGraphics.BeginImageContextWithOptions(newSize, false, 3.0f);
}
else
{
UIGraphics.BeginImageContext(newSize);
}
sourceImage.Draw(new CGRect(0, 0, newSize.Width, newSize.Height));
UIImage newImage = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
return newImage;
}
}
public static class TabbarExtensions
{
readonly static int tabBarItemTag = 9999;
public static void addItemBadge(this UITabBar tabbar, int index)
{
if (tabbar.Items != null && tabbar.Items.Length == 0) return;
if (index >= tabbar.Items.Length) return;
removeItemBadge(tabbar, index);
var badgeView = new UIView();
badgeView.Tag = tabBarItemTag + index;
badgeView.Layer.CornerRadius = 5;
badgeView.BackgroundColor = UIColor.Red;
var tabFrame = tabbar.Frame;
var percentX = (index + 0.56) / tabbar.Items.Length;
var x = percentX * tabFrame.Width;
var y = tabFrame.Height * 0.1;
badgeView.Frame = new CoreGraphics.CGRect(x, y, 10, 10);
tabbar.AddSubview(badgeView);
}
public static bool removeItemBadge(this UITabBar tabbar, int index)
{
foreach (var subView in tabbar.Subviews)
{
if (subView.Tag == tabBarItemTag + index)
{
subView.RemoveFromSuperview();
return true;
}
}
return false;
}
}
}
MainView.xaml.cs
public MainView(int index)
{
NavigationPage.SetHasNavigationBar(this, false);
InitializeComponent();
On<Xamarin.Forms.PlatformConfiguration.Android>().SetToolbarPlacement(ToolbarPlacement.Bottom);
SetPage(index);
MessagingCenter.Send<object, int>(this, "Add", 1);
}
void SetPage(int index)
{
CurrentPage = Children[index];
}
Update 2
CustomTabbedPageRenderer.cs in project Android
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Appssss.Droid.Renderers;
using Google.Android.Material.BottomNavigation;
using Google.Android.Material.Tabs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Xamarin.Forms.Platform.Android.AppCompat;
using static Google.Android.Material.BottomNavigation.BottomNavigationView;
using static Google.Android.Material.Tabs.TabLayout;
using View = Android.Views.View;
[assembly: ExportRenderer(typeof(TabbedPage), typeof(CustomTabbedPageRenderer))]
namespace Appssss.Droid.Renderers
{
public class CustomTabbedPageRenderer : TabbedPageRenderer
{
TabbedPage tabbedPage;
List<Android.Views.View> list = new List<Android.Views.View>();
public CustomTabbedPageRenderer(Context context) : base(context)
{
for (int i = 0; i < 5; i++)
{
Android.Views.View view = LayoutInflater.From(Context).Inflate(Resource.Drawable.MyView, null);
list.Add(view);
}
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.TabbedPage> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || e.OldElement != null)
return;
if (e.NewElement != null)
{
MessagingCenter.Subscribe<object, int>(this, "Add", (obj, index) => {
TabLayout tablayout = (TabLayout)ViewGroup.GetChildAt(1);
ViewGroup vgroup = (ViewGroup)tablayout.GetChildAt(0);
for (int i = 0; i < vgroup.ChildCount; i++)
{
if (index == i)
{
var view = list[i];
if (view.Parent != null) break;
ViewGroup vvgroup = (ViewGroup)vgroup.GetChildAt(i);
MarginLayoutParams layout1 = new MarginLayoutParams(MarginLayoutParams.MatchParent, MarginLayoutParams.MatchParent);
layout1.SetMargins(120, -135, 0, 0);
vvgroup.AddView(view, layout1);
var ll = vvgroup.LayoutParameters as LinearLayout.LayoutParams;
ll.SetMargins(0, 30, 0, 0);
vvgroup.LayoutParameters = ll;
}
}
});
MessagingCenter.Subscribe<object, int>(this, "Remove", (obj, index) => {
TabLayout tablayout = (TabLayout)ViewGroup.GetChildAt(1);
ViewGroup vgroup = (ViewGroup)tablayout.GetChildAt(0);
for (int i = 0; i < vgroup.ChildCount; i++)
{
if (index == i)
{
var view = list[i];
if (view.Parent == null) break;
ViewGroup vvgroup = (ViewGroup)vgroup.GetChildAt(i);
vvgroup.RemoveView(view);
var ll = vvgroup.LayoutParameters as LinearLayout.LayoutParams;
ll.SetMargins(0, 0, 0, 0);
vvgroup.LayoutParameters = ll;
}
}
});
IEnumerable<View> children = GetAllChildViews(ViewGroup);
BottomNavigationView bottomNavBar = (BottomNavigationView)children.SingleOrDefault(view => view is BottomNavigationView);
if (bottomNavBar != null)
{
tabbedPage = e.NewElement;
bottomNavBar.NavigationItemSelected += BottomNavBar_NavigationItemSelected;
bottomNavBar.Menu.GetItem(0).SetIcon(Resource.Drawable.homeiconselect);
}
}
}
int previous;
private void BottomNavBar_NavigationItemSelected(object sender, BottomNavigationView.NavigationItemSelectedEventArgs e)
{
var current = e.Item.ItemId;
switch (e.Item.ToString())
{
case "Page 1":
e.Item.SetIcon(Resource.Drawable.homeiconselect);
break;
case "Page 2":
e.Item.SetIcon(Resource.Drawable.feedsselect);
break;
case "Page 3":
e.Item.SetIcon(Resource.Drawable.moneysselect);
break;
case "Page 4":
e.Item.SetIcon(Resource.Drawable.chatsselect);
break;
case "Page 5":
e.Item.SetIcon(Resource.Drawable.usericonselect);
break;
default:
break;
}
var previousView = sender as BottomNavigationView;
IMenu menu = previousView.Menu;
var previousItem = menu.GetItem(previous);
if (previous != current)
{
if (previousItem.IsChecked)
{
switch (previousItem.ToString())
{
case "Page 1":
previousItem.SetIcon(Resource.Drawable.homeicon);
break;
case "Page 2":
previousItem.SetIcon(Resource.Drawable.feeds);
break;
case "Page 3":
previousItem.SetIcon(Resource.Drawable.moneys);
break;
case "Page 4":
previousItem.SetIcon(Resource.Drawable.chats);
break;
case "Page 5":
previousItem.SetIcon(Resource.Drawable.usericon);
break;
default:
break;
}
}
}
tabbedPage.CurrentPage = tabbedPage.Children[current];
previous = current;
}
private IEnumerable<View> GetAllChildViews(View view)
{
if (!(view is ViewGroup group))
return new List<View> { view };
List<View> result = new List<View>();
int childCount = group.ChildCount;
for (int i = 0; i < childCount; i++)
{
View child = group.GetChildAt(i);
List<View> childList = new List<View> { child };
childList.AddRange(GetAllChildViews(child));
result.AddRange(childList);
}
return result.Distinct();
}
}
}
MainView.xaml.cs
public Mainview(int index)
{
InitializeComponent();
On<Xamarin.Forms.PlatformConfiguration.Android>().SetToolbarPlacement(ToolbarPlacement.Bottom);
SetPage(index);
}
void SetPage(int index)
{
CurrentPage = Children[index];
}
protected override void OnAppearing()
{
base.OnAppearing();
MessagingCenter.Send<object, int>(this, "Add", 1);
///On iOS this works fine
}
Add a xml in drawable folder called MyView.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button android:background="#drawable/Reddot"
android:layout_width="12dp"
android:layout_height="12dp"/>
</LinearLayout>
Add a xml in drawable folder called Reddot.xml
<?xml version="1.0" encoding="utf-8" ?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#ff0000"/>
<size android:width="12dp" android:height="12dp"/>
</shape>
It could be achieved by creating a custom renderer for TabbedPage .
iOS Solution
[assembly: ExportRenderer(typeof(TabbedPage), typeof(MyTabRenderer))]
namespace MyForms.iOS
{
internal class MyTabRenderer : TabbedRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
MessagingCenter.Subscribe<object, int>(this, "Add", (obj,index) => {
TabBar.addItemBadge(index);
});
MessagingCenter.Subscribe<object, int>(this, "Remove", (obj,index) => {
TabBar.removeItemBadge(index);
});
}
}
public static class TabbarExtensions
{
readonly static int tabBarItemTag = 9999;
public static void addItemBadge(this UITabBar tabbar, int index)
{
if (tabbar.Items != null && tabbar.Items.Length == 0) return;
if (index >= tabbar.Items.Length) return;
removeItemBadge(tabbar, index);
var badgeView = new UIView();
badgeView.Tag = tabBarItemTag + index;
badgeView.Layer.CornerRadius = 5;
badgeView.BackgroundColor = UIColor.Red;
var tabFrame = tabbar.Frame;
var percentX = (index + 0.56) / tabbar.Items.Length;
var x = percentX * tabFrame.Width;
var y = tabFrame.Height * 0.1;
badgeView.Frame = new CoreGraphics.CGRect(x, y, 10, 10);
tabbar.AddSubview(badgeView);
}
public static bool removeItemBadge(this UITabBar tabbar, int index)
{
foreach(var subView in tabbar.Subviews)
{
if(subView.Tag == tabBarItemTag + index)
{
subView.RemoveFromSuperview();
return true;
}
}
return false;
}
}
}
Android Solution
Add a xml in drawable folder called Reddot.xml .
<?xml version="1.0" encoding="utf-8" ?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#ff0000"/>
<size android:width="12dp" android:height="12dp"/>
</shape>
Add a xml in drawable folder called MyView.xml .
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button android:background="#drawable/Reddot"
android:layout_width="12dp"
android:layout_height="12dp"/>
</LinearLayout>
3.custom renderer
[assembly: ExportRenderer(typeof(TabbedPage), typeof(CustomTabbedPageRenderer))]
namespace MyForms.Droid
{
public class CustomTabbedPageRenderer : TabbedPageRenderer
{
List<Android.Views.View> list = new List<Android.Views.View>();
public CustomTabbedPageRenderer(Context context) : base(context)
{
for (int i = 0; i < 3;i++)
{
Android.Views.View view = LayoutInflater.From(Context).Inflate(Resource.Drawable.MyView, null);
list.Add(view);
}
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.TabbedPage> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || e.OldElement != null)
return;
MessagingCenter.Subscribe<object, int>(this, "Add", (obj, index) => {
TabLayout tablayout = (TabLayout)ViewGroup.GetChildAt(1);
ViewGroup vgroup = (ViewGroup)tablayout.GetChildAt(0);
for (int i = 0; i < vgroup.ChildCount; i++)
{
if(index == i)
{
var view = list[i];
if (view.Parent != null) break;
ViewGroup vvgroup = (ViewGroup)vgroup.GetChildAt(i);
MarginLayoutParams layout1 = new MarginLayoutParams(MarginLayoutParams.MatchParent, MarginLayoutParams.MatchParent);
layout1.SetMargins(120, -135, 0, 0);
vvgroup.AddView(view,layout1);
var ll = vvgroup.LayoutParameters as LinearLayout.LayoutParams;
ll.SetMargins(0, 30, 0,0 );
vvgroup.LayoutParameters = ll;
}
}
});
MessagingCenter.Subscribe<object, int>(this, "Remove", (obj, index) => {
TabLayout tablayout = (TabLayout)ViewGroup.GetChildAt(1);
ViewGroup vgroup = (ViewGroup)tablayout.GetChildAt(0);
for (int i = 0; i < vgroup.ChildCount; i++)
{
if(index == i)
{
var view = list[i];
if (view.Parent == null) break;
ViewGroup vvgroup = (ViewGroup)vgroup.GetChildAt(i);
vvgroup.RemoveView(view);
var ll = vvgroup.LayoutParameters as LinearLayout.LayoutParams;
ll.SetMargins(0, 0, 0, 0);
vvgroup.LayoutParameters = ll;
}
}
});
}
}
}
Usage in Forms project
//add red dot
MessagingCenter.Send<object, int>(this, "Add", 1);
//remove red dot
MessagingCenter.Send<object, int>(this, "Remove", 1);
Screen shot

Video Recorder inside a app using xamarin forms

How can I implement a video recorder inside an application using xamarin forms?
You can use Xamarin.Plugin.Media. If it is not flexible enough for you, then you need to implement the video recorder using the native APIs. In which case you probably shouldn't be doing Xamarin.Forms at all, but if you insist that is the way.
Do you want to achieve the result like following GIF(Over 2M, SO cannot allow upload it )?
https://imgur.com/a/kpMl2ed
I achieve it in android by custom rendere. Here is code.
First of all, we should MediaRecorder to record the video.
public void startRecord(SurfaceView surfaceView)
{
Device.BeginInvokeOnMainThread(() => {
string path = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath + "/test.mp4";
recorder = new MediaRecorder();
//If you want to rotate the video screen, you can use following code
//Camera camera = Camera.Open();
//Camera.Parameters parameters = camera.GetParameters();
//parameters.SetPreviewSize(640, 480);
//parameters.SetPictureSize(640, 480);
//camera.SetParameters(parameters);
//camera.SetDisplayOrientation(90);
//camera.Unlock();
//recorder.SetCamera(camera);
recorder.SetVideoSource(VideoSource.Camera);
recorder.SetAudioSource(AudioSource.Mic);
recorder.SetOutputFormat(OutputFormat.Default);
recorder.SetVideoEncoder(VideoEncoder.Default);
recorder.SetAudioEncoder(AudioEncoder.Default);
recorder.SetOutputFile(path);
recorder.SetPreviewDisplay(surfaceView.Holder.Surface);
recorder.Prepare();
recorder.Start();
});
}
If you want to achieve the record the video inside the application, you should use custom renderer to view. Use SurfaceView to see the camera view.First of all. please create a CameraPreview
public sealed class CameraPreview : ViewGroup, ISurfaceHolderCallback
{
public SurfaceView surfaceView;
ISurfaceHolder holder;
Camera.Size previewSize;
IList<Camera.Size> supportedPreviewSizes;
Camera camera;
IWindowManager windowManager;
MediaRecorder recorder;
public bool IsPreviewing { get; set; }
public Camera Preview {
get { return camera; }
set {
camera = value;
if (camera != null) {
supportedPreviewSizes = Preview.GetParameters().SupportedPreviewSizes;
RequestLayout();
}
}
}
public CameraPreview (Context context)
: base (context)
{
surfaceView = new SurfaceView (context);
AddView (surfaceView);
windowManager = Context.GetSystemService (Context.WindowService).JavaCast<IWindowManager>();
IsPreviewing = false;
holder = surfaceView.Holder;
holder.AddCallback (this);
MessagingCenter.Subscribe<string>("111", "Hi", (expense) =>
{
startRecord(surfaceView);
});
MessagingCenter.Subscribe<string>("1112", "Hi2", (expense) =>
{
stopRecord(surfaceView);
});
}
private void stopRecord(SurfaceView surfaceView)
{
recorder.Stop();
recorder.Release();
}
public void startRecord(SurfaceView surfaceView)
{
Device.BeginInvokeOnMainThread(() => {
string path = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath + "/test.mp4";
recorder = new MediaRecorder();
//If you want to rotate the video screen, you can use following code
//Camera camera = Camera.Open();
//Camera.Parameters parameters = camera.GetParameters();
//parameters.SetPreviewSize(640, 480);
//parameters.SetPictureSize(640, 480);
//camera.SetParameters(parameters);
//camera.SetDisplayOrientation(90);
//camera.Unlock();
//recorder.SetCamera(camera);
recorder.SetVideoSource(VideoSource.Camera);
recorder.SetAudioSource(AudioSource.Mic);
recorder.SetOutputFormat(OutputFormat.Default);
recorder.SetVideoEncoder(VideoEncoder.Default);
recorder.SetAudioEncoder(AudioEncoder.Default);
recorder.SetOutputFile(path);
recorder.SetPreviewDisplay(surfaceView.Holder.Surface);
recorder.Prepare();
recorder.Start();
});
}
protected override void OnMeasure (int widthMeasureSpec, int heightMeasureSpec)
{
int width = ResolveSize (SuggestedMinimumWidth, widthMeasureSpec);
int height = ResolveSize (SuggestedMinimumHeight, heightMeasureSpec);
SetMeasuredDimension (width, height);
if (supportedPreviewSizes != null) {
previewSize = GetOptimalPreviewSize (supportedPreviewSizes, width, height);
}
}
protected override void OnLayout (bool changed, int l, int t, int r, int b)
{
var msw = MeasureSpec.MakeMeasureSpec (r - l, MeasureSpecMode.Exactly);
var msh = MeasureSpec.MakeMeasureSpec (b - t, MeasureSpecMode.Exactly);
surfaceView.Measure (msw, msh);
surfaceView.Layout (0, 0, r - l, b - t);
}
public void SurfaceCreated (ISurfaceHolder holder)
{
try {
if (Preview != null) {
Preview.SetPreviewDisplay (holder);
}
} catch (Exception ex) {
System.Diagnostics.Debug.WriteLine (#" ERROR: ", ex.Message);
}
}
public void SurfaceDestroyed (ISurfaceHolder holder)
{
if (Preview != null) {
Preview.StopPreview ();
}
}
public void SurfaceChanged (ISurfaceHolder holder, Android.Graphics.Format format, int width, int height)
{
var parameters = Preview.GetParameters ();
parameters.SetPreviewSize (previewSize.Width, previewSize.Height);
RequestLayout ();
//If you want to rotate the video screen, you can use following code
//switch (windowManager.DefaultDisplay.Rotation) {
//case SurfaceOrientation.Rotation0:
// camera.SetDisplayOrientation (90);
// break;
//case SurfaceOrientation.Rotation90:
// camera.SetDisplayOrientation (0);
// break;
//case SurfaceOrientation.Rotation270:
// camera.SetDisplayOrientation (180);
// break;
//}
Preview.SetParameters (parameters);
Preview.StartPreview ();
IsPreviewing = true;
}
Camera.Size GetOptimalPreviewSize (IList<Camera.Size> sizes, int w, int h)
{
const double AspectTolerance = 0.1;
double targetRatio = (double)w / h;
if (sizes == null) {
return null;
}
Camera.Size optimalSize = null;
double minDiff = double.MaxValue;
int targetHeight = h;
foreach (Camera.Size size in sizes) {
double ratio = (double)size.Width / size.Height;
if (Math.Abs (ratio - targetRatio) > AspectTolerance)
continue;
if (Math.Abs (size.Height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.Abs (size.Height - targetHeight);
}
}
if (optimalSize == null) {
minDiff = double.MaxValue;
foreach (Camera.Size size in sizes) {
if (Math.Abs (size.Height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.Abs (size.Height - targetHeight);
}
}
}
return optimalSize;
}
}
Then, Here is custom renderer.
[assembly: ExportRenderer(typeof(CustomRenderer.CameraPreview), typeof(CameraPreviewRenderer))]
namespace CustomRenderer.Droid
{
public class CameraPreviewRenderer : ViewRenderer
{
CameraPreview cameraPreview;
public CameraPreviewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<CustomRenderer.CameraPreview> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
// Unsubscribe
cameraPreview.Click -= OnCameraPreviewClicked;
}
if (e.NewElement != null)
{
if (Control == null)
{
cameraPreview = new CameraPreview(Context);
SetNativeControl(cameraPreview);
}
Control.Preview = Camera.Open((int)e.NewElement.Camera);
// Subscribe
cameraPreview.Click += OnCameraPreviewClicked;
}
}
void OnCameraPreviewClicked(object sender, EventArgs e)
{
if (cameraPreview.IsPreviewing)
{
cameraPreview.Preview.StopPreview();
cameraPreview.IsPreviewing = false;
}
else
{
cameraPreview.Preview.StartPreview();
cameraPreview.IsPreviewing = true;
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Control.Preview.Release();
}
base.Dispose(disposing);
}
}
}
If you want to know how to achieve Camera Preview in the IOS or UWP, you can refer to this link.
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/custom-renderer/view
Here is code in github.
https://github.com/851265601/FormsRecordVideoInside
Above way to achieve it, it a bit complexed. I suggest you to use the Xam.Plugin.Media to achieve it. Here is running GIF. it cannot see the video preview in the application, but you do not need to use custom renderer to achieve it just serveral lines code.
private async void PlayStopButtonText_Clicked(object sender, EventArgs e)
{
// throw new NotImplementedException();
var file = await CrossMedia.Current.TakeVideoAsync(new StoreVideoOptions
{
SaveToAlbum = true,Directory = "Sample"
});
//Get the public album path
if (file == null)
return;
await DisplayAlert("File Location", file.AlbumPath, "OK");
}
In the android, add the following permission.
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Here is running GIF(Over 2M, SO cannot allow upload it ).
https://imgur.com/a/9WE4szZ

Buttons inside infowindow doesn't get clicked - Xamarin forms

Hi everyone, i had to create custom info-window in xamarin forms as shown in the screenshot above. I have created a custom renderer for that but the problem i am having right now is that buttons are not getting clicked. Xamarin is taking entire info-window as a clickable view. Please guide me what i am doing wrong or is it possible to achieve button clicks in Xamarin forms. Thanks in advance.
Here is the code for custom renderer:
using System;
using System.Collections.Generic;
using Android.Content;
using Android.Gms.Maps;
using Android.Gms.Maps.Model;
using Xamarin.Forms;
using Xamarin.Forms.Maps;
using Xamarin.Forms.Maps.Android;
using SalesApp.Droid.CustomRenderer;
using Android.Widget;
using SalesApp.CustomControls;
using SalesApp.Droid.Listeners;
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace SalesApp.Droid.CustomRenderer
{
public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter, IOnMapReadyCallback
{
GoogleMap map;
List<CustomPin> customPins;
bool isDrawn;
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
map.InfoWindowClick -= OnInfoWindowClick;
}
if (e.NewElement != null)
{
var formsMap = (CustomMap)e.NewElement;
customPins = formsMap.CustomPins
Control.GetMapAsync(this);
}
}
public void OnMapReady(GoogleMap googleMap)
{
map = googleMap;
map.InfoWindowClick += OnInfoWindowClick;
map.SetInfoWindowAdapter(this);
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals("VisibleRegion") && !isDrawn)
{
map.Clear();
if (customPins != null)
{
foreach (var pin in customPins)
{
var marker = new MarkerOptions();
marker.SetPosition(new LatLng(pin.Pin.Position.Latitude, pin.Pin.Position.Longitude));
marker.SetTitle(pin.Pin.Label);
marker.SetSnippet(pin.Pin.Address);
marker.SetIcon(BitmapDescriptorFactory.FromResource((int)typeof(Resource.Drawable).GetField(pin.Image).GetValue(null)));
map.AddMarker(marker);
}
isDrawn = true;
}
}
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
if (changed)
{
isDrawn = false;
}
}
void OnInfoWindowClick(object sender, GoogleMap.InfoWindowClickEventArgs e)
{
var customPin = GetCustomPin(e.Marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
//if (!string.IsNullOrWhiteSpace(customPin.Url))
//{
// var url = Android.Net.Uri.Parse(customPin.Url);
// var intent = new Intent(Intent.ActionView, url);
// intent.AddFlags(ActivityFlags.NewTask);
// Android.App.Application.Context.StartActivity(intent);
//}
Android.App.Application.Context.StartActivity(new Intent(Android.App.Application.Context, typeof(DialogActivity)));
}
void IOnMapReadyCallback.OnMapReady(GoogleMap googleMap)
{
InvokeOnMapReadyBaseClassHack(googleMap);
map = googleMap;
map.SetInfoWindowAdapter(this);
map.InfoWindowClick += OnInfoWindowClick;
}
public Android.Views.View GetInfoContents(Marker marker)
{
var inflater = Android.App.Application.Context.GetSystemService(Context.LayoutInflaterService) as Android.Views.LayoutInflater;
if (inflater != null)
{
Android.Views.View view;
var customPin = GetCustomPin(marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
if (customPin.Id == "Xamarin")
{
view = inflater.Inflate(Resource.Layout.XamarinMapInfoWindow, null);
}
else
{
view = inflater.Inflate(Resource.Layout.MapInfoWindow, null);
}
var infoTitle = view.FindViewById<TextView>(Resource.Id.InfoWindowTitle);
var address = view.FindViewById<TextView>(Resource.Id.Address);
var contactPerson = view.FindViewById<TextView>(Resource.Id.ContactPerson);
var phone = view.FindViewById<TextView>(Resource.Id.Phone);
var zip = view.FindViewById<TextView>(Resource.Id.Zip);
var email = view.FindViewById<TextView>(Resource.Id.Email);
var cvr = view.FindViewById<TextView>(Resource.Id.CVR);
var turnover = view.FindViewById<TextView>(Resource.Id.Turnover);
var noOfEmp = view.FindViewById<TextView>(Resource.Id.NoOfEmp);
var leadStatus = view.FindViewById<TextView>(Resource.Id.LeadStatus);
var category = view.FindViewById<TextView>(Resource.Id.Cat);
if (infoTitle != null)
{
infoTitle.Text = customPin.LeedsAndCustomersData.FullName;
}
if (address != null)
{
address.Text = customPin.LeedsAndCustomersData.Address + " " + customPin.LeedsAndCustomersData.CityName;
}
if (contactPerson != null)
{
contactPerson.Text = customPin.LeedsAndCustomersData.FirstName;
}
if (phone != null)
{
phone.Text = customPin.LeedsAndCustomersData.Phone;
}
if (zip != null)
{
zip.Text = customPin.LeedsAndCustomersData.ZipCode;
}
if (email != null)
{
email.Text = customPin.LeedsAndCustomersData.Email;
}
if (cvr != null)
{
cvr.Text = customPin.LeedsAndCustomersData.CVR;
}
if (turnover != null)
{
turnover.Text = customPin.LeedsAndCustomersData.Turnover;
}
if (noOfEmp != null)
{
noOfEmp.Text = customPin.LeedsAndCustomersData.NoOfEmployees.ToString();
}
if (leadStatus != null)
{
leadStatus.Text = customPin.LeedsAndCustomersData.LeadStatus;
}
if (category != null)
{
category.Text = customPin.LeedsAndCustomersData.BusinessType;
}
//add listeners
OnTouchPhoneListener callButtonListener = new OnTouchPhoneListener();
phone.SetOnTouchListener(callButtonListener);
return view;
}
return null;
}
public Android.Views.View GetInfoWindow(Marker marker)
{
return null;
}
CustomPin GetCustomPin(Marker annotation)
{
var position = new Position(annotation.Position.Latitude, annotation.Position.Longitude);
foreach (var pin in customPins)
{
if (pin.Pin.Position == position)
{
return pin;
}
}
return null;
}
void InvokeOnMapReadyBaseClassHack(GoogleMap googleMap)
{
System.Reflection.MethodInfo onMapReadyMethodInfo = null;
Type baseType = typeof(MapRenderer);
foreach (var currentMethod in baseType.GetMethods(System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.DeclaredOnly))
{
if (currentMethod.IsFinal && currentMethod.IsPrivate)
{
if (string.Equals(currentMethod.Name, "OnMapReady", StringComparison.Ordinal))
{
onMapReadyMethodInfo = currentMethod;
break;
}
if (currentMethod.Name.EndsWith(".OnMapReady", StringComparison.Ordinal))
{
onMapReadyMethodInfo = currentMethod;
break;
}
}
}
if (onMapReadyMethodInfo != null)
{
onMapReadyMethodInfo.Invoke(this, new[] { googleMap });
}
}
}
}
OnInfoWindowElemTouchListener:
using Android.OS;
using Android.Views;
using Android.Widget;
using System.Threading.Tasks;
using Android.Gms.Maps.Model;
using Android.Graphics.Drawables;
using Java.Lang;
namespace SalesApp.Droid.Listeners
{
public abstract class OnInfoWindowElemTouchListener : Java.Lang.Object, View.IOnTouchListener
{
private View view;
private Drawable bgDrawableNormal;
private Drawable bgDrawablePressed;
private Handler handler = new Handler();
private Marker marker;
private static bool endPressStatus = false;
private bool pressed = false;
//public OnInfoWindowElemTouchListener(View view, Drawable bgDrawableNormal, Drawable bgDrawablePressed)
//{
// this.view = this.view;
// this.bgDrawableNormal = this.bgDrawableNormal;
// this.bgDrawablePressed = this.bgDrawablePressed;
//}
public OnInfoWindowElemTouchListener(View view)
{
this.view = this.view;
}
public OnInfoWindowElemTouchListener(Button button)
{
}
public OnInfoWindowElemTouchListener()
{
}
public void setMarker(Marker marker)
{
this.marker = this.marker;
}
/*public bool OnTouch(View v, MotionEvent e)
{
if (e.Action == MotionEventActions.Down)
{
// do stuff
return true;
}
if (e.Action == MotionEventActions.Up)
{
// do other stuff
return true;
}
return false;
}*/
public bool OnTouch(View vv, MotionEvent e)
{
if (0 <= e.GetX() && e.GetX() <= vv.Width && 0 <= e.GetY() && e.GetY() <= vv.Height)
{
switch (e.ActionMasked)
{
case MotionEventActions.Down:
startPress();
break;
// We need to delay releasing of the view a little so it shows the
// pressed state on the screen
case MotionEventActions.Up:
//handler.PostDelayed(ConfirmClickRunnable, 150);
//Task.Factory.StartNew(() =>onClickConfirmed(view, marker));
Task.Factory.StartNew(() => onClickConfirmed());
Task.Delay(150);
break;
case MotionEventActions.Cancel:
endPress();
break;
default:
break;
}
}
else
{
// If the touch goes outside of the view's area
// (like when moving finger out of the pressed button)
// just release the press
endPress();
}
return false;
}
private void startPress()
{
if (!pressed)
{
pressed = true;
//handler.RemoveCallbacks(ConfirmClickRunnable);
if ((marker != null))
{
marker.ShowInfoWindow();
}
}
}
public bool endPress()
{
if (pressed)
{
this.pressed = false;
//handler.RemoveCallbacks(ConfirmClickRunnable);
view.SetBackgroundColor(Android.Graphics.Color.Green);
if ((marker != null))
{
marker.ShowInfoWindow();
}
endPressStatus = true;
return true;
}
else
{
endPressStatus = false;
return false;
}
}
//private Runnable confirmClickRunnable = new RunnableAnonymousInnerClassHelper(this);
private Runnable ConfirmClickRunnable = new Java.Lang.Runnable(() =>
{
if (endPressStatus)
{
//onClickConfirmed(view, marker);
}
});
/*private class RunnableAnonymousInnerClassHelper : Java.Lang.Object, Java.Lang.IRunnable
{
private readonly Context outerInstance;
public RunnableAnonymousInnerClassHelper(Context outerInstance)
{
this.outerInstance = outerInstance;
}
public void Run()
{
if (endPressStatus)
{
onClickConfirmed();
}
}
}*/
//public abstract void onClickConfirmed(View v, Marker marker);
public abstract void onClickConfirmed();
}
}
OnTouchPhoneListener:
using Android.Widget;
using System;
namespace SalesApp.Droid.Listeners
{
public class OnTouchPhoneListener : OnInfoWindowElemTouchListener
//public class OnTouchPhoneListener
{
Button button;
public OnTouchPhoneListener(Button button)
{
}
public OnTouchPhoneListener()
{
}
public override void onClickConfirmed() {
Console.WriteLine("call Button Clicked");
}
}
}
Why is the info window implemented in your custom renderer? This makes the class CustomMapRenderer do (at least) do two things, violate the SRP and makes the code way harder to understand.
Instead I'd go with the custom renderer just for the map. Then in Xamarin.Forms you can implement the overlay with Xamarin.Forms means (e.g. the map view in an absolute or relative layout and the overlay in the same layout but a reduced size). Please see the following pseudo-XAML
<ContentPage [...]>
<AbsoluteLayout>
<local:MapView x:Name="MapView" AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds="0,0,1,1" />
<local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" />
</AbsoluteLayout>
</ContentPage>
Then you can introduce a viewmodel PinInfoViewModel which is exposed by MapView.SelectedPinInfo and can be set as MapOverlay.BindingContext
<local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" BindingContext="{Binding Source={x:Reference MapView}, Path=SelectedPinInfo}" />
MapOverlay - in turn - binds to all the properties of PinInfoViewModel. Last thing we'll have to do is making the overlay invisible if no pin is selected. For this purpose we expose MapView.IsPinSelected and bind MapOverlay.IsVisible to it
<local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" BindingContext="{Binding Source={x:Reference MapView}, Path=SelectedPinInfo}" IsVisible="{Binding Source=MapView, Path=IsPinSelected}" />
While up to this point we have not done anything to solve your issue, you can now implement the overlay way simpler, e.g. with a StackLayout. And you can bind the Buttons commands to do whatever you'd like to do.

Round Corner Gridlayout Xamarin Android

I want to Create RoundCorner GridLayout.I have added round corner to Gridlayot Control in Xamarin but Gridlayout BackgroundColor display in Back Side like this.Please help me to resolve issue.
GridLayout after Drawable Add
Xamarin.Forms Xaml Code :
xmlns:local="clr-namespace:XamainControl.Renderer;assembly=XamainKcsControl"
<Grid BackgroundColor="Purple" WidthRequest="300">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<local:KcsGrid Grid.Row="0"
BorderColor="Blue"
BackgroundColor="Yellow"
BorderRadius="20"
BorderWidth="10" Padding="15" >
<Label Text="Customization Page"
FontSize="25"
TextColor="Red"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"/>
</local:KcsGrid>
</Grid>
Custom Grid Property Create Xamarin.Forms
public class KcsGrid : Grid
{
#region Constructor
public KcsGrid() : base()
{
this.Margin = new Thickness(0, 0, 0, -6);
}
#endregion
#region Properties
public static BindableProperty BorderColorProperty = BindableProperty.Create<KcsGrid, Color>(o => o.BorderColor, Color.Transparent);
public Color BorderColor
{
get { return (Color)GetValue(BorderColorProperty); }
set { SetValue(BorderColorProperty, value); }
}
public static BindableProperty BorderWidthProperty = BindableProperty.Create<KcsGrid, float>(o => o.BorderWidth, 0);
public float BorderWidth
{
get { return (float)GetValue(BorderWidthProperty); }
set { SetValue(BorderWidthProperty, value); }
}
public static BindableProperty BorderRadiusProperty = BindableProperty.Create<KcsGrid, float>(o => o.BorderRadius, 0);
public float BorderRadius
{
get { return (float)GetValue(BorderRadiusProperty); }
set { SetValue(BorderRadiusProperty, value); }
}
public static BindableProperty WidthValueProperty = BindableProperty.Create<KcsGrid, int>(o => o.WidthValue,100);
public int WidthValue
{
get { return (int)GetValue(WidthValueProperty); }
set { SetValue(WidthValueProperty, value); }
}
public static BindableProperty HeightValueProperty = BindableProperty.Create<KcsGrid, int>(o => o.HeightValue,50);
public int HeightValue
{
get { return (int)GetValue(HeightValueProperty); }
set { SetValue(HeightValueProperty, value); }
}
#endregion
}
Xamarin.Android Code :
namespace XamainKcsControl.Droid.Renderer
{
public class CustomGridRender : ViewRenderer<KcsGrid,GridLayout>
{
private BorderRenderer _renderer;
GridLayout Cusgd;
protected override void OnElementChanged(ElementChangedEventArgs<KcsGrid> e)
{
base.OnElementChanged(e);
if (e.OldElement != null || this.Element == null)
return;
Cusgd = new GridLayout(Context);
if (Control == null)
{
var curgd = Element as KcsGrid;
SetNativeControl(Cusgd);
UpdateBackground(curgd);
//UpdateMeasurelayout(curgd);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (Element == null)
return;
var Gd = Element as KcsGrid;
if (e.PropertyName == KcsGrid.BorderColorProperty.PropertyName || e.PropertyName == KcsGrid.BorderWidthProperty.PropertyName || e.PropertyName == KcsGrid.BorderRadiusProperty.PropertyName)
{
UpdateBackground(Gd);
}
if (e.PropertyName == KcsGrid.WidthValueProperty.PropertyName || e.PropertyName == KcsGrid.HeightValueProperty.PropertyName)
{
UpdateMeasurelayout(Gd);
}
//SetNativeControl(Cusgd);
}
void UpdateMeasurelayout(KcsGrid Gd)
{
GridLayout.LayoutParams Gdparm = new GridLayout.LayoutParams();
Gdparm.Width = (int)Forms.Context.ToPixels(Gd.WidthValue);
Gdparm.Height = (int)Forms.Context.ToPixels(Gd.HeightValue);
Cusgd.LayoutParameters = Gdparm;
//SetNativeControl(Cusgd);
}
void UpdateBackground(KcsGrid Gd)
{
if (_renderer != null)
{
_renderer.Dispose();
_renderer = null;
}
_renderer = new BorderRenderer();
Cusgd.Background = _renderer.GetBorderBackground(Gd.BorderColor,Gd.BackgroundColor, Gd.BorderWidth, Gd.BorderRadius);
}
}
}
Gradient Drawable
public Drawable GetBorderBackground(Color borderColor, Color backgroundColor, float borderWidth, float borderRadius)
{
if (_background != null)
{
_background.Dispose();
_background = null;
}
borderWidth = borderWidth > 0 ? borderWidth : 0;
borderRadius = borderRadius > 0 ? borderRadius : 0;
borderColor = borderColor != Color.Default ? borderColor : Color.Transparent;
backgroundColor = backgroundColor != Color.Default ? backgroundColor : Color.Transparent;
var strokeWidth = Xamarin.Forms.Forms.Context.ToPixels(borderWidth);
var radius = Xamarin.Forms.Forms.Context.ToPixels(borderRadius);
_background = new GradientDrawable();
_background.SetColor(backgroundColor.ToAndroid());
if (radius > 0)
_background.SetCornerRadius(radius);
if (borderColor != Color.Transparent && strokeWidth > 0)
{
_background.SetStroke((int)strokeWidth, borderColor.ToAndroid());
}
return _background;
}

CircularImage in Xamarin.Forms

I need to have Image control in Xamarin.Forms with rounded corners. But I did not find any property that could make it. How to have circular Image ?
I use the FFImageLoading libraries CachedImage control with a circle transformation for circle images:
<ffimageloading:CachedImage
DownsampleToViewSize="true"
Aspect="AspectFill"
Source = "{Binding Image}"
LoadingPlaceholder = "{Binding DefaultImage}"
ErrorPlaceholder = "{Binding DefaultImage}">
<ffimageloading:CachedImage.Transformations>
<fftransformations:CircleTransformation />
</ffimageloading:CachedImage.Transformations>
</ffimageloading:CachedImage>
You can use Image Circle Control Plugin
<controls:CircleImage Source="{Binding Image}" Aspect="AspectFill">
<controls:CircleImage.WidthRequest>
<OnPlatform x:TypeArguments="x:Double"
iOS="55"
Android="55"
WinPhone="75"/>
</controls:CircleImage.WidthRequest>
<controls:CircleImage.HeightRequest>
<OnPlatform x:TypeArguments="x:Double"
iOS="55"
Android="55"
WinPhone="75"/>
</controls:CircleImage.HeightRequest>
</controls:CircleImage>
Read more at Project github readme
You can also use CircleImage from Xamarin-Forms-Labs project.
If you need a solution using Custom Renderers (to tweak the control the way you want), here is my implementation for that.
public class ImageCircle:Image
{
public ImageCircle ()
{
}
}
[assembly: ExportRenderer(typeof(ImageCircle), typeof(ImageCircleRenderer))]
namespace myNamespace.Droid
{
public class ImageCircleRenderer:ImageRenderer
{
public ImageCircleRenderer ()
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
if ((int)Android.OS.Build.VERSION.SdkInt < 20)
SetLayerType(Android.Views.LayerType.Software, null);
}
}
protected override bool DrawChild(Canvas canvas, global::Android.Views.View child, long drawingTime)
{
try
{
var radius = Math.Min(Width, Height) / 2;
var strokeWidth = 10;
radius -= strokeWidth / 2;
//Create path to clip
var path = new Path();
path.AddCircle(Width / 2, Height / 2, radius, Path.Direction.Ccw);
canvas.Save();
canvas.ClipPath(path);
var result = base.DrawChild(canvas, child, drawingTime);
canvas.Restore();
// Create path for circle border
path = new Path();
path.AddCircle(Width / 2, Height / 2, radius, Path.Direction.Ccw);
var paint = new Paint();
paint.AntiAlias = true;
paint.StrokeWidth = 5;
paint.SetStyle(Paint.Style.Stroke);
paint.Color = global::Android.Graphics.Color.White;
canvas.DrawPath(path, paint);
//Properly dispose
paint.Dispose();
path.Dispose();
return result;
}
catch (Exception ex)
{
Console.WriteLine("Unable to create circle image: " + ex);
}
return base.DrawChild(canvas, child, drawingTime);
}
}
}
[assembly: ExportRenderer(typeof(ImageCircle), typeof(ImageCircleRenderer))]
namespace LifesTopTen.iOS
{
public class ImageCircleRenderer:ImageRenderer
{
public ImageCircleRenderer ()
{
}
private void CreateCircle()
{
try
{
double min = Math.Min(Element.Width, Element.Height);
Control.Layer.CornerRadius = (float)(min / 2.0);
Control.Layer.MasksToBounds = false;
Control.Layer.BorderColor = Color.White.ToCGColor();
Control.Layer.BorderWidth = 3;
Control.ClipsToBounds = true;
}
catch(Exception ex)
{
Console.WriteLine ("Unable to create circle image: " + ex);
}
}
protected override void OnElementChanged (ElementChangedEventArgs<Image> e)
{
base.OnElementChanged (e);
if (e.OldElement != null || 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)
{
CreateCircle();
}
}
}
}

Resources