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.
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 View
s..
View
s 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 View
s / Widget
s 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 ;-)
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With