Draw line on UIView with CAGradientLayer as backing store - ios

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).

Related

Draw method doesn't get called from the custom UIView

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.

CATransform3D.MakeScale is moving layer

I am developing a Xamarin.Forms application for iOS. This app consists of an UIView, which has sublayers which are CALayers. They are added like this:
// draw all the pins from the list
foreach (var pin in _control.PinsSource)
{
var point = new CGPoint
{
X = pin.Longitude,
Y = pin.Latitude
};
var shapeLayer = new CAShapeLayer
{
Name = nameof(MapItem),
Path = MakeCircleAtLocation(point, PinRadius).CGPath,
FillColor = UIColor.Red.CGColor
};
Layer.AddSublayer(shapeLayer);
}
// Create a UIBezierPath which is a circle at a certain location of a certain radius.
private static UIBezierPath MakeCircleAtLocation(CGPoint location, nfloat radius)
{
var path = new UIBezierPath();
path.AddArc(location, radius, 0, (float)(Math.PI * 2.0), true);
return path;
}
Then I have a UIPinchGestureRecognizer which can scale the UIView and some other GestureRecognizers like panning.
Scaling and panning the base view works well. The UIView is scaled using a variable called _currentScale. See the full scale method here:
private void HandlePinch(UIPinchGestureRecognizer recognizer)
{
// Prevent the object to become too large or too small
var newScale = (nfloat)Math.Max(MinZoomLevel, Math.Min(_currentScale * recognizer.Scale, MaxZoomLevel));
if (_currentScale != newScale)
{
_currentScale = newScale;
Transform = CGAffineTransform.MakeScale(_currentScale, _currentScale);
foreach (var subLayer in Layer.Sublayers)
{
if (subLayer.Name == nameof(MapItem))
subLayer.Transform = CATransform3D.MakeScale(PinRadius / _currentScale, PinRadius / _currentScale, 1);
}
}
recognizer.Scale = 1;
}
If the sublayer is a map pin, I did like to NOT scale it with the _currentScale, so that's why I am dividing the scale using PinRadius / _currentScale.
The scaling is working fine, however the pin is moving across the map which is weird. See here:
How can I resolve this?
Unfortunately I couldn't find another way then recreating a new CAShapeLayer on every pinch. It's very ugly, but it's working.
private void HandlePinch(UIPinchGestureRecognizer recognizer)
{
// Prevent the object to become too large or too small
var newScale = (nfloat)Math.Max(MinZoomLevel, Math.Min(_currentScale * recognizer.Scale, _maxZoomLevel));
if (_currentScale != newScale)
{
_currentScale = newScale;
_currentPinRadius = _pinRadius / _currentScale;
Transform = CGAffineTransform.MakeScale(_currentScale, _currentScale);
// First layer is a CALayer, so start at 1
for (var i = 1; i < Layer.Sublayers.Length; i++)
{
var caLayer = ((CAShapeLayer)Layer.Sublayers[i]);
var cgPoint = new CGPoint
{
X = _control.PinsSource[i - 1].Longitude,
Y = _control.PinsSource[i - 1].Latitude
};
caLayer.Path = CreateCircle(cgPoint, _currentPinRadius);
}
}
recognizer.Scale = 1;
}

create skiasharp camera overlay xamarin ios

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.

Redraw saved in array UIBezierPaths on UIVew

I'm using subclass of UIView, in order to draw shapes with UIBezierPath and touchEvents.
I'm making an app (like paint), in which I have two modes of drawing (change stroke color and width in drawRect) and when I switch between them, the already drew paths change their colors and widhts to the new ones. In order to fix this problem, when touchesEnded, I save the current Path in array and create a new one. I want to redraw the saved path(s) on the view when the new one is made, so they can be visible.
In drawRect, foreach path in my array, I do path.Stroke(), in order to visualize it, but when I switch to a certain mode of drawing, the saved paths again change their width and color.
public override void TouchesBegan (Foundation.NSSet touches, UIEvent evt)
{
base.TouchesBegan (touches, evt);
this.Path = new UIBezierPath ();
var touch = touches.AnyObject as UITouch;
var point = touch.LocationInView (this);
this.Path.MoveTo (point);
}
public override void TouchesMoved (Foundation.NSSet touches, UIEvent evt)
{
base.TouchesMoved (touches, evt);
var touch = touches.AnyObject as UITouch;
this.Path.AddLineTo(touch.LocationInView(this));
this.SetNeedsDisplay ();
}
public override void TouchesEnded (Foundation.NSSet touches, UIEvent evt)
{
base.TouchesEnded (touches, evt);
this.TouchesMoved (touches, evt);
paths.Add (Path);
}
public override void Draw (CGRect rect)
{
base.Draw (rect);
DrawSavedPaths ();
if (isScribbling)
{
this.Path.LineWidth = 2.5f;
this.PathColor.SetStroke ();
} else if (isMarking)
{
this.Path.LineWidth = 15.0f;
UIColor.FromRGBA (255, 0, 0, 0.3f).SetStroke ();
}
this.Path.Stroke ();
}
private void DrawSavedPaths()
{
if (paths.Count > 0)
{
foreach (UIBezierPath path in paths)
{
path.Stroke ();
}
}
}
Basically, in the same UIView, where I'm drawing, I want to have the previous paths drewn, preventing them from being changed.

