Nemesis
Nemesis

Reputation: 109

.NET MAUI Create square SKCanvasView

I am porting an application from Xamarin to MAUI. I am trying to create a SkiaSharp SKCanvasView that is square. It is inside a Grid as I am oerlaying 2 SVG images and a Label. This is how I did it in Xamarin:

<Grid x:Name="compassGrid" Margin="50,10">
    <Grid.RowDefinitions>
        <RowDefinition Height="auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <skia:SKCanvasView x:Name="compassCanvasView" PaintSurface="compassCanvasView_PaintSurface"
                       HeightRequest="{Binding Width, Source={x:Reference compassGrid}}"
                       MinimumHeightRequest="{Binding Height, Source={x:Reference compassGrid}}"
                       WidthRequest="{Binding Height, Source={x:Reference compassGrid}}"
                       MinimumWidthRequest="{Binding Width, Source={x:Reference compassGrid}}"/>
    <skia:SKCanvasView x:Name="arrowCanvasView" PaintSurface="arrowCanvasView_PaintSurface" />
    <Label Text="{Binding CompassReading}" 
           TextColor="{StaticResource Gray-300}" 
           FontSize="40" 
           HorizontalTextAlignment="Center" 
           VerticalTextAlignment="Center"/>
</Grid>

And this worked fine, Resulting in this:

enter image description here

Now, with MAUI, the same does not work and results in this:

enter image description here

The Image is basically oval instead of round. Then when I rotate the compass, obviously the longer sides goes outside the canvas with is rectangular instead of square.

How can I get it to look like before?

Edit:
As per request from @FreakyAli here are sample apps for both Xamarin and MAUI (https://github.com/NemesisXB/CompassXamMaui). The maui version initially shows nothing until you rotate the screen once, then it exhibits the same problem.

Upvotes: 2

Views: 2158

Answers (2)

Nick Kovalsky
Nick Kovalsky

Reputation: 6472

You can drop those tons of bindings and just claim your own size within available constraints. Subclass your SKCanvasView then:

protected override Size MeasureOverride(double widthConstraint, double heightConstraint)
{
    if (check you need your own size)
    {
        // return your own size within constrains
        // like take var side = Math.Min(widthConstraint, heightConstraint) 
        // as square rect side and return Size(side,side).
        // use this to adapt maui view size to your needs
        return AdaptSizeToContentIfNeeded(widthConstraint, heightConstraint, NeedMeasure);
    }
    
    //this will be called several times, so make sure that
    // (check you need your own size) returns FALSE if new 
    // constrains are finally ok to you (square) so you'll fall here:
    
    Update(); // invalidate canvas (on main thread)
    
    return base.MeasureOverride(widthConstraint, heightConstraint);
}
    
protected override Size ArrangeOverride(Rect bounds)
{
    if (we have asked for our own size) 
    {
        // you return nothing just consume 
        // internally what they finally offer
        ReactToOfferedBounds(bounds.Width, bounds.Height);
    }
    
    return base.ArrangeOverride(bounds);
}

MAUI provides some methods to have your own view size to create custom controls that adapt to internally needed size. Like you have a maui view with no height and width requests but you want to load an image inside with dynamic size that is unknown yet. when the image is loaded you'll issue a hacky invalidation using :

//will trigger parent calling our MeasureOverride
//this can be called from main thread only !!!
MainThread.BeginInvokeOnMainThread(() =>
{
    InvalidateMeasureNonVirtual(InvalidationTrigger.HorizontalOptionsChanged);
});

Upvotes: 1

Julian
Julian

Reputation: 8883

Not sure how or why that worked with Xamarin.Forms before, because you're effectively binding to the size of the Grid while you're also telling the Grid at the same time to size itself based on the available width and auto-size the height of the row based on the content. In any case, your approach most likely results in excessive layout cycles.

To resolve this, you could either give the SKCanvasView elements of the Grid a fixed width and height or you need to actually set the HeightRequest based on the Width property of the Grid:

XAML

<Grid x:Name="compassGrid"
      Margin="50,10"
      HorizontalOptions="Fill"
      HeightRequest="{Binding Width, Source={RelativeSource Self}}">
    <skia:SKCanvasView x:Name="compassCanvasView" PaintSurface="compassCanvasView_PaintSurface"
                       HeightRequest="{Binding Height, Source={x:Reference compassGrid}}"
                       WidthRequest="{Binding Width, Source={x:Reference compassGrid}}" />
    <skia:SKCanvasView x:Name="arrowCanvasView" PaintSurface="arrowCanvasView_PaintSurface" />
    <Label Text="{Binding CompassReading}" 
           TextColor="{StaticResource Gray-300}" 
           FontSize="40" 
           HorizontalTextAlignment="Center" 
           VerticalTextAlignment="Center"/>
</Grid>

Note that this will only look good in Portrait mode. If you need to support Landscape mode as well, you'll need to either set the WidthRequest and HeightRequest from the code-behind or you can use OrientationStateTriggers to react to orientation changes.

Upvotes: 2

Related Questions