I have some animations that I want to trigger once the view has loaded.
Some of them rely on position values of other views on the page but at the time that OnAppearing fires, the X and Y values for these controls have not been set.
Others can just run by themselves but because they start in OnAppearing, the first few frames aren't rendered.
Adding a Task.Delay into the start of the methods solves the problem but is obviously not great.
Is there any way to create such a method or maybe a way to do it with behaviours? They need to trigger automatically, not in response to some control event like TextChanged etc.
Thanks!
One solution is to subclass ContentPage and PageViewController, and add a custom event that is called on ViewDidAppear. This requires that you set up handlers. I have added my code for iOS, but other platforms should be similar. You can read more about handlers at the MAUI documentation.
The nice thing about this approach is that you can access the event handler from XAML, and start your animation from the code behind:
<controls:TSContentPage
...
IOSViewDidAppear="TSContentPage_IOSViewDidAppear">
TSContentPage.cs:
public class TSContentPage : ContentPage
{
public event EventHandler IOViewDidAppear;
public void InvokeViewDidAppear()
{
IOSViewDidAppear.Invoke(this, new EventArgs());
}
}
TSContentPageHandler.cs (based on MAUIs PageHandler.cs):
public partial class TSContentPageHandler : ContentViewHandler, IPageHandler
{
public static new IPropertyMapper<IContentView, IPageHandler> Mapper =
new PropertyMapper<IContentView, IPageHandler>(ContentViewHandler.Mapper)
{
#if IOS || TIZEN
[nameof(IContentView.Background)] = MapBackground,
#endif
[nameof(ITitledElement.Title)] = MapTitle,
};
public static new CommandMapper<IContentView, IPageHandler> CommandMapper =
new(ContentViewHandler.CommandMapper);
public TSContentPageHandler() : base(Mapper, CommandMapper)
{
}
public TSContentPageHandler(IPropertyMapper? mapper)
: base(mapper ?? Mapper, CommandMapper)
{
}
public TSContentPageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper)
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
{
}
}
TSContentPageHandler.MaciOS.cs (based on MAUIs PageHandler.iOS.cs):
public partial class TSContentPageHandler : ContentViewHandler, IPlatformViewHandler
{
protected override ContentView CreatePlatformView()
{
_ = VirtualView ?? throw new InvalidOperationException($"{nameof(VirtualView)} must be set to create a LayoutView");
_ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} cannot be null");
if (ViewController == null)
ViewController = new TSPageViewController(VirtualView, MauiContext);
if (ViewController is TSPageViewController pc && pc.CurrentPlatformView is ContentView pv)
return pv;
if (ViewController.View is ContentView cv)
return cv;
throw new InvalidOperationException($"PageViewController.View must be a {nameof(ContentView)}");
}
public static void MapBackground(IPageHandler handler, IContentView page)
{
if (handler is IPlatformViewHandler invh && invh.ViewController is not null)
{
invh.ViewController.View?.UpdateBackground(page);
}
}
public static void MapTitle(IPageHandler handler, IContentView page)
{
if (handler is IPlatformViewHandler invh && invh.ViewController is not null)
{
if (page is ITitledElement titled)
{
invh.ViewController.Title = titled.Title;
}
}
}
}
TSPageViewController.cs:
public class TSPageViewController : PageViewController
{
readonly TSContentPage _contentPage;
public TSPageViewController(IView page, IMauiContext mauiContext) : base(page, mauiContext)
{
if(page is TSContentPage)
{
_contentPage = page as TSContentPage;
}
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
_contentPage?.InvokeViewDidAppear();
}
}
Remember to register your new handler in MauiProgram.cs:
builder.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler(typeof(TSContentPage), typeof(TSContentPageHandler));
});
You can do it from the native side, In Ios, you can override the ViewDidLoad method in custom renderer like:
public class MyPageRenderer : PageRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
//call before ViewWillAppear and only called once
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
}
}
and android, override the OnAttachedToWindow method:
public class MyPageRenderer : PageRenderer
{
public MyPageRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
}
protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
}
}
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