maui
maui copied to clipboard
Drawing of Borders lags behind other elements on iOS creating bizarre overlaps and glitches (okay in Windows/Android)
Description
Currently, whenever a Border is resized in iOS it lags behind other elements by a large margin creating strange visual overlaps and effects.
This is likely the same overlap phenomenon seen here, which indicates the overlap/lag phenomenon is due to Borders, not Editor: https://github.com/dotnet/maui/issues/17757
That thread is more relevant therefore for the other strange Editor bugs like the textfield scrolling itself off the screen, unless perhaps these are all related and caused by the same thing.
DEMO PROJECT This is the same project used to demonstrate the Android Shadow bug here: https://github.com/dotnet/maui/issues/18202
It draws 3 purple borders on screen and then oscillates their height continuously.
Correct animation should look like this:
Nothing should overlap at any time. However, in iOS we get the following bizarre overlaps as the animated Borders lag behind everything else and desynchronize in how they are drawn:
Steps to Reproduce
- Open demo project.
- Play in Windows/Android and see the Border elements all remain in sync.
- Play in iOS and observe that nothing is staying in sync as the Border elements seem to lag behind where they should be drawn.
Link to public reproduction project repository
https://github.com/jonmdev/Resize-Lag-Bug
Version with bug
8.0.0-rc.2.9373
Is this a regression from previous behavior?
No, this is something new
Affected platforms
iOS
Verified this on Visual Studio Enterprise 17.8.0 Preview 4.0(8.0.0-rc.2.9373). Repro on iOS 16.4, not repro on Windows 11 and Android 13.0-API33 with below Project: ResizeLagBug.zip
We've added this issue to our backlog, and we will work to address it as time and resources allow. If you have any additional information or questions about this issue, please leave a comment. For additional info about issue management, please read our Triage Process.
I suspect this is due to iOS animation that needs to be disabled on changing requested View size:
https://stackoverflow.com/questions/10975449/changing-uiviews-transformation-rotation-scale-without-animation https://stackoverflow.com/questions/13709634/resizing-a-uiview-without-animation?rq=3
It took me a lot of time to find a workaround, but here it is. I hope this also helps fixing the issue.
handlers.AddHandler<Border, NotAnimatedBorderHandler>();
The following workaround removes sizing animations but keeps other kind of animations which are good (i.e. background).
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using ContentView = Microsoft.Maui.Platform.ContentView;
namespace YourNamespace;
public class NotAnimatedBorderHandler : BorderHandler
{
private class BorderContentView : ContentView
{
public override void LayoutSubviews()
{
// This is the only workaround I found to avoid the animation when the border size is updated.
// https://github.com/dotnet/maui/issues/15363
// https://github.com/dotnet/maui/issues/18204
if (Layer.Sublayers?.FirstOrDefault(layer => layer is MauiCALayer) is { AnimationKeys: not null } caLayer)
{
caLayer.RemoveAnimation("bounds");
caLayer.RemoveAnimation("position");
}
base.LayoutSubviews();
}
}
protected override ContentView CreatePlatformView()
{
_ = VirtualView ?? throw new InvalidOperationException($"{nameof(VirtualView)} must be set to create a {nameof(ContentView)}");
_ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} cannot be null");
return new BorderContentView
{
CrossPlatformLayout = VirtualView
};
}
}
Please note that setting Frame property (and probably also other things - happens for example on MapStrokeShape) implicitly re-adds animation keys, that's why I'm removing them every time.
CC @durandt
Related #15363 (I see now it was closed during Fall, but from my latest tests on net8 the animation still existed) Thanks @albyrock87 , I will make sure to try this next week ! Are you on net7 or net8 ? I will make sure to test again whether the fix was really fixed in net8 (I've successfully upgraded a couple of weeks ago but haven't had time to do all tests and release yet)
@durandt I am on .NET MAUI 8.0.3 and I can confirm what they did to prevent this issue is NOT effective, my workaround appears the only way to get around it. When I have time I'll try to make a PR to fix the issue in one of the following SR releases.
@albyrock87 Now with Maui 8.0.6/8.0.7 removing a few blocking bugs I have finally had time to upgrade and test my app. As you say, the Border animations seem even worse but your work-around seems to work well for me too! Thank you.
Thank you very much @albyrock87!!! I finally worked up the courage to test your fix and it did work as you suggested. It was my first time adding a custom Handler.
I have one question as I am very curious about how this works in terms of the custom Handler.
When you add the custom Handler in MauiProgram.cs, does this automatically replace the standard Border Handler inside Maui? Or is it a second Handler now attached to Border? Ie. Would each Border with this method now have two Handlers? One standard BorderHandler and one NotAnimatedBorderHandler? Or does the command addHandler check for duplicate handlers of a given type (Border) and use the one we added in exclusion to the default Maui one for Border type?
Obviously your NotAnimatedBorderHandler inherits from BorderHandler so we don't need BorderHandler anymore. I am just wondering if this is automatically dealt with in Maui or if this method is then adding two handlers per Border.
Thanks for any clarification. This is a nice relatively easy technique to customize Handlers if it doesn't just add a duplicate Handler which would be less ideal. (All commands may run twice if two handlers per Border now, if that is even possible?)
Thanks for any clarification.
For anyone who has never done this before, this is how I used it (more specific instructions for amateurs like me who have to figure it out 😊).
1) MauiProgram.cs
Add to default builder string so it reads:
public static MauiApp CreateMauiApp() {
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureMauiHandlers((handlers) => {
#if IOS
handlers.AddHandler<Border, NotAnimatedBorderHandler>();
#endif
});
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
2) NotAnimatedBorderHandler.cs
Create a file by this name and enter in its body within the namespace:
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
#if IOS
using ContentView = Microsoft.Maui.Platform.ContentView;
#endif
namespace YOUR_NAMESPACE {
public class NotAnimatedBorderHandler : BorderHandler {
#if IOS
private class BorderContentView : ContentView {
public override void LayoutSubviews() {
if (Layer.Sublayers?.FirstOrDefault(layer => layer is MauiCALayer) is { AnimationKeys: not null } caLayer) {
caLayer.RemoveAnimation("bounds");
caLayer.RemoveAnimation("position");
}
base.LayoutSubviews();
}
}
protected override ContentView CreatePlatformView() {
_ = VirtualView ?? throw new InvalidOperationException($"{nameof(VirtualView)} must be set to create a {nameof(ContentView)}");
_ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} cannot be null");
return new BorderContentView {
CrossPlatformLayout = VirtualView
};
}
#endif
}
}
@jonmdev I can confirm it replaces the default handler! :) Especially because there can be only one handler instance on a view.
And btw, you can place iOS specific handler in Platforms/iOS folder so that you can avoid compilation flags (at least on the handler definition).
I've found a related issue. https://github.com/dotnet/maui/issues/21643
I tried to use the mentioned workaround with handler, but it doesn't work (v. 8.0.20)
When i was working with .NET Maui 6.0 this workaround fixed the issue, but now the workaround dont work anymore. We are in 8.0.21 version.
@matheusouz have you tried the one above? https://github.com/dotnet/maui/issues/18204#issuecomment-1900122924
Yes i tried too but no lucky.
The mentioned workaround works for me (8.0.21) but corner radius is still animated when border size changes.
I tried with caLayer.RemoveAnimation("cornerRadius"); and caLayer.RemoveAllAnimations();
Border within a CollectionView appears to have a default BackgroundColor animation. This seems to be related to this issue, as indicated by the more relevant closed/duplicated issue #22329
This can be observed at the bottom of the CollectionView, where views are recycled with different colors.
It can be see also on top, but it is less relevant due of the light color.
The mentioned workaround works for me (8.0.21) but corner radius is still animated when border size changes. I tried with
caLayer.RemoveAnimation("cornerRadius");andcaLayer.RemoveAllAnimations();
Same here, corner radius is still animated when using the workaround mentioned above. I can reproduce the animation if a Border that has CornerRadius set, is hidden and then gets visible with IsVisible = true (only for the very first appearance).
Border within a CollectionView appears to have a default BackgroundColor animation. This seems to be related to this issue, as indicated by the more relevant closed/duplicated issue #22329
This can be observed at the bottom of the CollectionView, where views are recycled with different colors. It can be see also on top, but it is less relevant due of the light color.
![]()
![]()
I've spent a lot of time trying to fix that thing. In your case you can just put everything into Grid and highlight the BoxView behind. Could you create a new issue? If not I could do it my self somewhen
The mentioned workaround works for me (8.0.21) but corner radius is still animated when border size changes. I tried with
caLayer.RemoveAnimation("cornerRadius");andcaLayer.RemoveAllAnimations();Same here, corner radius is still animated when using the workaround mentioned above. I can reproduce the animation if a Border that has CornerRadius set, is hidden and then gets visible with
IsVisible = true(only for the very first appearance).
Also having the same issue. My work around for now instead of relying
border.StrokeShape = new RoundRectangle {CornerRadius = cornerRadius}
I am using
border.Clip = new RoundRectangleGeometry(new CornerRadius(cornerRadius), new Rect(0, 0, width, height));
the downside is I need to know the height and width of the border first but this can be done by just relying on creating custom view for it then override OnSizeAllocated. And also still need NotAnimatedBorderHandler from above
Alternative Solutions:
I looked into this a bit more. The best reference I can find is this one: https://stackoverflow.com/questions/5833488/how-to-disable-calayer-implicit-animations
There it is explained there are three ways to stop animations in iOS:
- Run RemoveAllAnimations on the individual layers
- Use SetDisableActions in CATransaction to true
- Set CALayer.Action to NSDictionary specifying null animation types
Definitely the correct solution for our main problem of the Borders resizing wildly (like in my OP bug report) is the RemoveAllAnimations. Or more specifically removing certain animations like @albyrock87 posted above.
Comprehensive Solution?
Out of curiosity I tried to apply all three and this is what I get for a new NotAnimatedBorderHandler:
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
#if IOS
using UIKit;
using ContentView = Microsoft.Maui.Platform.ContentView;
using Foundation;
using CoreAnimation;
#endif
namespace MauiProject {
//TEMP BUG FIX FOR: https://github.com/dotnet/maui/issues/18204
public class NotAnimatedBorderHandler : BorderHandler {
#if IOS
public override void PlatformArrange(Rect rect) {
CATransaction.Begin();
CATransaction.DisableActions = true;
CATransaction.AnimationDuration = 0;
//ISSUE:
base.PlatformArrange(rect);
//this doesn't help our problem now as it calls BorderHandler.iOS.cs 's PlatformArrange which restarts the CATransaction
//technically need base.base.PlatformArrange(rect) but need reflection to get it as per:
//https://stackoverflow.com/questions/2323401/how-to-call-base-base-method
//https://stackoverflow.com/questions/1006530/how-to-call-a-second-level-base-class-method-like-base-base-gethashcode
CATransaction.Commit();
}
private class BorderContentView : ContentView {
public override void LayoutSubviews() {
NSMutableDictionary dict = new();
dict.TryAdd(new NSString("sublayers"), new NSNull());
dict.TryAdd(new NSString("content"), new NSNull());
Layer.RemoveAllAnimations();
Layer.Actions = dict;
if (Layer.Sublayers != null) {
for (int i=0; i < Layer.Sublayers.Count(); i++) {
Layer.Sublayers[i].RemoveAllAnimations();
Layer.Sublayers[i].Actions = dict; // layer.actions = ["sublayers":NSNull()]
}
}
base.LayoutSubviews();
}
}
protected override ContentView CreatePlatformView() {
_ = VirtualView ?? throw new InvalidOperationException($"{nameof(VirtualView)} must be set to create a {nameof(ContentView)}");
_ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} cannot be null");
return new BorderContentView {
CrossPlatformLayout = VirtualView
};
}
#endif
}
}
Problem
This works just as fine as the original posted above by @albyrock87 for the main problem. The problem with this code in the public override void PlatformArrange(Rect rect) { is that this calls base.PlatformArrange(rect); which means it calls BorderHandler's method here which I think just wipes this method and starts all over (starts a new CATransaction). Technically we need base.base.PlatformArrange(rect); here but that requires reflection as per the links in the code above.
So public override void PlatformArrange is useless here but no harm. Could be useful if someone wants to implement reflection to get base.base.PlatformArrange.
Or maybe running CATransaction.Begin(); twice is no problem. Maybe this doesn't wipe anything? I don't know.
Remaining Issue
The only remaining issue I see is that Borders with corners in iOS now have what looks like a single frame lag before their corners are drawn correctly. I don't believe this is an Animation, as it happens too quickly. Even on my very slow running Debug builds to iPhone I only see the glitch for a moment, so I think this is just a drawing function order error of not refreshing corners correctly until a frame later each time something is resized or shown/hidden.
What do you see?
@albilaga do you agree it is just a frame lag on the correct drawing of the corner border you are seeing when you use the corner radius?
@Phenek out of curiosity does this approach in general (RemoveAllAnimations & setting each Action to the new NSDictionary) fix your background issue also?
@jonmdev I am not sure if that is just lag. Basically what we have issue setting view which contains Border from IsVisible=false to IsVisible=true is causing the animations shown like it is just rendered from bottom of view. Use Clip helping this issue but we still have like some frame lag like the view is animated to correct size. With the NotAnimatedBorderHandler this is helping to disable the view animation
@jonmdev I am not sure if that is just lag. Basically what we have issue setting view which contains
BorderfromIsVisible=falsetoIsVisible=trueis causing the animations shown like it is just rendered from bottom of view. Use Clip helping this issue but we still have like some frame lag like the view is animated to correct size. With theNotAnimatedBorderHandlerthis is helping to disable the view animation
Okay you are seeing something different than me. You are still getting some animations of the Border sizes from what it sounds like. I am not anymore in any real world circumstance.
I just see the corner radius of the rounded Border for a fractional second is shaped wrong (too square or round for a single frame) on each resize or hide/display. Which I think is just a drawing lag that needs to be fixed separately.
Could you make a demo project to try to repro it? You could set up a basic Border with rounded corners and run a timer. If you take a default Maui project, then just edit App.xaml.cs to something like this: https://github.com/jonmdev/Border-iOS-Mask-Bug/blob/main/Border%20iOS%20Mask%20Bug/App.xaml.cs
And add a timer to modulate the Border to that code after the border is set up with reasonable corner radius like 20 or so:
var timer = Application.Current.Dispatcher.CreateTimer();
timer.Interval = TimeSpan.FromSeconds(1 / 2f); //run timer at 2 fps
timer.IsRepeating = true;
timer.Tick += delegate{
border.isVisible = !border.isVisible;
border.HeightRequest = Random.Shared.Next(100,200);
border.WidthRequest = Random.Shared.Next(100,200);
};
timer.Start();
That should resize it and hide/show it on periodic basis to repro the issue if it can be still reproduced.
@jonmdev found some time to reproduce the issue. This is the issue that I have. Looks like this is only happen when attaching Shadow to Border
Here is the video of it
And here si the source code https://github.com/albilaga/MauiIssues/blob/main/MauiIssues/BorderShadowIssuePage.xaml
@jonmdev found some time to reproduce the issue. This is the issue that I have. Looks like this is only happen when attaching
ShadowtoBorderHere is the video of it !And here si the source code https://github.com/albilaga/MauiIssues/blob/main/MauiIssues/BorderShadowIssuePage.xaml
I copied and pasted your page code into MainPage.xaml of a new Maui project and added my newer version of the NotAnimatedBorderHandler just posted above here: https://github.com/dotnet/maui/issues/18204#issuecomment-2164238141
I see no further animations like your video.
You can download my project here to see and confirm:
https://github.com/jonmdev/iOSBorderShadowAnimationBug
@jonmdev with your solution I still see some animation (which cause little bit flickering). Btw I am using slow animations here in simulator. The 2nd border below is using clip which have no flickering at all. I also updated the code here
@jonmdev with your solution I still see some animation (which cause little bit flickering). Btw I am using slow animations here in simulator. The 2nd border below is using clip which have no flickering at all. I also updated the code here
![]()
![]()
Yes. Now you are seeing the same behavior as me. What I think we are seeing is a drawing error. As you can see, they are momentarily square on the corners, then a frame later they are drawn correctly with the rounded corners.
I believe this is now likely not an animation issue but that they are being drawn wrong the first frame which is likely a separate bug.
@albilaga I have posted the new "1 frame lag on redraw" bug report here:
https://github.com/dotnet/maui/issues/23070
I used your test project and another one I made combined into one to show the issue, as your test project and gif best showed the square border effect which I am also seeing my real projects but had trouble figuring out how to reproduce consistently.
My project in there shows there is a redraw lag also on the background color. Likely this is all the same bug and the order of operations must be changed on Border redraw to get them to redraw immediately when needed rather than after 1 frame to fix both.
If anyone has any understanding of the Border and can think of any way to fix it that would be great also. I looked at BorderHandler and Border code and I can't see how it really works to understand a fix.
One problem down, one more to go.
Also just for reference the simplest NotAnimatedBorderHandler.cs code I have tested and works is:
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
#if IOS
using UIKit;
using ContentView = Microsoft.Maui.Platform.ContentView;
using Foundation;
using CoreAnimation;
#endif
namespace iOSBorderShadowAnimationBug {
//TEMP BUG FIX FOR: https://github.com/dotnet/maui/issues/18204
public class NotAnimatedBorderHandler : BorderHandler {
#if IOS
private class BorderContentView : ContentView {
public override void LayoutSubviews() {
Layer.RemoveAllAnimations();
if (Layer.Sublayers != null) {
for (int i=0; i < Layer.Sublayers.Count(); i++) {
Layer.Sublayers[i].RemoveAllAnimations();
}
}
base.LayoutSubviews();
}
}
protected override ContentView CreatePlatformView() {
_ = VirtualView ?? throw new InvalidOperationException($"{nameof(VirtualView)} must be set to create a {nameof(ContentView)}");
_ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} cannot be null");
return new BorderContentView {
CrossPlatformLayout = VirtualView
};
}
#endif
}
}
We agree that this is an important issue. As our roadmap indicates, for the near future, we are focused on:
- Issues impacting Xamarin.Forms upgrades to .NET MAUI
- CollectionView
- Layout
I am marking this as a p/2 issue because it does not meet our focus areas right now, but we are constantly revisiting the backlog for important issues to address as time allows. Thank you for your patience and understanding!