Skiasharp: Improvements for SKCanvasViewRenderer

Created on 8 Mar 2017  路  8Comments  路  Source: mono/SkiaSharp

Hi!

I'ts been a while that we touched our SVG project, but now we're on the go again and it is amazing to see how SkiaSharp has evolved so far. Congratulations - you're doing a great job!

That said, we recently came across a problem when inheriting the SKCanvasViewRenderer as described in: https://github.com/mattleibow/SkiaSharpFormsRendererDemo

The problem is, that we need to create our own view on Android and iOS, as we need to override their "public override void TouchesBegan(NSSet touches, UIEvent evt)" and or "public override bool OnTouchEvent(MotionEvent ev)" methods.

Problem: when inheriting your SKCanvasViewRenderer, that viewtype is basically fixed.
So we refactored it a bit and created a "SKCanvasViewRendererBase"

Note: I would love to create a PR for this, but it is sooooo cumbersome to get skia to build on my windows machine. If there was a script that did not only update the submodule but also build it, that would be soooooo great! (and bring a lot of contributors to your project)

In order to create our own renderer, we copied your SkiaSharp.Views.Forms.SKCanvasView (because its interface ISKCanvasViewController is internal)
The only thing we changed is the "X" at the end of the classname and that the interface is now public

using System;
using Xamarin.Forms;

namespace SkiaSharp.Views.Forms
{
    public class SKCanvasViewX : View, ISKCanvasViewController
    {
        public static readonly BindableProperty IgnorePixelScalingProperty =
            BindableProperty.Create(nameof(IgnorePixelScaling), typeof(bool), typeof(SKCanvasViewX), default(bool));

        // the user can subscribe to repaint
        public event EventHandler<SKPaintSurfaceEventArgs> PaintSurface;

        // the native listens to this event
        private event EventHandler SurfaceInvalidated;
        private event EventHandler<GetCanvasSizeEventArgs> GetCanvasSize;

        // the user asks the for the size
        public SKSize CanvasSize
        {
            get
            {
                // send a mesage to the native view
                var args = new GetCanvasSizeEventArgs();
                GetCanvasSize?.Invoke(this, args);
                return args.CanvasSize;
            }
        }

        public bool IgnorePixelScaling
        {
            get { return (bool)GetValue(IgnorePixelScalingProperty); }
            set { SetValue(IgnorePixelScalingProperty, value); }
        }

        // the user asks to repaint
        public void InvalidateSurface()
        {
            // send a mesage to the native view
            SurfaceInvalidated?.Invoke(this, EventArgs.Empty);
        }

        // the native view tells the user to repaint
        protected virtual void OnPaintSurface(SKPaintSurfaceEventArgs e)
        {
            PaintSurface?.Invoke(this, e);
        }

        // ISKViewController implementation

        event EventHandler ISKCanvasViewController.SurfaceInvalidated
        {
            add { SurfaceInvalidated += value; }
            remove { SurfaceInvalidated -= value; }
        }

        event EventHandler<GetCanvasSizeEventArgs> ISKCanvasViewController.GetCanvasSize
        {
            add { GetCanvasSize += value; }
            remove { GetCanvasSize -= value; }
        }

        void ISKCanvasViewController.OnPaintSurface(SKPaintSurfaceEventArgs e)
        {
            OnPaintSurface(e);
        }
    }

    public interface ISKCanvasViewController : IViewController
    {
        // the native listens to this event
        event EventHandler SurfaceInvalidated;
        event EventHandler<GetCanvasSizeEventArgs> GetCanvasSize;

        // the native view tells the user to repaint
        void OnPaintSurface(SKPaintSurfaceEventArgs e);
    }
    public class GetCanvasSizeEventArgs : EventArgs
    {
        public SKSize CanvasSize { get; set; }
    }
}

with that, we could e.g. create a SKCanvasViewRendererBase for Android (iOS looks the same):

using System;
using System.ComponentModel;
using Xamarin.Forms.Platform.Android;

using SKFormsView = SkiaSharp.Views.Forms.SKCanvasViewX;
using SKNativeView = SkiaSharp.Views.Android.SKCanvasView;

namespace SkiaSharp.Views.Forms
{
    public class SKCanvasViewRendererBase<TFormsView, TNativeView> : ViewRenderer<TFormsView, TNativeView>
        where TNativeView : SKNativeView, IPaintSurface
        where TFormsView : SKFormsView
    {
        protected override void OnElementChanged(ElementChangedEventArgs<TFormsView> e)
        {
            if (e.OldElement != null)
            {
                var oldController = (ISKCanvasViewController)e.OldElement;

                // unsubscribe from events
                oldController.SurfaceInvalidated -= OnSurfaceInvalidated;
                oldController.GetCanvasSize -= OnGetCanvasSize;
            }
            if (Control != null)
            {
                Control.PaintSurface -= OnPaintSurface;
            }

            if (e.NewElement != null)
            {
                var newController = (ISKCanvasViewController)e.NewElement;

                // create the native view
                var view = CreateNativeView();
                view.IgnorePixelScaling = e.NewElement.IgnorePixelScaling;
                view.PaintSurface += OnPaintSurface;
                SetNativeControl(view);

                // subscribe to events from the user
                newController.SurfaceInvalidated += OnSurfaceInvalidated;
                newController.GetCanvasSize += OnGetCanvasSize;

                // paint for the first time
                Control.Invalidate();
            }

            base.OnElementChanged(e);
        }

        protected virtual TNativeView CreateNativeView()
        {
            var view = (TNativeView)Activator.CreateInstance(typeof(TNativeView), new object[] {Context, null});
            return view;
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == nameof(SKFormsView.IgnorePixelScaling))
            {
                Control.IgnorePixelScaling = Element.IgnorePixelScaling;
            }
        }

