I want to draw something with GPU acceleration in xamarin.forms. (and save the image later). When I call the following method the first time, I get an image and can show it in SKGLView. But when I call the method again, there is nothing (no error, too)
Can somebody give me a hint?
private SKImage CreateSKImage()
{
SKImage sKImage;
var glInterface = GRGlInterface.CreateNativeAngleInterface();
var grContext = GRContext.Create(GRBackend.OpenGL, glInterface);
var surface = SKSurface.Create(grContext, true, new SKImageInfo(500, 500));
var canvas = surface.Canvas;
var paintStroke = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Stroke,
Color = SKColors.Purple,
StrokeWidth = 1
};
canvas.DrawLine(new SKPoint(0, 0), new SKPoint(500, 500), paintStroke);
surface.Canvas.Flush();
sKImage = surface.Snapshot().ToRasterImage();
return sKImage;
}
edit: when I call the method from SKGLView onPaint, then I get something. But when I call the method from anywhere else the glInterface is null ...hmmmm
How to create the OpenGL Interface the right way???
Related
My target is to generate image with A5 format and place some text on it. At the beggining i was trying to accomplish that using System.Drawing.Imaging but seems like Xamarin is not able to work with this. Then I found out SkiaSharp library which seems like would be valid to do this job but i am a bit lost here. How can i correctly generate an image, put some text on it and send via Stream. Note that if there is more text that couldn't fit on single A5 next image should be created.
This is what i got so far:
private void CreateBitmapFromText(List<string> texts)
{
using var surface = SKSurface.Create(width: 640, height: 480, SKColorType.Gray8, SKAlphaType.Premul);
SKCanvas myCanvas = surface.Canvas;
// clear the canvas / fill with white
myCanvas.DrawColor(SKColors.White);
// set up drawing tools
using (var paint = new SKPaint())
{
paint.TextSize = 64.0f;
paint.IsAntialias = true;
paint.Color = new SKColor(0x42, 0x81, 0xA4);
paint.IsStroke = false;
// draw the text
foreach (var text in texts)
{
myCanvas.DrawText(text, ??, ??, paint);
}
}
}
EDIT: with help of #Jason i did this:
var aaa = ToStream(CreateBitmapFromText(new List<string>() {"asas", "vvvv"}), SKEncodedImageFormat.Png);
_printService.PrintImage(aaa);
public Stream ToStream(SKImage image, SKEncodedImageFormat format)
{
SKData encoded = image.Encode(format, 90);
return encoded.AsStream();
}
public SKImage CreateBitmapFromText(List<string> texts)
{
using var surface = SKSurface.Create(width: 640, height: 480, SKColorType.Gray8, SKAlphaType.Premul);
SKCanvas myCanvas = surface.Canvas;
// clear the canvas / fill with white
myCanvas.DrawColor(SKColors.White);
// set up drawing tools
using (var paint = new SKPaint())
{
paint.TextSize = 64.0f;
paint.IsAntialias = true;
paint.Color = new SKColor(0x42, 0x81, 0xA4);
paint.IsStroke = false;
// draw the text
foreach (var text in texts)
{
myCanvas.DrawText(text, 0.0f, 0.0f, paint);
}
}
return surface.Snapshot();
}
When i send it to printer i see this (white paper with some little black chars ?? at the top?)
I was able to easily modify the Xamarin FramedText sample to save a bitmap by adding this to the end of OnCanvasViewPaintSurface
// get the SKImage from SKSurface
var image = surface.Snapshot();
// Encode as PNG, returns SKData
var data = image.Encode(SKEncodedImageFormat.Png, 100);
string path = Path.GetTempFileName();
// write byte[] to file, verify data with image viewer
File.WriteAllBytes(path, data.ToArray());
you should be able to easily modify this to return a byte[] or stream
This is fairly straight forward, but I have some custom ShellRenderers for my App in Xamarin Forms, and I'm having issues with the scaling of images. In particular, Android looks fantastic with the more pixel-dense image (800x200). It looks great on every screen.
The renderer just ensures that the top of the screen is made of the "theme" color, and is presented in the center of the it. It's that simple/straight-forward.
On iOS, it looks awful and blurred. I'm not sure if there's something I need to set or update (or maybe a different graphics stack?) to make it render with higher pixel density... See my attached renderer:
public class CustomShellRenderer : ShellRenderer
{
#region Methods
protected override IShellSectionRenderer CreateShellSectionRenderer(ShellSection shellSection)
{
var renderer = base.CreateShellSectionRenderer(shellSection);
if (renderer != null)
{
var navigationBar = (renderer as ShellSectionRenderer).NavigationBar;
if (navigationBar != null)
{
navigationBar.BarTintColor = Colors.LightColor.ToUIColor(); //static colors
navigationBar.TintColor = Colors.LightColor.ToUIColor();
using (var logo = UIImage.FromResource(typeof(Images).Assembly, "Path.To.Logo.png"))
{
var statusFrame = UIApplication.SharedApplication.StatusBarFrame;
var navigationFrame = navigationBar.Frame;
var width = Math.Max(navigationFrame.Width, statusFrame.Width);
var fullArea = new CGRect(0, 0, width, navigationFrame.Height + statusFrame.Height);
UIGraphics.BeginImageContext(fullArea.Size);
var backgroundColor = Colors.PrimaryColor.ToUIColor();
backgroundColor.SetFill();
UIGraphics.RectFill(fullArea);
var logoArea = new CGRect(width * .35, statusFrame.Height, width * .3, navigationFrame.Height);
logo.Draw(logoArea);
var backgroundImage = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
(renderer as ShellSectionRenderer).NavigationBar.SetBackgroundImage(backgroundImage, UIBarMetrics.Default);
backgroundImage.Dispose();
}
}
}
return renderer;
}
#endregion
}
Any thoughts? Right now, the problem is that when it renders, there is an 800x200 image trying to squeeze into... 140x30 or so space on some emulators...
Thank you!
For personal needs, for the Xamarin.Forms.Map control, I need to create a CustomPin extension. UWP part (PCL project)
I create a MapIcon like it:
nativeMap.MapElements.Add(new MapIcon()
{
Title = pin.Name,
Image = RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///Assets/Pin/customicon.png")),
Location = new Geopoint(new BasicGeoposition() { Latitude = pin.Position.Latitude, Longitude = pin.Position.Longitude }),
NormalizedAnchorPoint = new Windows.Foundation.Point(0.5, 1.0)
});
However, by this way, I can't set the Image's size.
I then want to use an Image from my PCL part, resize it and convert it into a IRandomAccessStreamReference. To realize it, I need to convert my Image into a stream, but I can't find the way to make it works ><
Example of the function needed:
private IRandomAccessStreamReference ImageToIRandomAccessStreamReference(Image image)
{
//Here I can set the size of my Image
//I convert it into a stream
IRandomAccessStreamReference irasr = RandomAccessStreamReference.CreateFromStream(/* img? */);
//irasr is then created from img
//I return the IRandomAccessStreamReference needed by the MapIcon element
return irasr;
}
Note: The Image paramter img is a Xamarin.Forms.Image
So first, is it possible? If yes, then thank for any help which could help me.. I already search about how to resize the MapIcon and it's not possible directly from the class [MapIcon].(https://msdn.microsoft.com/library/windows/apps/windows.ui.xaml.controls.maps.mapicon.aspx)
Thank for help !
You are right. We can't resize the MapIcon directly as it doesn't provide such properties or methods. MapIcon's size is mostly controlled by the size of image which is set by MapIcon.Image property. And we can set this image's size without using Xamarin.Forms.Image.
To set this image's size, we can take advantage of BitmapDecoder class, BitmapEncoder class and BitmapTransform class like following:
private async System.Threading.Tasks.Task<RandomAccessStreamReference> ResizeImage(StorageFile imageFile, uint scaledWidth, uint scaledHeight)
{
using (IRandomAccessStream fileStream = await imageFile.OpenAsync(FileAccessMode.Read))
{
var decoder = await BitmapDecoder.CreateAsync(fileStream);
//create a RandomAccessStream as output stream
var memStream = new InMemoryRandomAccessStream();
//creates a new BitmapEncoder and initializes it using data from an existing BitmapDecoder
BitmapEncoder encoder = await BitmapEncoder.CreateForTranscodingAsync(memStream, decoder);
//resize the image
encoder.BitmapTransform.ScaledWidth = scaledWidth;
encoder.BitmapTransform.ScaledHeight = scaledHeight;
//commits and flushes all of the image data
await encoder.FlushAsync();
//return the output stream as RandomAccessStreamReference
return RandomAccessStreamReference.CreateFromStream(memStream);
}
}
And then we can use this method to create a resized image stream reference first and then set it as MapIcon's Image like:
var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Pin/customicon.png"));
var imageReference = await ResizeImage(file, 64, 64);
nativeMap.MapElements.Add(new MapIcon()
{
Title = pin.Name,
Image = imageReference,
Location = new Geopoint(new BasicGeoposition() { Latitude = pin.Position.Latitude, Longitude = pin.Position.Longitude }),
NormalizedAnchorPoint = new Windows.Foundation.Point(0.5, 1.0)
});
I already added a stop button with autoplay but I need to make it so when you click the button again after you had stopped it, the music starts playing.
Source code:
var music:Sound = new Sound(new URLRequest("calmingsong.mp3"));
var sc:SoundChannel = music.play();
button1.addEventListener(MouseEvent.CLICK, stopMusic);
function stopMusic(e:Event):void
{
sc.stop();
}
If you just want to play the sound over from the beginning, just call the Sound object's play() method again (you get a new SoundChannel object when you do this).
If you'd like to resume playing the sound at the point where the user stopped it, you'll need to add additional variables to store the current "playback state"... Something like this:
var music:Sound = new Sound(new URLRequest("calmingsong.mp3"));
var sc:SoundChannel = music.play();
var startPosition:Number = 0;
var isPlaying = true; // default to true cause you auto play...
button1.addEventListener(MouseEvent.CLICK, togglePlayback);
function togglePlayback(e:Event):void
{
if (isPlaying)
{
startPosition = sc.position;
sc.stop();
isPlaying = false;
}
else
{
sc = music.play(startPosition);
isPlaying = true;
}
}
I have a some UI elements on the right of my map (sometimes), and I'd like to offset my panTo() calls (sometimes).
So I figured:
get the original latlng
convert it to screen pixels
add an offset
convert it back to latlng.
But I must misunderstand what Google Maps API refers to as the "Point Plane":
http://code.google.com/apis/maps/documentation/javascript/reference.html#Projection
Here is my code that seems to offset by lat-long:
function getCentreOffset( alatlng ) {
var PIXEL_OFFSET= 100;
var aPoint = me.gmap.getProjection().fromLatLngToPoint(alatlng);
aPoint.x=aPoint.x + OFFSET;
return me.gmap.getProjection().fromPointToLatLng(aPoint);
}
Here's a simpler version of Ashley's solution:
google.maps.Map.prototype.panToWithOffset = function(latlng, offsetX, offsetY) {
var map = this;
var ov = new google.maps.OverlayView();
ov.onAdd = function() {
var proj = this.getProjection();
var aPoint = proj.fromLatLngToContainerPixel(latlng);
aPoint.x = aPoint.x+offsetX;
aPoint.y = aPoint.y+offsetY;
map.panTo(proj.fromContainerPixelToLatLng(aPoint));
};
ov.draw = function() {};
ov.setMap(this);
};
You can then use it like this:
var latlng = new google.maps.LatLng(-34.397, 150.644);
var map = new google.maps.Map(document.getElementById("map_canvas"), {
zoom: 8,
mapTypeId: google.maps.MapTypeId.ROADMAP,
center: latlng
});
setTimeout(function() { map.panToWithOffset(latlng, 0, 150); }, 1000);
Here is a working example.
Let me explain in detail. This extends the Map object itself. So you can use it just like panTo() with extra parameters for offsets. This uses the fromLatLngToContainerPixel() and fromContainerPixelToLatLng() methods of the MapCanvasProjecton class. This object has no contructor and has to be gotten from the getProjection() method of the OverlayView class; the OverlayView class is used for the creation of custom overlays by implementing its interface, but here we just use it directly. Because getProjection() is only available after onAdd() has been called. The draw() method is called after onAdd() and is defined for our instance of OverlayView to be a function that does nothing. Not doing so will otherwise cause an error.
Answer by Dean looks a lot cleaner as said in some comments, but was looking a little complicated to me. This single line solution is looking more elegant to me.
var map = $('#map_canvas').gmap3("get")
map.panBy(-500,-500); // (x,y)
Set center of map first. Then panyBy will shift the center in (x,y) direction. The more negative x, map will shift right. The more negative y, map will shift down.
Ok I found the answer here: How to call fromLatLngToDivPixel in Google Maps API V3?
First create function/prototpe to access the map's projection (difficult in V3)
//declare function/prototpe
function CanvasProjectionOverlay() {}
//define..
CanvasProjectionOverlay.prototype = new google.maps.OverlayView();
CanvasProjectionOverlay.prototype.constructor = CanvasProjectionOverlay;
CanvasProjectionOverlay.prototype.onAdd = function(){};
CanvasProjectionOverlay.prototype.draw = function(){};
CanvasProjectionOverlay.prototype.onRemove = function(){};
var gmap;
var canvasProjectionOverlay;
var PIXEL_OFFSET= 100;
function showUluru(isOffset=false){
//create map
var gmap = new google.maps.Map($('#map_canvas', {});
//create projection
canvasProjectionOverlay = new CanvasProjectionOverlay();
canvasProjectionOverlay.setMap(gmap);
var uluruRock = new google.maps.LatLng(-25.335448,135.745076);
if (isOffset)
uluruRock = getCentreOffset(uluruRock);
gmap.panTo( uluruRock )
}
//Use this function on LatLng you want to PanTo();
function getCentreOffset( alatlng ) {
var proj = canvasProjectionOverlay.getProjection();
var aPoint = proj.fromLatLngToContainerPixel(alatlng);
aPoint.x=aPoint.x+PIXEL_OFFSET;
return proj.fromContainerPixelToLatLng(aPoint);
}