I'm creating a custom map pin (annotation) for iOS, draw its path and then convert the UIView to an image, using this method:
public static UIImage AsImage(this UIView view)
{
try
{
UIGraphics.BeginImageContextWithOptions(view.Bounds.Size, true, 0);
var ctx = UIGraphics.GetCurrentContext();
view.Layer.RenderInContext(ctx);
var img = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
return img;
}
catch (Exception)
{
return null;
}
}
and this is the UIView-derived class, where I draw the annotation path:
public class PinView : UIView
{
public PinView()
{
ContentMode = UIViewContentMode.Redraw;
SetNeedsDisplay();
}
private readonly CGPoint[] markerPathPoints = new[]
{
new CGPoint(25f, 5f),
new CGPoint(125f, 5f),
//other points };
public override void Draw(CGRect rect)
{
base.Draw(rect);
//get graphics context
CGContext context = UIGraphics.GetCurrentContext();
//set up drawing attributes
context.SetLineWidth(10);
UIColor.White.SetFill();
UIColor.Black.SetStroke();
//create geometry
var path = new CGPath();
path.MoveToPoint(markerPathPoints[0].X, markerPathPoints[0].Y);
for (var i = 1; i < markerPathPoints.Length; i++)
{
path.AddLineToPoint(markerPathPoints[i].X, markerPathPoints[i].Y);
}
path.CloseSubpath();
//add geometry to graphics context and draw it
context.AddPath(path);
context.DrawPath(CGPathDrawingMode.FillStroke);
}
I'm modifying the Xamarin.Forms sample of the MapCustomRenderer, instead of loading the annotation from a file, I'm loading it from the view:
annotationView.Image = new PinView().AsImage();
but the PinView.Draw() never get called!
The method Draw will been invoked after the method ViewDidLoadwhen the control first been loaded , which is called automatically by the system .
In your case , you didn't set the Rect(Frame) of the PinView . So the method Draw will never been called because the Rect is zero in default .
So you could set a default frame in the constructor
public PinView()
{
Frame = new CGRect (0,0,xx,xxx);
ContentMode = UIViewContentMode.Redraw;
SetNeedsDisplay();
}
The view needs to be added to the views hierarchy, in my case, to the MKAnnotationView,
so in the CustomMKAnnotationView constructor:
public CustomMKAnnotationView(IMKAnnotation annotation, string id)
: base(annotation, id)
{
var pin = new PinView(new CGRect(0, 0, 110, 110));
pin.BackgroundColor = UIColor.White.ColorWithAlpha(0);
Add(pin);
}
and in the UIView's constructor, the Frame should be initialized as Lucas Zhang stated:
public PinView(CGRect rectangle)
{
Frame = rectangle;
ContentMode = UIViewContentMode.Redraw;
SetNeedsDisplay();
}
With these changes the Draw method is called.
Related
I am trying to create a app that uses skiasharp on an overlay for the camera on iPhone (native). My camera stream appears nicely but no overlay. the initial viewcontroller that creates the stream looks like:
public async override void ViewDidLoad()
{
base.ViewDidLoad();
await AuthorizeCameraUse();
imagePicker = new UIImagePickerController();
// set our source to the camera
imagePicker.SourceType = UIImagePickerControllerSourceType.Camera;
// set
imagePicker.MediaTypes = new string[] { "public.image" };
imagePicker.CameraOverlayView = new CameraOverlayView();
imagePicker.ModalPresentationStyle = UIModalPresentationStyle.CurrentContext;
. . .
my CameraOverlayView looks like:
public class CameraOverlayView :SKCanvasView
{
public CameraOverlayView() : base()
{
Initialize();
}
public CameraOverlayView(CGRect frame) : base(frame) {
Initialize();
}
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Red,
StrokeWidth = 25
};
protected void Initialize()
{
this.PaintSurface += CameraOverlayView_PaintSurface;
//using (var surface = SKSurface.Create( 640, 480, SKColorType.Rgba8888, SKAlphaType.Premul))
//{
// SKCanvas myCanvas = surface.Canvas;
// myCanvas.DrawCircle(300, 300, 100, paint);
// // Your drawing code goes here.
//}
}
private void CameraOverlayView_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
var surface = e.Surface;
// then we get the canvas that we can draw on
var canvas = surface.Canvas;
// clear the canvas / view
canvas.Clear(SKColors.White);
// DRAWING SHAPES
// create the paint for the filled circle
var circleFill = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
Color = SKColors.Blue
};
// draw the circle fill
canvas.DrawCircle(100, 100, 40, circleFill);
}
}
my Paint event is never fired , if I run this code in simple example it does.
thanks to Russ Fustino help it turns out I was not using the CameraOverlayView (CGRect frame) : base (frame) constructor and because of that the DrawInSurface overload is not called.
Nothing to draw into. when I did this the paint event fired.
Hello I currently have this method which draws line on an UIImageView.
However I am trying to make it compatible with a UIImage and have not had any luck. This example here works beautifully for text but not great for lines.
DrawOnUIImageView.cs
private void Draw(Face face, UIImageView imageView)
{
CAShapeLayer boundingBoxLayer = new CAShapeLayer();
boundingBoxLayer.Frame = face.rect;
boundingBoxLayer.FillColor = null;
boundingBoxLayer.StrokeColor = UIColor.Red.CGColor;
imageView.Layer.AddSublayer(boundingBoxLayer);
CAShapeLayer secondBoxLayer = new CAShapeLayer();
secondBoxLayer.FillColor = null;
secondBoxLayer.StrokeColor = UIColor.Green.CGColor;
boundingBoxLayer.AddSublayer(secondBoxLayer);
var path = new CGPath();
List<LandmarkLine> lines = new List<LandmarkLine>();
foreach (var landmark in face.landmarks)
{
List<CGPoint> addTo = new List<CGPoint>();
foreach (var point in landmark.points)
{
addTo.Add(new CGPoint((point.X * face.rect.Width), (1 - point.Y) * face.rect.Height));
}
CGPath outline = new CGPath();
outline.AddLines(addTo.ToArray());
outline.CloseSubpath();
path.AddPath(outline);
}
secondBoxLayer.Path = path;
//imageView.Layer.AddSublayer(outline);
}
Any advice on this would be great. Thanks
You can draw a line on your image like this:
private UIImage drawLineOnImage(UIImage img)
{
//UIImage orgImage = <YOUR IMAGE>
UIGraphics.BeginImageContext(orgImage.Size);
// 1: Draw the original image as the background
orgImage.Draw(new RectangleF(0,0,(float)orgImage.Size.Width,(float)orgImage.Size.Height));
// 2: Draw the line on the image
CGContext context = UIGraphics.GetCurrentContext();
context.SetLineWidth(1.0f);
context.MoveTo(0, 80);
context.AddLineToPoint(orgImage.Size.Width, 80);
context.SetStrokeColor(UIColor.Blue.CGColor);
context.StrokePath();
// Create new image
UIImage image = UIGraphics.GetImageFromCurrentImageContext();
// Tidy up
UIGraphics.EndImageContext();
return image;
}
This code will create a new image as the original image size, then draw a copy of the original image onto the new image and draw a line on the new image.
I wrote the UIView descendant in Xamarin and overrided Draw method.
Among other staff it draws text. It looks good in any screen orientation, but when I rotate device to the other orientation the text is stretched or shrinks.
The drawing code is:
public override void Draw(CGRect rect)
{
base.Draw(rect);
var ss = new UIStringAttributes();
ss.ForegroundColor = mTextColor;
ss.Font = mTextFont;
ss.ParagraphStyle = new NSMutableParagraphStyle() { Alignment = UITextAlignment.Center };
using (var g = UIGraphics.GetCurrentContext())
{
for (nint i = 0; i < mBars.Length; ++i)
{
var r = mBars[i];
g.SetFillColor(i + 1 == mBars.Length ? mLastBarColor : mBarColor);
g.FillRect(r);
var ns = new NSAttributedString(mDataSource[i].Item2, ss);
var textRect = new CGRect(r.Left, r.Bottom, r.Width, mTextRectHeight);
ns.DrawString(textRect);
}
}
}
Russell is right. I didn't know about UIView contentMode.
It has to be set to 'redraw'
I created a Xamarin Form using ContentView and created a renderer for Android. Now I have to create a renderer for IOS.
In android renderer, I can override onSizeChanged and passing those width/height value to the custom xamarin-form view.
protected override void OnSizeChanged(int w, int h, int oldw, int oldh) {
base.OnSizeChanged(w,h, oldw, old);
Element.SizChanged(w,h);
}
Does UIView in IOS has the similar method to override? I have tried override the Frame property and invoke in Element.SizeChanged(w,h), but I will get TargetInvocationException.
P.S. SizeChanged method is my custom virtual method not android method.
Overriding LayoutSubviews is normally used to track resize events as a UIView is responsible to for all of it's subviews also.
I tend to also override Bounds and Frame depending upon what I am doing and what this view contains... Depending upon the parent, the View's LayoutSubviews can be called a lot...
Assuming a UIView subclass like:
public class UIViewResizing : UIView
{
public UIViewResizing() { }
public UIViewResizing(CGRect frame) : base(frame) { }
public UIViewResizing(NSCoder coder) : base(coder) { }
public override CGRect Bounds
{
get
{
return base.Bounds;
}
set
{
if (value != base.Bounds)
{
Console.WriteLine($"Bounds changing: {this}");
}
base.Bounds = value;
}
}
public override CGRect Frame
{
get
{
return base.Frame;
}
set
{
if (value != base.Frame)
{
Console.WriteLine($"Frame changing: {this}.");
}
base.Frame = value;
}
}
public override void LayoutSubviews()
{
Console.WriteLine($"LayoutSubViews requested: {this}");
base.LayoutSubviews();
}
}
Exercising it with:
var view = new UIViewResizing(new CGRect(100, 100, 100, 100));
view.BackgroundColor = UIColor.Red;
Add(view);
await Task.Delay(2500);
view.Frame = new CGRect(100, 100, 200, 200);
await Task.Delay(2500);
view.Bounds = new CGRect(20, 20, 100, 200);
Results in:
Frame changing: <uiviewresize_UIViewResizing: 0x79a6cc00; frame = (-50 -50; 100 100); layer = <CALayer: 0x78628230>>.
LayoutSubViews requested: <uiviewresize_UIViewResizing: 0x79a6cc00; frame = (100 100; 100 100); layer = <CALayer: 0x78628230>>
Frame changing: <uiviewresize_UIViewResizing: 0x79a6cc00; frame = (100 100; 100 100); layer = <CALayer: 0x78628230>>.
LayoutSubViews requested: <uiviewresize_UIViewResizing: 0x79a6cc00; frame = (100 100; 200 200); layer = <CALayer: 0x78628230>>
Bounds changing: <uiviewresize_UIViewResizing: 0x79a6cc00; frame = (100 100; 200 200); layer = <CALayer: 0x78628230>>
LayoutSubViews requested: <uiviewresize_UIViewResizing: 0x79a6cc00; frame = (150 100; 100 200); layer = <CALayer: 0x78628230>>
Consult this SO for additional information: Is there a UIView resize event?
I have created a UIView subclass which uses a CAGradientLayer for its layer. This is working fine. Now I want to draw a line at the end of the gradient (used as separator). I thought I can use the drawRect method for this:
public class GradientView : UIView
{
// accessors
private CAGradientLayer gradientLayer {
// read-only
get { return (CAGradientLayer)this.Layer; }
}
public CGColor[] Colors {
// set the colors of the gradient layer
get { return this.gradientLayer.Colors; }
set { this.gradientLayer.Colors = value; }
}
[Export ("layerClass")]
public static Class LayerClass ()
{
// use a different Core Animation layer for its backing store
// normally a CALayer is used for a UIView
return new Class (typeof(CAGradientLayer));
}
public override void Draw (RectangleF rect)
{
PointF pointA = new PointF (0, rect.Height);
PointF pointB = new PointF (rect.Width, rect.Height);
CAShapeLayer line = new CAShapeLayer ();
UIBezierPath linePath = new UIBezierPath ();
linePath.MoveTo (pointA);
linePath.AddLineTo(pointB);
line.Path = linePath.CGPath;
line.FillColor = null;
line.Opacity = 1.0f;
line.StrokeColor = UIColor.Black.CGColor;
this.gradientLayer.AddSublayer (line);
base.Draw (rect);
}
}
But I get a black background. Can I use drawRect for that? Or how do I add a line on it?
I'm using C# as language but you can provide your solution in Objective-C as well. Auto Layout determines the size of the GradientView and my old solution also adapted to orientation changes.
Edit:
I tried what Cyrille said:
public class GradientView : UIView
{
// accessors
private CAShapeLayer line;
private CAGradientLayer gradientLayer {
// read-only
get { return (CAGradientLayer)this.Layer; }
}
public CGColor[] Colors {
// set the colors of the gradient layer
get { return this.gradientLayer.Colors; }
set { this.gradientLayer.Colors = value; }
}
public GradientView()
{
PointF pointA = new PointF (0, this.Bounds.Height);
PointF pointB = new PointF (this.Bounds.Width, this.Bounds.Height);
line = new CAShapeLayer ();
UIBezierPath linePath = new UIBezierPath ();
linePath.MoveTo (pointA);
linePath.AddLineTo(pointB);
line.Path = linePath.CGPath;
line.FillColor = null;
line.Opacity = 1.0f;
line.StrokeColor = UIColor.Black.CGColor;
line.LineWidth = 1.0f;
this.gradientLayer.AddSublayer (line);
}
[Export ("layerClass")]
public static Class LayerClass ()
{
// use a different Core Animation layer for its backing store
// normally a CALayer is used for a UIView
return new Class (typeof(CAGradientLayer));
}
public override void LayoutSubviews ()
{
PointF pointA = new PointF (0, this.Bounds.Height-2);
PointF pointB = new PointF (this.Bounds.Width, this.Bounds.Height-2);
UIBezierPath linePath = new UIBezierPath ();
linePath.MoveTo (pointA);
linePath.AddLineTo(pointB);
line.Path = linePath.CGPath;
base.LayoutSubviews ();
}
}
This seems to do what I want except that the line is not a thin hairline. Is CAShapeLayer inappropriate for this?
Solution:
Of course it would be possible to use CAShapeLayer, but if I have to setup things in drawRect, then I completely draw my line there. Now the solution for me does look like the following:
public class GradientView : UIView
{
// accessors
private CAShapeLayer line;
private CAGradientLayer gradientLayer {
// read-only
get { return (CAGradientLayer)this.Layer; }
}
public CGColor[] Colors {
// set the colors of the gradient layer
get { return this.gradientLayer.Colors; }
set { this.gradientLayer.Colors = value; }
}
public GradientView()
{
this.BackgroundColor = UIColor.Clear;
}
[Export ("layerClass")]
public static Class LayerClass ()
{
// use a different Core Animation layer for its backing store
// normally a CALayer is used for a UIView
return new Class (typeof(CAGradientLayer));
}
public override void Draw (RectangleF rect)
{
base.Draw (rect);
// get graphics context
CGContext context = UIGraphics.GetCurrentContext ();
// start point
context.MoveTo (0, rect.Height);
// end point
context.AddLineToPoint (rect.Width, rect.Height);
context.SetLineWidth (1.0f);
context.SetShouldAntialias(false);
// draw the path
context.DrawPath (CGPathDrawingMode.Stroke);
}
Using a CAGradientLayer as the backing layer of your view does not prevent you from adding another CAShapeLayer inside your view upon initialization. Then you can use this shape layer to draw your straight line, by setting its path property accordingly (and recalculate it any time the bounds change, in -layoutSubviews, to react correctly to orientation/layout changes).