-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Android drawable perf #31567
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jfversluis
merged 15 commits into
dotnet:inflight/current
from
albyrock87:android-drawable-perf
Mar 2, 2026
Merged
Android drawable perf #31567
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
60a5991
Improve Android background rendering performance
albyrock87 c8d73f4
Fix gradient not working
albyrock87 9707737
Fix undetected shape changes
albyrock87 932c72a
Update screenshots
albyrock87 42acb28
Fixes remaining tests
albyrock87 2ed2820
Add missing public API on Windows
albyrock87 20b246d
Improve performance by avoiding arrays as much as possible
albyrock87 6289579
Remove old device tests
albyrock87 c113d61
Improve documentation
albyrock87 27ba335
Merge branch 'main' into android-drawable-perf
albyrock87 cc395cf
Merge branch 'main' into android-drawable-perf
albyrock87 f6e023c
Merge remote-tracking branch 'origin/main' into android-drawable-perf
PureWeen f9af3fd
Update src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/P…
albyrock87 3582b39
Fix issue with theme color
albyrock87 bb0fb11
Merge branch 'inflight/current' into android-drawable-perf
jfversluis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 2 additions & 1 deletion
3
src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| #nullable enable | ||
| override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void | ||
| ~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView<TItemsView, TAdapter, TItemsViewSource>.OnInterceptTouchEvent(Android.Views.MotionEvent e) -> bool | ||
| ~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView<TItemsView, TAdapter, TItemsViewSource>.OnTouchEvent(Android.Views.MotionEvent e) -> bool | ||
| ~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView<TItemsView, TAdapter, TItemsViewSource>.OnTouchEvent(Android.Views.MotionEvent e) -> bool |
1 change: 1 addition & 0 deletions
1
src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| #nullable enable | ||
| override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void | ||
| ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void | ||
| override Microsoft.Maui.Controls.Platform.Compatibility.ShellTableViewController.LoadView() -> void | ||
| *REMOVED*~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRootRenderer.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void |
1 change: 1 addition & 0 deletions
1
src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| #nullable enable | ||
| override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void | ||
| ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void | ||
| override Microsoft.Maui.Controls.Platform.Compatibility.ShellTableViewController.LoadView() -> void | ||
| *REMOVED*~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRootRenderer.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void |
1 change: 1 addition & 0 deletions
1
src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| #nullable enable | ||
| override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void |
1 change: 1 addition & 0 deletions
1
src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| #nullable enable | ||
| override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| #nullable enable | ||
| override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void |
1 change: 1 addition & 0 deletions
1
src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| #nullable enable | ||
| override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file modified
BIN
-539 Bytes
(99%)
...es.Android.Tests/snapshots/android/BackgroundGradientsShouldRenderCorrectly.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
-1.06 KB
(96%)
...tCases.Android.Tests/snapshots/android/RadioButtonUpdateValueInsideBorderNo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
-5.59 KB
(87%)
...Cases.Android.Tests/snapshots/android/RadioButtonUpdateValueInsideBorderYes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
-185 Bytes
(99%)
...estCases.Android.Tests/snapshots/android/RadioButtonWithValueChangeSelected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
-1.27 KB
(97%)
...d.Tests/snapshots/android/SelectionShouldNotMovedToTopWithGroupedCollection.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
-2.36 KB
(92%)
...Controls/tests/TestCases.Android.Tests/snapshots/android/ShadowShouldUpdate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
285 changes: 285 additions & 0 deletions
285
src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformDrawable.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,285 @@ | ||
| package com.microsoft.maui; | ||
|
|
||
| import android.content.Context; | ||
| import android.content.res.Resources; | ||
| import android.graphics.BlurMaskFilter; | ||
| import android.graphics.Canvas; | ||
| import android.graphics.Color; | ||
| import android.graphics.Paint; | ||
| import android.graphics.Path; | ||
| import android.graphics.PathEffect; | ||
| import android.graphics.Rect; | ||
| import android.graphics.drawable.PaintDrawable; | ||
| import android.graphics.drawable.shapes.Shape; | ||
| import android.graphics.drawable.shapes.RectShape; | ||
|
|
||
| import androidx.annotation.NonNull; | ||
|
|
||
| public abstract class PlatformDrawable extends PaintDrawable implements PlatformShadowDrawable { | ||
| private float strokeThickness; | ||
| private Paint.Join strokeLineJoin; | ||
| private Paint.Cap strokeLineCap; | ||
| private float strokeMiterLimit; | ||
| private PathEffect borderPathEffect; | ||
| private Paint borderPaint; | ||
|
|
||
| // Shape and clipping | ||
| private Path clipPath; | ||
| private Path fullClipPath; | ||
| private boolean hasShape; | ||
|
|
||
| // Style properties | ||
| private PlatformDrawableStyle backgroundStyle; | ||
| private PlatformDrawableStyle borderStyle; | ||
|
|
||
| // Dimensions | ||
| private int width; | ||
| private int height; | ||
|
|
||
| // State flags | ||
| private boolean invalidatePath = true; | ||
|
|
||
| // Constructor (note: we're keeping Context here in case we need it for future style computations) | ||
| public PlatformDrawable(Context context) { | ||
| super(); | ||
| this.clipPath = new Path(); | ||
| this.fullClipPath = new Path(); | ||
| this.backgroundStyle = new PlatformDrawableStyle(); | ||
| this.borderStyle = new PlatformDrawableStyle(); | ||
| setShape(new RectShape()); | ||
| } | ||
|
|
||
| public void setStrokeThickness(float thickness) { | ||
| this.strokeThickness = thickness; | ||
| if (thickness == 0) { | ||
| this.borderPaint = null; | ||
| } else if (this.borderPaint == null) { | ||
| this.borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); | ||
| this.borderPaint.setStyle(Paint.Style.STROKE); | ||
| } | ||
|
|
||
| this.invalidateShapePath(); | ||
| } | ||
|
|
||
| public float getStrokeThickness() { | ||
| return strokeThickness; | ||
| } | ||
|
|
||
| public void setStrokeLineJoin(Paint.Join join) { | ||
| this.strokeLineJoin = join; | ||
| this.invalidateSelf(); | ||
| } | ||
|
|
||
| public Paint.Join getStrokeLineJoin() { | ||
| return strokeLineJoin; | ||
| } | ||
|
|
||
| public void setStrokeLineCap(Paint.Cap cap) { | ||
| this.strokeLineCap = cap; | ||
| this.invalidateSelf(); | ||
| } | ||
|
|
||
| public Paint.Cap getStrokeLineCap() { | ||
| return strokeLineCap; | ||
| } | ||
|
|
||
| public void setStrokeMiterLimit(float limit) { | ||
| this.strokeMiterLimit = limit; | ||
albyrock87 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| this.invalidateSelf(); | ||
| } | ||
|
|
||
| public float getStrokeMiterLimit() { | ||
| return strokeMiterLimit; | ||
| } | ||
|
|
||
| public void setBorderPathEffect(PathEffect effect) { | ||
| this.borderPathEffect = effect; | ||
| this.invalidateSelf(); | ||
| } | ||
|
|
||
| public PathEffect getBorderPathEffect() { | ||
| return borderPathEffect; | ||
| } | ||
|
|
||
| // Shape and clipping | ||
| public void setClipPath(Path path) { | ||
| if (this.clipPath != null) { | ||
| this.clipPath.reset(); | ||
| this.clipPath.set(path); | ||
| } | ||
| } | ||
|
|
||
| public Path getClipPath() { | ||
| return clipPath; | ||
| } | ||
|
|
||
| public void setFullClipPath(Path path) { | ||
| if (this.fullClipPath != null) { | ||
| this.fullClipPath.reset(); | ||
| this.fullClipPath.set(path); | ||
| } | ||
| } | ||
|
|
||
| public Path getFullClipPath() { | ||
| return fullClipPath; | ||
| } | ||
|
|
||
| public void shapeChanged(boolean hasShape) { | ||
| this.hasShape = hasShape; | ||
| this.invalidateShapePath(); | ||
| } | ||
|
|
||
| // Simplified style management | ||
| public void setLinearGradientBackground(float x1, float y1, float x2, float y2, int[] colors, float[] positions) { | ||
| float[] bounds = { x1, y1, x2, y2 }; | ||
albyrock87 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| this.backgroundStyle.setStyle(PlatformPaintType.LINEAR, 0, colors, positions, bounds); | ||
| invalidateSelf(); | ||
| } | ||
|
|
||
| public void setRadialGradientBackground(float x, float y, float radius, int[] colors, float[] positions) { | ||
| float[] bounds = { x, y, radius }; | ||
| this.backgroundStyle.setStyle(PlatformPaintType.RADIAL, 0, colors, positions, bounds); | ||
| invalidateSelf(); | ||
| } | ||
|
|
||
| public void setSolidBackground(int solidColor) { | ||
| this.backgroundStyle.setStyle(PlatformPaintType.SOLID, solidColor, null, null, null); | ||
| invalidateSelf(); | ||
| } | ||
|
|
||
| public void setNoBackground() { | ||
| this.backgroundStyle.setStyle(PlatformPaintType.NONE, 0, null, null, null); | ||
| invalidateSelf(); | ||
| } | ||
|
|
||
| public void setLinearGradientBorder(float x1, float y1, float x2, float y2, int[] colors, float[] positions) { | ||
| float[] bounds = { x1, y1, x2, y2 }; | ||
| this.borderStyle.setStyle(PlatformPaintType.LINEAR, 0, colors, positions, bounds); | ||
| invalidateSelf(); | ||
| } | ||
|
|
||
| public void setRadialGradientBorder(float x, float y, float radius, int[] colors, float[] positions) { | ||
| float[] bounds = { x, y, radius }; | ||
| this.borderStyle.setStyle(PlatformPaintType.RADIAL, 0, colors, positions, bounds); | ||
| invalidateSelf(); | ||
| } | ||
|
|
||
| public void setSolidBorder(int solidColor) { | ||
| this.borderStyle.setStyle(PlatformPaintType.SOLID, solidColor, null, null, null); | ||
| invalidateSelf(); | ||
| } | ||
|
|
||
| public void setNoBorder() { | ||
| this.borderStyle.setStyle(PlatformPaintType.NONE, 0, null, null, null); | ||
| invalidateSelf(); | ||
| } | ||
|
|
||
| // State management | ||
| public void invalidateShapePath() { | ||
| this.invalidatePath = true; | ||
| invalidateSelf(); | ||
| } | ||
|
|
||
| @Override | ||
| protected void onBoundsChange(Rect bounds) { | ||
| int newWidth = bounds.width(); | ||
| int newHeight = bounds.height(); | ||
|
|
||
| if (this.width != newWidth || this.height != newHeight) { | ||
| this.width = newWidth; | ||
| this.height = newHeight; | ||
| this.invalidatePath = true; | ||
| } | ||
|
|
||
| super.onBoundsChange(bounds); | ||
| } | ||
|
|
||
| @Override | ||
| protected void onDraw(Shape shape, Canvas canvas, Paint paint) { | ||
| // Set background paint | ||
| this.backgroundStyle.applyStyle(paint, this.width, this.height, null); | ||
|
|
||
| if (this.hasShape) { | ||
|
|
||
| // Configure border paint (when set it means there is a border - aka border thickness > 0) | ||
| if (this.borderPaint != null) { | ||
| Paint borderPaint = this.borderPaint; | ||
| borderPaint.setStrokeWidth(strokeThickness); | ||
| borderPaint.setStrokeJoin(strokeLineJoin); | ||
| borderPaint.setStrokeCap(strokeLineCap); | ||
| borderPaint.setStrokeMiter(this.strokeMiterLimit * 2); | ||
| borderPaint.setPathEffect(this.borderPathEffect); | ||
| this.borderStyle.applyStyle(borderPaint, this.width, this.height, this.backgroundStyle); | ||
| } | ||
|
|
||
| tryUpdateClipPath(); | ||
|
|
||
| if (this.clipPath != null) { | ||
| boolean hasBackgroundPaint = this.backgroundStyle.getPaintType() != PlatformPaintType.NONE; | ||
| if (hasBackgroundPaint) { | ||
| canvas.drawPath(this.clipPath, paint); | ||
| } | ||
|
|
||
| boolean hasBorderPaint = this.borderStyle.getPaintType() != PlatformPaintType.NONE; | ||
| if (this.borderPaint != null && (hasBorderPaint || hasBackgroundPaint)) { | ||
| canvas.drawPath(this.clipPath, this.borderPaint); | ||
| } | ||
|
|
||
| return; | ||
| } | ||
| // else fallback to simple background drawing | ||
| } | ||
|
|
||
| // Simple background drawing (border is supported **only** with a shape) | ||
| super.onDraw(shape, canvas, paint); | ||
| } | ||
|
|
||
| private void tryUpdateClipPath() { | ||
| if (this.invalidatePath) { | ||
| this.invalidatePath = false; | ||
|
|
||
| if (this.hasShape) { | ||
| updateClipPath(this.width, this.height); | ||
| } else { | ||
| this.fullClipPath = null; | ||
| this.clipPath = null; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| protected abstract void updateClipPath(int width, int height); | ||
|
|
||
| // PlatformShadowDrawable implementation | ||
| @Override | ||
| public boolean canDrawShadow() { | ||
| return this.backgroundStyle.getIsSolid() && (this.strokeThickness == 0 || this.borderStyle.getPaintType() == PlatformPaintType.NONE || this.borderStyle.getIsSolid()); | ||
| } | ||
|
|
||
| @Override | ||
| public void drawShadow(Canvas canvas, Paint shadowPaint, Path outerClipPath) { | ||
| if (canvas == null || shadowPaint == null) { | ||
| return; | ||
| } | ||
|
|
||
| Path contentPath; | ||
|
|
||
| if (this.hasShape) { | ||
| tryUpdateClipPath(); | ||
| if (this.fullClipPath == null) { | ||
| return; | ||
| } | ||
| contentPath = this.fullClipPath; | ||
| } else { | ||
| contentPath = new Path(); | ||
| contentPath.addRect(0, 0, this.width, this.height, Path.Direction.CW); | ||
| } | ||
|
|
||
| if (outerClipPath != null) { | ||
| Path clippedPath = new Path(); | ||
| clippedPath.op(contentPath, outerClipPath, Path.Op.INTERSECT); | ||
| canvas.drawPath(clippedPath, shadowPaint); | ||
| } else { | ||
| canvas.drawPath(contentPath, shadowPaint); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.