From 7708cdcceff9a268ae19c196c2b8baeadf4d9d83 Mon Sep 17 00:00:00 2001 From: Harrison Spain Date: Thu, 25 Aug 2022 14:25:08 -0700 Subject: [PATCH] Anti-alias rounded borders on overflow: hidden views Summary: When a view is rendered with a combination of `overflow: hidden` and `borderRadius: >0`, the `ReactViewGroup` would apply the border radius using Canvas `clipPath`. In Android graphics, clipPath is not an anti-aliased operation and caused aliasing artifacts. Changing the method to a bitmask using the `PorterDuff` method results in the same functionality, but with hardware accelerated antialiasing. https://github.com/facebook/react-native/issues/24486 Changelog: [Android][Change] - Views with overflow: hidden and borderRadius: >0 now render anti-aliased borders. Reviewed By: javache Differential Revision: D38914878 fbshipit-source-id: 45ac7e4aece7a76c4216412175e49d3d73b6f391 --- .../react/config/ReactFeatureFlags.java | 6 +++ .../react/views/view/ReactViewGroup.java | 42 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index a9961cb2f4d..74ecedbba96 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -144,4 +144,10 @@ public class ReactFeatureFlags { /** Temporary flag to allow execution of mount items up to 15ms earlier than normal. */ public static boolean enableEarlyScheduledMountItemExecution = false; + + /** + * Use a bitmap mask instead of clipPath for rounding corners so that they are antialiased in + * Android + */ + public static boolean antiAliasRoundedOverflowCorners = false; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index 93afe79dc7f..f3867ec2bad 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -14,7 +14,10 @@ import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Paint; import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; @@ -129,6 +132,7 @@ public class ReactViewGroup extends ViewGroup private boolean mNeedsOffscreenAlphaCompositing; private @Nullable ViewGroupDrawingOrderHelper mDrawingOrderHelper; private @Nullable Path mPath; + private @Nullable Paint mPaint; private int mLayoutDirection; private float mBackfaceOpacity; private String mBackfaceVisibility; @@ -159,6 +163,7 @@ public class ReactViewGroup extends ViewGroup mNeedsOffscreenAlphaCompositing = false; mDrawingOrderHelper = null; mPath = null; + mPaint = null; mLayoutDirection = 0; // set when background is created mBackfaceOpacity = 1.f; mBackfaceVisibility = "visible"; @@ -806,8 +811,16 @@ public class ReactViewGroup extends ViewGroup @Override protected void dispatchDraw(Canvas canvas) { try { - dispatchOverflowDraw(canvas); - super.dispatchDraw(canvas); + if (ReactFeatureFlags.antiAliasRoundedOverflowCorners && hasRoundedOverflow()) { + int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null); + super.dispatchDraw(canvas); + dispatchOverflowDraw(canvas); + canvas.restoreToCount(saveCount); + } else { + dispatchOverflowDraw(canvas); + super.dispatchDraw(canvas); + } + } catch (NullPointerException | StackOverflowError e) { // Adding special exception management for StackOverflowError for logging purposes. // This will be removed in the future. @@ -860,7 +873,7 @@ public class ReactViewGroup extends ViewGroup boolean hasClipPath = false; - if (mReactBackgroundDrawable != null) { + if (mReactBackgroundDrawable != null && mReactBackgroundDrawable.hasRoundedBorders()) { final RectF borderWidth = mReactBackgroundDrawable.getDirectionAwareBorderInsets(); if (borderWidth.top > 0 @@ -980,7 +993,21 @@ public class ReactViewGroup extends ViewGroup Math.max(bottomLeftBorderRadius - borderWidth.bottom, 0), }, Path.Direction.CW); - canvas.clipPath(mPath); + + if (ReactFeatureFlags.antiAliasRoundedOverflowCorners) { + mPath.setFillType(Path.FillType.INVERSE_WINDING); + + if (mPaint == null) { + mPaint = new Paint(); + mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + mPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + mPaint.setColor(getContext().getColor(android.R.color.white)); + } + + canvas.drawPath(mPath, mPaint); + } else { + canvas.clipPath(mPath); + } hasClipPath = true; } } @@ -995,6 +1022,13 @@ public class ReactViewGroup extends ViewGroup } } + private boolean hasRoundedOverflow() { + return mOverflow != null + && (mOverflow.equals(ViewProps.HIDDEN.toString()) + || mOverflow.equals(ViewProps.SCROLL.toString())) + && (mReactBackgroundDrawable != null && mReactBackgroundDrawable.hasRoundedBorders()); + } + public void setOpacityIfPossible(float opacity) { mBackfaceOpacity = opacity; setBackfaceVisibilityDependantOpacity();