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.
I am using this code to capture screen on iOS but while capturing browser which has YouTube Videos I get black Video
var view = UIApplication.SharedApplication.KeyWindow.RootViewController.View;
UIGraphics.BeginImageContext(view.Frame.Size);
view.DrawViewHierarchy(view.Frame, true);
var image = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
I tried this code to send the webview as a parameter to be captured the view refreshed and the webveiw show progress ring all the time and never reloaded:
public async Task<byte[]> CaptureAsync(Xamarin.Forms.View fView)
{
CGRect rect = new CGRect(fView.X, fView.Y, fView.Height, fView.Width);
var view = ConvertFormsToNative(fView, rect);
UIGraphics.BeginImageContext(view.Frame.Size);
view.DrawViewHierarchy(view.Frame, true);
var image = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
using (var imageData = image.AsPNG())
{
var bytes = new byte[imageData.Length];
System.Runtime.InteropServices.Marshal.Copy(imageData.Bytes, bytes, 0, Convert.ToInt32(imageData.Length));
return bytes;
}
}
private UIView ConvertFormsToNative(Xamarin.Forms.View view, CGRect size)
{
var renderer = RendererFactory.GetRenderer(view);
renderer.NativeView.Frame = size;
renderer.NativeView.AutoresizingMask = UIViewAutoresizing.All;
renderer.NativeView.ContentMode = UIViewContentMode.ScaleToFill;
renderer.Element.Layout(size.ToRectangle());
var nativeView = renderer.NativeView;
nativeView.SetNeedsLayout();
return nativeView;
}
It is because you captured the RootViewController.View not the webview or something else.
Just make sure that the view (which you want to convert to screenshot ) is the video view (WebView or WKWebvView or SFSafariViewController.view..)
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 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).
Okay so I'm starting to make a main menu for a small flash game and to do this I want to use the mouse to click on buttons etc. I have a button class in which I create two rectangles: a rectangle for the button and a rectangle for the mouse based on its X and Y, 1 pixel by 1 pixel. I use Rectangle.Intersects to check if they are touching before seeing if left mouse button is down. Problem is, the thing the mouse position is relative to changes every time so no matter where the mouse button is on the screen, it's never the same co-ordinates as in a different build in that exact same position. I seriously just need ideas now as I'm running out. If I didn't explain it very well or you need further details to help please ask - I WOULD BE SO GRATEFUL.
Will post back if I find an answer
Update - Okay here's the button class
class OnScreenButton
{
public Texture2D texture;
Vector2 position;
Rectangle rectangle;
Color colour = new Color(255, 255, 255, 255);
public Vector2 size;
public OnScreenButton(Texture2D newtexture, GraphicsDevice graphics)
{
texture = newtexture;
// ScreenW = 500, ScreenH = 600
// Img W = 80, Img H = 20
size = new Vector2(graphics.Viewport.Width / 10, graphics.Viewport.Height / 30);
size = new Vector2(texture.Width, texture.Height);
}
bool down;
public bool isClicked;
public void Update(MouseState mouseState)
{
rectangle = new Rectangle((int)position.X, (int)position.Y, (int)size.X, (int)size.Y);
Rectangle mouseRectangle = new Rectangle(mouseState.X, mouseState.Y, 1, 1);
if (mouseRectangle.Intersects(rectangle))
{
if (colour.A == 255)
{
down = false;
}
if (colour.A == 0)
{
down = true;
}
if (down)
{
colour.A += 3;
}
else
{
colour.A -= 3;
}
if (mouseState.LeftButton == ButtonState.Pressed)
{
isClicked = true;
}
}
else if (colour.A < 255)
{
colour.A += 3;
isClicked = false;
colour.A = (255);
}
}
public void SetPosition(Vector2 newPos)
{
position = newPos;
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(texture, rectangle, colour);
}
}
}
(Sorry for weird formatting, brand new to stack overflow and the posting is still a little confusing)
Here is some other code I think is relevent...
Game.1 initializing stuff
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
protected override void Initialize()
{
// TODO: Add your initialization logic here
Mouse.WindowHandle = Window.Handle;
base.Initialize();
}
public Main()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
KeyboardState keyboardState;
MouseState mouseState;
Main menu update routine...
private void UpdateMainMenu(GameTime gameTime)
{
// Button options
if (buttonPlay.isClicked == true)
{
CreateNewGame();
currentGameState = GameState.playing;
}
buttonPlay.Update(mouseState);
if (buttonExit.isClicked == true)
{
this.Exit();
}
buttonExit.Update(mouseState);
// Press enter to play
if (keyboardState.IsKeyDown(Keys.Enter))
{
CreateNewGame();
currentGameState = GameState.playing;
}
}
Here's thee draw routine for main menu...
public void DrawMainMenu()
{
spriteBatch.Draw(mainMenuBackground, new Vector2(0, 0), Color.White);
buttonPlay.Draw(spriteBatch);
buttonExit.Draw(spriteBatch);
spriteBatch.DrawString(playerAmmoFont, String.Format("{0}", mouseState), new Vector2(0, 0), Color.White);
}
okay that's all I can think of
UPDATE - Okay so I know a few things that aren't the problem...
The whole of my button class is fine, I made a new project and inserted all the relevant code into it and it worked absolutely perfectly so I'm starting to think its something to do with the code positioning and the graphics device stuff although I still don't have a clue how to fix it.
the window appears at the same spot every time
there is no pattern to the change in coordinates at all
this is really annoying
UPDATE - OKAY. I spent a long time writing down the coordinates that I got each time I ran the code and stuck to cursor in the top right corner of the screen. Here is what I got.
(-203, -225)
(-253, -275)
(-53, -75)
(-103, -125)
(-153, -175)
(-203, -225)
(-253, -275)
(-53, -75)
(-103, -125)
(-153, -175)
(-203, -225)
(-253, -275)
(-53, -75)
(-103, -125)
(-153, -175)
(-203, -225)
(-253, -275)
(-53, -75)
(-103, -125)
(-153, -175)
(-203, -225)
(-78, -100)
(-128, -150)
(-178, -200)
(-228, -250)
(-28, -50)
(-53, -75)
(-103, -125)
(-153, -175) < AND FROM HERE THE PATTERN LOOPS ROUND.
I just don't get how the same code can execute a different bug on different executions like this.
Also, mouse.Wheel doesn't go up or down whereas it works on the project that I made to test the relevant code where the mouse position was relevant to the top left pixel of the game window.
UPDATE - EVEN MORE DAMN COMPLICATIONS - So I just rand it a few times again and the offset values are offset... the increase is the same but I got values like (-178, -200) then (-228, -250). I have also discovered that the mouse is not relative to the game window what so ever, if I jam the mouse in the top right corner of the screen and check the coordinates, then move the game window and do the same again, the coordinates don't change. Please please please help me, or tell me if I'm being stupid, or something. Thanks.
The mouse coordinates are relative to the monitor. Here is my general button class to try and work for your situation.
public class Button
{
public event EventHandler<EventArgs> Clicked;
public Vector2 Position { get; set;}
public Texture2D Texture { get; set;}
public Color Tint { get; set; }
public float Scale { get; set; }
public float Rotation { get; set; }
public int Width
{
get
{
if (texture == null)
return 0;
else
return texture.Width;
}
}
public int Height
{
get
{
if (texture == null)
return 0;
else
return texture.Height;
}
}
private void OnClick()
{
if (Clicked != null)
Clicked(this, EventArgs.Empty);
}
public Button(Vector2 position, Texture2D texture)
: base(parent)
{
Position = position;
Texture = texture;
Tint = Color.White;
Scale = 1.0f;
Rotation = 0.0f;
}
public bool HandleClick(Vector2 vector)
{
if (vector.X >= Position.X)
{
if (vector.X <= Position.X + Width)
{
if (vector.Y >= Position.Y)
{
if (vector.Y <= Position.Y + Height)
{
OnClick();
return true;
}
}
}
}
return false;
}
public bool HandleEntered(Vector2 vector)
{
if (vector.X >= Position.X)
{
if (vector.X <= Position.X + Width)
{
if (vector.Y >= Position.Y)
{
if (vector.Y <= Position.Y + Height)
{
return true;
}
}
}
}
return false;
}
public override void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(Texture, Position, null, Tint, Rotation, Vector2.Zero, Scale, SpriteEffects.None, 0.0f);
}
Declare a button:
Button btn = new Button(position where you want the button, texture for the button);
btn.Clicked += () => { /* Handle button clicked code here */ };
In your update method inside your main game:
public void Update (GameTime gameTime)
{
MouseState mouseState = Mouse.GetState();
if(mouseState.LeftButton == ButtonState.Pressed) // check if mouse is clicked
{
btn.HandleClicked(new Vector2(mouseState.X, mouseState.Y)); // If true then the button clicked event will fire
// Here you can also change the color of the button if the button is currently clicked
}
// Here you can change the color of the button if the mouse is hover over the control
// Example:
btn.Tint = btn.HandleEntered(new Vector2(mouseState.X, mouseState.Y)) ? Color.White * 0.75f : Color.White;
}
Note: You can also use a rectangle for the button to adjust its size instead of strictly using the textures dimensions. Hope this gives some insight.
So here's what was going on: I had a bullet class in my game for every bullet shot. In this class I check whether the bullets hits nothing, hits the asteroid, or destroys the asteroids. If the latter is true then I would increment playerScore by 5. PlayerScore was a Game1 attribute so I thought the easiest way to do this would be to create a new Game1 in bullet.cs to allow me to refer to the variable. Deleting the "Main mainGame = new Main():" in Bullet.cs fixed this issue and I think the issue was coming from a new graphics device being made every single time I fired a single bullet.