Repository: dotnet/maui
Pull request #25223: [Android] Fix ImageButton Padding the third
Author: login: aheubusch
Labels: name: platform/android
name: community ✨
description: Community Contribution
name: area-controls-button
description: Button, ImageButton
name: partner/syncfusion/review
Created: 2024-10-11T22:46:30Z | Merged: 2025-03-20T11:31:54Z

### Description of Change

Improve the workaround (#22298) for the iffy Android ShapeImageView contentPadding/padding handling.
The underlying problem is that ShapeImageView updates Padding in onMeasure leading to a double padding. The previous workaround reset the Padding after one eventloop hoping that onMeasure was called in the meantime. This worked for most cases but not all. The "improved" workaround overrides onMeasure and resets the Padding as soon as it was set.

I ran all ImageButton tests I could find and they are all green. So thats a good sign.

### Issues Fixed

Fixes #25201 
Fixes #16713
Fixes #18001
Fixes #13101

Linked issue #25201: [Android] Invisible ImageButton Padding Broken
### Description

On Android setting a Padding on a not visible ImageButton produces incorrect results when it becomes visible.
Expected
![Image](https://github.com/user-attachments/assets/6f42fae9-78bd-43c9-b5f6-8d6766a8cf01)

Actual
![Image](https://github.com/user-attachments/assets/8c623d08-735f-4779-b784-680c494cf6d8)


### Steps to Reproduce

Clone repo, start and click on button "click me". Observe incorrect ImageButton.
or
1. Add an ImageButton like so:
```xaml
<ImageButton
    Background="Red"
    Padding="50"
    IsVisible="False"

    Source="dotnet_bot.png"
    HeightRequest="185"
    x:Name="imageButton"
    />
```
2. Make the ImageButton visible in a button click event (`imageButton.IsVisible = true;`).

### Link to public reproduction project repository

https://github.com/aheubusch/MauiAndroidImageButtonPadding

### Version with bug

8.0.91 SR9.1

### Is this a regression from previous behavior?

Not sure, did not test other versions

### Last version that worked well

_No response_

### Affected platforms

Android

### Affected platform versions

_No response_

### Did you find any workaround?

Nope :(

### Relevant log output

_No response_
Linked issue comments: user: login: similar-issues-ai[bot]
created_at: 2024-10-11T13:53:57Z
body: We've found some similar issues:


- #18001 , similarity score: 90%
- #16713 , similarity score: 88%
- #13101 , similarity score: 87%
- #25034 , similarity score: 86%
- #22364 , similarity score: 85%
- #22521 , similarity score: 83%
- #11874 , similarity score: 82%
  

If any of the above are duplicates, please consider closing this issue out and adding additional context in the original issue.

> Note: You can give me feedback by 👍 or 👎 this comment.
author_association: NONE
user: login: aheubusch
created_at: 2024-10-11T13:55:35Z
body: Ups, I should have used the search before creating this Issue. Sorry.
author_association: CONTRIBUTOR

Diff:
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonPaddingDoesNotChangeWhenIsVisibleChanges.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonPaddingDoesNotChangeWhenIsVisibleChanges.png
new file mode 100644
index 000000000000..632835b4fed6
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageButtonPaddingDoesNotChangeWhenIsVisibleChanges.png differ
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue25201.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue25201.xaml
new file mode 100644
index 000000000000..6a0037a2cce5
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue25201.xaml
@@ -0,0 +1,42 @@
+﻿<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage
+  xmlns="http://schemas.microsoft.com/dotnet/2021/maui" 
+  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+  xmlns:ns="clr-namespace:Maui.Controls.Sample.Issues"
+  x:Class="Maui.Controls.Sample.Issues.Issue25201">
+
+  <VerticalStackLayout>
+    <!-- https://github.com/dotnet/maui/issues/18001 -->
+    <!-- https://github.com/dotnet/maui/issues/25201 -->
+    <Switch x:Name="Switch1" AutomationId="Switch1" IsToggled="False" />
+    <ImageButton BackgroundColor="Red"
+                 WidthRequest="100"
+                 HeightRequest="100"
+                 Padding="20"
+                 Source="dotnet_bot"
+                 IsVisible="{Binding Source={x:Reference Switch1}, Path=IsToggled}"
+                 />
+
+    <!-- https://github.com/dotnet/maui/issues/16713 -->
+    <Switch x:Name="Switch2" AutomationId="Switch2" IsToggled="True" />
+    <AbsoluteLayout>
+      <ImageButton
+        AbsoluteLayout.LayoutFlags="PositionProportional"
+        AbsoluteLayout.LayoutBounds="0.5,0.5,120,120"
+        WidthRequest="100"
+        HeightRequest="100"
+        Padding="20"
+        Source="dotnet_bot.png"
+        CornerRadius="60"
+        BackgroundColor="Beige"
+        />
+
+      <ContentView
+            AbsoluteLayout.LayoutFlags="SizeProportional"
+            AbsoluteLayout.LayoutBounds="0,0,1.0,1.0"
+            BackgroundColor="#80FFFFFF"
+            IsVisible="{Binding Source={x:Reference Switch2}, Path=IsToggled}"
+            />
+    </AbsoluteLayout>
+  </VerticalStackLayout>
+</ContentPage>
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue25201.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue25201.xaml.cs
new file mode 100644
index 000000000000..7106fe9d7e24
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue25201.xaml.cs
@@ -0,0 +1,12 @@
+﻿namespace Maui.Controls.Sample.Issues
+{
+	[XamlCompilation(XamlCompilationOptions.Compile)]
+	[Issue(IssueTracker.Github, "25201", "[Android] ImageButton Padding Incorrect After IsVisible False", PlatformAffected.Android)]
+	public partial class Issue25201 : ContentPage
+	{
+		public Issue25201()
+		{
+			InitializeComponent();
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25201.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25201.cs
new file mode 100644
index 000000000000..fc4b98460980
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25201.cs
@@ -0,0 +1,35 @@
+﻿#if ANDROID
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues
+{
+	public class Issue25201 : _IssuesUITest
+	{
+		public Issue25201(TestDevice testDevice) : base(testDevice){}
+
+		public override string Issue => "[Android] ImageButton Padding Incorrect After IsVisible False";
+		
+		[Test]
+		[Category(UITestCategories.ImageButton)]
+		public void ImageButtonPaddingDoesNotChangeWhenIsVisibleChanges()
+		{
+			App.WaitForElement("Switch1");
+
+			// https://github.com/dotnet/maui/issues/25201
+			App.Tap("Switch1"); // ImageButton IsVisible changes to true
+			// https://github.com/dotnet/maui/issues/16713
+			App.Tap("Switch2"); // Hides overlay ContentView
+
+			VerifyScreenshot();
+
+			// https://github.com/dotnet/maui/issues/18001
+			App.Tap("Switch1"); // ImageButton IsVisible changes to false
+			App.Tap("Switch1"); // ImageButton IsVisible changes to true
+
+			VerifyScreenshot();
+		}
+	}
+}
+#endif
diff --git a/src/Core/src/Handlers/ImageButton/ImageButtonHandler.Android.cs b/src/Core/src/Handlers/ImageButton/ImageButtonHandler.Android.cs
index 2132103c7db5..c5e22533c37a 100644
--- a/src/Core/src/Handlers/ImageButton/ImageButtonHandler.Android.cs
+++ b/src/Core/src/Handlers/ImageButton/ImageButtonHandler.Android.cs
@@ -9,7 +9,7 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, ShapeableIma
 	{
 		protected override ShapeableImageView CreatePlatformView()
 		{
-			var platformView = new ShapeableImageView(Context);
+			var platformView = new MauiShapeableImageView(Context);
 
 			// These set the defaults so visually it matches up with other platforms
 			platformView.SetPadding(0, 0, 0, 0);
diff --git a/src/Core/src/Platform/Android/ImageButtonExtensions.cs b/src/Core/src/Platform/Android/ImageButtonExtensions.cs
index a081ba2cfbfa..f9f5ad1f72c1 100644
--- a/src/Core/src/Platform/Android/ImageButtonExtensions.cs
+++ b/src/Core/src/Platform/Android/ImageButtonExtensions.cs
@@ -20,7 +20,7 @@ public static void UpdateStrokeThickness(this ShapeableImageView platformButton,
 		public static void UpdateCornerRadius(this ShapeableImageView platformButton, IButtonStroke buttonStroke) =>
 			platformButton.UpdateButtonStroke(buttonStroke);
 
-		public static async void UpdatePadding(this ShapeableImageView platformButton, IImageButton imageButton)
+		public static void UpdatePadding(this ShapeableImageView platformButton, IImageButton imageButton)
 		{
 			var padding = platformButton.Context!.ToPixels(imageButton.Padding);
 			var (strokeWidth, _, _) = imageButton.GetStrokeProperties(platformButton.Context!, true);
@@ -32,29 +32,9 @@ public static async void UpdatePadding(this ShapeableImageView platformButton, I
 
 			// Because there is only a single padding property, we need to reset the padding to 0 otherwise we
 			// probably will get a double padding. Trust me. I've seen it happen. It's not pretty.
+			// The padding is also reset in MauiShapeableImageView.
 			platformButton.SetPadding(0, 0, 0, 0);
 
-			// The padding has a few issues, but setting and then resetting after some calculations
-			// are done seems to work. This is a workaround for the following issue:
-			// https://github.com/material-components/material-components-android/issues/2063
-			await Task.Yield();
-
-			if (!platformButton.IsAlive())
-				return;
-
-			// We must re-set all the paddings because the first time was not hard enough.
-			platformButton.SetContentPadding((int)padding.Left, (int)padding.Top, (int)padding.Right, (int)padding.Bottom);
-			platformButton.SetPadding(0, 0, 0, 0);
-
-			// Just like before, the bugs are not done. This needs to trigger a re-calculation of
-			// the shape appearance mode to avoid clipping issues.
-			if (platformButton.Drawable is not null)
-			{
-				platformButton.ShapeAppearanceModel =
-					platformButton.ShapeAppearanceModel
-						.ToBuilder()
-						.Build();
-			}
 		}
 
 		internal static void UpdateButtonStroke(this ShapeableImageView platformView, IButtonStroke button)
diff --git a/src/Core/src/Platform/Android/MauiShapeableImageView.cs b/src/Core/src/Platform/Android/MauiShapeableImageView.cs
new file mode 100644
index 000000000000..a79f8a60b4bf
--- /dev/null
+++ b/src/Core/src/Platform/Android/MauiShapeableImageView.cs
@@ -0,0 +1,39 @@
+﻿using Android.Content;
+using Android.Runtime;
+using Android.Util;
+using Google.Android.Material.ImageView;
+
+namespace Microsoft.Maui.Platform
+{
+	internal class MauiShapeableImageView : ShapeableImageView
+	{
+		public MauiShapeableImageView(Context? context) : base(context)
+		{
+		}
+
+		public MauiShapeableImageView(Context? context, IAttributeSet? attrs) : base(context, attrs)
+		{
+		}
+
+		public MauiShapeableImageView(Context? context, IAttributeSet? attrs, int defStyle) : base(context, attrs, defStyle)
+		{
+		}
+
+		protected MauiShapeableImageView(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
+		{
+		}
+
+		protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
+		{
+			// The padding has a few issues. This is a workaround for the following issue:
+			// https://github.com/material-components/material-components-android/issues/2063
+
+			// ShapeableImageView combines ContentPadding with Padding and updates
+			// Padding with the result.
+			base.OnMeasure(widthMeasureSpec, heightMeasureSpec);
+
+			// We need to reset the padding to 0 to avoid a double padding.
+			SetPadding(0, 0, 0, 0);
+		}
+	}
+}
\ No newline at end of file


Reviews:
user: login: aheubusch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-11T22:52:09Z
user: login: mattleibow
body: I am super scared with this as the ImageButton was super fragile and I think I have seen padding issues but was super hard to repro in a test or I was not able to reliably repro.

I added tests as I did things, and TJ probably added more. But, I am not sure there are enough.

This comment does not help move the PR along, but I need to play a bit more and try and repro the issues that required the terrible hacks.
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-12-13T14:29:20Z
user: login: jsuarezruiz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:45:54Z
user: login: rmarinho
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-20T11:11:07Z
user: login: mattleibow
body: Now that I have had a nap, I don't think we should be writing C# anymore ;) OK, maybe we just need to put that new maui shape view into Java and use it there. The hopping back and forth will be a perf hit.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-20T11:17:49Z
user: login: mattleibow
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-20T11:31:42Z

Review comments:
diff_hunk: @@ -29,29 +29,9 @@ public static async void UpdatePadding(this ShapeableImageView platformButton, I
 
 			// Because there is only a single padding property, we need to reset the padding to 0 otherwise we
 			// probably will get a double padding. Trust me. I've seen it happen. It's not pretty.
+			// The padding is also reset in MauiShapeableImageView.
 			platformButton.SetPadding(0, 0, 0, 0);
 
-			// The padding has a few issues, but setting and then resetting after some calculations
-			// are done seems to work. This is a workaround for the following issue:
-			// https://github.com/material-components/material-components-android/issues/2063
-			await Task.Yield();
-
-			if (!platformButton.IsAlive())
-				return;
-
-			// We must re-set all the paddings because the first time was not hard enough.
-			platformButton.SetContentPadding((int)padding.Left, (int)padding.Top, (int)padding.Right, (int)padding.Bottom);
-			platformButton.SetPadding(0, 0, 0, 0);
-
-			// Just like before, the bugs are not done. This needs to trigger a re-calculation of
path: src/Core/src/Platform/Android/ImageButtonExtensions.cs
user: login: aheubusch
body: @mattleibow You have added this part (ShapeAppearanceModel) in #22298. I am not sure if I can just remove it. None of the tests that looked relevant failed on my machine.
created_at: 2024-10-11T22:52:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,29 +29,9 @@ public static async void UpdatePadding(this ShapeableImageView platformButton, I
 
 			// Because there is only a single padding property, we need to reset the padding to 0 otherwise we
 			// probably will get a double padding. Trust me. I've seen it happen. It's not pretty.
+			// The padding is also reset in MauiShapeableImageView.
 			platformButton.SetPadding(0, 0, 0, 0);
 
-			// The padding has a few issues, but setting and then resetting after some calculations
-			// are done seems to work. This is a workaround for the following issue:
-			// https://github.com/material-components/material-components-android/issues/2063
-			await Task.Yield();
-
-			if (!platformButton.IsAlive())
-				return;
-
-			// We must re-set all the paddings because the first time was not hard enough.
-			platformButton.SetContentPadding((int)padding.Left, (int)padding.Top, (int)padding.Right, (int)padding.Bottom);
-			platformButton.SetPadding(0, 0, 0, 0);
-
-			// Just like before, the bugs are not done. This needs to trigger a re-calculation of
path: src/Core/src/Platform/Android/ImageButtonExtensions.cs
user: login: mattleibow
body: I am scared tbh. I forget the corner cases where this happens - and they yield is especially scary. I think I was going off the fixes the native side was doing.

The current test set may not be enough and we will have to go remove this and see what breaks manually, and then make your change to see if it fixes it.
created_at: 2024-12-13T14:26:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,35 @@
+﻿#if ANDROID
path: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25201.cs
user: login: jsuarezruiz
body: Could we run the test in all the platforms? In that way, we can just verify that the Padding property behavior is aligned.
created_at: 2025-03-10T14:45:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+﻿using Android.Content;
+using Android.Runtime;
+using Android.Util;
+using Google.Android.Material.ImageView;
+
+namespace Microsoft.Maui.Platform
+{
+	internal class MauiShapeableImageView : ShapeableImageView
path: src/Core/src/Platform/Android/MauiShapeableImageView.cs
user: login: mattleibow
body: Just looking at this and I think that this is all Java code so we probably should just do it in Java and let the bonding do the magic.

Doing it in C# is probably going have a big perf impact.
created_at: 2025-03-20T11:16:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+﻿using Android.Content;
+using Android.Runtime;
+using Android.Util;
+using Google.Android.Material.ImageView;
+
+namespace Microsoft.Maui.Platform
+{
+	internal class MauiShapeableImageView : ShapeableImageView
path: src/Core/src/Platform/Android/MauiShapeableImageView.cs
user: login: mattleibow
body: New issue for this Java work: https://github.com/dotnet/maui/issues/28532
created_at: 2025-03-20T11:31:38Z
author_association: MEMBER

PR comments:
user: login: jsuarezruiz
created_at: 2024-10-14T06:25:04Z
body: /azp run 
author_association: CONTRIBUTOR
user: login: azure-pipelines[bot]
created_at: 2024-10-14T06:25:24Z
body: <samp>
Azure Pipelines successfully started running 3 pipeline(s).<br>

</samp>
author_association: NONE
user: login: aheubusch
created_at: 2024-10-14T09:50:57Z
body: Not sure if the test failure are because of this PR. They all fail with a `System.TimeoutException`.
author_association: CONTRIBUTOR
user: login: jfversluis
created_at: 2024-12-06T15:01:35Z
body: /rebase
author_association: MEMBER
user: login: azure-pipelines[bot]
created_at: 2024-12-06T15:04:49Z
body: <samp>
Azure Pipelines successfully started running 3 pipeline(s).<br>

</samp>
author_association: NONE
user: login: BlueRaja
created_at: 2025-02-19T01:49:44Z
body: Any chance to get this in for the next release? Both `Border` and `Padding` on `ImageButton` are [still broken](https://github.com/dotnet/maui/issues/13101#issuecomment-2645984846), with no known workaround. This seems like pretty basic functionality to just straight up not work at all.
author_association: NONE
user: login: karthikraja-arumugam
created_at: 2025-03-14T05:38:49Z
body: This PR also resolves the issue https://github.com/dotnet/maui/issues/18875
author_association: CONTRIBUTOR
user: login: mattleibow
created_at: 2025-03-20T11:30:48Z
body: We can do Java in a new PR: https://github.com/dotnet/maui/issues/28532
author_association: MEMBER