oxyplot-xamarin/Source/OxyPlot.Xamarin.iOS/CoreGraphicsRenderContext.cs

572 строки
22 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="CoreGraphicsRenderContext.cs" company="OxyPlot">
// Copyright (c) 2014 OxyPlot contributors
// </copyright>
// <summary>
// Implements a <see cref="IRenderContext"/> for MonoTouch CoreGraphics.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace OxyPlot.Xamarin.iOS
{
using System;
using System.Collections.Generic;
using System.Linq;
using CoreGraphics;
using CoreText;
using Foundation;
using UIKit;
/// <summary>
/// Implements a <see cref="IRenderContext"/> for CoreGraphics.
/// </summary>
public class CoreGraphicsRenderContext : RenderContextBase, IDisposable
{
/// <summary>
/// The images in use.
/// </summary>
private readonly HashSet<OxyImage> imagesInUse = new HashSet<OxyImage>();
/// <summary>
/// The fonts cache.
/// </summary>
private readonly Dictionary<string, CTFont> fonts = new Dictionary<string, CTFont>();
/// <summary>
/// The image cache.
/// </summary>
private readonly Dictionary<OxyImage, UIImage> imageCache = new Dictionary<OxyImage, UIImage>();
/// <summary>
/// The graphics context.
/// </summary>
private readonly CGContext gctx;
/// <summary>
/// Initializes a new instance of the <see cref="CoreGraphicsRenderContext"/> class.
/// </summary>
/// <param name="context">The context.</param>
public CoreGraphicsRenderContext(CGContext context)
{
this.gctx = context;
// Set rendering quality
this.gctx.SetAllowsFontSmoothing(true);
this.gctx.SetAllowsFontSubpixelQuantization(true);
this.gctx.SetAllowsAntialiasing(true);
this.gctx.SetShouldSmoothFonts(true);
this.gctx.SetShouldAntialias(true);
this.gctx.InterpolationQuality = CGInterpolationQuality.High;
this.gctx.SetTextDrawingMode(CGTextDrawingMode.Fill);
}
/// <summary>
/// Draws an ellipse.
/// </summary>
/// <param name="rect">The rectangle.</param>
/// <param name="fill">The fill color.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The thickness.</param>
public override void DrawEllipse(OxyRect rect, OxyColor fill, OxyColor stroke, double thickness)
{
this.SetAlias(false);
var convertedRectangle = rect.Convert();
if (fill.IsVisible())
{
this.SetFill(fill);
using (var path = new CGPath())
{
path.AddEllipseInRect(convertedRectangle);
this.gctx.AddPath(path);
}
this.gctx.DrawPath(CGPathDrawingMode.Fill);
}
if (stroke.IsVisible() && thickness > 0)
{
this.SetStroke(stroke, thickness);
using (var path = new CGPath())
{
path.AddEllipseInRect(convertedRectangle);
this.gctx.AddPath(path);
}
this.gctx.DrawPath(CGPathDrawingMode.Stroke);
}
}
/// <summary>
/// Draws the specified portion of the specified <see cref="OxyImage" /> at the specified location and with the specified size.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="srcX">The x-coordinate of the upper-left corner of the portion of the source image to draw.</param>
/// <param name="srcY">The y-coordinate of the upper-left corner of the portion of the source image to draw.</param>
/// <param name="srcWidth">Width of the portion of the source image to draw.</param>
/// <param name="srcHeight">Height of the portion of the source image to draw.</param>
/// <param name="destX">The x-coordinate of the upper-left corner of drawn image.</param>
/// <param name="destY">The y-coordinate of the upper-left corner of drawn image.</param>
/// <param name="destWidth">The width of the drawn image.</param>
/// <param name="destHeight">The height of the drawn image.</param>
/// <param name="opacity">The opacity.</param>
/// <param name="interpolate">Interpolate if set to <c>true</c>.</param>
public override void DrawImage(OxyImage source, double srcX, double srcY, double srcWidth, double srcHeight, double destX, double destY, double destWidth, double destHeight, double opacity, bool interpolate)
{
var image = this.GetImage(source);
if (image == null)
{
return;
}
this.gctx.SaveState();
double x = destX - (srcX / srcWidth * destWidth);
double y = destY - (srcY / srcHeight * destHeight);
this.gctx.ScaleCTM(1, -1);
this.gctx.TranslateCTM((float)x, -(float)(y + destHeight));
this.gctx.SetAlpha((float)opacity);
this.gctx.InterpolationQuality = interpolate ? CGInterpolationQuality.High : CGInterpolationQuality.None;
var destRect = new CGRect(0f, 0f, (float)destWidth, (float)destHeight);
this.gctx.DrawImage(destRect, image.CGImage);
this.gctx.RestoreState();
}
/// <summary>
/// Cleans up resources not in use.
/// </summary>
/// <remarks>This method is called at the end of each rendering.</remarks>
public override void CleanUp()
{
var imagesToRelease = this.imageCache.Keys.Where(i => !this.imagesInUse.Contains(i)).ToList();
foreach (var i in imagesToRelease)
{
var image = this.GetImage(i);
image.Dispose();
this.imageCache.Remove(i);
}
this.imagesInUse.Clear();
}
/// <summary>
/// Sets the clip rectangle.
/// </summary>
/// <param name="rect">The clip rectangle.</param>
/// <returns>True if the clip rectangle was set.</returns>
public override bool SetClip(OxyRect rect)
{
this.gctx.SaveState();
this.gctx.ClipToRect(rect.Convert());
return true;
}
/// <summary>
/// Resets the clip rectangle.
/// </summary>
public override void ResetClip()
{
this.gctx.RestoreState();
}
/// <summary>
/// Draws a polyline.
/// </summary>
/// <param name="points">The points.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The stroke thickness.</param>
/// <param name="dashArray">The dash array.</param>
/// <param name="lineJoin">The line join type.</param>
/// <param name="aliased">if set to <c>true</c> the shape will be aliased.</param>
public override void DrawLine(IList<ScreenPoint> points, OxyColor stroke, double thickness, double[] dashArray, LineJoin lineJoin, bool aliased)
{
if (stroke.IsVisible() && thickness > 0)
{
this.SetAlias(aliased);
this.SetStroke(stroke, thickness, dashArray, lineJoin);
using (var path = new CGPath())
{
var convertedPoints = (aliased ? points.Select(p => p.ConvertAliased()) : points.Select(p => p.Convert())).ToArray();
path.AddLines(convertedPoints);
this.gctx.AddPath(path);
}
this.gctx.DrawPath(CGPathDrawingMode.Stroke);
}
}
/// <summary>
/// Draws a polygon. The polygon can have stroke and/or fill.
/// </summary>
/// <param name="points">The points.</param>
/// <param name="fill">The fill color.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The stroke thickness.</param>
/// <param name="dashArray">The dash array.</param>
/// <param name="lineJoin">The line join type.</param>
/// <param name="aliased">If set to <c>true</c> the shape will be aliased.</param>
public override void DrawPolygon(IList<ScreenPoint> points, OxyColor fill, OxyColor stroke, double thickness, double[] dashArray, LineJoin lineJoin, bool aliased)
{
this.SetAlias(aliased);
var convertedPoints = (aliased ? points.Select(p => p.ConvertAliased()) : points.Select(p => p.Convert())).ToArray();
if (fill.IsVisible())
{
this.SetFill(fill);
using (var path = new CGPath())
{
path.AddLines(convertedPoints);
path.CloseSubpath();
this.gctx.AddPath(path);
}
this.gctx.DrawPath(CGPathDrawingMode.Fill);
}
if (stroke.IsVisible() && thickness > 0)
{
this.SetStroke(stroke, thickness, dashArray, lineJoin);
using (var path = new CGPath())
{
path.AddLines(convertedPoints);
path.CloseSubpath();
this.gctx.AddPath(path);
}
this.gctx.DrawPath(CGPathDrawingMode.Stroke);
}
}
/// <summary>
/// Draws a rectangle.
/// </summary>
/// <param name="rect">The rectangle.</param>
/// <param name="fill">The fill color.</param>
/// <param name="stroke">The stroke color.</param>
/// <param name="thickness">The stroke thickness.</param>
public override void DrawRectangle(OxyRect rect, OxyColor fill, OxyColor stroke, double thickness)
{
this.SetAlias(true);
var convertedRect = rect.ConvertAliased();
if (fill.IsVisible())
{
this.SetFill(fill);
using (var path = new CGPath())
{
path.AddRect(convertedRect);
this.gctx.AddPath(path);
}
this.gctx.DrawPath(CGPathDrawingMode.Fill);
}
if (stroke.IsVisible() && thickness > 0)
{
this.SetStroke(stroke, thickness);
using (var path = new CGPath())
{
path.AddRect(convertedRect);
this.gctx.AddPath(path);
}
this.gctx.DrawPath(CGPathDrawingMode.Stroke);
}
}
/// <summary>
/// Draws the text.
/// </summary>
/// <param name="p">The position of the text.</param>
/// <param name="text">The text.</param>
/// <param name="fill">The fill color.</param>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontSize">Size of the font.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="rotate">The rotation angle.</param>
/// <param name="halign">The horizontal alignment.</param>
/// <param name="valign">The vertical alignment.</param>
/// <param name="maxSize">The maximum size of the text.</param>
public override void DrawText(ScreenPoint p, string text, OxyColor fill, string fontFamily, double fontSize, double fontWeight, double rotate, HorizontalAlignment halign, VerticalAlignment valign, OxySize? maxSize)
{
if (string.IsNullOrEmpty(text))
{
return;
}
var fontName = GetActualFontName(fontFamily, fontWeight);
var font = this.GetCachedFont(fontName, fontSize);
using (var attributedString = new NSAttributedString(text, new CTStringAttributes { ForegroundColorFromContext = true, Font = font }))
{
using (var textLine = new CTLine(attributedString))
{
nfloat width;
nfloat height;
this.gctx.TextPosition = new CGPoint(0, 0);
nfloat lineHeight, delta;
this.GetFontMetrics(font, out lineHeight, out delta);
var bounds = textLine.GetImageBounds(this.gctx);
if (maxSize.HasValue || halign != HorizontalAlignment.Left || valign != VerticalAlignment.Bottom)
{
width = bounds.Left + bounds.Width;
height = lineHeight;
}
else
{
width = height = 0f;
}
if (maxSize.HasValue)
{
if (width > maxSize.Value.Width)
{
width = (float)maxSize.Value.Width;
}
if (height > maxSize.Value.Height)
{
height = (float)maxSize.Value.Height;
}
}
var dx = halign == HorizontalAlignment.Left ? 0d : (halign == HorizontalAlignment.Center ? -width * 0.5 : -width);
var dy = valign == VerticalAlignment.Bottom ? 0d : (valign == VerticalAlignment.Middle ? height * 0.5 : height);
var x0 = -bounds.Left;
var y0 = delta;
this.SetFill(fill);
this.SetAlias(false);
this.gctx.SaveState();
this.gctx.TranslateCTM((float)p.X, (float)p.Y);
if (!rotate.Equals(0))
{
this.gctx.RotateCTM((float)(rotate / 180 * Math.PI));
}
this.gctx.TranslateCTM((float)dx + x0, (float)dy + y0);
this.gctx.ScaleCTM(1f, -1f);
if (maxSize.HasValue)
{
var clipRect = new CGRect (-x0, y0, (float)Math.Ceiling (width), (float)Math.Ceiling (height));
this.gctx.ClipToRect(clipRect);
}
textLine.Draw(this.gctx);
this.gctx.RestoreState();
}
}
}
/// <summary>
/// Measures the text.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontSize">Size of the font.</param>
/// <param name="fontWeight">The font weight.</param>
/// <returns>
/// The size of the text.
/// </returns>
public override OxySize MeasureText(string text, string fontFamily, double fontSize, double fontWeight)
{
if (string.IsNullOrEmpty(text) || fontFamily == null)
{
return OxySize.Empty;
}
var fontName = GetActualFontName(fontFamily, fontWeight);
var font = this.GetCachedFont(fontName, (float)fontSize);
using (var attributedString = new NSAttributedString(text, new CTStringAttributes { ForegroundColorFromContext = true, Font = font }))
{
using (var textLine = new CTLine(attributedString))
{
nfloat lineHeight, delta;
this.GetFontMetrics(font, out lineHeight, out delta);
this.gctx.TextPosition = new CGPoint(0, 0);
var bounds = textLine.GetImageBounds(this.gctx);
return new OxySize(bounds.Left + bounds.Width, lineHeight);
}
}
}
/// <summary>
/// Releases all resource used by the <see cref="OxyPlot.Xamarin.iOS.CoreGraphicsRenderContext"/> object.
/// </summary>
/// <remarks>Call <see cref="Dispose"/> when you are finished using the
/// <see cref="OxyPlot.Xamarin.iOS.CoreGraphicsRenderContext"/>. The <see cref="Dispose"/> method leaves the
/// <see cref="OxyPlot.Xamarin.iOS.CoreGraphicsRenderContext"/> in an unusable state. After calling
/// <see cref="Dispose"/>, you must release all references to the
/// <see cref="OxyPlot.Xamarin.iOS.CoreGraphicsRenderContext"/> so the garbage collector can reclaim the memory that
/// the <see cref="OxyPlot.Xamarin.iOS.CoreGraphicsRenderContext"/> was occupying.</remarks>
public void Dispose()
{
foreach (var image in this.imageCache.Values)
{
image.Dispose();
}
foreach (var font in this.fonts.Values)
{
font.Dispose();
}
}
/// <summary>
/// Gets the actual font for iOS.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontWeight">The font weight.</param>
/// <returns>The actual font name.</returns>
private static string GetActualFontName(string fontFamily, double fontWeight)
{
string fontName;
switch (fontFamily)
{
case null:
case "Segoe UI":
fontName = "HelveticaNeue";
break;
case "Arial":
fontName = "ArialMT";
break;
case "Times":
case "Times New Roman":
fontName = "TimesNewRomanPSMT";
break;
case "Courier New":
fontName = "CourierNewPSMT";
break;
default:
fontName = fontFamily;
break;
}
if (fontWeight >= 700)
{
fontName += "-Bold";
}
return fontName;
}
/// <summary>
/// Gets font metrics for the specified font.
/// </summary>
/// <param name="font">The font.</param>
/// <param name="defaultLineHeight">Default line height.</param>
/// <param name="delta">The vertical delta.</param>
private void GetFontMetrics(CTFont font, out nfloat defaultLineHeight, out nfloat delta)
{
var ascent = font.AscentMetric;
var descent = font.DescentMetric;
var leading = font.LeadingMetric;
//// http://stackoverflow.com/questions/5511830/how-does-line-spacing-work-in-core-text-and-why-is-it-different-from-nslayoutm
leading = leading < 0 ? 0 : (float)Math.Floor(leading + 0.5f);
var lineHeight = (nfloat)Math.Floor(ascent + 0.5f) + (nfloat)Math.Floor(descent + 0.5) + leading;
var ascenderDelta = leading >= 0 ? 0 : (nfloat)Math.Floor((0.2 * lineHeight) + 0.5);
defaultLineHeight = lineHeight + ascenderDelta;
delta = ascenderDelta - descent;
}
/// <summary>
/// Gets the specified from cache.
/// </summary>
/// <returns>The font.</returns>
/// <param name="fontName">Font name.</param>
/// <param name="fontSize">Font size.</param>
private CTFont GetCachedFont(string fontName, double fontSize)
{
var key = fontName + fontSize.ToString("0.###");
CTFont font;
if (this.fonts.TryGetValue(key, out font))
{
return font;
}
return this.fonts[key] = new CTFont(fontName, (float)fontSize);
}
/// <summary>
/// Sets the alias state.
/// </summary>
/// <param name="alias">alias if set to <c>true</c>.</param>
private void SetAlias(bool alias)
{
this.gctx.SetShouldAntialias(!alias);
}
/// <summary>
/// Sets the fill color.
/// </summary>
/// <param name="c">The color.</param>
private void SetFill(OxyColor c)
{
this.gctx.SetFillColor(c.ToCGColor());
}
/// <summary>
/// Sets the stroke style.
/// </summary>
/// <param name="c">The stroke color.</param>
/// <param name="thickness">The stroke thickness.</param>
/// <param name="dashArray">The dash array.</param>
/// <param name="lineJoin">The line join.</param>
private void SetStroke(OxyColor c, double thickness, double[] dashArray = null, LineJoin lineJoin = LineJoin.Miter)
{
this.gctx.SetStrokeColor(c.ToCGColor());
this.gctx.SetLineWidth((float)thickness);
this.gctx.SetLineJoin(lineJoin.Convert());
if (dashArray != null)
{
var lengths = dashArray.Select(d => (nfloat)d).ToArray();
this.gctx.SetLineDash(0f, lengths);
}
else
{
this.gctx.SetLineDash(0, null);
}
}
/// <summary>
/// Gets the image from cache or converts the specified <paramref name="source"/> <see cref="OxyImage"/>.
/// </summary>
/// <param name="source">The source.</param>
/// <returns>The image.</returns>
private UIImage GetImage(OxyImage source)
{
if (source == null)
{
return null;
}
if (!this.imagesInUse.Contains(source))
{
this.imagesInUse.Add(source);
}
UIImage src;
if (!this.imageCache.TryGetValue(source, out src))
{
using (var data = NSData.FromArray(source.GetData()))
{
src = UIImage.LoadFromData(data);
}
this.imageCache.Add(source, src);
}
return src;
}
}
}