Skip to content

Commit

Permalink
[android] improve ToolbarExtensions.UpdateIconColor performance (#17241)
Browse files Browse the repository at this point in the history
I've been working to get back some of our lost startup performance on
Android in .NET MAUI. We are really close, a few changes will get .NET 8
on par or faster than .NET 7.

Profiling startup of a `dotnet new maui` app on a Pixel 5,
`dotnet-trace` reported time spent in:

    16.62ms microsoft.maui.controls!Microsoft.Maui.Controls.Platform.ToolbarExtensions.UpdateIconColor(

If you drill in further in this method, you can see the time spent in:

    10.19ms xamarin.androidx.appcompat!AndroidX.AppCompat.Widget.Toolbar.get_OverflowIcon()
     6.42ms microsoft.maui!Microsoft.Maui.Platform.DrawableExtensions.SetColorFilter()

The project template doesn't even display a `Toolbar` icon, but it looks
like there are some straightforward performance wins here.

In 9b091fd, accessing `OverflowIcon` was introduced to fix the color of
the overflow icon. This code had the pattern:

    if (navIconColor != null && nativeToolbar.OverflowIcon != null)
    {
        nativeToolbar.OverflowIcon.SetColorFilter(navIconColor, FilterMode.SrcAtop);
    }

This accesses `OverflowIcon` twice, where we can use pattern matching
instead to avoid this.

Additionally, `SetColorFilter` appears to be called in a couple places
throughout .NET MAUI, we can move most of its logic to Java to avoid
interop.

So for example:

    switch (mode)
    {
        case FilterMode.SrcIn:
            return BlendMode.SrcIn;
        case FilterMode.Multiply:
            return BlendMode.Multiply;
        case FilterMode.SrcAtop:
            return BlendMode.SrcAtop;
    }

`Enum` values are just classes in Java, so this calling a Java method
from C# that returns an object. We then have to do various bookkeeping
around handling this Java object instance in C#.

Then the other call, also does Java interop for `BlendModeColorFilter`'s
constructor and the `SetColorFilter()`:

    drawable.SetColorFilter(new BlendModeColorFilter(color, filterMode29));

By moving this logic to Java, we can instead call:

    PlatformInterop.SetColorFilter(drawable, color, (int)mode);

We also do the API Q/29 platform checks on the Java side.

With these changes in place, the three methods above are improved:

    6.19ms microsoft.maui.controls!Microsoft.Maui.Controls.Platform.ToolbarExtensions.UpdateIconColor()
    3.87ms xamarin.androidx.appcompat!AndroidX.AppCompat.Widget.Toolbar.get_OverflowIcon()
    1.15ms microsoft.maui!Microsoft.Maui.Platform.DrawableExtensions.SetColorFilter()

I would estimate these changes improve startup by about ~10ms on a Pixel
5, seeing the savings on the topmost method, `UpdateIconColor()`.
  • Loading branch information
jonathanpeppers authored Sep 7, 2023
1 parent 3bc8c37 commit 8203e4d
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,21 @@ public static void UpdateBarBackground(this AToolbar nativeToolbar, Toolbar tool
public static void UpdateIconColor(this AToolbar nativeToolbar, Toolbar toolbar)
{
var navIconColor = toolbar.IconColor;
if (navIconColor != null && nativeToolbar.NavigationIcon != null)
if (navIconColor is null)
return;

var platformColor = navIconColor.ToPlatform();
if (nativeToolbar.NavigationIcon is Drawable navigationIcon)
{
if (nativeToolbar.NavigationIcon is DrawerArrowDrawable dad)
if (navigationIcon is DrawerArrowDrawable dad)
dad.Color = AGraphics.Color.White;

nativeToolbar.NavigationIcon.SetColorFilter(navIconColor, FilterMode.SrcAtop);
navigationIcon.SetColorFilter(platformColor, FilterMode.SrcAtop);
}

if (navIconColor != null && nativeToolbar.OverflowIcon != null)
if (nativeToolbar.OverflowIcon is Drawable overflowIcon)
{
nativeToolbar.OverflowIcon.SetColorFilter(navIconColor, FilterMode.SrcAtop);
overflowIcon.SetColorFilter(platformColor, FilterMode.SrcAtop);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.BlendMode;
import android.graphics.BlendModeColorFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PorterDuff;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.PaintDrawable;
Expand All @@ -26,6 +29,7 @@

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.TintTypedArray;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
Expand Down Expand Up @@ -224,6 +228,42 @@ public static ViewPager2 createShellViewPager(Context context, CoordinatorLayout
return pager;
}

/**
* Call setColorFilter on a Drawable, passing in (int)Microsoft.Maui.FilterMode
* Calls the appropriate methods for Android API 29/Q+
* @param drawable android.graphics.Drawable
* @param color android.graphics.Color
* @param mode (int)Microsoft.Maui.FilterMode
*/
public static void setColorFilter(@NonNull Drawable drawable, int color, int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
drawable.setColorFilter(new BlendModeColorFilter(color, getBlendMode(mode)));
} else {
drawable.setColorFilter(color, getPorterMode(mode));
}
}

@RequiresApi(api = Build.VERSION_CODES.Q)
static BlendMode getBlendMode(int mode) {
// NOTE: keep in sync with src/Core/src/Primitives/FilterMode.cs
switch (mode) {
case 0: return BlendMode.SRC_IN;
case 1: return BlendMode.MULTIPLY;
case 2: return BlendMode.SRC_ATOP;
default: throw new RuntimeException("Invalid Mode");
}
}

static PorterDuff.Mode getPorterMode(int mode) {
// NOTE: keep in sync with src/Core/src/Primitives/FilterMode.cs
switch (mode) {
case 0: return PorterDuff.Mode.SRC_IN;
case 1: return PorterDuff.Mode.MULTIPLY;
case 2: return PorterDuff.Mode.SRC_ATOP;
default: throw new RuntimeException("Invalid Mode");
}
}

private static void prepare(RequestBuilder<Drawable> builder, Target<Drawable> target, Boolean cachingEnabled, ImageLoaderCallback callback) {
// A special value to work around https://github.com/dotnet/maui/issues/6783 where targets
// are actually re-used if all the variables are the same.
Expand Down
39 changes: 2 additions & 37 deletions src/Core/src/Platform/Android/DrawableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,6 @@ public static class DrawableExtensions
throw new Exception("Invalid Mode");
}

[Obsolete]
static PorterDuff.Mode? GetFilterModePre29(FilterMode mode)
{
switch (mode)
{
case FilterMode.SrcIn:
return PorterDuff.Mode.SrcIn;
case FilterMode.Multiply:
return PorterDuff.Mode.Multiply;
case FilterMode.SrcAtop:
return PorterDuff.Mode.SrcAtop;
}

throw new Exception("Invalid Mode");
}

public static AColorFilter? GetColorFilter(this ADrawable drawable)
{
if (drawable == null)
Expand Down Expand Up @@ -71,27 +55,8 @@ public static void SetColorFilter(this ADrawable drawable, Graphics.Color color,

public static void SetColorFilter(this ADrawable drawable, AColor color, FilterMode mode)
{
if (drawable == null)
return;

if (OperatingSystem.IsAndroidVersionAtLeast(29))
{
BlendMode? filterMode29 = GetFilterMode(mode);

if (filterMode29 != null)
drawable.SetColorFilter(new BlendModeColorFilter(color, filterMode29));
}
else
{
#pragma warning disable CS0612 // Type or member is obsolete
PorterDuff.Mode? filterModePre29 = GetFilterModePre29(mode);
#pragma warning restore CS0612 // Type or member is obsolete

if (filterModePre29 != null)
#pragma warning disable CS0618 // Type or member is obsolete
drawable.SetColorFilter(color, filterModePre29);
#pragma warning restore CS0618 // Type or member is obsolete
}
if (drawable is not null)
PlatformInterop.SetColorFilter(drawable, color, (int)mode);
}
}
}
7 changes: 4 additions & 3 deletions src/Core/src/Primitives/FilterMode.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
namespace Microsoft.Maui
{
// Keep in sync with src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java
public enum FilterMode
{
SrcIn,
Multiply,
SrcAtop
SrcIn = 0,
Multiply = 1,
SrcAtop = 2,
}
}
Binary file modified src/Core/src/maui.aar
Binary file not shown.

0 comments on commit 8203e4d

Please sign in to comment.