IOS Xamarin - Realtime plot draw lines

I'm working in a cross test project for display ecg graph in realtime. it's work fine on windows and android using Xamarin, BUT in IOS i have slow and slow performarces.
I think the problem is due to my lack of expertise in ios..
I've done two tests both failed, someone have a solutions to speed up ?
TEST A
I call Plot and then assign the UIImage wBitmap as MyView Backgroud every < 10ms
public void Plot(PointF pPrec, PointF pNext)
{
SizeF bitmapSize = new SizeF(wBitmap.Size);
using (CGBitmapContext context2 = new CGBitmapContext(IntPtr.Zero, (int)bitmapSize.Width, (int)bitmapSize.Height, 8, (int)(4 * bitmapSize.Width), CGColorSpace.CreateDeviceRGB(), CGImageAlphaInfo.PremultipliedFirst))
{
context2.DrawImage(new RectangleF(0, 0, wBitmap.Size.Width, wBitmap.Size.Height), wBitmap.CGImage);
context2.SetLineWidth(1);
context2.AddLineToPoint(pNext.X, pNext.Y);
context2.DrawPath(CGPathDrawingMode.Stroke);
// output the drawing to the view
wBitmap = UIImage.FromImage(context2.ToImage());
}
}
TEST B
I call Plot and then assign the UIImage wBitmap as MyView Backgroud every < 10ms
public void Plot2(PointF pPrec, PointF pNext) { UIGraphics.BeginImageContext(wBitmap.Size); context = UIGraphics.GetCurrentContext();
using (context)
{
if (wBitmap != null)
{
context.TranslateCTM(0f, wBitmap.Size.Height);
context.ScaleCTM(1.0f, -1.0f);
context.DrawImage(new RectangleF(0f, 0f, wBitmap.Size.Width, wBitmap.Size.Height), wBitmap.CGImage);
context.ScaleCTM(1.0f, -1.0f);
context.TranslateCTM(0f, -wBitmap.Size.Height);
}
context.SetLineWidth(1);
context.MoveTo(pPrec.X, pPrec.Y);
context.AddLineToPoint(pNext.X, pNext.Y);
context.DrawPath(CGPathDrawingMode.Stroke);
wBitmap = UIGraphics.GetImageFromCurrentImageContext();
}//end using cont
UIGraphics.EndImageContext();
}
You don't need to draw into a bitmap first and then use that bitmap in a view. The correct approach to this is to subclass UIView and then override the Draw() method and paint your stuff in there.
You can then call SetNeedsDisplay() on the view to schedule a redraw.
The example below draw an arrow shaped background:
class YourView: UIView
{
public override void Draw(RectangleF rect)
{
const float inset = 15f;
UIColor.Blue.SetFill();
var path = new UIBezierPath();
path.MoveTo(new PointF(0, 0));
path.AddLineTo(new PointF(rect.Width - inset, 0));
path.AddLineTo(new PointF(rect.Width, rect.Height * 0.5f));
path.AddLineTo(new PointF(rect.Width - inset, rect.Bottom));
path.AddLineTo(new PointF(0, rect.Bottom));
path.AddLineTo(new PointF(0, 0));
path.Fill();
}
}
}
Alternatively you might want to look at existing components, like CorePlot or TeeChart available in the Xamarin Components store. TeeChart would even be cross platform, so you wouldn't have to worry about Android vs. iOS.

Resources