diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt index 9c70c4bb6c67..cf63344a2780 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt @@ -8028,14 +8028,12 @@ Methods: void Microsoft.Maui.PlatformDispatcher:.cctor () void Microsoft.Maui.PlatformDispatcher:.ctor (intptr,Android.Runtime.JniHandleOwnership) void Microsoft.Maui.PlatformInterop:.cctor () - void Microsoft.Maui.PlatformInterop:DrawMauiDrawablePath (Android.Graphics.Drawables.PaintDrawable,Android.Graphics.Canvas,int,int,Android.Graphics.Path,Android.Graphics.Paint) void Microsoft.Maui.PlatformInterop:LoadImageFromFont (Android.Content.Context,int,string,Android.Graphics.Typeface,single,Microsoft.Maui.IImageLoaderCallback) void Microsoft.Maui.PlatformInterop:LoadImageFromFont (Android.Widget.ImageView,int,string,Android.Graphics.Typeface,single,Microsoft.Maui.IImageLoaderCallback) void Microsoft.Maui.PlatformInterop:RemoveFromParent (Android.Views.View) void Microsoft.Maui.PlatformInterop:RequestLayoutIfNeeded (Android.Views.View) void Microsoft.Maui.PlatformInterop:Set (Android.Views.View,int,int,int,int,bool,single,single,single,single,single,single,single,single,single,single) void Microsoft.Maui.PlatformInterop:SetColorFilter (Android.Graphics.Drawables.Drawable,int,int) - void Microsoft.Maui.PlatformInterop:SetPaintValues (Android.Graphics.Paint,single,Android.Graphics.Paint/Join,Android.Graphics.Paint/Cap,single,Android.Graphics.PathEffect) void Microsoft.Maui.PlatformInterop:SetPivotXIfNeeded (Android.Views.View,single) void Microsoft.Maui.PlatformInterop:SetPivotYIfNeeded (Android.Views.View,single) void Microsoft.Maui.PlatformMauiAppCompatActivity:.cctor () diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index 6b13b646e109..cc66326af4d7 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ #nullable enable +override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void ~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnInterceptTouchEvent(Android.Views.MotionEvent e) -> bool -~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnTouchEvent(Android.Views.MotionEvent e) -> bool \ No newline at end of file +~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnTouchEvent(Android.Views.MotionEvent e) -> bool diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 0c300d7e5d2e..f340c9351931 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -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 diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 0c300d7e5d2e..f340c9351931 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -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 diff --git a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 7dc5c58110bf..173b902095ea 100644 --- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 7dc5c58110bf..173b902095ea 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index 7dc5c58110bf..173b902095ea 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 7dc5c58110bf..173b902095ea 100644 --- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void diff --git a/src/Controls/src/Core/Shapes/Shape.cs b/src/Controls/src/Core/Shapes/Shape.cs index 9f33599d469c..f7654422bfa1 100644 --- a/src/Controls/src/Core/Shapes/Shape.cs +++ b/src/Controls/src/Core/Shapes/Shape.cs @@ -9,11 +9,12 @@ namespace Microsoft.Maui.Controls.Shapes /// /// Base class for shape elements, such as , , , , and . /// - public abstract partial class Shape : View, IShapeView, IShape + public abstract partial class Shape : View, IShapeView, IShape, IVersionedShape { WeakBrushChangedProxy? _fillProxy = null; WeakBrushChangedProxy? _strokeProxy = null; EventHandler? _fillChanged, _strokeChanged; + int _version; /// /// Initializes a new instance of the class. @@ -27,6 +28,20 @@ public Shape() _fillProxy?.Unsubscribe(); _strokeProxy?.Unsubscribe(); } + + int IVersionedShape.Version => _version; + + protected override void OnPropertyChanged(string? propertyName = null) + { + unchecked + { + // Increase the version before propagating the property changed notification + // so that any code responding to the notification can get the latest version. + ++_version; + } + + base.OnPropertyChanged(propertyName); + } public abstract PathF GetPath(); diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/BackgroundGradientsShouldRenderCorrectly.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/BackgroundGradientsShouldRenderCorrectly.png index 688d08ae0066..845147249838 100644 Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/BackgroundGradientsShouldRenderCorrectly.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/BackgroundGradientsShouldRenderCorrectly.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonUpdateValueInsideBorderNo.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonUpdateValueInsideBorderNo.png index 63bd0521699c..ebfef4a5b63c 100644 Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonUpdateValueInsideBorderNo.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonUpdateValueInsideBorderNo.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonUpdateValueInsideBorderYes.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonUpdateValueInsideBorderYes.png index 51cb1ae70a60..08e2b261186a 100644 Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonUpdateValueInsideBorderYes.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonUpdateValueInsideBorderYes.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonWithValueChangeSelected.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonWithValueChangeSelected.png index ffd5010ec411..3093c37c6dff 100644 Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonWithValueChangeSelected.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/RadioButtonWithValueChangeSelected.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SelectionShouldNotMovedToTopWithGroupedCollection.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SelectionShouldNotMovedToTopWithGroupedCollection.png index d9fb5af378d1..83f332f30b2c 100644 Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SelectionShouldNotMovedToTopWithGroupedCollection.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SelectionShouldNotMovedToTopWithGroupedCollection.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShadowShouldUpdate.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShadowShouldUpdate.png index 0741c7fd102f..bf06839abe91 100644 Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShadowShouldUpdate.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ShadowShouldUpdate.png differ diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformDrawable.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformDrawable.java new file mode 100644 index 000000000000..9a32c1aac9a9 --- /dev/null +++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformDrawable.java @@ -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; + 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 }; + 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); + } + } +} diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformDrawableStyle.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformDrawableStyle.java new file mode 100644 index 000000000000..7e0d03d7ba64 --- /dev/null +++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformDrawableStyle.java @@ -0,0 +1,119 @@ +package com.microsoft.maui; + +import android.content.res.Resources; +import android.graphics.Shader; +import android.graphics.Paint; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.RadialGradient; +import android.os.Build; +import android.util.TypedValue; + +public class PlatformDrawableStyle { + // Style properties + private boolean isSolid; + private boolean isSolidInvalidated = true; + private int paintType = PlatformPaintType.NONE; + private int solidColor; + private int[] gradientColors; + private float[] gradientPositions; + private float[] gradientBounds; + private Shader shader; + private int shaderWidth; + private int shaderHeight; + + // Getters and setters + public boolean getIsSolid() { + if (this.isSolidInvalidated) { + this.isSolid = computeIsSolid(); + this.isSolidInvalidated = false; + } + + return this.isSolid; + } + + private boolean computeIsSolid() { + if (this.gradientColors != null) { + for (int i = 0; i < this.gradientColors.length; i++) { + if (Color.alpha(this.gradientColors[i]) != 255) { + return false; + } + } + + return true; + } + + if (this.paintType == PlatformPaintType.NONE) { + // None means no background, so that's = to transparent + return false; + } + + return Color.alpha(this.solidColor) == 255; + } + + public int getPaintType() { + return this.paintType; + } + + private Shader getShader(int width, int height) { + if (this.paintType == PlatformPaintType.NONE || this.paintType == PlatformPaintType.SOLID) { + return null; + } + + if (width != this.shaderWidth || height != this.shaderHeight) { + this.shaderWidth = width; + this.shaderHeight = height; + this.shader = null; + } + + if (this.shader == null) { + if (this.paintType == PlatformPaintType.LINEAR) { + this.shader = new LinearGradient( + this.gradientBounds[0] * width, this.gradientBounds[1] * height, // Start point + this.gradientBounds[2] * width, this.gradientBounds[3] * height, // End point + this.gradientColors, + this.gradientPositions, + Shader.TileMode.CLAMP + ); + } + else if (this.paintType == PlatformPaintType.RADIAL) { + this.shader = new RadialGradient( + this.gradientBounds[0] * width, this.gradientBounds[1] * height, // Center point + this.gradientBounds[2] * Math.max(width, height), // Radius + this.gradientColors, + this.gradientPositions, + Shader.TileMode.CLAMP + ); + } + } + + return this.shader; + } + + // Convenience method to apply all style properties at once + public void setStyle(int paintType, int solidColor, int[] colors, float[] positions, float[] bounds) { + this.paintType = paintType; + this.solidColor = solidColor; + this.gradientColors = colors; + this.gradientPositions = positions; + this.gradientBounds = bounds; + this.shader = null; // Reset shader when style changes + this.isSolidInvalidated = true; // isSolid needs to be re-evaluated + } + + public void applyStyle(Paint paint, int width, int height, PlatformDrawableStyle fallbackStyle) { + if (this.paintType == PlatformPaintType.SOLID) { + paint.setShader(null); + paint.setColor(this.solidColor); + } else if (this.paintType == PlatformPaintType.LINEAR || this.paintType == PlatformPaintType.RADIAL) { + // Reset the color to its default value so that a shader can be applied on top of it + paint.setColor(Color.BLACK); + paint.setShader(getShader(width, height)); + } else if (fallbackStyle != null) { + fallbackStyle.applyStyle(paint, width, height, null); + } else { + paint.setShader(null); + paint.setColor(Color.TRANSPARENT); + } + } +} diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java index a7131ea853d1..546d40fe1cb9 100644 --- a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java +++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java @@ -481,77 +481,6 @@ public static ColorStateList createEditTextColorStateList(ColorStateList colorSt return null; } - /** - * Sets many values at once on a Paint object - * @param paint - * @param strokeWidth - * @param strokeJoin - * @param strokeCap - * @param strokeMiter - * @param pathEffect - */ - public static void setPaintValues(Paint paint, float strokeWidth, Paint.Join strokeJoin, Paint.Cap strokeCap, float strokeMiter, PathEffect pathEffect) - { - paint.setStrokeWidth(strokeWidth); - paint.setStrokeJoin(strokeJoin); - paint.setStrokeCap(strokeCap); - paint.setStrokeMiter(strokeMiter); - if (pathEffect != null) { - paint.setPathEffect(pathEffect); - } - } - - /** - * Draws the background and the border (if any). - * @param drawable - * @param canvas - * @param width - * @param height - * @param clipPath - * @param borderPaint - */ - public static void drawMauiDrawablePath(PaintDrawable drawable, Canvas canvas, int width, int height, @NonNull Path clipPath, Paint borderPaint) - { - Paint paint = drawable.getPaint(); - if (paint != null) { - canvas.drawPath(clipPath, paint); - } - if (borderPaint != null) { - canvas.drawPath(clipPath, borderPaint); - } - } - - /** - * Gets the value of android.R.attr.windowBackground from the given Context - * @param context - * @return the color or -1 if not found - */ - public static int getWindowBackgroundColor(Context context) - { - TypedValue value = new TypedValue(); - if (!context.getTheme().resolveAttribute(android.R.attr.windowBackground, value, true) && isColorType(value)) { - return value.data; - } else { - return -1; - } - } - - /** - * Needed because TypedValue.isColorType() is only API Q+ - * https://github.com/aosp-mirror/platform_frameworks_base/blob/1d896eeeb8744a1498128d62c09a3aa0a2a29a16/core/java/android/util/TypedValue.java#L266-L268 - * @param value - * @return true if the TypedValue is a Color - */ - private static boolean isColorType(TypedValue value) - { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return value.isColorType(); - } else { - // Implementation from AOSP - return (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT); - } - } - /** * Sets the maxLength of an EditText * @param editText diff --git a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformWrapperView.java b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformWrapperView.java index ddd82a51ae1c..17f24676ba32 100644 --- a/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformWrapperView.java +++ b/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformWrapperView.java @@ -15,6 +15,7 @@ import android.graphics.Rect; import android.graphics.Shader; + import android.view.View; import androidx.annotation.NonNull; @@ -48,83 +49,103 @@ public PlatformWrapperView(Context context) { private boolean shadowInvalidated = true; private boolean hasClip = false; - private int paintType = PlatformPaintType.NONE; private float offsetX = 0; private float offsetY = 0; private float radius = 0; - private int[] colors = new int[0]; - private float[] positions = new float[0]; - private float[] bounds = new float[0]; + + private PlatformDrawableStyle shadowStyle = new PlatformDrawableStyle(); @Override protected void setHasClip(boolean hasClip) { super.setHasClip(hasClip); this.hasClip = hasClip; - shadowInvalidated = true; + this.shadowInvalidated = true; } - protected final void updateShadow(int paintType, float radius, float offsetX, float offsetY, int[] colors, float[] positions, float[] bounds) { - this.paintType = paintType; + protected final void setLinearGradientShadow(float radius, float offsetX, float offsetY, float x1, float y1, float x2, float y2, int[] colors, float[] positions) { + this.radius = radius; + this.offsetX = offsetX; + this.offsetY = offsetY; + float[] bounds = { x1, y1, x2, y2 }; + this.shadowStyle.setStyle(PlatformPaintType.LINEAR, 0, colors, positions, bounds); + onShadowStyleChanged(); + } + + protected final void setRadialGradientShadow(float radius, float offsetX, float offsetY, float x, float y, float gradientRadius, int[] colors, float[] positions) { + this.radius = radius; + this.offsetX = offsetX; + this.offsetY = offsetY; + float[] bounds = { x, y, gradientRadius }; + this.shadowStyle.setStyle(PlatformPaintType.RADIAL, 0, colors, positions, bounds); + onShadowStyleChanged(); + } + + protected final void setSolidShadow(float radius, float offsetX, float offsetY, int solidColor) { this.radius = radius; this.offsetX = offsetX; this.offsetY = offsetY; - this.colors = colors; - this.positions = positions; - this.bounds = bounds; - - if (paintType == PlatformPaintType.NONE) { - shadowPaint = null; - shadowCanvas = null; - if (shadowBitmap != null) { - bitmapPool.put(shadowBitmap); - shadowBitmap = null; + this.shadowStyle.setStyle(PlatformPaintType.SOLID, solidColor, null, null, null); + onShadowStyleChanged(); + } + + protected final void setNoShadow() { + this.radius = 0; + this.offsetX = 0; + this.offsetY = 0; + this.shadowStyle.setStyle(PlatformPaintType.NONE, 0, null, null, null); + onShadowStyleChanged(); + } + + private void onShadowStyleChanged() { + if (this.shadowStyle.getPaintType() == PlatformPaintType.NONE) { + this.shadowPaint = null; + this.shadowCanvas = null; + if (this.shadowBitmap != null) { + this.bitmapPool.put(this.shadowBitmap); + this.shadowBitmap = null; } } else { - shadowCanvas = new Canvas(); - shadowPaint = new Paint(); - shadowPaint.setAntiAlias(true); - shadowPaint.setDither(true); - shadowPaint.setFilterBitmap(true); - shadowPaint.setStyle(Paint.Style.FILL_AND_STROKE); - - if (radius > 0) { - shadowPaint.setMaskFilter(new BlurMaskFilter(radius, BlurMaskFilter.Blur.NORMAL)); - } - - if (paintType == PlatformPaintType.SOLID) { - shadowPaint.setColor(colors.length > 0 ? colors[0] : android.graphics.Color.BLACK); + this.shadowCanvas = new Canvas(); + this.shadowPaint = new Paint(); + this.shadowPaint.setAntiAlias(true); + this.shadowPaint.setDither(true); + this.shadowPaint.setFilterBitmap(true); + this.shadowPaint.setStyle(Paint.Style.FILL_AND_STROKE); + + if (this.radius > 0) { + this.shadowPaint.setMaskFilter(new BlurMaskFilter(this.radius, BlurMaskFilter.Blur.NORMAL)); } } - shadowInvalidated = true; + this.shadowInvalidated = true; invalidate(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - shadowInvalidated = true; - if (shadowBitmap != null) { - bitmapPool.put(shadowBitmap); - shadowBitmap = null; + this.shadowInvalidated = true; + if (this.shadowBitmap != null) { + this.bitmapPool.put(this.shadowBitmap); + this.shadowBitmap = null; } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - shadowInvalidated = true; + this.shadowInvalidated = true; } @Override public void requestLayout() { super.requestLayout(); - shadowInvalidated = true; + this.shadowInvalidated = true; } @Override public void invalidate() { super.invalidate(); - shadowInvalidated = true; + this.shadowInvalidated = true; } @Override @@ -142,9 +163,9 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @Override protected void dispatchDraw(Canvas canvas) { - if (paintType != PlatformPaintType.NONE) { - int viewWidth = viewBounds.width(); - int viewHeight = viewBounds.height(); + if (this.shadowStyle.getPaintType() != PlatformPaintType.NONE) { + int viewWidth = this.viewBounds.width(); + int viewHeight = this.viewBounds.height(); if (getChildCount() > 0) { View child = getChildAt(0); @@ -190,20 +211,20 @@ private void drawShadowViaPlatformShadowDrawable(@NonNull Canvas canvas, @NonNul int bitmapWidth = viewWidth + radiusSafeSpace; int bitmapHeight = viewHeight + radiusSafeSpace; - // Apply shader if needed - updateShadowShader(bitmapWidth, bitmapHeight); + // Apply shadow style + this.shadowStyle.applyStyle(this.shadowPaint, bitmapWidth, bitmapHeight, null); - Path clipPath = hasClip ? getClipPath(viewWidth, viewHeight) : null; + Path clipPath = this.hasClip ? getClipPath(viewWidth, viewHeight) : null; canvas.save(); - canvas.translate(offsetX, offsetY); - drawable.drawShadow(canvas, shadowPaint, clipPath); + canvas.translate(this.offsetX, this.offsetY); + drawable.drawShadow(canvas, this.shadowPaint, clipPath); canvas.restore(); } private void drawShadowViaDispatchDraw(@NonNull Canvas canvas, int viewWidth, int viewHeight) { - if (shadowInvalidated) { - shadowInvalidated = false; + if (this.shadowInvalidated) { + this.shadowInvalidated = false; int radiusSafeSpace = getRadiusSafeSpace(); int bitmapWidth = normalizeForPool(viewWidth + radiusSafeSpace); @@ -211,76 +232,50 @@ private void drawShadowViaDispatchDraw(@NonNull Canvas canvas, int viewWidth, in int drawOriginX = (bitmapWidth - viewWidth) / 2; int drawOriginY = (bitmapHeight - viewHeight) / 2; - if (shadowBitmap != null) { - if (shadowBitmap.getWidth() == bitmapWidth && shadowBitmap.getHeight() == bitmapHeight) { - shadowBitmap.eraseColor(Color.TRANSPARENT); + if (this.shadowBitmap != null) { + if (this.shadowBitmap.getWidth() == bitmapWidth && this.shadowBitmap.getHeight() == bitmapHeight) { + this.shadowBitmap.eraseColor(Color.TRANSPARENT); } else { - bitmapPool.put(shadowBitmap); - shadowBitmap = bitmapPool.get(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + this.bitmapPool.put(this.shadowBitmap); + this.shadowBitmap = this.bitmapPool.get(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); } } else { - shadowBitmap = bitmapPool.get(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + this.shadowBitmap = this.bitmapPool.get(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); } - shadowCanvas.setBitmap(shadowBitmap); + this.shadowCanvas.setBitmap(this.shadowBitmap); // Create the local copy of all content to draw bitmap as a bottom layer of natural canvas. - Bitmap extractAlpha = bitmapPool.get(normalizeForPool(viewWidth), normalizeForPool(viewHeight), Bitmap.Config.ALPHA_8); + Bitmap extractAlpha = this.bitmapPool.get(normalizeForPool(viewWidth), normalizeForPool(viewHeight), Bitmap.Config.ALPHA_8); Canvas alphaCanvas = new Canvas(extractAlpha); super.dispatchDraw(alphaCanvas); - // Apply shader if needed - updateShadowShader(bitmapWidth, bitmapHeight); + // Apply shadow style + this.shadowStyle.applyStyle(this.shadowPaint, bitmapWidth, bitmapHeight, null); // Why don't we simply draw the alpha bitmap directly on the view canvas? // Reason: setMaskFilter (used by shadowPaint) is *not* supported in hardware accelerated mode // https://developer.android.com/develop/ui/views/graphics/hardware-accel // If we use `SOFTWARE` layer, than we fall into a view-clipped `Canvas` where we can't draw the outer shadow. - shadowCanvas.drawBitmap(extractAlpha, drawOriginX, drawOriginY, shadowPaint); + this.shadowCanvas.drawBitmap(extractAlpha, drawOriginX, drawOriginY, this.shadowPaint); - bitmapPool.put(extractAlpha); + this.bitmapPool.put(extractAlpha); - shadowBitmapX = offsetX - drawOriginX; - shadowBitmapY = offsetY - drawOriginY; + this.shadowBitmapX = this.offsetX - drawOriginX; + this.shadowBitmapY = this.offsetY - drawOriginY; } // Draw shadow rectangle - canvas.drawBitmap(shadowBitmap, shadowBitmapX, shadowBitmapY, null); + canvas.drawBitmap(this.shadowBitmap, this.shadowBitmapX, this.shadowBitmapY, null); } private int getRadiusSafeSpace() { // Account for potentially different blurring algorithms - return (int)(radius * 3); + return (int)(this.radius * 3); } private static int normalizeForPool(int pixels) { // We want to reuse memory as much as possible so let's normalize bitmaps to the nearest 48px grid. return (int)(Math.ceil(((double)pixels) / 48.0) * 48.0); } - - private void updateShadowShader(int bitmapWidth, int bitmapHeight) { - Shader shader = null; - - if (paintType == PlatformPaintType.LINEAR) { - shader = new android.graphics.LinearGradient( - bounds[0] * bitmapWidth, bounds[1] * bitmapHeight, // Start point - bounds[2] * bitmapWidth, bounds[3] * bitmapHeight, // End point - colors, - positions, - android.graphics.Shader.TileMode.CLAMP - ); - } else if (paintType == PlatformPaintType.RADIAL) { - shader = new android.graphics.RadialGradient( - bounds[0] * bitmapWidth, bounds[1] * bitmapHeight, // Center point - bounds[2] * Math.max(bitmapWidth, bitmapHeight), // Radius - colors, - positions, - android.graphics.Shader.TileMode.CLAMP - ); - } - - if (shader != null) { - shadowPaint.setShader(shader); - } - } } diff --git a/src/Core/src/Graphics/IShape.cs b/src/Core/src/Graphics/IShape.cs index 9887963f047a..6fcd0731aead 100644 --- a/src/Core/src/Graphics/IShape.cs +++ b/src/Core/src/Graphics/IShape.cs @@ -14,4 +14,17 @@ internal interface IRoundRectangle : IShape PathF InnerPathForBounds(Rect bounds, float strokeThickness); PathF InnerPath(); } + + /// + /// Provides a version for the so that every time something + /// changes in the shape definition the is increased by 1. + /// + /// + /// This is especially useful in handler's mappers to improve the performance + /// by avoiding useless calls to platform when nothing has changed. + /// + internal interface IVersionedShape : IShape + { + int Version { get; } + } } \ No newline at end of file diff --git a/src/Core/src/Graphics/MauiDrawable.Android.cs b/src/Core/src/Graphics/MauiDrawable.Android.cs index ff86649d457c..0a2df48b1bbd 100644 --- a/src/Core/src/Graphics/MauiDrawable.Android.cs +++ b/src/Core/src/Graphics/MauiDrawable.Android.cs @@ -1,18 +1,15 @@ using System; using System.Linq; using Android.Graphics; -using Android.Graphics.Drawables; -using Android.Graphics.Drawables.Shapes; using static Android.Graphics.Paint; using AColor = Android.Graphics.Color; using AContext = Android.Content.Context; using APaint = Android.Graphics.Paint; -using ARect = Android.Graphics.Rect; using GPaint = Microsoft.Maui.Graphics.Paint; namespace Microsoft.Maui.Graphics { - public class MauiDrawable : PaintDrawable, IPlatformShadowDrawable + public class MauiDrawable : PlatformDrawable { static Join? JoinMiter; static Join? JoinBevel; @@ -23,121 +20,115 @@ public class MauiDrawable : PaintDrawable, IPlatformShadowDrawable static Cap? CapRound; readonly AContext? _context; - readonly float _density; - - bool _invalidatePath; - bool _isBackgroundSolid; - bool _isBorderSolid; - - bool _disposed; - - int _width; - int _height; - - Path? _clipPath; - Path? _fullClipPath; - APaint? _borderPaint; - - IShape? _shape; + // Cache values on .NET side to avoid unnecessary calls to Java GPaint? _background; - AColor? _backgroundColor; - GPaint? _stroke; - AColor? _borderColor; - PathEffect? _borderPathEffect; - + IShape? _shape; + int _shapeVersion; + double _strokeWidth; + float _strokeMiterLimit; Join? _strokeLineJoin; Cap? _strokeLineCap; - + float[]? _strokeDashArray; float _strokeThickness; - float _strokeMiterLimit; + double _strokeDashOffset; - public MauiDrawable(AContext? context) + public MauiDrawable(AContext? context) : base(context) { - Shape = new RectShape(); - - _invalidatePath = true; - - _clipPath = new Path(); - _fullClipPath = new Path(); - _context = context; - _density = context.GetDisplayDensity(); + + // Initialize cached values + _strokeWidth = 0; + _strokeMiterLimit = 0; + _strokeLineJoin = null; + _strokeLineCap = null; } + [Obsolete("Use `SetBackground(Microsoft.Maui.Graphics.Paint paint)` instead.")] public void SetBackgroundColor(AColor? backgroundColor) { - if (_backgroundColor == backgroundColor) - return; - - _backgroundColor = backgroundColor; - _isBackgroundSolid = backgroundColor?.A is 255; - - InvalidateSelf(); + if (backgroundColor is null) + { + SetBackground((GPaint?)null); + } + else + { + var solidPaint = new SolidPaint(backgroundColor.Value.ToColor()); + SetBackground(solidPaint); + } } public void SetBackground(GPaint? paint) { - if (paint is SolidPaint solidPaint) - SetBackground(solidPaint); - else if (paint is LinearGradientPaint linearGradientPaint) - SetBackground(linearGradientPaint); - else if (paint is RadialGradientPaint radialGradientPaint) - SetBackground(radialGradientPaint); - else if (paint is ImagePaint imagePaint) - SetBackground(imagePaint); - else if (paint is PatternPaint patternPaint) - SetBackground(patternPaint); - else - SetDefaultBackgroundColor(); + switch (paint) + { + case SolidPaint solidPaint: + SetBackground(solidPaint); + break; + case LinearGradientPaint linearGradientPaint: + SetBackground(linearGradientPaint); + break; + case RadialGradientPaint radialGradientPaint: + SetBackground(radialGradientPaint); + break; + case ImagePaint imagePaint: + SetBackground(imagePaint); + break; + case PatternPaint patternPaint: + SetBackground(patternPaint); + break; + default: + SetDefaultBackgroundColor(); + break; + } } public void SetBackground(SolidPaint solidPaint) { - _invalidatePath = true; - _backgroundColor = null; - _background = null; + if (_background == solidPaint) + { + return; + } - var color = solidPaint.Color; - if (color == null) - SetDefaultBackgroundColor(); + _background = solidPaint; + + if (solidPaint.Color is { } color) + { + SetSolidBackground(color.ToPlatform()); + } else { - var backgroundColor = color.ToPlatform(); - SetBackgroundColor(backgroundColor); - _isBackgroundSolid = color.Alpha == 1; + SetDefaultBackgroundColor(); } } public void SetBackground(LinearGradientPaint linearGradientPaint) { if (_background == linearGradientPaint) + { return; + } - _invalidatePath = true; - - _backgroundColor = null; _background = linearGradientPaint; - _isBackgroundSolid = linearGradientPaint.GradientStops.All(s => s.Color.Alpha == 1); - - InitializeBorderIfNeeded(); - InvalidateSelf(); + + var gradientData = linearGradientPaint.GetGradientData(1.0f); + + SetLinearGradientBackground(gradientData.X1, gradientData.Y1, gradientData.X2, gradientData.Y2, gradientData.Colors, gradientData.Offsets); } public void SetBackground(RadialGradientPaint radialGradientPaint) { if (_background == radialGradientPaint) + { return; + } - _invalidatePath = true; - - _backgroundColor = null; _background = radialGradientPaint; - _isBackgroundSolid = radialGradientPaint.GradientStops.All(s => s.Color.Alpha == 1); - InitializeBorderIfNeeded(); - InvalidateSelf(); + var gradientData = radialGradientPaint.GetGradientData(1.0f); + + SetRadialGradientBackground(gradientData.CenterX, gradientData.CenterY, gradientData.Radius, gradientData.Colors, gradientData.Offsets); } public void SetBackground(ImagePaint imagePaint) @@ -150,115 +141,141 @@ public void SetBackground(PatternPaint patternPaint) throw new NotImplementedException(); } - public void SetBorderShape(IShape? shape) + void SetDefaultBackgroundColor() { - if (_shape == shape) + if (_background is null) + { return; + } - _invalidatePath = true; - - _shape = shape; + _background = null; - InitializeBorderIfNeeded(); - InvalidateSelf(); + SetNoBackground(); } - public void SetBorderColor(AColor? borderColor) + public void SetBorderShape(IShape? shape) { - if (_borderColor == borderColor) - return; + // Border's shape is allowed to be a BindableObject so it can change even without changing instance + // and trigger a shape update, so even if the instance is the same we need to update the shape unless + // it implements our internal IVersionedShape and the version hasn't changed + // (this is an internal optimization to avoid unnecessary updates) + if (shape is IVersionedShape versionedShape) + { + if (shape == _shape && _shapeVersion == versionedShape.Version) + { + return; + } - _borderColor = borderColor; - _isBorderSolid = borderColor?.A is 255; + _shapeVersion = versionedShape.Version; + } + else + { + // When not versioned, we have no way to know if something changed, + // so we just assume it did at cost of performance. + _shapeVersion = 0; + } - InitializeBorderIfNeeded(); - InvalidateSelf(); + _shape = shape; + + ShapeChanged(shape != null); } - public void SetBorderBrush(GPaint? paint) + [Obsolete("Use `SetBorderBrush(Microsoft.Maui.Graphics.Paint paint)` instead.")] + public void SetBorderColor(AColor? borderColor) { - if (paint is SolidPaint solidPaint) + if (borderColor is null) + { + SetEmptyBorderBrush(); + } + else + { + var solidPaint = new SolidPaint(borderColor.Value.ToColor()); SetBorderBrush(solidPaint); + } + } - else if (paint is LinearGradientPaint linearGradientPaint) - SetBorderBrush(linearGradientPaint); - - else if (paint is RadialGradientPaint radialGradientPaint) - SetBorderBrush(radialGradientPaint); - - else if (paint is ImagePaint imagePaint) - SetBorderBrush(imagePaint); - - else if (paint is PatternPaint patternPaint) - SetBorderBrush(patternPaint); - - else - SetEmptyBorderBrush(); + public void SetBorderBrush(GPaint? paint) + { + switch (paint) + { + case SolidPaint solidPaint: + SetBorderBrush(solidPaint); + break; + case LinearGradientPaint linearGradientPaint: + SetBorderBrush(linearGradientPaint); + break; + case RadialGradientPaint radialGradientPaint: + SetBorderBrush(radialGradientPaint); + break; + case ImagePaint imagePaint: + SetBorderBrush(imagePaint); + break; + case PatternPaint patternPaint: + SetBorderBrush(patternPaint); + break; + default: + SetEmptyBorderBrush(); + break; + } } public void SetBorderBrush(SolidPaint solidPaint) { - _invalidatePath = true; - _borderColor = null; - _borderPaint = null; - - var color = solidPaint.Color; - var borderColor = color?.ToPlatform(); - - _stroke = null; - SetBorderColor(borderColor); - _isBorderSolid = solidPaint.IsSolid(); + if (_stroke == solidPaint) + { + return; + } + + _stroke = solidPaint; + + if (solidPaint.Color is { } color) + { + SetSolidBorder(color.ToPlatform()); + } + else + { + SetEmptyBorderBrush(); + } } public void SetBorderBrush(LinearGradientPaint linearGradientPaint) { if (_stroke == linearGradientPaint) + { return; - - _invalidatePath = true; - - _borderColor = null; + } + _stroke = linearGradientPaint; - _isBorderSolid = linearGradientPaint.IsSolid(); - - InitializeBorderIfNeeded(); - InvalidateSelf(); + + var gradientData = linearGradientPaint.GetGradientData(1.0f); + + SetLinearGradientBorder(gradientData.X1, gradientData.Y1, gradientData.X2, gradientData.Y2, gradientData.Colors, gradientData.Offsets); } public void SetBorderBrush(RadialGradientPaint radialGradientPaint) { if (_stroke == radialGradientPaint) + { return; - - _invalidatePath = true; - - _borderColor = null; + } + _stroke = radialGradientPaint; - _isBorderSolid = radialGradientPaint.IsSolid(); - InitializeBorderIfNeeded(); - InvalidateSelf(); + var gradientData = radialGradientPaint.GetGradientData(1.0f); + + SetRadialGradientBorder(gradientData.CenterX, gradientData.CenterY, gradientData.Radius, gradientData.Colors, gradientData.Offsets); } public void SetEmptyBorderBrush() { - _invalidatePath = true; - - if (_backgroundColor != null) - { - _borderColor = _backgroundColor.Value; - _stroke = null; - } - else + if (_stroke is null) { - _borderColor = null; - - if (_background != null) - SetBorderBrush(_background); + return; } + + _stroke = null; - InitializeBorderIfNeeded(); - InvalidateSelf(); + SetNoBorder(); } public void SetBorderBrush(ImagePaint imagePaint) @@ -273,45 +290,59 @@ public void SetBorderBrush(PatternPaint patternPaint) public void SetBorderWidth(double strokeWidth) { - float strokeThickness = (float)(strokeWidth * _density); - - if (_strokeThickness == strokeThickness) + if (_strokeWidth == strokeWidth) + { return; + } - _invalidatePath = true; + _strokeWidth = strokeWidth; + float density = _context.GetDisplayDensity(); + var strokeThickness = (float)(strokeWidth * density); _strokeThickness = strokeThickness; - - InitializeBorderIfNeeded(); - InvalidateSelf(); + StrokeThickness = strokeThickness; } public void SetBorderDash(float[]? strokeDashArray, double strokeDashOffset) { - if (strokeDashArray is null || strokeDashArray.Length == 0) - _borderPathEffect = null; - else + if (strokeDashArray == _strokeDashArray && _strokeDashOffset == strokeDashOffset) + { + return; + } + + _strokeDashArray = strokeDashArray; + _strokeDashOffset = strokeDashOffset; + + PathEffect? pathEffect = null; + if (strokeDashArray is not null && strokeDashArray.Length > 0) { float[] strokeDash = new float[strokeDashArray.Length]; + float strokeThickness = _strokeThickness; for (int i = 0; i < strokeDashArray.Length; i++) - strokeDash[i] = strokeDashArray[i] * _strokeThickness; + { + strokeDash[i] = strokeDashArray[i] * strokeThickness; + } if (strokeDash.Length > 1) - _borderPathEffect = new DashPathEffect(strokeDash, (float)strokeDashOffset * _strokeThickness); + { + pathEffect = new DashPathEffect(strokeDash, (float)strokeDashOffset * strokeThickness); + } } - InvalidateSelf(); + BorderPathEffect = pathEffect; } public void SetBorderMiterLimit(float strokeMiterLimit) { if (_strokeMiterLimit == strokeMiterLimit) + { return; + } _strokeMiterLimit = strokeMiterLimit; - InvalidateSelf(); + StrokeMiterLimit = strokeMiterLimit; } public void SetBorderLineJoin(LineJoin lineJoin) @@ -325,11 +356,13 @@ public void SetBorderLineJoin(LineJoin lineJoin) }; if (_strokeLineJoin == aLineJoin) + { return; + } _strokeLineJoin = aLineJoin; - InvalidateSelf(); + StrokeLineJoin = aLineJoin; } public void SetBorderLineCap(LineCap lineCap) @@ -343,11 +376,13 @@ public void SetBorderLineCap(LineCap lineCap) }; if (_strokeLineCap == aLineCap) + { return; + } _strokeLineCap = aLineCap; - InvalidateSelf(); + StrokeLineCap = aLineCap; } public void InvalidateBorderBounds() @@ -355,319 +390,41 @@ public void InvalidateBorderBounds() InvalidateSelf(); } - protected override void OnBoundsChange(ARect bounds) - { - var width = bounds.Width(); - var height = bounds.Height(); - - if (_width == width && _height == height) - return; - - _invalidatePath = true; - - _width = width; - _height = height; - - base.OnBoundsChange(bounds); - } - - bool IPlatformShadowDrawable.CanDrawShadow() - { - return _isBackgroundSolid && (_strokeThickness == 0 || _isBorderSolid); - } - - void IPlatformShadowDrawable.DrawShadow(Canvas? canvas, APaint? shadowPaint, Path? outerClipPath) - { - if (_disposed || canvas is null || shadowPaint is null) - return; - - Path contentPath; - - if (HasBorder()) - { - if (!TryUpdateClipPath() || _fullClipPath == null) - { - return; - } - - contentPath = _fullClipPath; - } - else - { - contentPath = new Path(); - contentPath.AddRect(0, 0, _width, _height, Path.Direction.Cw!); - } - - if (outerClipPath != null) - { - var clippedPath = new Path(); - clippedPath.InvokeOp(contentPath, outerClipPath, Path.Op.Intersect!); - canvas.DrawPath(clippedPath, shadowPaint); - clippedPath.Dispose(); - } - else - { - canvas.DrawPath(contentPath, shadowPaint); - } - - if (contentPath != _fullClipPath) - { - contentPath.Dispose(); - } - } - - protected override void OnDraw(Shape? shape, Canvas? canvas, APaint? paint) - { - if (_disposed) - return; - - if (HasBorder()) - { - if (Paint != null) - SetBackground(Paint); - - if (_borderPaint != null) - { - PlatformInterop.SetPaintValues(_borderPaint, _strokeThickness, _strokeLineJoin, _strokeLineCap, _strokeMiterLimit * 2, _borderPathEffect); - - if (_borderColor != null) - { - _borderPaint.Color = _borderColor.Value; - } - else - { - if (_stroke != null) - SetPaint(_borderPaint, _stroke); - } - } - - if (!TryUpdateClipPath()) - { - return; - } - - if (canvas == null || _clipPath == null) - return; - - PlatformInterop.DrawMauiDrawablePath(this, canvas, _width, _height, _clipPath, _borderPaint); - } - else - { - if (paint != null) - SetBackground(paint); - - base.OnDraw(shape, canvas, paint); - } - } - - bool TryUpdateClipPath() - { - if (_invalidatePath) - { - _invalidatePath = false; - - if (_shape != null) - { - float strokeThickness = _strokeThickness / _density; - float fw = _width / _density; - float w = fw - strokeThickness; - float fh = _height / _density; - float h = fh - strokeThickness; - float x = strokeThickness / 2; - float y = strokeThickness / 2; - - var bounds = new Rect(x, y, w, h); - var clipPath = _shape?.ToPlatform(bounds, strokeThickness, _density); - - if (clipPath == null) - return false; - - if (_clipPath != null) - { - _clipPath.Reset(); - _clipPath.Set(clipPath); - } - - var fullClipPath = _shape!.ToPlatform(new Rect(0, 0, fw, fh), 0, _density); - if (_fullClipPath != null) - { - _fullClipPath.Reset(); - _fullClipPath.Set(fullClipPath); - } - } - } - - return true; - } - - protected override void Dispose(bool disposing) - { - if (_disposed) - return; - - _disposed = true; - - if (disposing) - { - _borderPathEffect?.Dispose(); - _borderPathEffect = null; - - _clipPath?.Dispose(); - _clipPath = null; - - _fullClipPath?.Dispose(); - _fullClipPath = null; - } - - DisposeBorder(disposing); - - base.Dispose(disposing); - } - - protected virtual void DisposeBorder(bool disposing) + internal override void UpdateClipPath(int width, int height) { - if (disposing) + if (_shape is null) { - _borderPaint?.Dispose(); - _borderPaint = null; - } - } - - bool HasBorder() - { - InitializeBorderIfNeeded(); - - return _shape != null; - } - - void InitializeBorderIfNeeded() - { - if (_strokeThickness == 0) - { - DisposeBorder(true); return; } - if (_borderPaint == null) - { - _borderPaint = new APaint(PaintFlags.AntiAlias); - _borderPaint.SetStyle(APaint.Style.Stroke); - } - } - - void SetDefaultBackgroundColor() - { - var color = PlatformInterop.GetWindowBackgroundColor(_context); - if (color != -1) - { - var backgroundColor = new AColor(color); - _backgroundColor = backgroundColor; - _isBackgroundSolid = backgroundColor.IsSolid(); - } - } - - void SetBackground(APaint platformPaint) - { - if (platformPaint != null) - { - if (_backgroundColor != null) - { - platformPaint.SetShader(null); -#pragma warning disable CA1416 // https://github.com/xamarin/xamarin-android/issues/6962 - platformPaint.Color = _backgroundColor.Value; -#pragma warning restore CA1416 - } - else if (_background != null) - { - SetPaint(platformPaint, _background); - } - else - { - platformPaint.Color = AColor.Transparent; - } - } - } - - void SetPaint(APaint platformPaint, GPaint paint) - { - if (paint is LinearGradientPaint linearGradientPaint) - SetLinearGradientPaint(platformPaint, linearGradientPaint); - - if (paint is RadialGradientPaint radialGradientPaint) - SetRadialGradientPaint(platformPaint, radialGradientPaint); - } - - void SetLinearGradientPaint(APaint platformPaint, LinearGradientPaint linearGradientPaint) - { - var p1 = linearGradientPaint.StartPoint; - var x1 = (float)p1.X; - var y1 = (float)p1.Y; - var p2 = linearGradientPaint.EndPoint; - var x2 = (float)p2.X; - var y2 = (float)p2.Y; - - var data = GetGradientPaintData(linearGradientPaint); - var shader = new LinearGradientData(data.Colors, data.Offsets, x1, y1, x2, y2); - if (_width == 0 && _height == 0) - return; + float density = _context.GetDisplayDensity(); + float strokeWidth = (float)(_strokeThickness / density); + float fw = width / density; + float w = fw - strokeWidth; + float fh = height / density; + float h = fh - strokeWidth; + float x = strokeWidth / 2; + float y = strokeWidth / 2; - if (shader.Colors == null || shader.Colors.Length < 2) - return; + var bounds = new Rect(x, y, w, h); + var clipPath = _shape.ToPlatform(bounds, strokeWidth, density); - var linearGradientShader = new LinearGradient( - _width * shader.X1, - _height * shader.Y1, - _width * shader.X2, - _height * shader.Y2, - shader.Colors, - shader.Offsets, - Shader.TileMode.Clamp!); + ClipPath = clipPath; - platformPaint.SetShader(linearGradientShader); + var fullClipPath = _shape.ToPlatform(new Rect(0, 0, fw, fh), 0, density); + FullClipPath = fullClipPath; } + [Obsolete("This was part of internal logic and was public by mistake, it will be removed in the next major version.")] public void SetRadialGradientPaint(APaint platformPaint, RadialGradientPaint radialGradientPaint) { - var center = radialGradientPaint.Center; - float centerX = (float)center.X; - float centerY = (float)center.Y; - float radius = (float)radialGradientPaint.Radius; - - var gradientData = GetGradientPaintData(radialGradientPaint); - var radialGradientData = new RadialGradientData(gradientData.Colors, gradientData.Offsets, centerX, centerY, radius); - - if (_width == 0 && _height == 0) - return; - - if (radialGradientData.Colors == null || radialGradientData.Colors.Length < 2) - return; - - var radialGradient = new RadialGradient( - _width * radialGradientData.CenterX, - _height * radialGradientData.CenterY, - Math.Max(_height, _width) * radialGradientData.Radius, - radialGradientData.Colors, - radialGradientData.Offsets, - Shader.TileMode.Clamp!); - - platformPaint.SetShader(radialGradient); + // No-op - functionality moved to Java } - static GradientData GetGradientPaintData(GradientPaint gradientPaint) + [Obsolete("This was part of internal logic and was public by mistake, it will be removed in the next major version.")] + protected virtual void DisposeBorder(bool disposing) { - var orderStops = gradientPaint.GradientStops; - - var data = new GradientData(orderStops.Length); - - int count = 0; - foreach (var orderStop in orderStops.OrderBy(s => s.Offset)) - { - data.Colors[count] = orderStop.Color.ToPlatform().ToArgb(); - data.Offsets[count] = orderStop.Offset; - count++; - } - - return data; + // No-op - functionality moved to Java } } } \ No newline at end of file diff --git a/src/Core/src/Graphics/PaintExtensions.Android.cs b/src/Core/src/Graphics/PaintExtensions.Android.cs index 7ae548ace8f5..766a07fa4373 100644 --- a/src/Core/src/Graphics/PaintExtensions.Android.cs +++ b/src/Core/src/Graphics/PaintExtensions.Android.cs @@ -77,21 +77,6 @@ public static partial class PaintExtensions return drawable; } - internal static bool IsSolid(this AColor color) - { - return color.A is 1; - } - - internal static bool IsSolid(this SolidPaint paint) - { - return !paint.IsNullOrEmpty() && paint.Color.Alpha == 1; - } - - internal static bool IsSolid(this GradientPaint paint) - { - return !paint.IsNullOrEmpty() && paint.GradientStops.All(s => s.Color?.Alpha == 1); - } - internal static bool IsValid(this GradientPaint? gradientPaint) => gradientPaint?.GradientStops?.Length > 0; diff --git a/src/Core/src/Platform/Android/WrapperView.cs b/src/Core/src/Platform/Android/WrapperView.cs index d426a3a28eee..8f56c13c8961 100644 --- a/src/Core/src/Platform/Android/WrapperView.cs +++ b/src/Core/src/Platform/Android/WrapperView.cs @@ -67,45 +67,32 @@ partial void ShadowChanged() float radius = context.ToPixels(Shadow.Radius); float offsetX = context.ToPixels(Shadow.Offset.X); float offsetY = context.ToPixels(Shadow.Offset.Y); - int paintType; - int[] colors; - float[] positions; - float[] bounds; switch (shadowPaint) { + case SolidPaint solidPaint: + // If the alpha is set in the color value, the shadow transparency is applied based on that alpha. + // If the Opacity property is set directly, the shadow transparency is applied based on the Opacity. + // If both values are provided, the color alpha is combined with the Opacity to apply a unified transparency effect to the shadow, ensuring consistent behavior across platforms. + SetSolidShadow(radius, offsetX, offsetY, solidPaint.Color.WithAlpha(solidPaint.Color.Alpha * shadowOpacity) + .ToPlatform() + .ToArgb()); + break; case LinearGradientPaint linearGradientPaint: var linearGradientData = linearGradientPaint.GetGradientData(shadowOpacity); - paintType = PlatformPaintType.Linear; - colors = linearGradientData.Colors; - positions = linearGradientData.Offsets; - bounds = [linearGradientData.X1, linearGradientData.Y1, linearGradientData.X2, linearGradientData.Y2]; + SetLinearGradientShadow(radius, offsetX, offsetY, linearGradientData.X1, linearGradientData.Y1, linearGradientData.X2, linearGradientData.Y2, linearGradientData.Colors, linearGradientData.Offsets); break; case RadialGradientPaint radialGradientPaint: var radialGradientData = radialGradientPaint.GetGradientData(shadowOpacity); - paintType = PlatformPaintType.Radial; - colors = radialGradientData.Colors; - positions = radialGradientData.Offsets; - bounds = [radialGradientData.CenterX, radialGradientData.CenterY, radialGradientData.Radius]; - break; - case SolidPaint solidPaint: - paintType = PlatformPaintType.Solid; - // If the alpha is set in the color value, the shadow transparency is applied based on that alpha. - // If the Opacity property is set directly, the shadow transparency is applied based on the Opacity. - // If both values are provided, the color alpha is combined with the Opacity to apply a unified transparency effect to the shadow, ensuring consistent behavior across platforms. - colors = [solidPaint.Color.WithAlpha(solidPaint.Color.Alpha * shadowOpacity).ToPlatform().ToArgb()]; - positions = null; - bounds = null; + SetRadialGradientShadow(radius, offsetX, offsetY, radialGradientData.CenterX, radialGradientData.CenterY, radialGradientData.Radius, radialGradientData.Colors, radialGradientData.Offsets); break; default: throw new NotSupportedException("Unsupported shadow paint type."); } - - UpdateShadow(paintType, radius, offsetX, offsetY, colors, positions, bounds); } else { - UpdateShadow(PlatformPaintType.None, 0, 0, 0, null, null, null); + SetNoShadow(); } } diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index ebf19fa9dfd5..d4e2d8099070 100644 --- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -1,2 +1,11 @@ #nullable enable +Microsoft.Maui.PlatformDrawable +Microsoft.Maui.PlatformDrawable.PlatformDrawable(Android.Content.Context? context) -> void +Microsoft.Maui.PlatformDrawable.PlatformDrawable(nint javaReference, Android.Runtime.JniHandleOwnership transfer) -> void +override Microsoft.Maui.PlatformDrawable.JniPeerMembers.get -> Java.Interop.JniPeerMembers! +override Microsoft.Maui.PlatformDrawable.ThresholdClass.get -> nint +override Microsoft.Maui.PlatformDrawable.ThresholdType.get -> System.Type! +*REMOVED*override Microsoft.Maui.Graphics.MauiDrawable.Dispose(bool disposing) -> void +*REMOVED*override Microsoft.Maui.Graphics.MauiDrawable.OnBoundsChange(Android.Graphics.Rect! bounds) -> void +*REMOVED*override Microsoft.Maui.Graphics.MauiDrawable.OnDraw(Android.Graphics.Drawables.Shapes.Shape? shape, Android.Graphics.Canvas? canvas, Android.Graphics.Paint? paint) -> void override Microsoft.Maui.Handlers.LabelHandler.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size diff --git a/src/Core/src/Transforms/Metadata.xml b/src/Core/src/Transforms/Metadata.xml index 04519794d25a..22bbd0b411b5 100644 --- a/src/Core/src/Transforms/Metadata.xml +++ b/src/Core/src/Transforms/Metadata.xml @@ -10,12 +10,19 @@ public public public + public + internal public public internal internal - internal + internal + internal + internal + internal + internal + public diff --git a/src/Core/tests/DeviceTests.Shared/Stubs/SolidPaintStub.cs b/src/Core/tests/DeviceTests.Shared/Stubs/SolidPaintStub.cs index e7379c45ee95..3f47992cbc69 100644 --- a/src/Core/tests/DeviceTests.Shared/Stubs/SolidPaintStub.cs +++ b/src/Core/tests/DeviceTests.Shared/Stubs/SolidPaintStub.cs @@ -21,7 +21,7 @@ public CoreAnimation.CALayer ToCALayer(CoreGraphics.CGRect frame = default) => public global::Android.Graphics.Drawables.Drawable ToDrawable() { var drawable = new Microsoft.Maui.Graphics.MauiDrawable(MauiProgramDefaults.DefaultContext); - drawable.SetBackgroundColor(Color.ToPlatform()); + drawable.SetBackground(new SolidPaint(Color)); return drawable; } #endif diff --git a/src/Core/tests/DeviceTests/Graphics/GraphicsTests.Android.cs b/src/Core/tests/DeviceTests/Graphics/GraphicsTests.Android.cs index 02841089a2be..e4bfd5208df7 100644 --- a/src/Core/tests/DeviceTests/Graphics/GraphicsTests.Android.cs +++ b/src/Core/tests/DeviceTests/Graphics/GraphicsTests.Android.cs @@ -141,71 +141,4 @@ public void PointExplicitConversionTest(float x, float y) Assert.Equal(point.X, aPoint.X); Assert.Equal(point.Y, aPoint.Y); } - - [Theory] - [InlineData("#FF0000")] - [InlineData("#00FF00")] - [InlineData("#0000FF")] - public void SolidPaintTest(string hexColor) - { - var color = Color.FromArgb(hexColor); - var solidPaint = new SolidPaint(color); - - Assert.True(solidPaint.IsSolid()); - } - - [Fact] - public void NullSolidPaintTest() - { - Color nullColor = null; - var solidPaintNullColor = new SolidPaint(nullColor); - - Assert.False(solidPaintNullColor.IsSolid()); - - SolidPaint nullSolidPaint = null; - - Assert.False(nullSolidPaint.IsSolid()); - } - - [Theory] - [InlineData("#FF0000", "#00FF00")] - [InlineData("#00FF00", "#0000FF")] - [InlineData("#0000FF", "#FF0000")] - public void LinearGradientPaintTest(string startHexColor, string endHexColor) - { - var startColor = Color.FromArgb(startHexColor); - var endColor = Color.FromArgb(endHexColor); - var linearGradientPaint = new LinearGradientPaintStub(startColor, endColor); - - Assert.True(linearGradientPaint.IsSolid()); - } - - [Fact] - public void NullLinearGradientPaintTest() - { - LinearGradientPaintStub nullLinearGradientPaint = null; - - Assert.False(nullLinearGradientPaint.IsSolid()); - } - - [Theory] - [InlineData("#FF0000", "#00FF00")] - [InlineData("#00FF00", "#0000FF")] - [InlineData("#0000FF", "#FF0000")] - public void RadialGradientPaintTest(string startHexColor, string endHexColor) - { - var startColor = Color.FromArgb(startHexColor); - var endColor = Color.FromArgb(endHexColor); - var radialGradientPaint = new RadialGradientPaintStub(startColor, endColor); - - Assert.True(radialGradientPaint.IsSolid()); - } - - [Fact] - public void NullRadialGradientPaintTest() - { - RadialGradientPaintStub nullRadialGradientPaint = null; - - Assert.False(nullRadialGradientPaint.IsSolid()); - } } \ No newline at end of file