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
This commit is contained in:
Harrison Spain 2022-08-25 14:25:08 -07:00 коммит произвёл Facebook GitHub Bot
Родитель 18542b6ef5
Коммит 7708cdccef
2 изменённых файлов: 44 добавлений и 4 удалений

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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();