It’s July again and that means that it’s also time for the .NET MAUI UI July challenge! This year I’ll be participating as well by helping you to create a Hue Color Slider with .NET MAUI and SkiaSharp, leading you step-by-step on how to achieve this.
Before we start diving into the wonderful world of Skia, a big shoutout to Matt Goldman for hosting this year’s .NET MAUI UI July challenge! Every year there are so many great blogpost for .NET MAUI developers to learn from. So, thank you for doing this again this year and thank you for giving me a spot on the calendar as well. It’s now July 14th and already so many great blog post are available, so after reading this blog post, make sure to have a look here for all the other ones: https://goforgoldman.com/posts/mauiuijuly-25/
In this post we’re going to create a Hue Color Slider component based on SkiaSharp. Searching on Dribbble for some inspiration, I’ve found this nice color/hue slider component: https://dribbble.com/shots/5279702-HTML5-Color-Hue-Slider. Let’s try to implement this. We’ll go through this step by step, and I’ll try to explain the how’s and why’s along the way. Let’s see what steps we need to take to create this HueColorSlider component:
So now it’s time to get our hands dirty.
After you’ve created a plain .NET MAUI project, install the following NuGet package:
Then configure SkiaSharp by adding the .UseSkiaSharp()
to our MauiAppBuilder code like this:
This extension method configures/registers the handler for the SKCanvasView
and some other basic stuff which we won’t be covering in today’s blog post. Our component will be based on this SKCanvasView
, which allows us to draw directly on a canvas to create our component.
Now let’s create our component and place this on our main page.
As you can see our component derives from SKCanvasView
. This gives us a canvas where we can directly draw on all the particles we need to make our component. Also does this give us the OnPaintSurface(SKPaintSurfaceEventArgs e)
method.
Let’s put a breakpoint inside the overridden method and see when it gets called. As you can see is that this method is called just once, after the page is rendered and the surface became available for us to use by the SKCanvasView
. But what happens if you rotate your device? The OnPaintSurface()
is called again, but with difference surface/canvas dimensions. This allows us to recalculate and redraw our component every time the device rotates, or our window is getting resized etc.
Before we are creating the entire component including interactions, first let’s draw the slider bar and thumb on a fixed location.
Starting off with defining some constant values for our component, the component height, slider bar height, thumb radius and thumb thickness.
Inside the OnPaintSurface()
let’s store some important information like the available canvas width and height and the canvas itself. And call the DrawComponent()
method that we’ll be creating.
Now create the DrawComponent()
method where we are going to add all the particles we need to draw the entire component, like the slider bar and thumb. Inside this method start with clearing the canvas, as on every surface invalidation, we need to clear the canvas and re-draw everything. Then call the DrawSliderBar()
and DrawThumb()
, which we will be implementing in a bit.
Last thing that we’ve forgot to do is to give the component a fixed height to use. We only need to use the height that the thumb needs, so we can set the canvas height fixed to the const value we’ve defined earlier:
One other very important thing that I’ve set in the constructor is the IgnorePixelScaling
property. As we want our component to look the same on every devices (in size), independent on the screens pixel density, we need to set IgnorePixelScaling = true
. This will prevent SkiaSharp using every single pixel available on the screen, but scales the canvas for us depending on the screens pixel density. F.e. if a screen has a resolution of 1920x1080 with a pixel density of 3, now we have an available screen height and width of 640x360.
If we don’t set this, devices with a higher screen pixel density will draw our component a lot smaller, or we need to scale our component ourselves. When creating my first .NET MAUI SkiaSharp component I wasn’t aware of this option and I did all the calculations myself. Which makes your code less readable and more complex, totally unneccessary. So make it easier for yourselve and use this option!
For the slider bar we need to draw a rounded rectangle. To be able to do that we need to calculate the rectangle's size and position, create a paint with a gradient shader and draw the rounded rectangle on our canvas.
Let’s calculate the slider bar size and position. As the thumb should be centered on top of the slider bar on the left and right, we need to add a ‘margin’ for the slider bar to prevent the thumb falling of our canvas. Also we need to define the top and bottom so that the slider bar is vertically centered on our canvas.
Next up is defining our SKPaint
with a gradient shader. For the shader we need to define a set of colors that will be used to create a lineair gradient. Define this in our constructor:
And then back inside the DrawSliderBar()
method create a SKPaint
with the gradient shader:
Last thing we need to do is to actually draw our slider with the calculated size and position and the paint we’ve defined on our canvas.
The SKCanvas
contains tons of very useful drawing methods that we could use. F.e.:
For our slider bar we are drawing a rounded rectangle to be able to round our corners with our defined SliderBarRadius
:
Now let’s run the app and see how our slider bar looks like:
For the thumb we are drawing 2 circles, one with a filled paint style and one with a stroke paint style. The latter one will be the border of the thumb. So let’s start with creating both paints:
Then, for now, we’ll show the thumb in the middle of our canvas. Since a circle is drawn from it’s center x and y position, we need to calculate these:
And finally we draw the thumb by drawing 2 circles, where the second is the actual thumb border due to the stroke typed paint. Please note that we change the size (or radius) of the circle half the stroke thickness smaller, because the border is positioned half inside and half outside the circle.
And then if we run the code, our slider bar with thumb looks like this:
First thing to do is to enable touch events in the constructor of our component. By default this is disabled and therefor we need to enable this:
Next thing is to listen to the touch event by overriding the OnTouch(SKTouchEventArgs e)
method. Inside the event arguments, amongst others, the location and action type are available. Let’s create a few methods to route/handle the pressed, moved, canceled and released actions (which we will implement in the next step). We also need to set the handled property to true to tell Skia that we have handled it and it should not have to be propagated any further.
Try setting a debug point inside the OnTouch()
method and see what happens when you click, hold and or move your touch inside the component. It's quite hard to debug as when a debug point hits, you will release your touch to check out any debug details and then the released touch action is invoked.
Next step is to actually move the thumb when touching and moving it. First we need to move the thumbCenterX
and thumbCenterY
variables outside of the DrawThumb()
, otherwise the thumb will remain on the static defined location. Add this to the OnPaintSurface()
method just before calling the DrawComponent()
:
Now we’ve set the initial position of the thumb, so next up is to move the thumb position while holding and moving the thumb. We’ve already routed all the touch actions, let’s keep track of when the user is holding the thumb, so we know when to move the thumb and when to ignore any other touch actions.
Now inside the HandleMovedTouchAction()
method we can actually move the thumb position. We only need to move the ‘X’ position, as we want the thumb to remain on the slider bar.
Let’s put a break point inside this method and see if the thumb position is being changed.
As you can see the _thumbCenterX
is being updated properly, but why is the thumb not being moved on the UI?
We forgot to re-draw the UI… So, can we just clear the canvas and call DrawComponent()
and we are done? If we do so, it could happen that our app freezes, as it potentially can’t handle the amount of requested re-draws. One thing I found out the hard way…
Luckily for us there is a much easier option available: InvalidateSurface()
. If we call this Skia knows that the surface is invalid and a re-draw needs to happen. Skia will handle the ‘refresh rate’ so that the app doesn’t run in too much re-draws that it can’t handle it anymore.
On the Dribbble design when the thumb is being selected and hold, we see that the thumb is being resized by +/- 10%. This results in a nice animation-like feedback that we actually hold the thumb and we can move it around. Add the following code just before drawing both the thumb inner circle and the border and use this thumbRadius
instead for drawing both circles:
And next is we need to call the InvalidateSurface()
after the _isHoldingThumb
changes inside the HandlePressedTouchAction()
and HandleReleasedTouchAction()
methods. As easy as it can be!
Now our component is almost done, we need to expose the selected color to the page that holds our component. When we are able to define which color is actually selected, we can also change the thumb border color to the selected color, just as the Dribbble design shows. One other issue will also be fixed when implemented this. Now we set the ‘X’ position of the thumb when moving the thumb, but what if the screen resizes, or you rotate your device? Then the selected color will be different, as the thumb will still be shown at position ‘X’, only that position could potentially be different as the screen has changed. So, we need to calculate the x and y position of the thumb according to the selected color instead.
Let’s first create a Bindable Property:
Then update the selected color when the thumb moves:
Then, update the thumb location according to the SelectedColor
(via the UpdateThumbLocation()
method). This way we also implement binding both ways:
Final thing to do is set the initial thumb position in the InvalidateSurface()
when this component is created with a different default selected color:
And now we can also easily use the selected color as our thumb’s border color that automatically changes along moving the thumb like this:
Now we have the selected color from our HueColorSlider component available on our page to be used like this (for example):
Check out the full sample project on my GitHub.
Let me know if it was helpful to you and what you think of it and if you would use this in any of your apps. Feel free to reach out to me on LinkedIn or simply by sending me an e-mail. I would love to hear from you! Also, if you are struggling with some .NET MAUI related stuff, feel free to contact me.