oxyplot-xamarin/Source/OxyPlot.Xamarin.Android/CanvasRenderContext.cs

576 строки
22 KiB
C#
Исходник Ответственный История

Этот файл содержит неоднозначные символы Юникода!

Этот файл содержит неоднозначные символы Юникода, которые могут быть перепутаны с другими в текущей локали. Если это намеренно, можете спокойно проигнорировать это предупреждение. Используйте кнопку Экранировать, чтобы подсветить эти символы.

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="CanvasRenderContext.cs" company="OxyPlot">
// Copyright (c) 2014 OxyPlot contributors
// </copyright>
// <summary>
// Provides a render context for Android.Graphics.Canvas.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace OxyPlot.Xamarin.Android
{
using System;
using System.Collections.Generic;
using System.Linq;
using global::Android.Graphics;
/// <summary>
/// Provides a render context for Android.Graphics.Canvas.
/// </summary>
public class CanvasRenderContext : RenderContextBase
{
/// <summary>
/// The images in use
/// </summary>
private readonly HashSet<OxyImage> imagesInUse = new HashSet<OxyImage>();
/// <summary>
/// The image cache
/// </summary>
private readonly Dictionary<OxyImage, Bitmap> imageCache = new Dictionary<OxyImage, Bitmap>();
/// <summary>
/// The current paint.
/// </summary>
private readonly Paint paint;
/// <summary>
/// A reusable path object.
/// </summary>
private readonly Path path;
/// <summary>
/// A reusable bounds rectangle.
/// </summary>
private readonly Rect bounds;
/// <summary>
/// A reusable list of points.
/// </summary>
private readonly List<float> pts;
/// <summary>
/// The canvas to draw on.
/// </summary>
private Canvas canvas;
/// <summary>
/// Initializes a new instance of the <see cref="CanvasRenderContext" /> class.
/// </summary>
/// <param name="scale">The scale.</param>
public CanvasRenderContext(double scale)
{
this.paint = new Paint();
this.path = new Path();
this.bounds = new Rect();
this.pts = new List<float>();
this.Scale = scale;
}
/// <summary>
/// Gets the factor that this.Scales from OxyPlot´s device independent pixels (96 dpi) to
/// Android´s density-independent pixels (160 dpi).
/// </summary>
/// <remarks>See <a href="http://developer.android.com/guide/practices/screens_support.html">Supporting multiple screens.</a>.</remarks>
public double Scale { get; private set; }
/// <summary>
/// Sets the target.
/// </summary>
/// <param name="c">The canvas.</param>
public void SetTarget(Canvas c)
{
this.canvas = c;
}
/// <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.paint.Reset();
{
if (fill.IsVisible())
{
this.SetFill(fill);
this.canvas.DrawOval(this.Convert(rect), this.paint);
}
if (stroke.IsVisible())
{
this.SetStroke(stroke, thickness);
this.canvas.DrawOval(this.Convert(rect), this.paint);
}
}
}
/// <summary>
/// Draws the collection of ellipses, where all have the same stroke and fill.
/// This performs better than calling DrawEllipse multiple times.
/// </summary>
/// <param name="rectangles">The rectangles.</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 DrawEllipses(IList<OxyRect> rectangles, OxyColor fill, OxyColor stroke, double thickness)
{
this.paint.Reset();
{
foreach (var rect in rectangles)
{
if (fill.IsVisible())
{
this.SetFill(fill);
this.canvas.DrawOval(this.Convert(rect), this.paint);
}
if (stroke.IsVisible())
{
this.SetStroke(stroke, thickness);
this.canvas.DrawOval(this.Convert(rect), this.paint);
}
}
}
}
/// <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)
{
this.paint.Reset();
{
this.path.Reset();
{
this.SetPath(points, aliased);
this.SetStroke(stroke, thickness, dashArray, lineJoin, aliased);
this.canvas.DrawPath(this.path, this.paint);
}
}
}
/// <summary>
/// Draws multiple line segments defined by points (0,1) (2,3) (4,5) etc.
/// This should have better performance than calling DrawLine for each segment.
/// </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 DrawLineSegments(IList<ScreenPoint> points, OxyColor stroke, double thickness, double[] dashArray, LineJoin lineJoin, bool aliased)
{
this.paint.Reset();
{
this.SetStroke(stroke, thickness, dashArray, lineJoin, aliased);
this.pts.Clear();
if (aliased)
{
foreach (var p in points)
{
this.pts.Add(this.ConvertAliased(p.X));
this.pts.Add(this.ConvertAliased(p.Y));
}
}
else
{
foreach (var p in points)
{
this.pts.Add(this.Convert(p.X));
this.pts.Add(this.Convert(p.Y));
}
}
this.canvas.DrawLines(this.pts.ToArray(), this.paint);
}
}
/// <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.paint.Reset();
{
this.path.Reset();
{
this.SetPath(points, aliased);
this.path.Close();
if (fill.IsVisible())
{
this.SetFill(fill);
this.canvas.DrawPath(this.path, this.paint);
}
if (stroke.IsVisible())
{
this.SetStroke(stroke, thickness, dashArray, lineJoin, aliased);
this.canvas.DrawPath(this.path, this.paint);
}
}
}
}
/// <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.paint.Reset();
{
if (fill.IsVisible())
{
this.SetFill(fill);
this.canvas.DrawRect(this.ConvertAliased(rect.Left), this.ConvertAliased(rect.Top), this.ConvertAliased(rect.Right), this.ConvertAliased(rect.Bottom), this.paint);
}
if (stroke.IsVisible())
{
this.SetStroke(stroke, thickness, aliased: true);
this.canvas.DrawRect(this.ConvertAliased(rect.Left), this.ConvertAliased(rect.Top), this.ConvertAliased(rect.Right), this.ConvertAliased(rect.Bottom), this.paint);
}
}
}
/// <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)
{
this.paint.Reset();
{
this.paint.TextSize = this.Convert(fontSize);
this.SetFill(fill);
float width;
float height;
float lineHeight, delta;
this.GetFontMetrics(this.paint, out lineHeight, out delta);
if (maxSize.HasValue || halign != HorizontalAlignment.Left || valign != VerticalAlignment.Bottom)
{
this.paint.GetTextBounds(text, 0, text.Length, this.bounds);
width = this.bounds.Left + this.bounds.Width();
height = lineHeight;
}
else
{
width = height = 0f;
}
if (maxSize.HasValue)
{
var maxWidth = this.Convert(maxSize.Value.Width);
var maxHeight = this.Convert(maxSize.Value.Height);
if (width > maxWidth)
{
width = maxWidth;
}
if (height > maxHeight)
{
height = maxHeight;
}
}
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 = -this.bounds.Left;
var y0 = delta;
this.canvas.Save();
this.canvas.Translate(this.Convert(p.X), this.Convert(p.Y));
this.canvas.Rotate((float)rotate);
this.canvas.Translate((float)dx + x0, (float)dy + y0);
if (maxSize.HasValue)
{
var x1 = -x0;
var y1 = -height - y0;
this.canvas.ClipRect(x1, y1, x1 + width, y1 + height);
this.canvas.Translate(0, lineHeight - height);
}
this.canvas.DrawText(text, 0, 0, this.paint);
this.canvas.Restore();
}
}
/// <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 text size.</returns>
public override OxySize MeasureText(string text, string fontFamily, double fontSize, double fontWeight)
{
if (string.IsNullOrEmpty(text))
{
return OxySize.Empty;
}
this.paint.Reset();
{
this.paint.AntiAlias = true;
this.paint.TextSize = this.Convert(fontSize);
float lineHeight, delta;
this.GetFontMetrics(this.paint, out lineHeight, out delta);
this.paint.GetTextBounds(text, 0, text.Length, this.bounds);
return new OxySize(this.bounds.Width() / this.Scale, lineHeight / this.Scale);
}
}
/// <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.canvas.Save();
this.canvas.ClipRect(this.Convert(rect));
return true;
}
/// <summary>
/// Resets the clip rectangle.
/// </summary>
public override void ResetClip()
{
this.canvas.Restore();
}
/// <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;
}
var src = new Rect((int)srcX, (int)srcY, (int)(srcX + srcWidth), (int)(srcY + srcHeight));
var dest = new RectF(this.Convert(destX), this.Convert(destY), this.Convert(destX + destWidth), this.Convert(destY + destHeight));
this.paint.Reset();
// TODO: support opacity
this.canvas.DrawBitmap(image, src, dest, this.paint);
}
/// <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>
/// Gets font metrics for the font in the specified paint.
/// </summary>
/// <param name="paint">The paint.</param>
/// <param name="defaultLineHeight">Default line height.</param>
/// <param name="delta">The vertical delta.</param>
private void GetFontMetrics(Paint paint, out float defaultLineHeight, out float delta)
{
var metrics = paint.GetFontMetrics();
var ascent = -metrics.Ascent;
var descent = metrics.Descent;
var leading = metrics.Leading;
//// 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 = (float)Math.Floor(ascent + 0.5f) + (float)Math.Floor(descent + 0.5) + leading;
var ascenderDelta = leading >= 0 ? 0 : (float)Math.Floor((0.2 * lineHeight) + 0.5);
defaultLineHeight = lineHeight + ascenderDelta;
delta = ascenderDelta - descent;
}
/// <summary>
/// Converts the specified coordinate to a scaled coordinate.
/// </summary>
/// <param name="x">The coordinate to convert.</param>
/// <returns>The converted coordinate.</returns>
private float Convert(double x)
{
return (float)(x * this.Scale);
}
/// <summary>
/// Converts the specified rectangle to a scaled rectangle.
/// </summary>
/// <param name="rect">The rectangle to convert.</param>
/// <returns>The converted rectangle.</returns>
private RectF Convert(OxyRect rect)
{
return new RectF(this.ConvertAliased(rect.Left), this.ConvertAliased(rect.Top), this.ConvertAliased(rect.Right), this.ConvertAliased(rect.Bottom));
}
/// <summary>
/// Converts the specified coordinate to a pixel-aligned scaled coordinate.
/// </summary>
/// <param name="x">The coordinate to convert.</param>
/// <returns>The converted coordinate.</returns>
private float ConvertAliased(double x)
{
return (int)(x * this.Scale) + 0.5f;
}
/// <summary>
/// Sets the path to the specified points.
/// </summary>
/// <param name="points">The points defining the path.</param>
/// <param name="aliased">If set to <c>true</c> aliased.</param>
private void SetPath(IList<ScreenPoint> points, bool aliased)
{
if (aliased)
{
this.path.MoveTo(this.ConvertAliased(points[0].X), this.ConvertAliased(points[0].Y));
for (int i = 1; i < points.Count; i++)
{
this.path.LineTo(this.ConvertAliased(points[i].X), this.ConvertAliased(points[i].Y));
}
}
else
{
this.path.MoveTo(this.Convert(points[0].X), this.Convert(points[0].Y));
for (int i = 1; i < points.Count; i++)
{
this.path.LineTo(this.Convert(points[i].X), this.Convert(points[i].Y));
}
}
}
/// <summary>
/// Sets the fill style.
/// </summary>
/// <param name="fill">The fill color.</param>
private void SetFill(OxyColor fill)
{
this.paint.SetStyle(Paint.Style.Fill);
this.paint.Color = fill.ToColor();
this.paint.AntiAlias = true;
}
/// <summary>
/// Sets the stroke style.
/// </summary>
/// <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.</param>
/// <param name="aliased">Use aliased strokes if set to <c>true</c>.</param>
private void SetStroke(OxyColor stroke, double thickness, double[] dashArray = null, LineJoin lineJoin = LineJoin.Miter, bool aliased = false)
{
this.paint.SetStyle(Paint.Style.Stroke);
this.paint.Color = stroke.ToColor();
this.paint.StrokeWidth = this.Convert(thickness);
this.paint.StrokeJoin = lineJoin.Convert();
if (dashArray != null)
{
var dashArrayF = dashArray.Select(this.Convert).ToArray();
this.paint.SetPathEffect(new DashPathEffect(dashArrayF, 0f));
}
this.paint.AntiAlias = !aliased;
}
/// <summary>
/// Gets the image from cache or creates a new <see cref="Bitmap" />.
/// </summary>
/// <param name="source">The source image.</param>
/// <returns>A <see cref="Bitmap" />.</returns>
private Bitmap GetImage(OxyImage source)
{
if (source == null)
{
return null;
}
if (!this.imagesInUse.Contains(source))
{
this.imagesInUse.Add(source);
}
Bitmap bitmap;
if (!this.imageCache.TryGetValue(source, out bitmap))
{
var bytes = source.GetData();
bitmap = BitmapFactory.DecodeByteArray(bytes, 0, bytes.Length);
this.imageCache.Add(source, bitmap);
}
return bitmap;
}
}
}