Search code examples
iosxamarin.formsuiimageview

How can an effect find out the display size of the UIImageView it is attached to?


In Xamarin.Forms, an Effect can be attached to a View. In my case, the View is displaying an Image. And the effect is making a colored "glow" around the visible pixels of the image. XAML:

<Image Source="{Binding LogoImage}" ...>
    <Image.Effects>
        <effects:GlowEffect Radius="5" Color="White" />
    </Image.Effects>
</Image>

The effect is implemented as a subclass of RoutingEffect:

public class GlowEffect : Xamarin.Forms.RoutingEffect
{
    public GlowEffect() : base("Core.ImageGlowEffect")
    {
    }
...
}

On each platform, there is a PlatformEffect to implement the effect. For iOS:

using ...

[assembly: ExportEffect(typeof(Core.Effects.ImageGlowEffect), "ImageGlowEffect")]
namespace Core.Effects
{
  public class ImageGlowEffect : PlatformEffect
  {
    protected override void OnAttached()
    {
        ApplyGlow();
    }

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

        if (e.PropertyName == "Source") {
            ApplyGlow();
        }
    }

    private void ApplyGlow()
    {
        var imageView = Control as UIImageView;
        if (imageView.Image == null)
            return;

        var effect = (GlowEffect)Element.Effects.FirstOrDefault(e => e is GlowEffect);

        if (effect != null) {
            CGRect outSize = AVFoundation.AVUtilities.WithAspectRatio( new CGRect( new CGPoint(), imageView.Image.Size ), imageView.Frame.Size );
                ...
        }
    }
  ...
  }
}

The above code works if Source is changed dynamically: at the time Source changes, the UIImageView (Bounds or Frame) has a size. BUT if the Source is set statically in XAML, that logic does not run: so the only call to ApplyGlow is during OnAttach. UNFORTUNATELY, during OnAttach, the UIImageView has size (0, 0).

How get this effect to work on iOS, with a static Source?

NOTE: The equivalent Android effect works via a handler attached to ImageView.ViewTreeObserver.PreDraw - at which time the size is known. So if there is an iOS equivalent event, that would be one way to solve.


More Details:

  • The original implementation used the original image size (imageView.Image.Size) - which is available during OnAttach. This can be made to "work", but is not satisfactory: the glow is applied to the full size image. If the image is significantly larger than the view area, the glow becomes much too small a radius (iOS shrinks the image+glow as it renders): it does not have the desired appearance.

  • ApplyGlow has an option to apply a tint color to the image. That tint color is different than the glow color. I mention this because it restricts the possible solutions: AFAIK, can't just set options on an image and let iOS figure out how to render it - need to explicitly resize the image and draw the resized tinted image on top of a blurred version (the glow). This code all works - if imageView.Bounds.Size (or imageView.Frame.Size) is available (and non-zero).

  • With a breakpoint in OnElementPropertyChanged, I've checked to see if imageView size is known for any property that is always set. No; if no properties are dynamically set, the property changes all occur before imageView has a size.


Solution

  • Maybe it's a workaround and I don't know if it is acceptable to you.

    Add a little delay before calling ApplyGlow(); in OnAttached. After the delay, you will get the imageView.Frame.Size or imageView.Bounds.Size.

    protected override async void OnAttached()
    {
        await Task.Delay(TimeSpan.FromSeconds(0.3));// it can be 0.2s,0.1s, depending on you
        ApplyGlow();
    }
    

    And if you have set WidthRequest, HeightRequest, you can get there without the delay:

    private void ApplyGlow()
    {
        var imageView = Control as UIImageView;
        if (imageView.Image == null)
            return;
    
        Console.WriteLine(imageView.Image.Size.Width);
        Console.WriteLine(imageView.Image.Size.Height);
    
        CoreGraphics.CGSize rect = imageView.Bounds.Size;
        CoreGraphics.CGSize rect2 = imageView.Frame.Size;
    
        Console.WriteLine(rect);
        Console.WriteLine(rect2);
    
        double width = (double)Element.GetValue(VisualElement.WidthRequestProperty);
        double height = (double)Element.GetValue(VisualElement.HeightRequestProperty);
    
        Console.WriteLine(width);
        Console.WriteLine(height);
    
        double width2 = (double)Element.GetValue(VisualElement.WidthProperty);
        double height2 = (double)Element.GetValue(VisualElement.HeightProperty);
    
        Console.WriteLine(width2);
        Console.WriteLine(height2);
    }