-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Improve shadow rendering on Android, fix shadow clipping on iOS #26789
base: main
Are you sure you want to change the base?
Conversation
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
/rebase |
2a51aee
to
2a218db
Compare
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
2f0b86f
to
eccf37e
Compare
I'm late to the party but an attempt to improve shadow's performance was made before here #10523. Moving the code to the Java side might help but I think what would benefit the most is a shadow cache. All views that have the same size and the same shadow properties (very common scenario in CollectionView items for example) should use the same bitmap. This will eliminate a huge number of calls including C# to Java ones. There is an implementation of the shadow cache in the PR. |
@AmrAlSayed0 thanks for sharing, I'll check it out. |
d40a883
to
5cb0404
Compare
// Use application context to avoid memory leaks | ||
Context applicationContext = context.getApplicationContext(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have any details why to use getApplicationContext
?
It looks like the ShadowBitmapPool
is used once per PlatformWrapperView
, which would have the same lifetime as an activity anyway?
If we used a single ShadowBitmapPool
for the entire application, seems like getApplicationContext
would be appropriate in that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment below: we use only one pool.
public PlatformWrapperView(Context context) { | ||
super(context); | ||
this.viewBounds = new Rect(); | ||
this.bitmapPool = ShadowBitmapPool.get(context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, we use 1 pool per PlatformWrapperView
.
Can we use 1 pool for the entire application? That would allow many views to share the pool.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually that static method returns one pool for the entire application, so exactly what you're asking :)
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
d01b92c
to
a9b540b
Compare
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
Correct me if I'm wrong but what this is doing is using a pool not a cache. Which means that no 2 views will ever use the same bitmap, it's just that that bitmaps will be reused and recycled instead of being created each time it's needed to better conserve memory. I was thinking of a cache for when, for example, 200 items in a CollectionView request the same exact bitmap cuz they have MeasureFirstItem set (hence same size) and have the same template (hence the same shadow properties) and the same bitmap can be set for both. I know this PR is already doing too much so this maybe an optimization for a later time .. |
@AmrAlSayed0 there's a small "cache" at the view's level which reuses the bitmap content when not But from my analysis, this is probably ineffective as every time something changes on the descendants ( The reason for this invalidation is that the shadow is based on content pixels. This is really a pain as Android doesn't offer a better way to draw a shadow based on the content. What I've done here though, is detecting if you have a shadow applied to a solid rectangle, or a solid
Thanks for your input on this! |
e5e9143
to
14eecfe
Compare
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
@@ -83,19 +54,55 @@ public override bool DispatchTouchEvent(MotionEvent e) | |||
|
|||
partial void ClipChanged() | |||
{ | |||
_invalidateClip = _invalidateShadow = true; | |||
_invalidateClip = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you change the clipping of a View at runtime, and it has a shadow, should it be invalidated, would this still happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it will, and this is also covered in the new 24114
UI test.
The invalidation happens on Java side.
float[] positions; | ||
float[] bounds; | ||
|
||
switch (shadowPaint) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one, but we don't have gradient shadows implemented on other platforms. After merging this, we should check if we can implement it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was already implemented android-only.
This can't be done on other platforms, for example iOS doesn't support this, it only takes a Color
parameter.
_backgroundColor = new AColor(color); | ||
var backgroundColor = new AColor(color); | ||
_backgroundColor = backgroundColor; | ||
_isBackgroundSolid = backgroundColor.A is 255; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be an extension method in the ColorExtensions class.
@@ -224,6 +234,7 @@ public void SetBorderBrush(RadialGradientPaint radialGradientPaint) | |||
|
|||
_borderColor = null; | |||
_stroke = radialGradientPaint; | |||
_isBorderSolid = radialGradientPaint.GradientStops.All(s => s.Color.Alpha == 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like with Color, could be a nice addition to the PaintExtensions class.
shadowPaint = null; | ||
shadowCanvas = null; | ||
if (shadowBitmap != null) { | ||
bitmapPool.put(shadowBitmap); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good improvement!
|
||
private int getRadiusSafeSpace() { | ||
if (radius <= 0) { | ||
return 4; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use this specific value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot to remove this, thanks!
UpdateLabel(); | ||
} | ||
|
||
private void OnTapGestureRecognizerTapped(object sender, EventArgs e) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could also update the sample adding another Button to remove/add the shadows at runtime?
@@ -16,8 +16,34 @@ public Issue24414(TestDevice device) | |||
[Category(UITestCategories.Visual)] | |||
public void Issue24414Test() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test is failing in all the platforms by differences in the shadows:
Maybe commits 14eecfe after include the snapshots had an impact?
Could you review the tests? Let me know if needs help with it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I have to capture all the shapshots because I changed the UI test.
I will on next commit+azp.
@@ -16,8 +16,34 @@ public Issue24414(TestDevice device) | |||
[Category(UITestCategories.Visual)] | |||
public void Issue24414Test() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Android, could use
public static IList<object> GetPerformanceData(this IApp app, string performanceDataType) |
@jsuarezruiz I should have addressed all your feedbacks (except for screenshots which will be updated after the next AZP) |
# Conflicts: # src/TestUtils/src/UITest.Appium/HelperExtensions.cs
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
Android Shadow Rendering
Description of Change
Glide
'sLruBitmapPool
to reduce the memory pressure on JavaPaint
only when shadow changesCanvas
API to speedup the shadow rendering performance by a lotsaveLayer
call from platform interop: it's a very expensive operation done for no reasons hereBenchmark
Here I've executed the testing host app with
return new ShadowBenchmark();
as main page and scrolling the CV once.Before
After (with transparency)
You can see this is a bit faster, and for sure it is creating less GC/allocation work due to the bitmap pool.
Still timings are not good enough.
After (with solid background)
This time no need to create bitmaps and render descendants, we can simply draw the shadow.
And this is exactly the speed we want.
Notes
To collect those speedscopes I've temporary re-added:
iOS Shadow Rendering
Layer
considering it's the parent of the clipped content.Issues Fixed
Fixes #27156
Fixes #23630
Fixes #20518
Fixes #18202
Fixes #17886
Fixes #13981
Fixes #9539
Fixes #4106
Relates to #17881
Relates to #17257
Relates to #10401