Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I know when a view is finished rendering?

I've noticed that when OnElementPropertyChanged is fired on a VisualElement like a BoxView, the properties of the underlying platform view are not updated at that time.

I want to know when the VisualElement's corresponding platform view is finished rendering, something like:

this.someBoxView.ViewHasRendered += (sender, e) => { // Here I would know underlying UIView (in iOS) has finished rendering };

Looking through some code inside of Xamarin.Forms, namely VisualElementRenderer.cs, it would seem that I could raise an event after OnPropertyChanged has finished. Something like:

protected virtual void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName)
        SetBackgroundColor(Element.BackgroundColor);
    else if (e.PropertyName == Layout.IsClippedToBoundsProperty.PropertyName)
        UpdateClipToBounds();
    else if (e.PropertyName == PlatformConfiguration.iOSSpecific.VisualElement.BlurEffectProperty.PropertyName)
        SetBlur((BlurEffectStyle)Element.GetValue(PlatformConfiguration.iOSSpecific.VisualElement.BlurEffectProperty));

    // Raise event
    VisualElement visualElement = sender as VisualElement;
    visualElement.ViewHasRendered();        
}

Naturally there's a few more complexities to adding an event to the VisualElement class, as it would need to be subclassed. But I think you can see what I'm after.

While poking around I've noticed properties on VisualElement like IsInNativeLayout. But that only seems to be implementing in Win/WP8. Also, UpdateNativeWidget on VisualElementRenderer as well, however I can't figure out the proper way to leverage them.

Any ideas? Much appreciated.

like image 448
Jon Scalet Avatar asked Jan 25 '17 01:01

Jon Scalet


2 Answers

TL;DR : Run away, do not go down this path...

On iOS everything that displays content to the screen happens within a UIView (or subclass) and drawRect: is the method that does the drawing. So when drawRect: is done, the UIView is drawing is done.

Note: Animations could be occurring and you might see hundreds of completed rendering cycles completed. You might need to hook into every animation's completion handler to determine when things really are done "rendering".

Note: The drawing is done off-screen and depending upon the iDevice, the screen refresh Hz could 30FPS, 60FPS or in the case of iPad Pro it is variable (30-60hz)...

Example:

public class CustomRenderer : ButtonRenderer
{
    public override void Draw(CGRect rect)
    {
        base.Draw(rect);
        Console.WriteLine("A UIView is finished w/ drawRect: via msg/selector)");
    }
}

On Android the common way to draw content is via a View or subclass, you could obtain a surface, draw/bilt via OpenGL to a screen, etc... and that might not be within a View, but for your use-case, think Views..

Views have Draw methods you can override, and you can also hook into ViewTreeObserver and monitor OnPreDraw and OnDraw, etc, etc, etc... Sometimes you have to monitor the View's parent (ViewGroup) to determine when drawing is going to be done or when is completed.

Also all standard Widgets are completely inflated via xml resources and that is optimized so you will never see a Draw/OnDraw method call (Note: You should always(?) get a OnPreDraw listener call if you force it).

Different Views / Widgets behave differently and there no way to review all the challenges you will have determining when a View is really done "rendering"...

Example:

public class CustomButtonRenderer : Xamarin.Forms.Platform.Android.AppCompat.ButtonRenderer, 
    ViewTreeObserver.IOnDrawListener, ViewTreeObserver.IOnPreDrawListener
{
    public bool OnPreDraw() // IOnPreDrawListener
    {
        System.Console.WriteLine("A View is *about* to be Drawn");
        return true;
    }

    public void OnDraw() // IOnDrawListener
    {
        System.Console.WriteLine("A View is really *about* to be Drawn");
    }

    public override void Draw(Android.Graphics.Canvas canvas)
    {
        base.Draw(canvas);
        System.Console.WriteLine("A View was Drawn");
    }

    protected override void Dispose(bool disposing)
    {
        Control?.ViewTreeObserver.RemoveOnDrawListener(this);
        Control?.ViewTreeObserver.RemoveOnPreDrawListener(this);
        base.Dispose(disposing);
    }

    protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
    {           
        base.OnElementChanged(e);
        if (e.OldElement == null)
        {
            Control?.SetWillNotDraw(false); // force the OnPreDraw to be called :-(
            Control?.ViewTreeObserver.AddOnDrawListener(this); // API16+
            Control?.ViewTreeObserver.AddOnPreDrawListener(this); // API16+
            System.Console.WriteLine($"{Control?.ViewTreeObserver.IsAlive}");
        }
    }

}

Misc:

Note: Layout Optimizations, Content caching, GPU caching, is hardware acceleration enabled in the Widget/View or not, etc... can prevent the Draw methods from being called...

Note: Animation, effects, etc... can cause these these methods to be call many, many, many times before an area of the screen is completely finished displaying and ready for user interaction.

Personal Note: I've gone down this path once due to a crazy client requirements, and after banging my head on the desk for some time, a review of the actual goal of that area of the UI was done and I re-wrote the requirement and never tried this again ;-)

like image 197
SushiHangover Avatar answered Sep 20 '22 19:09

SushiHangover


I'm going to answer my own question it hopes that the solution will help someone who is struggling with this issue in the future.

Follow @SushiHangover's advice and RUN don't WALK away from doing something like this. (Although his recommendation will work and is sound). Attempting to listen/be notified when the platform has finished rendering a view, is a terrible idea. As @SushiHangover mentions there's simply too many things that can go wrong.

pin code ui

So what brought me down this path?

I have a requirement for a pin code UI similar to the one in iOS to unlock your device, and in many other apps. When a user presses a digit on the pad I want to update the corresponding display "box" (boxes above the pad). When the user inputs the last digit I want the last "box" to be filled in, in my case a Background color change, and then execution to continue in which the view would transition to the next screen in the workflow.

A problem arose as I tried to set the BackgroundColor property on the fourth box and then transition the screen. However, since execution doesn't wait for the property to change the screen transitions before the change is rendered. Naturally this makes for a bad user experience.

In an attempt to fix it, I thought "Oh! I simply need to be notified when the view has been rendered". Not smart, as I've mentioned.

After looking at some objective C implementations of similar UIs I realizes that the fix it quite simple. The UI should wait for a brief moment and allow the BackgroundColor property to render.

Solution

private async Task HandleButtonTouched(object parameter)    
{
    if (this.EnteredEmployeeCode.Count > 4)
        return;         

    string digit = parameter as string;
    this.EnteredEmployeeCode.Add(digit);
    this.UpdateEmployeeCodeDigits();

    if (this.EnteredEmployeeCode.Count == 4) {
        // let the view render on the platform
        await Task.Delay (1);

        this.SignIn ();
    }
}

A small millisecond delay is enough to let the view finished rendering without having to go down a giant rabbit hole and attempt to listen for it.

Thanks again to @SushiHangover for his detailed response. You are awesome my friend! :D

like image 24
Jon Scalet Avatar answered Sep 20 '22 19:09

Jon Scalet