        protected override void Dispose(bool disposing)
        {
            // detach all events before disposing
            var controller = (ISKCanvasViewController)Element;
            if (controller != null)
            {
                controller.SurfaceInvalidated -= OnSurfaceInvalidated;
            }

            base.Dispose(disposing);
        }

        private void OnSurfaceInvalidated(object sender, EventArgs eventArgs)
        {
            // repaint the native control
            Control.Invalidate();
        }

        // the user asked for the size
        private void OnGetCanvasSize(object sender, GetCanvasSizeEventArgs e)
        {
            e.CanvasSize = Control?.CanvasSize ?? SKSize.Empty;
        }

        private void OnPaintSurface(object sender, Android.SKPaintSurfaceEventArgs e)
        {
            var controller = this.Element as ISKCanvasViewController;

            // the control is being repainted, let the user know
            controller?.OnPaintSurface(new SKPaintSurfaceEventArgs(e.Surface, e.Info));
        }
    }
}

=> thanks to "protected virtual TNativeView CreateNativeView()" we can now create our very own view - yay!!

Now the problem you tried to solve by the internal class was to call the "SKCanvasView.OnPaintSurface" method once the native view was rendered, so skia could work its magic.

We solved that by simply creating an interface (which is basically already implemented by all your SkiaSharp views)

using System;
using SkiaSharp;
#if WINDOWS_UWP
using SkiaSharp.Views.UWP;
#elif PLATFORM_ANDROID
using SkiaSharp.Views.Android;
#else
using SkiaSharp.Views.iOS;
#endif

namespace SkiaSharp.Views
{
    public interface IPaintSurface
    {
        event EventHandler<SKPaintSurfaceEventArgs> PaintSurface;
    }
}

so the renderer can forward the rendering using that interface. This is the reason for the generic definition:
public class SKCanvasViewRendererBase : ViewRenderer
where TNativeView : SKNativeView, IPaintSurface
where TFormsView : SKFormsView

Now using this base renderer, we can easily create our own one, which uses it own tailor-made native view:

[assembly: ExportRenderer(typeof( Svg.Editor.Forms.SvgCanvasEditorView), typeof(SvgCanvasEditorViewRenderer))]
namespace Svg.Editor.Forms.Droid
{
    public class SvgCanvasEditorViewRenderer : SKCanvasViewRendererBase< Svg.Editor.Forms.SvgCanvasEditorView, Svg.Editor.Views.Droid.SvgCanvasEditorView>
    {
     }
}

For a running example, look at our "uwp_ios" branch: https://github.com/gentledepp/SVG/tree/uwp_ios

;-)

Most helpful comment

Moving to the next release so we can get the new skia out.

All 8 comments

Thanks for this! I will get this in for the next release. I am just about to release v1.56.2, so I don't want to change too much at this point. Thanks again.

About building on Windows, I left a comment on your other issue. I want to get this all smooth!

Ok, but if you let your "SKCanvasViewRenderer" inherit from "SKCanvasViewRendererBase" then this is even downwardly compatible ;-)

That is what I will probably do.

Is there a reason you didn't just inherit from my Forms SKCanvasView? Did you really need access to one of the members of the ISKCanvasViewController interface, or was it just as a result of creating a new type?

Yes: SKCanvasViewhas the interface ISKCanvasViewControllerinternal
which in turn is required for the SKCanvasViewRenderer

so that is the only reason.

Had I had been able to compile SkiaSharp, I would just have

  1. created that SKCanvasViewRendererBase
  2. let SKCanvasViewRenderer inherit from it
  3. and implement the IPaintSurface interface in all native view classes (which is needed to get rid of your internal "NativeView" and allow for using our custom native views)

Still have some problems building, but I hope I will eventually be able to solve that.
Meanwhile I had a closer look at the SKGLView and its renderers and as it looks, the renderers could be equally refactored to support custom native views using the following no-brainer interface:

using System;
using SkiaSharp;
#if WINDOWS_UWP
using SkiaSharp.Views.UWP;
#elif PLATFORM_ANDROID
using SkiaSharp.Views.Android;
#else
using SkiaSharp.Views.iOS;
#endif

namespace SkiaSharp.Views
{
    public interface IPaintSurface
    {
        event EventHandler<SKPaintGLSurfaceEventArgs> PaintSurface;
    }
}

So it would be reeeeeally great if that could be implemented some time. Maybe I will even be able to create a PR for this 馃憤

@gentledepp I am still working on this, but here is what I have so far: PR #269

I have basically created a base type for the renderers (one for GL and one for Raster). This base does nothing except hook up events.

Then, for each Raster platform there can be some customization. Right now there is none, except for iOS which changes the opacity.

For the GL platforms, I hook up the render loops.

For extension purposes, you can inherit from the base renderer and this gives you the "factory" state for your own view. If you use the "platform" renderer type, then you will get some "consistency" (iOS opacity) changes and the render loop.

In each case, all you should have to do is to implement the CreateNativeControl method:

protected override SKCanvasView CreateNativeControl()
{
    // use the base / default type
    //var view = base.CreateNativeControl();

    // use a custom type
    return new MyCustomCanvasView();
}

Moving to the next release so we can get the new skia out.

Closed after #269 merged.

Was this page helpful?
0 / 5 - 0 ratings