Refactor rounded corner logic in order to check for hardware acceleration

The outline provider only works with hardware acceleration, meaning that devices that don't support hw acceleration get no rounded corners when that strategy is used to round corners.

Unfortunately, a view doesn't know if it's hardware accelerated until it's a attached to a window, so when these views are created in StyleProvider, it's too early--it has to happen after the view is created, meaning all strategies need to be able to be used from one Android View class.

Most of the logic was the same between different strategies. All of the strategy-specific logic is in delegates now. The delegates are not Android Views (they can't be), but they can manipulate the view so that RoundedCornerWrapperView doesn't need to know the details of the different rounding strategies.

It doesn't make sense anymore to to keep the RoundedCornerWrapperView inheritance, since there would only be one class inheriting from the base class. I pulled some things into delegates, and some shared code into RoundedCornerViewHelper.

PiperOrigin-RevId: 253104022
Change-Id: I19e23fe6bb4778653ff343acac7112a67edfaaf4
diff --git a/src/main/java/com/google/android/libraries/feed/piet/AdapterParameters.java b/src/main/java/com/google/android/libraries/feed/piet/AdapterParameters.java
index 075ea86..b4bc095 100644
--- a/src/main/java/com/google/android/libraries/feed/piet/AdapterParameters.java
+++ b/src/main/java/com/google/android/libraries/feed/piet/AdapterParameters.java
@@ -44,8 +44,8 @@
   final Clock clock;
   final PietStylesHelperFactory pietStylesHelperFactory;
   final RoundedCornerMaskCache roundedCornerMaskCache;
-  final boolean useLegacyRoundedCornerImpl;
-  final boolean useOutlineRoundedCornerImpl;
+  final boolean allowLegacyRoundedCornerImpl;
+  final boolean allowOutlineRoundedCornerImpl;
 
   // Doesn't like passing "this" to the new ElementAdapterFactory; however, nothing in the factory's
   // construction will reference the elementAdapterFactory member of this, so should be safe.
@@ -55,8 +55,8 @@
       Supplier</*@Nullable*/ ViewGroup> parentViewSupplier,
       HostProviders hostProviders,
       Clock clock,
-      boolean useLegacyRoundedCornerImpl,
-      boolean useOutlineRoundedCornerImpl) {
+      boolean allowLegacyRoundedCornerImpl,
+      boolean allowOutlineRoundedCornerImpl) {
     this.context = context;
     this.parentViewSupplier = parentViewSupplier;
     this.hostProviders = hostProviders;
@@ -74,8 +74,8 @@
     this.pietStylesHelperFactory = new PietStylesHelperFactory();
     this.roundedCornerMaskCache = new RoundedCornerMaskCache();
 
-    this.useLegacyRoundedCornerImpl = useLegacyRoundedCornerImpl;
-    this.useOutlineRoundedCornerImpl = useOutlineRoundedCornerImpl;
+    this.allowLegacyRoundedCornerImpl = allowLegacyRoundedCornerImpl;
+    this.allowOutlineRoundedCornerImpl = allowOutlineRoundedCornerImpl;
   }
 
   /** Testing-only constructor for mocking the internally-constructed objects. */
@@ -114,8 +114,8 @@
       Clock clock,
       PietStylesHelperFactory pietStylesHelperFactory,
       RoundedCornerMaskCache maskCache,
-      boolean useLegacyRoundedCornerImpl,
-      boolean useOutlineRoundedCornerImpl) {
+      boolean allowLegacyRoundedCornerImpl,
+      boolean allowOutlineRoundedCornerImpl) {
     this.context = context;
     this.parentViewSupplier = parentViewSupplier;
     this.hostProviders = hostProviders;
@@ -127,7 +127,7 @@
     this.clock = clock;
     this.pietStylesHelperFactory = pietStylesHelperFactory;
     this.roundedCornerMaskCache = maskCache;
-    this.useLegacyRoundedCornerImpl = useLegacyRoundedCornerImpl;
-    this.useOutlineRoundedCornerImpl = useOutlineRoundedCornerImpl;
+    this.allowLegacyRoundedCornerImpl = allowLegacyRoundedCornerImpl;
+    this.allowOutlineRoundedCornerImpl = allowOutlineRoundedCornerImpl;
   }
 }
diff --git a/src/main/java/com/google/android/libraries/feed/piet/ElementAdapter.java b/src/main/java/com/google/android/libraries/feed/piet/ElementAdapter.java
index 9324eef..abd2990 100644
--- a/src/main/java/com/google/android/libraries/feed/piet/ElementAdapter.java
+++ b/src/main/java/com/google/android/libraries/feed/piet/ElementAdapter.java
@@ -180,8 +180,8 @@
         elementStyle.createWrapperView(
             context,
             parameters.roundedCornerMaskCache,
-            parameters.useLegacyRoundedCornerImpl,
-            parameters.useOutlineRoundedCornerImpl);
+            parameters.allowLegacyRoundedCornerImpl,
+            parameters.allowOutlineRoundedCornerImpl);
     wrapper.addView(getBaseView());
 
     if (baseElement.getOverlaysCount() > 0) {
diff --git a/src/main/java/com/google/android/libraries/feed/piet/PietManager.java b/src/main/java/com/google/android/libraries/feed/piet/PietManager.java
index 7d9d6c0..dbc9d54 100644
--- a/src/main/java/com/google/android/libraries/feed/piet/PietManager.java
+++ b/src/main/java/com/google/android/libraries/feed/piet/PietManager.java
@@ -79,8 +79,8 @@
     /*@MonotonicNonNull*/ private CustomElementProvider customElementProvider = null;
     /*@MonotonicNonNull*/ private HostBindingProvider hostBindingProvider = null;
     /*@MonotonicNonNull*/ private Clock clock = null;
-    private boolean useLegacyRoundedCornerImpl = false;
-    private boolean useOutlineRoundedCornerImpl = false;
+    private boolean allowLegacyRoundedCornerImpl = false;
+    private boolean allowOutlineRoundedCornerImpl = false;
 
     private Builder() {}
 
@@ -108,8 +108,8 @@
      * Use the rounded corner optimizations on JB/KK for better performance at the expense of
      * antialiasing.
      */
-    public Builder setUseLegacyRoundedCornerImpl(boolean useLegacyRoundedCornerImpl) {
-      this.useLegacyRoundedCornerImpl = useLegacyRoundedCornerImpl;
+    public Builder setAllowLegacyRoundedCornerImpl(boolean allowLegacyRoundedCornerImpl) {
+      this.allowLegacyRoundedCornerImpl = allowLegacyRoundedCornerImpl;
       return this;
     }
 
@@ -117,8 +117,8 @@
      * Use the clipToOutline rounded corner optimizations on L+ when all four corners are rounded
      * for better performance.
      */
-    public Builder setUseOutlineRoundedCornerImpl(boolean useOutlineRoundedCornerImpl) {
-      this.useOutlineRoundedCornerImpl = useOutlineRoundedCornerImpl;
+    public Builder setAllowOutlineRoundedCornerImpl(boolean allowOutlineRoundedCornerImpl) {
+      this.allowOutlineRoundedCornerImpl = allowOutlineRoundedCornerImpl;
       return this;
     }
 
@@ -184,8 +184,8 @@
           customElementProvider,
           hostBindingProvider,
           clock,
-          useLegacyRoundedCornerImpl,
-          useOutlineRoundedCornerImpl);
+          allowLegacyRoundedCornerImpl,
+          allowOutlineRoundedCornerImpl);
     }
   }
 }
diff --git a/src/main/java/com/google/android/libraries/feed/piet/PietManagerImpl.java b/src/main/java/com/google/android/libraries/feed/piet/PietManagerImpl.java
index 851be89..5afdc4d 100644
--- a/src/main/java/com/google/android/libraries/feed/piet/PietManagerImpl.java
+++ b/src/main/java/com/google/android/libraries/feed/piet/PietManagerImpl.java
@@ -35,8 +35,8 @@
   private final CustomElementProvider customElementProvider;
   private final HostBindingProvider hostBindingProvider;
   private final Clock clock;
-  private final boolean useLegacyRoundedCornerImpl;
-  private final boolean useOutlineRoundedCornerImpl;
+  private final boolean allowLegacyRoundedCornerImpl;
+  private final boolean allowOutlineRoundedCornerImpl;
   @VisibleForTesting /*@Nullable*/ AdapterParameters adapterParameters = null;
 
   PietManagerImpl(
@@ -45,15 +45,15 @@
       CustomElementProvider customElementProvider,
       HostBindingProvider hostBindingProvider,
       Clock clock,
-      boolean useLegacyRoundedCornerImpl,
-      boolean useOutlineRoundedCornerImpl) {
+      boolean allowLegacyRoundedCornerImpl,
+      boolean allowOutlineRoundedCornerImpl) {
     this.debugBehavior = debugBehavior;
     this.assetProvider = assetProvider;
     this.customElementProvider = customElementProvider;
     this.hostBindingProvider = hostBindingProvider;
     this.clock = clock;
-    this.useLegacyRoundedCornerImpl = useLegacyRoundedCornerImpl;
-    this.useOutlineRoundedCornerImpl = useOutlineRoundedCornerImpl;
+    this.allowLegacyRoundedCornerImpl = allowLegacyRoundedCornerImpl;
+    this.allowOutlineRoundedCornerImpl = allowOutlineRoundedCornerImpl;
   }
 
   @Override
@@ -94,8 +94,8 @@
               new HostProviders(
                   assetProvider, customElementProvider, hostBindingProvider, logDataCallback),
               clock,
-              useLegacyRoundedCornerImpl,
-              useOutlineRoundedCornerImpl);
+              allowLegacyRoundedCornerImpl,
+              allowOutlineRoundedCornerImpl);
     }
     return adapterParameters;
   }
diff --git a/src/main/java/com/google/android/libraries/feed/piet/StyleProvider.java b/src/main/java/com/google/android/libraries/feed/piet/StyleProvider.java
index 9cb2b23..a48d494 100644
--- a/src/main/java/com/google/android/libraries/feed/piet/StyleProvider.java
+++ b/src/main/java/com/google/android/libraries/feed/piet/StyleProvider.java
@@ -30,13 +30,11 @@
 import android.widget.ImageView.ScaleType;
 import com.google.android.libraries.feed.common.ui.LayoutUtils;
 import com.google.android.libraries.feed.piet.host.AssetProvider;
-import com.google.android.libraries.feed.piet.ui.BitmapMaskingRoundedCornerWrapperView;
 import com.google.android.libraries.feed.piet.ui.BorderDrawable;
 import com.google.android.libraries.feed.piet.ui.GradientDrawable;
-import com.google.android.libraries.feed.piet.ui.LegacyRoundedCornerWrapperView;
-import com.google.android.libraries.feed.piet.ui.OutlineRoundedCornerWrapperView;
 import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache;
 import com.google.android.libraries.feed.piet.ui.RoundedCornerViewHelper;
+import com.google.android.libraries.feed.piet.ui.RoundedCornerWrapperView;
 import com.google.search.now.ui.piet.ErrorsProto.ErrorCode;
 import com.google.search.now.ui.piet.GradientsProto.Fill;
 import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners;
@@ -413,8 +411,8 @@
   FrameLayout createWrapperView(
       Context context,
       RoundedCornerMaskCache maskCache,
-      boolean useLegacyImpl,
-      boolean useOutlineImpl) {
+      boolean allowClipPathRounding,
+      boolean allowOutlineRounding) {
     if (!hasRoundedCorners()) {
       FrameLayout view = new FrameLayout(context);
       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
@@ -426,33 +424,15 @@
     int radiusOverride =
         getRoundedCorners().getUseHostRadiusOverride() ? assetProvider.getDefaultCornerRadius() : 0;
 
-    boolean allFourCornersRounded =
-        getRoundedCorners().getBitmask() == 0 || getRoundedCorners().getBitmask() == 15;
-    if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP && useLegacyImpl) {
-      return new LegacyRoundedCornerWrapperView(
-          context,
-          getRoundedCorners(),
-          assetProvider.isRtLSupplier(),
-          radiusOverride,
-          getBorders());
-    } else if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP
-        && useOutlineImpl
-        && allFourCornersRounded) {
-      return new OutlineRoundedCornerWrapperView(
-          context,
-          getRoundedCorners(),
-          assetProvider.isRtLSupplier(),
-          radiusOverride,
-          getBorders());
-    } else {
-      return new BitmapMaskingRoundedCornerWrapperView(
-          context,
-          getRoundedCorners(),
-          maskCache,
-          assetProvider.isRtLSupplier(),
-          radiusOverride,
-          getBorders());
-    }
+    return new RoundedCornerWrapperView(
+        context,
+        getRoundedCorners(),
+        maskCache,
+        assetProvider.isRtLSupplier(),
+        radiusOverride,
+        getBorders(),
+        allowClipPathRounding,
+        allowOutlineRounding);
   }
 
   /**
diff --git a/src/main/java/com/google/android/libraries/feed/piet/ui/BitmapMaskingRoundedCornerDelegate.java b/src/main/java/com/google/android/libraries/feed/piet/ui/BitmapMaskingRoundedCornerDelegate.java
new file mode 100644
index 0000000..e93e1bc
--- /dev/null
+++ b/src/main/java/com/google/android/libraries/feed/piet/ui/BitmapMaskingRoundedCornerDelegate.java
@@ -0,0 +1,202 @@
+// Copyright 2018 The Feed Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.libraries.feed.piet.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache.Corner;
+import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache.RoundedCornerBitmaps;
+import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners.Corners;
+
+/**
+ * Rounding delegate for the bitmap masking strategy.
+ *
+ * <p>Handles creation of corner mask bitmaps. Since creating bitmaps is expensive, this class also
+ * saves them and makes sure they are not re-created unless necessary. {@link
+ * RoundedCornerWrapperView} decides which rounding strategy to use and sets the appropriate
+ * delegate.
+ */
+class BitmapMaskingRoundedCornerDelegate implements RoundedCornerDelegate {
+  private final Paint paint;
+  private final Paint maskPaint;
+  private final RoundedCornerMaskCache maskCache;
+  private final int bitmask;
+  private final Canvas offscreenCanvas;
+
+  // Masks for each of the corners of the view; null if that corner is not rounded.
+  /*@Nullable*/ private Bitmap cornerTL = null;
+  /*@Nullable*/ private Bitmap cornerTR = null;
+  /*@Nullable*/ private Bitmap cornerBL = null;
+  /*@Nullable*/ private Bitmap cornerBR = null;
+
+  // Keep track of current mask configuration so we can use cached values if nothing has changed.
+  private int lastRadius = -1;
+  private boolean lastRtL;
+
+  BitmapMaskingRoundedCornerDelegate(
+      RoundedCornerMaskCache maskCache, int bitmask, boolean isRtL) {
+    this(maskCache, bitmask, isRtL, new Canvas());
+  }
+
+  BitmapMaskingRoundedCornerDelegate(
+      RoundedCornerMaskCache maskCache, int bitmask, boolean isRtL, Canvas canvas) {
+    this.maskCache = maskCache;
+    offscreenCanvas = canvas;
+    this.lastRtL = !isRtL; // Flip this so we must update the layout on the first time.
+    this.bitmask = bitmask;
+    this.paint = maskCache.getPaint();
+    this.maskPaint = maskCache.getMaskPaint();
+  }
+
+  /** Defensively make sure outline clipping is turned off, although that should be the default. */
+  @Override
+  public void initializeForView(ViewGroup view) {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+      view.setClipToOutline(false);
+      view.setClipChildren(false);
+    }
+  }
+
+  private void initCornerMasks(int radius, boolean isRtL) {
+    if (radius < 1) {
+      return;
+    }
+    RoundedCornerBitmaps masks = maskCache.getMasks(radius);
+
+    if ((RoundedCornerViewHelper.shouldRoundCorner(Corners.TOP_START, bitmask) && !isRtL)
+        || (RoundedCornerViewHelper.shouldRoundCorner(Corners.TOP_END, bitmask) && isRtL)) {
+      cornerTL = masks.get(Corner.TOP_LEFT);
+    } else {
+      cornerTL = null;
+    }
+
+    if ((RoundedCornerViewHelper.shouldRoundCorner(Corners.TOP_END, bitmask) && !isRtL)
+        || (RoundedCornerViewHelper.shouldRoundCorner(Corners.TOP_START, bitmask) && isRtL)) {
+      cornerTR = masks.get(Corner.TOP_RIGHT);
+    } else {
+      cornerTR = null;
+    }
+
+    if ((RoundedCornerViewHelper.shouldRoundCorner(Corners.BOTTOM_START, bitmask) && !isRtL)
+        || (RoundedCornerViewHelper.shouldRoundCorner(Corners.BOTTOM_END, bitmask) && isRtL)) {
+      cornerBL = masks.get(Corner.BOTTOM_LEFT);
+    } else {
+      cornerBL = null;
+    }
+
+    if ((RoundedCornerViewHelper.shouldRoundCorner(Corners.BOTTOM_END, bitmask) && !isRtL)
+        || (RoundedCornerViewHelper.shouldRoundCorner(Corners.BOTTOM_START, bitmask) && isRtL)) {
+      cornerBR = masks.get(Corner.BOTTOM_RIGHT);
+    } else {
+      cornerBR = null;
+    }
+  }
+
+  /**
+   * Creates corner masks (which cover the parts of the corners that should not be shown).
+   *
+   * <p>Masks must be created after the radius is known. However, if the size of the view and the
+   * LtR remain the same, it will only create the masks once.
+   */
+  @Override
+  public void onLayout(int radius, boolean isRtL, int width, int height) {
+    if (radius == 0) {
+      return;
+    }
+    if (radius == lastRadius && isRtL == lastRtL) {
+      return;
+    }
+
+    initCornerMasks(radius, isRtL);
+
+    lastRadius = radius;
+    lastRtL = isRtL;
+  }
+
+  /**
+   * Ensures that the wrapper view is invalidated when child views are invalidated.
+   *
+   * <p>This method only exists in Android O+.
+   */
+  @Override
+  public void onDescendantInvalidated(View roundedCornerView, View invalidatedDescendant) {
+    Rect targetRect = new Rect();
+    invalidatedDescendant.getDrawingRect(targetRect);
+    roundedCornerView.invalidate(targetRect);
+  }
+
+  /**
+   * Ensures that the wrapper view is invalidated when the child view is.
+   *
+   * <p>This is only used in Android N- and is deprecated, but we must use it because
+   * onDescendantInvalidated only exists in O+.
+   */
+  @Override
+  public void invalidateChildInParent(View view, final Rect dirty) {
+    view.invalidate(dirty);
+  }
+
+  /**
+   * Creates a local bitmap and draws the content and corner masks on top of that.
+   *
+   * <p>The bitmap is drawn directly to the {@link Canvas} that is passed into this method.
+   */
+  @Override
+  public void draw(RoundedCornerWrapperView view, Canvas canvas) {
+    int width = view.getWidth();
+    int height = view.getHeight();
+    if (width == 0 || height == 0) {
+      // The view is not visible, and offscreenBitmap creation will fail. Stop here. Call the super
+      // method to make sure the lifecycle is handled properly.
+      view.drawSuper(canvas);
+      return;
+    }
+    int radius = view.getRadius(width, height);
+    Bitmap localOffscreenBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+    offscreenCanvas.setBitmap(localOffscreenBitmap);
+    view.drawSuper(offscreenCanvas);
+    drawWithCornerMasks(canvas, radius, width, height, localOffscreenBitmap);
+  }
+
+  private void drawWithCornerMasks(
+      Canvas canvas, int radius, int width, int height, Bitmap offscreenBitmap) {
+    // Crop the corners off using masks
+    maskCorners(offscreenCanvas, width, height, radius);
+
+    // Draw the offscreen bitmap (view with rounded corners) to the target canvas.
+    canvas.drawBitmap(offscreenBitmap, 0f, 0f, paint);
+  }
+
+  /** Draws a mask on each corner that is rounded. */
+  private void maskCorners(Canvas canvas, int width, int height, int radius) {
+    if (cornerTL != null) {
+      canvas.drawBitmap(cornerTL, 0, 0, maskPaint);
+    }
+    if (cornerTR != null) {
+      canvas.drawBitmap(cornerTR, width - radius, 0, maskPaint);
+    }
+    if (cornerBL != null) {
+      canvas.drawBitmap(cornerBL, 0, height - radius, maskPaint);
+    }
+    if (cornerBR != null) {
+      canvas.drawBitmap(cornerBR, width - radius, height - radius, maskPaint);
+    }
+  }
+}
diff --git a/src/main/java/com/google/android/libraries/feed/piet/ui/BitmapMaskingRoundedCornerWrapperView.java b/src/main/java/com/google/android/libraries/feed/piet/ui/BitmapMaskingRoundedCornerWrapperView.java
deleted file mode 100644
index b530c98..0000000
--- a/src/main/java/com/google/android/libraries/feed/piet/ui/BitmapMaskingRoundedCornerWrapperView.java
+++ /dev/null
@@ -1,254 +0,0 @@
-// Copyright 2018 The Feed Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.android.libraries.feed.piet.ui;
-
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Outline;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.drawable.shapes.RoundRectShape;
-import android.os.Build;
-import android.view.View;
-import android.view.ViewOutlineProvider;
-import android.view.ViewParent;
-import com.google.android.libraries.feed.common.functional.Supplier;
-import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache.Corner;
-import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache.RoundedCornerBitmaps;
-import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners;
-import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners.Corners;
-import com.google.search.now.ui.piet.StylesProto.Borders;
-
-/**
- * Generic wrapper for {@link View} instances in Piet that require rounded corners.
- *
- * <p>This implementation should work in all cases for all SDKs, but has the worst performance in
- * CPU and memory usage.
- */
-public class BitmapMaskingRoundedCornerWrapperView extends RoundedCornerWrapperView {
-
-  private final Paint paint;
-  private final Paint maskPaint;
-  private final RoundedCornerMaskCache maskCache;
-  private final Canvas offscreenCanvas;
-
-  /*@Nullable*/ private RoundRectShape outlineShape = null;
-
-  // Masks for each of the corners of the view; null if that corner is not rounded.
-  /*@Nullable*/ private Bitmap cornerTL = null;
-  /*@Nullable*/ private Bitmap cornerTR = null;
-  /*@Nullable*/ private Bitmap cornerBL = null;
-  /*@Nullable*/ private Bitmap cornerBR = null;
-
-  // Keep track of current mask configuration so we can use cached values if nothing has changed.
-  private int lastRadius = -1;
-  private boolean lastRtL;
-
-  // Doesn't like the call to setOutlineProvider
-  @SuppressWarnings("initialization")
-  public BitmapMaskingRoundedCornerWrapperView(
-      Context context,
-      RoundedCorners roundedCorners,
-      RoundedCornerMaskCache maskCache,
-      Supplier<Boolean> isRtLSupplier,
-      int radiusOverride,
-      Borders borders) {
-    super(context, roundedCorners, isRtLSupplier, radiusOverride, borders);
-
-    this.maskCache = maskCache;
-    offscreenCanvas = new Canvas();
-    lastRtL = !isRtLSupplier.get(); // Flip this so we must update the layout on the first time.
-
-    this.paint = maskCache.getPaint();
-    this.maskPaint = maskCache.getMaskPaint();
-
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-      if (hasRoundedCorners()) {
-        super.setOutlineProvider(
-            new ViewOutlineProvider() {
-              @Override
-              public void getOutline(View view, Outline outline) {
-                RoundRectShape localOutlineShape = outlineShape;
-                if (localOutlineShape == null
-                    || localOutlineShape.getHeight() != view.getHeight()
-                    || localOutlineShape.getWidth() != view.getWidth()) {
-                  int radius = getRadius(view.getWidth(), view.getHeight());
-                  float[] radii =
-                      RoundedCornerViewHelper.createRoundedCornerMask(
-                          radius, roundedCorners.getBitmask(), isRtLSupplier.get());
-                  localOutlineShape = new RoundRectShape(radii, null, null);
-                  localOutlineShape.resize(view.getWidth(), view.getHeight());
-                  outlineShape = localOutlineShape;
-                }
-                localOutlineShape.getOutline(outline);
-              }
-            });
-      } else {
-        super.setOutlineProvider(ViewOutlineProvider.BOUNDS);
-      }
-    }
-
-    setWillNotDraw(false);
-  }
-
-  private void initCornerMasks(int radius, boolean isRtL) {
-    if (radius < 1) {
-      return;
-    }
-    RoundedCornerBitmaps masks = maskCache.getMasks(radius);
-
-    if ((shouldRoundCorner(Corners.TOP_START) && !isRtL)
-        || (shouldRoundCorner(Corners.TOP_END) && isRtL)) {
-      cornerTL = masks.get(Corner.TOP_LEFT);
-    } else {
-      cornerTL = null;
-    }
-
-    if ((shouldRoundCorner(Corners.TOP_END) && !isRtL)
-        || (shouldRoundCorner(Corners.TOP_START) && isRtL)) {
-      cornerTR = masks.get(Corner.TOP_RIGHT);
-    } else {
-      cornerTR = null;
-    }
-
-    if ((shouldRoundCorner(Corners.BOTTOM_START) && !isRtL)
-        || (shouldRoundCorner(Corners.BOTTOM_END) && isRtL)) {
-      cornerBL = masks.get(Corner.BOTTOM_LEFT);
-    } else {
-      cornerBL = null;
-    }
-
-    if ((shouldRoundCorner(Corners.BOTTOM_END) && !isRtL)
-        || (shouldRoundCorner(Corners.BOTTOM_START) && isRtL)) {
-      cornerBR = masks.get(Corner.BOTTOM_RIGHT);
-    } else {
-      cornerBR = null;
-    }
-  }
-
-  /**
-   * Creates corner masks (which cover the parts of the corners that should not be shown) and
-   * borders, as necessary. Both must be created after the radius is known. However, if the size of
-   * the view and the LtR remain the same, it will only create the masks and borders once.
-   */
-  private void setupCornerMasksAndBorders(int radius) {
-    if (!hasRoundedCorners() || radius == 0) {
-      return;
-    }
-    boolean isRtL = isRtLSupplier.get();
-    if (radius == lastRadius && isRtL == lastRtL) {
-      return;
-    }
-
-    initCornerMasks(radius, isRtL);
-    addBorders(radius);
-
-    lastRadius = radius;
-    lastRtL = isRtL;
-  }
-
-  /**
-   * Ensures that the wrapper view is invalidated when child views are invalidated. This method only
-   * exists in Android O+.
-   */
-  @Override
-  public void onDescendantInvalidated(View child, View target) {
-    super.onDescendantInvalidated(child, target);
-    if (hasRoundedCorners()) {
-      Rect targetRect = new Rect();
-      target.getDrawingRect(targetRect);
-      invalidate(targetRect);
-    }
-  }
-
-  /**
-   * Using as an indicator that the child view was invalidated. By overriding this method, we ensure
-   * that the wrapper view is invalidated when the child view is. This is only used in Android N-
-   * and is deprecated, but we must use it because onDescendantInvalidated only exists in O+.
-   */
-  @Override
-  public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
-    if (hasRoundedCorners()) {
-      invalidate(dirty);
-    }
-    return super.invalidateChildInParent(location, dirty);
-  }
-
-  @Override
-  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-    super.onLayout(changed, left, top, right, bottom);
-    if (!changed || !hasRoundedCorners()) {
-      return;
-    }
-
-    int width = getWidth();
-    int height = getHeight();
-    if (width == 0 || height == 0) {
-      // The view is not visible; no further processing is needed.
-      return;
-    }
-
-    int radius = getRadius(width, height);
-
-    // Set up the corner masks and borders, both of which require knowing the radius.
-    // This should no-op if radius and isLtR have not changed.
-    setupCornerMasksAndBorders(radius);
-  }
-
-  @Override
-  public void draw(Canvas canvas) {
-    if (!hasRoundedCorners()) {
-      super.draw(canvas);
-      return;
-    }
-    int width = getWidth();
-    int height = getHeight();
-    if (width == 0 || height == 0) {
-      // The view is not visible, and offscreenBitmap creation will fail. Stop here.
-      return;
-    }
-
-    int radius = getRadius(width, height);
-
-    // Draw the view without rounded corners on the offscreen canvas.
-    Bitmap localOffscreenBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-    offscreenCanvas.setBitmap(localOffscreenBitmap);
-    super.draw(offscreenCanvas);
-
-    // Crop the corners off using masks
-    maskCorners(offscreenCanvas, width, height, radius, maskPaint);
-
-    // Draw the offscreen bitmap (view with rounded corners) to the target canvas.
-    canvas.drawBitmap(localOffscreenBitmap, 0f, 0f, paint);
-  }
-
-  /** Draws a mask on each corner that is rounded. */
-  private void maskCorners(Canvas canvas, int width, int height, int radius, Paint paint) {
-    if (cornerTL != null) {
-      canvas.drawBitmap(cornerTL, 0, 0, paint);
-    }
-    if (cornerTR != null) {
-      canvas.drawBitmap(cornerTR, width - radius, 0, paint);
-    }
-    if (cornerBL != null) {
-      canvas.drawBitmap(cornerBL, 0, height - radius, paint);
-    }
-    if (cornerBR != null) {
-      canvas.drawBitmap(cornerBR, width - radius, height - radius, paint);
-    }
-  }
-}
diff --git a/src/main/java/com/google/android/libraries/feed/piet/ui/ClipPathRoundedCornerDelegate.java b/src/main/java/com/google/android/libraries/feed/piet/ui/ClipPathRoundedCornerDelegate.java
new file mode 100644
index 0000000..24dc7be
--- /dev/null
+++ b/src/main/java/com/google/android/libraries/feed/piet/ui/ClipPathRoundedCornerDelegate.java
@@ -0,0 +1,62 @@
+// Copyright 2019 The Feed Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.libraries.feed.piet.ui;
+
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.os.Build;
+import android.view.ViewGroup;
+
+/**
+ * Rounding delegate for the clip path strategy.
+ *
+ * <p>Adds logic to create a path along the edge of the view and clip to it. {@link
+ * RoundedCornerWrapperView} decides which rounding strategy to use and sets the appropriate
+ * delegate.
+ */
+class ClipPathRoundedCornerDelegate implements RoundedCornerDelegate {
+
+  private final Path clipPath = new Path();
+
+  /**
+   * Turns off clipping to the outline.
+   *
+   * <p>This rounding strategy should not be doing outline clipping. This defensively makes sure
+   * it's turned off, although that should be the default.
+   */
+  @Override
+  public void initializeForView(ViewGroup view) {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+      view.setClipToOutline(false);
+      view.setClipChildren(false);
+    }
+  }
+
+  /** Updates the path every time the size changes. */
+  @Override
+  public void onSizeChanged(int radius, int width, int height, int bitmask, boolean isRtL) {
+    float[] radii = RoundedCornerViewHelper.createRoundedCornerBitMask(radius, bitmask, isRtL);
+    clipPath.addRoundRect(new RectF(0, 0, width, height), radii, Path.Direction.CW);
+  }
+
+  /** Clips the path that was created in onSizeChanged(). */
+  @Override
+  public void dispatchDraw(Canvas canvas) {
+    if (clipPath != null) {
+      canvas.clipPath(clipPath);
+    }
+  }
+}
diff --git a/src/main/java/com/google/android/libraries/feed/piet/ui/LegacyRoundedCornerWrapperView.java b/src/main/java/com/google/android/libraries/feed/piet/ui/LegacyRoundedCornerWrapperView.java
deleted file mode 100644
index f29f6d2..0000000
--- a/src/main/java/com/google/android/libraries/feed/piet/ui/LegacyRoundedCornerWrapperView.java
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright 2018 The Feed Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.android.libraries.feed.piet.ui;
-
-import static com.google.android.libraries.feed.common.Validators.checkState;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Path;
-import android.graphics.RectF;
-import android.os.Build;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.view.View;
-import com.google.android.libraries.feed.common.functional.Supplier;
-import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners;
-import com.google.search.now.ui.piet.StylesProto.Borders;
-
-/**
- * Wrapper for {@link View} instances in Piet that require rounded corners.
- *
- * <p>This class does not support antialiasing on rounded corners, but has much better memory and
- * CPU performance on JB and KK (API 16 and 19)
- */
-public class LegacyRoundedCornerWrapperView extends RoundedCornerWrapperView {
-
-  /*@Nullable*/ private Path clipPath = null;
-
-  // Doesn't like the call to setLayerType
-  @SuppressWarnings("initialization")
-  public LegacyRoundedCornerWrapperView(
-      Context context,
-      RoundedCorners roundedCorners,
-      Supplier<Boolean> isRtLSupplier,
-      int radiusOverride,
-      Borders borders) {
-    super(context, roundedCorners, isRtLSupplier, radiusOverride, borders);
-
-    checkState(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP);
-
-    if (VERSION.SDK_INT < VERSION_CODES.JELLY_BEAN_MR2) {
-      // clipPath doesn't work with hardware rendering on < 18.
-      setLayerType(LAYER_TYPE_SOFTWARE, null);
-    }
-  }
-
-  @Override
-  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-    super.onSizeChanged(w, h, oldw, oldh);
-
-    int radius = getRadius(w, h);
-    float[] radii =
-        RoundedCornerViewHelper.createRoundedCornerMask(
-            radius, roundedCorners.getBitmask(), isRtLSupplier.get());
-
-    clipPath = new Path();
-    clipPath.addRoundRect(new RectF(0, 0, w, h), radii, Path.Direction.CW);
-  }
-
-  @Override
-  protected void dispatchDraw(Canvas canvas) {
-    if (clipPath != null) {
-      canvas.clipPath(clipPath);
-    }
-    super.dispatchDraw(canvas);
-  }
-
-  @Override
-  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-    super.onLayout(changed, left, top, right, bottom);
-    if (!changed || !hasRoundedCorners()) {
-      return;
-    }
-
-    int width = getWidth();
-    int height = getHeight();
-    if (width == 0 || height == 0) {
-      // The view is not visible; no further processing is needed.
-      return;
-    }
-
-    int radius = getRadius(width, height);
-
-    addBorders(radius);
-  }
-}
diff --git a/src/main/java/com/google/android/libraries/feed/piet/ui/OutlineProviderRoundedCornerDelegate.java b/src/main/java/com/google/android/libraries/feed/piet/ui/OutlineProviderRoundedCornerDelegate.java
new file mode 100644
index 0000000..bd10160
--- /dev/null
+++ b/src/main/java/com/google/android/libraries/feed/piet/ui/OutlineProviderRoundedCornerDelegate.java
@@ -0,0 +1,45 @@
+// Copyright 2019 The Feed Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.libraries.feed.piet.ui;
+
+import android.view.ViewGroup;
+
+/**
+ * Rounding delegate for the outline provider strategy.
+ *
+ * <p>Adds logic to clip rounded corner views to their outline. {@link RoundedCornerWrapperView}
+ * decides which rounding strategy to use and sets the appropriate delegate.
+ */
+class OutlineProviderRoundedCornerDelegate implements RoundedCornerDelegate {
+
+  /**
+   * Sets clipToOutline to true.
+   *
+   * <p>Setting it in an initializer rather than a constructor means a new instance of this delegate
+   * doesn't need to be created every time an outline provider is needed.
+   */
+  @Override
+  public void initializeForView(ViewGroup view) {
+    view.setClipToOutline(true);
+    view.setClipChildren(true);
+  }
+
+  /** Reset clipToOutline to false (the default) when this strategy stops being used. */
+  @Override
+  public void destroy(ViewGroup view) {
+    view.setClipToOutline(false);
+    view.setClipChildren(false);
+  }
+}
diff --git a/src/main/java/com/google/android/libraries/feed/piet/ui/OutlineRoundedCornerWrapperView.java b/src/main/java/com/google/android/libraries/feed/piet/ui/OutlineRoundedCornerWrapperView.java
deleted file mode 100644
index 37096bc..0000000
--- a/src/main/java/com/google/android/libraries/feed/piet/ui/OutlineRoundedCornerWrapperView.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright 2018 The Feed Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.android.libraries.feed.piet.ui;
-
-import static com.google.android.libraries.feed.common.Validators.checkState;
-
-import android.content.Context;
-import android.graphics.Outline;
-import android.graphics.Color;
-import android.graphics.drawable.ShapeDrawable;
-import android.graphics.drawable.shapes.RoundRectShape;
-import android.os.Build;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.view.View;
-import android.view.ViewOutlineProvider;
-import com.google.android.libraries.feed.common.functional.Supplier;
-import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners;
-import com.google.search.now.ui.piet.StylesProto.Borders;
-
-/**
- * Wrapper for {@link View} instances in Piet that require rounded corners.
- *
- * <p>This version uses a much higher-performance clipToOutline strategy, which only works when all
- * 4 corners are rounded.
- */
-public class OutlineRoundedCornerWrapperView extends RoundedCornerWrapperView {
-
-  // Doesn't like the call to setOutlineProvider
-  @SuppressWarnings("initialization")
-  public OutlineRoundedCornerWrapperView(
-      Context context,
-      RoundedCorners roundedCorners,
-      Supplier<Boolean> isRtLSupplier,
-      int radiusOverride,
-      Borders borders) {
-    super(context, roundedCorners, isRtLSupplier, radiusOverride, borders);
-
-    checkState(
-        Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
-            && (roundedCorners.getBitmask() == 0 || roundedCorners.getBitmask() == 15));
-
-    if (hasRoundedCorners()) {
-      super.setOutlineProvider(
-          new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-              int radius = getRadius(view.getWidth(), view.getHeight());
-              outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
-              return;
-            }
-          });
-      setClipToOutline(true);
-      setClipChildren(true);
-    } else {
-      super.setOutlineProvider(ViewOutlineProvider.BOUNDS);
-    }
-
-    setWillNotDraw(false);
-  }
-
-  @Override
-  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-    super.onSizeChanged(w, h, oldw, oldh);
-    if (VERSION.SDK_INT <= VERSION_CODES.M) {
-      // Set a background so that shadows work on M-
-      int radius = getRadius(w, h);
-      float[] radii =
-          RoundedCornerViewHelper.createRoundedCornerMask(
-              radius, roundedCorners.getBitmask(), isRtLSupplier.get());
-      ShapeDrawable backgroundForShadow = new ShapeDrawable(new RoundRectShape(radii, null, null));
-      backgroundForShadow.getPaint().setColor(Color.TRANSPARENT);
-      setBackground(backgroundForShadow);
-    }
-  }
-
-  @Override
-  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-    super.onLayout(changed, left, top, right, bottom);
-    if (!changed || !hasRoundedCorners()) {
-      return;
-    }
-
-    int width = getWidth();
-    int height = getHeight();
-    if (width == 0 || height == 0) {
-      // The view is not visible; no further processing is needed.
-      return;
-    }
-
-    int radius = getRadius(width, height);
-    addBorders(radius);
-  }
-}
diff --git a/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerDelegate.java b/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerDelegate.java
new file mode 100644
index 0000000..39a2998
--- /dev/null
+++ b/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerDelegate.java
@@ -0,0 +1,89 @@
+// Copyright 2019 The Feed Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.libraries.feed.piet.ui;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+/**
+ * Abstract delegate that allows its children to add logic to round the corners of a {@link View}.
+ *
+ * <p>This class is abstract as it is not meant to be directly instantiated. Multiple delegates with
+ * different rounding strategies subclass this delegate. Each delegate can add logic to different
+ * {@link View} lifecycle methods, as well as initialize and destroy logic. Since {@link
+ * RoundedCornerWrapperView} actually inherits from {@link View}, it takes care of calling the super
+ * methods of these lifecycle events, in addition to calling these delegate methods.
+ *
+ * <p>Since not all rounding strategies need to override all of these methods, and {@link
+ * RoundedCornerWrapperView} calls the default 'super' implementation, they are left as empty
+ * methods by default. Having empty methods here simplifies the logic in {@link
+ * RoundedCornerWrapperView}, because that view doesn't need to know which strategies require extra
+ * logic in specific methods.
+ */
+interface RoundedCornerDelegate {
+
+  /**
+   * Initializes anything that needs to be changed on the view that is being rounded.
+   *
+   * <p>This should be the opposite of what is torn down in destroy().
+   */
+  default void initializeForView(ViewGroup view) {}
+
+  /** Allows a rounded corner delegate to add logic when child views are drawn. */
+  default void dispatchDraw(Canvas canvas) {}
+
+  /** Allows a rounded corner delegate to add logic during layout. */
+  default void onLayout(int radius, boolean isRtL, int width, int height) {}
+
+  /**
+   * Allows a rounded corner delegate to add logic during {@link
+   * ViewParent#onDescendantInvalidated}.
+   */
+  default void onDescendantInvalidated(View roundedCornerView, View invalidatedDescendant) {}
+
+  /**
+   * Allows a rounded corner delegate to add logic during {@link
+   * ViewParent#invalidateChildInParent}.
+   */
+  default void invalidateChildInParent(View view, final Rect dirty) {}
+
+  /** Allows a rounded corner delegate to add logic when the size changes. */
+  default void onSizeChanged(int radius, int width, int height, int bitmask, boolean isRtL) {}
+
+  /**
+   * Renders the view to this canvas.
+   *
+   * <p>The default call to the super method is here, rather than in {@link
+   * RoundedCornerWrapperView}, because an implementation may override this and set a bitmap
+   * directly to the canvas.
+   *
+   * <p>If this is overridden, view.drawSuper() must be called, even if it is with a different
+   * {@link Canvas}.
+   */
+  default void draw(RoundedCornerWrapperView view, Canvas canvas) {
+    view.drawSuper(canvas);
+  }
+
+  /**
+   * Removes any modifications the delegate has made to the view that is being rounded.
+   *
+   * <p>This should be called on the current {@link RoundedCornerDelegate} when switching from one
+   * delegate to another.
+   */
+  default void destroy(ViewGroup view) {}
+}
diff --git a/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerDelegateFactory.java b/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerDelegateFactory.java
new file mode 100644
index 0000000..476286f
--- /dev/null
+++ b/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerDelegateFactory.java
@@ -0,0 +1,56 @@
+// Copyright 2019 The Feed Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.libraries.feed.piet.ui;
+
+/**
+ * Factory of {@link RoundedCornerDelegate}.
+ *
+ * <p>This factory creates the delegate that corresponds with the {@link RoundingStrategy} that is
+ * passed in.
+ */
+class RoundedCornerDelegateFactory {
+
+  // The outline provider delegate has no state, so optimize by making a single static instance.
+  private static final OutlineProviderRoundedCornerDelegate OUTLINE_DELEGATE =
+      new OutlineProviderRoundedCornerDelegate();
+
+  /** Strategies that correspond with different {@link RoundedCornerDelegate}s. */
+  enum RoundingStrategy {
+    UNKNOWN,
+    CLIP_PATH,
+    OUTLINE_PROVIDER,
+    BITMAP_MASKING
+  }
+
+  static RoundedCornerDelegate getDelegate(
+      RoundingStrategy strategy, RoundedCornerMaskCache maskCache, int bitmask, boolean isRtL) {
+    // This switch omits the default statement to force a compile failure if a new strategy is added
+    // but not handled.
+    switch (strategy) {
+      case CLIP_PATH:
+        return new ClipPathRoundedCornerDelegate();
+      case OUTLINE_PROVIDER:
+        return OUTLINE_DELEGATE;
+      case BITMAP_MASKING:
+        return new BitmapMaskingRoundedCornerDelegate(maskCache, bitmask, isRtL);
+      case UNKNOWN:
+        // This should never happen, but if it does, bitmap masking works for everything.
+        return new BitmapMaskingRoundedCornerDelegate(maskCache, bitmask, isRtL);
+    }
+    throw new AssertionError(
+        String.format(
+            "RoundedCornerDelegateFactory doesn't handle %s rounding strategy", strategy));
+  }
+}
diff --git a/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerViewHelper.java b/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerViewHelper.java
index 2cdade3..c6131aa 100644
--- a/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerViewHelper.java
+++ b/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerViewHelper.java
@@ -22,9 +22,12 @@
 /** Helper class to help work with rounded corner views. */
 public class RoundedCornerViewHelper {
 
-  public static float[] createRoundedCornerMask(float radius, int cornerField, boolean isRtL) {
+  /**
+   * Returns a float[] representing the mask for which corners should be rounded with the radius.
+   */
+  static float[] createRoundedCornerBitMask(float radius, int cornerField, boolean isRtL) {
     final int layoutDirection = isRtL ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR;
-    return createRoundedCornerMask(radius, cornerField, layoutDirection);
+    return createRoundedCornerBitMask(radius, cornerField, layoutDirection);
   }
 
   /**
@@ -34,7 +37,7 @@
    *     {@link android.view.View#LAYOUT_DIRECTION_RTL}. If an invalid value is passed in, default
    *     to LTR.
    */
-  private static float[] createRoundedCornerMask(
+  private static float[] createRoundedCornerBitMask(
       float radius, int cornerField, int layoutDirection) {
     float[] radii = new float[8];
     // If we don't have any radius, don't bother creating the mask.
@@ -99,6 +102,65 @@
         || (radiusOverride > 0));
   }
 
+  /**
+   * Returns the radius that was passed in, or a smaller radius if necessary.
+   *
+   * <p>If the current radius is bigger than the width or height, or if it has adjacent rounded
+   * corners and the radius is more than half of the width or height, the radius is made smaller. It
+   * shrinks on all sides, even if only one corner needs to shrink--Piet does not allow different
+   * corners to have different radii.
+   */
+  static int adjustRadiusIfTooBig(
+      int width, int height, int currentRadius, RoundedCorners roundedCorners) {
+    int smallerSide = Math.min(height, width);
+    currentRadius = Math.min(currentRadius, smallerSide);
+
+    // This is expected to be by far the most common case, so check this first and return fast.
+    if (allFourCornersRounded(roundedCorners.getBitmask())) {
+      return Math.min(currentRadius, smallerSide / 2);
+    }
+
+    // If not all corners are rounded, it may not be necessary to truncate to half the smaller side.
+    // If there are only horizontally-adjacent or only vertically-adjacent corners, truncate the
+    // radius to half of that side of the view. Both need to be checked in case there are 3 corners
+    // rounded, which ends up being equivalent to truncating to half the smaller side.
+    if (hasVerticallyAdjacentRoundedCorners(roundedCorners.getBitmask())) {
+      currentRadius = Math.min(currentRadius, height / 2);
+    }
+    if (hasHorizontallyAdjacentRoundedCorners(roundedCorners.getBitmask())) {
+      currentRadius = Math.min(currentRadius, width / 2);
+    }
+    return currentRadius;
+  }
+
+  static boolean allFourCornersRounded(int bitmask) {
+    return bitmask == 0 || bitmask == 15;
+  }
+
+  private static boolean hasVerticallyAdjacentRoundedCorners(int bitmask) {
+    return (shouldRoundCorner(Corners.TOP_START, bitmask)
+            && shouldRoundCorner(Corners.BOTTOM_START, bitmask))
+        || (shouldRoundCorner(Corners.TOP_END, bitmask)
+            && shouldRoundCorner(Corners.BOTTOM_END, bitmask));
+  }
+
+  private static boolean hasHorizontallyAdjacentRoundedCorners(int bitmask) {
+    return (shouldRoundCorner(Corners.TOP_START, bitmask)
+            && shouldRoundCorner(Corners.TOP_END, bitmask))
+        || (shouldRoundCorner(Corners.BOTTOM_START, bitmask)
+            && shouldRoundCorner(Corners.BOTTOM_END, bitmask));
+  }
+
+  /**
+   * Returns whether the corner should be rounded, based on the bitmask.
+   *
+   * <p>A bitmask of 0 implies that all corners should be rounded. The binary bitmask digits
+   * correspond to the {@link Corners} value.
+   */
+  static boolean shouldRoundCorner(Corners corner, int bitmask) {
+    return (bitmask == 0) || (bitmask & corner.getNumber()) != 0;
+  }
+
   // Prevent instantiation
   private RoundedCornerViewHelper() {}
 }
diff --git a/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerWrapperView.java b/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerWrapperView.java
index 7b6c76e..d90cedd 100644
--- a/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerWrapperView.java
+++ b/src/main/java/com/google/android/libraries/feed/piet/ui/RoundedCornerWrapperView.java
@@ -14,54 +14,281 @@
 
 package com.google.android.libraries.feed.piet.ui;
 
+import static com.google.android.libraries.feed.common.Validators.checkState;
 
 import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
 import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.view.ViewParent;
 import android.widget.FrameLayout;
 import com.google.android.libraries.feed.common.functional.Supplier;
 import com.google.android.libraries.feed.common.ui.LayoutUtils;
+import com.google.android.libraries.feed.piet.ui.RoundedCornerDelegateFactory.RoundingStrategy;
 import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners;
-import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners.Corners;
 import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners.RadiusOptionsCase;
 import com.google.search.now.ui.piet.StylesProto.Borders;
 
-/** Wrapper for {@link View} instances in Piet that require rounded corners. */
-public abstract class RoundedCornerWrapperView extends FrameLayout {
-  final int radiusOverride;
-  final RoundedCorners roundedCorners;
-  final Supplier<Boolean> isRtLSupplier;
-  final Context context;
-  final Borders borders;
+/**
+ * Wrapper for {@link View} instances in Piet that require rounded corners.
+ *
+ * <p>There are three strategies used to round corners in different situations. This class decides
+ * which to use based on the following rules:
+ *
+ * <p>OUTLINE_PROVIDER: This is always used when possible (unless disallowed by the host). It
+ * requires hardware acceleration, does not work if 1-3 corners are rounded, and does not work on
+ * K-.
+ *
+ * <p>CLIP_PATH: This works in all situations, but does not support anti-aliasing. Used when the
+ * device does not support hardware acceleration, and in K- where outline provider is not supported.
+ * Although BITMAP_MASKING would work for those cases, this strategy is much more performant. Older
+ * SDKs or no hw acceleration support typically are signs of less-performant phones. The lack of
+ * anti-aliasing does not negatively effect metrics for these users. This strategy can be disallowed
+ * by the host, in which case BITMAP_MASKING will be used where OUTLINE_PROVIDER is not possible.
+ *
+ * <p>BITMAP_MASKING: This implementation should work in all cases for all SDKs, but has the worst
+ * performance in CPU and memory usage. It is only used when 1-3 corners are rounded in L+, or if
+ * the other strategies are disallowed by the host or not possible.
+ *
+ * <p>A {@link RoundedCornerDelegate} is called in places where special logic is needed for
+ * different rounding strategies. Each strategy has its own delegate, which is created when the
+ * strategy is selected. This class doesn't know about the implementation details of the rounding
+ * strategies.
+ */
+public class RoundedCornerWrapperView extends FrameLayout {
+  private final int radiusOverride;
+  private final RoundedCorners roundedCorners;
+  private final Supplier<Boolean> isRtLSupplier;
+  private final Context context;
+  private final Borders borders;
+  private final boolean allowClipPath;
+  private final boolean allowOutlineRounding;
+  private final boolean hasRoundedCorners;
+  private final boolean allFourCornersRounded;
+  private final RoundedCornerMaskCache maskCache;
 
-  int roundedCornerRadius;
+  private RoundedCornerDelegate roundingDelegate;
+  private boolean drawSuperCalled;
 
   // Keep track of current mask configuration so we can use cached values if nothing has changed.
   int lastWidth = -1;
   int lastHeight = -1;
 
-  RoundedCornerWrapperView(
+  private int roundedCornerRadius;
+
+  // Doesn't like the call to setOutlineProvider
+  @SuppressWarnings("initialization")
+  public RoundedCornerWrapperView(
       Context context,
       RoundedCorners roundedCorners,
+      RoundedCornerMaskCache maskCache,
       Supplier<Boolean> isRtLSupplier,
       int radiusOverride,
-      Borders borders) {
+      Borders borders,
+      boolean allowClipPath,
+      boolean allowOutlineRounding) {
     super(context);
 
     this.radiusOverride = radiusOverride;
     this.roundedCorners = roundedCorners;
     this.isRtLSupplier = isRtLSupplier;
-
+    this.allowClipPath = allowClipPath;
+    this.allowOutlineRounding = allowOutlineRounding;
     this.context = context;
     this.borders = borders;
+    this.maskCache = maskCache;
+    this.hasRoundedCorners =
+        RoundedCornerViewHelper.hasValidRoundedCorners(roundedCorners, radiusOverride);
+    this.allFourCornersRounded =
+        RoundedCornerViewHelper.allFourCornersRounded(roundedCorners.getBitmask());
+
+    setRoundingStrategy();
+
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+      // Even when not using the outline provider strategy, the outline needs to be set for
+      // shadows to render properly.
+      setupOutlineProvider();
+    }
+    setWillNotDraw(false);
+  }
+
+  private void setRoundingStrategy() {
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && allowClipPath) {
+      setRoundingDelegate(RoundingStrategy.CLIP_PATH);
+      if (VERSION.SDK_INT < VERSION_CODES.JELLY_BEAN_MR2) {
+        // clipPath doesn't work with hardware rendering on < 18.
+        setLayerType(LAYER_TYPE_SOFTWARE, null);
+      }
+    } else if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP
+        && allFourCornersRounded
+        && allowOutlineRounding) {
+      setRoundingDelegate(RoundingStrategy.OUTLINE_PROVIDER);
+    } else {
+      setRoundingDelegate(RoundingStrategy.BITMAP_MASKING);
+    }
+  }
+
+  private void updateRoundingStrategy() {
+    if (roundingDelegate instanceof OutlineProviderRoundedCornerDelegate
+        && this.isAttachedToWindow()
+        && !this.isHardwareAccelerated()) {
+      if (allowClipPath) {
+        setRoundingDelegate(RoundingStrategy.CLIP_PATH);
+      } else {
+        setRoundingDelegate(RoundingStrategy.BITMAP_MASKING);
+      }
+    }
+  }
+
+  void setRoundingDelegate(RoundingStrategy strategy) {
+    if (roundingDelegate != null) {
+      roundingDelegate.destroy(this);
+    }
+    roundingDelegate =
+        RoundedCornerDelegateFactory.getDelegate(
+            strategy, maskCache, roundedCorners.getBitmask(), isRtLSupplier.get());
+    roundingDelegate.initializeForView(this);
+  }
+
+  private void setupOutlineProvider() {
+    if (hasRoundedCorners) {
+      super.setOutlineProvider(
+          new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+              int radius = getRadius(view.getWidth(), view.getHeight());
+              if (allFourCornersRounded) {
+                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
+                return;
+              }
+              float[] radii =
+                  RoundedCornerViewHelper.createRoundedCornerBitMask(
+                      radius, roundedCorners.getBitmask(), isRtLSupplier.get());
+              RoundRectShape outlineShape = new RoundRectShape(radii, null, null);
+              outlineShape.resize(view.getWidth(), view.getHeight());
+              // This actually sets the outline to use this shape
+              outlineShape.getOutline(outline);
+            }
+          });
+    } else {
+      super.setOutlineProvider(ViewOutlineProvider.BOUNDS);
+    }
   }
 
   /**
-   * Creates a border drawable and adds it to this view's foreground. This is called from {@link
-   * RoundedCornerWrapperView} because when the corners are rounded, the borders also need to be
-   * rounded, and the radius can't properly be calculated until the view has been laid out and the
-   * height and width are known. From this class, it is easy to make sure the border is created
-   * after layout happens, since we are overriding draw. Draw is guaranteed to happen after layout,
-   * so we can make sure that the borders are drawn with the appropriate measurements.
+   * Calls the rounding delegate to perform any additional work specific to a certain rounding
+   * strategy during dispatchDraw().
+   */
+  @Override
+  protected void dispatchDraw(Canvas canvas) {
+    roundingDelegate.dispatchDraw(canvas);
+    super.dispatchDraw(canvas);
+  }
+
+  /**
+   * Allows the {@link RoundedCornerDelegate} to add logic during onDescendantInvalidated().
+   *
+   * <p>The bitmap masking rounded corner strategy requires this hook, to make sure the wrapper view
+   * is invalidated when there are animations. The delegate handles that logic.
+   */
+  @Override
+  public void onDescendantInvalidated(View child, View target) {
+    super.onDescendantInvalidated(child, target);
+    roundingDelegate.onDescendantInvalidated(this, target);
+  }
+
+  /**
+   * Allows the {@link RoundedCornerDelegate} to add logic during invalidateChildInParent().
+   *
+   * <p>The bitmap masking rounded corner strategy requires this hook, to make sure the wrapper view
+   * is invalidated when there are animations. The delegate handles that logic.
+   */
+  @Override
+  public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
+    roundingDelegate.invalidateChildInParent(this, dirty);
+    return super.invalidateChildInParent(location, dirty);
+  }
+
+  /**
+   * Calls the {@link RoundedCornerDelegate} to draw to the {@link Canvas}, in case extra logic is
+   * needed to round the corners.
+   *
+   * <p>Draws as normal for everything except the bitmap masking strategy. The bitmap masking
+   * strategy requires manipulating the {@link Canvas}, which the delegate handles.
+   */
+  @Override
+  public void draw(Canvas canvas) {
+    drawSuperCalled = false;
+    roundingDelegate.draw(this, canvas);
+    checkState(drawSuperCalled, "View.draw() never called in RoundedCornerWrapperView.draw()");
+  }
+
+  public void drawSuper(Canvas canvas) {
+    drawSuperCalled = true;
+    super.draw(canvas);
+  }
+
+  /**
+   * Updates rounded corner information when the size changes.
+   *
+   * <p>At this point, the view will actually be attached to the window, meaning we can detect
+   * whether it is hardware accelerated. The outline provider strategy doesn't work without hardware
+   * acceleration, so the rounding strategy may need to be updated. The size always changes at least
+   * once--from 0 to final dimensions--so it is safe to assume that this will catch devices without
+   * hw acceleration.
+   *
+   * <p>Call the rounding delegate to perform any additional work specific to a certain rounding
+   * strategy.
+   */
+  @Override
+  protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+    super.onSizeChanged(width, height, oldWidth, oldHeight);
+    updateRoundingStrategy();
+    roundingDelegate.onSizeChanged(
+        getRadius(width, height), width, height, roundedCorners.getBitmask(), isRtLSupplier.get());
+  }
+
+  /**
+   * Lays out the view, calling the rounding delegate to perform any additional work specific to a
+   * certain rounding strategy.
+   *
+   * <p>Borders must be created after the radius is known, so they are added from onLayout().
+   */
+  @Override
+  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+    super.onLayout(changed, left, top, right, bottom);
+    if (!changed || !hasRoundedCorners) {
+      return;
+    }
+
+    int width = getWidth();
+    int height = getHeight();
+    if (width == 0 || height == 0) {
+      // The view is not visible; no further processing is needed.
+      return;
+    }
+
+    int radius = getRadius(width, height);
+    addBorders(radius);
+
+    roundingDelegate.onLayout(radius, isRtLSupplier.get(), width, height);
+  }
+
+  /**
+   * Creates a border drawable and adds it to this view's foreground.
+   *
+   * <p>This is called from {@link RoundedCornerWrapperView} because when the corners are rounded,
+   * the borders also need to be rounded, and the radius can't properly be calculated until the view
+   * has been laid out and the height and width are known. From this class, it is easy to make sure
+   * the border is created after layout happens, since we are overriding draw. Draw is guaranteed to
+   * happen after layout, so we can make sure that the borders are drawn with the appropriate
+   * measurements.
    */
   void addBorders(int radius) {
     if (borders.getWidth() <= 0) {
@@ -70,7 +297,8 @@
     boolean isRtL = isRtLSupplier.get();
     // Set up outline of borders
     float[] outerRadii =
-        RoundedCornerViewHelper.createRoundedCornerMask(radius, roundedCorners.getBitmask(), isRtL);
+        RoundedCornerViewHelper.createRoundedCornerBitMask(
+            radius, roundedCorners.getBitmask(), isRtL);
 
     // Create a drawable to stroke the border
     BorderDrawable borderDrawable =
@@ -79,13 +307,15 @@
   }
 
   /**
-   * Returns the radius, which is only calculated if necessary. If the width and height are the same
-   * as the previous width and height, there's no need to re-calculate, and the previous radius is
-   * returned. The width and height are needed for radii calculated as a percentage of width or
-   * height, and to make sure that the radius isn't too big for the size of the view.
+   * Returns the radius, which is only calculated if necessary.
+   *
+   * <p>If the width and height are the same as the previous width and height, there's no need to
+   * re-calculate, and the previous radius is returned. The width and height are needed for radii
+   * calculated as a percentage of width or height, and to make sure that the radius isn't too big
+   * for the size of the view.
    */
   public int getRadius(int width, int height) {
-    if (!hasRoundedCorners() || width == 0 || height == 0) {
+    if (!hasRoundedCorners || width == 0 || height == 0) {
       return 0;
     }
     if (radiusOverride > 0) {
@@ -103,7 +333,6 @@
 
   /** Calculates the corner radius, clipping to width or height when necessary. */
   private int makeRadius(int width, int height) {
-
     int radius = 0;
     RadiusOptionsCase radiusOptions = roundedCorners.getRadiusOptionsCase();
 
@@ -124,47 +353,8 @@
         }
     }
 
-    roundedCornerRadius = adjustRadiusIfTooBig(width, height, radius);
+    roundedCornerRadius =
+        RoundedCornerViewHelper.adjustRadiusIfTooBig(width, height, radius, roundedCorners);
     return roundedCornerRadius;
   }
-
-  /**
-   * Returns the radius that was passed in, or a smaller radius if necessary. If the current radius
-   * is bigger than the width or height, or if it has adjacent rounded corners and the radius is
-   * more than half of the width or height, the radius is made smaller. It shrinks on all sides,
-   * even if only one corner needs to shrink--Piet does not allow different corners to have
-   * different radii.
-   */
-  private int adjustRadiusIfTooBig(int width, int height, int currentRadius) {
-    currentRadius = Math.min(currentRadius, height);
-    currentRadius = Math.min(currentRadius, width);
-
-    if (hasVerticallyAdjacentRoundedCorners()) {
-      currentRadius = Math.min(currentRadius, height / 2);
-    }
-    if (hasHorizontallyAdjacentRoundedCorners()) {
-      currentRadius = Math.min(currentRadius, width / 2);
-    }
-    return currentRadius;
-  }
-
-  /** This should always be true; we should not be using this view when corners are not round. */
-  public boolean hasRoundedCorners() {
-    return RoundedCornerViewHelper.hasValidRoundedCorners(roundedCorners, radiusOverride);
-  }
-
-  private boolean hasVerticallyAdjacentRoundedCorners() {
-    return (shouldRoundCorner(Corners.TOP_START) && shouldRoundCorner(Corners.BOTTOM_START))
-        || (shouldRoundCorner(Corners.TOP_END) && shouldRoundCorner(Corners.BOTTOM_END));
-  }
-
-  private boolean hasHorizontallyAdjacentRoundedCorners() {
-    return (shouldRoundCorner(Corners.TOP_START) && shouldRoundCorner(Corners.TOP_END))
-        || (shouldRoundCorner(Corners.BOTTOM_START) && shouldRoundCorner(Corners.BOTTOM_END));
-  }
-
-  boolean shouldRoundCorner(Corners corner) {
-    int bitmask = roundedCorners.getBitmask();
-    return (bitmask == 0) || (bitmask & corner.getNumber()) != 0;
-  }
 }
diff --git a/src/test/java/com/google/android/libraries/feed/piet/ElementAdapterTest.java b/src/test/java/com/google/android/libraries/feed/piet/ElementAdapterTest.java
index e630f2f..43f96b3 100644
--- a/src/test/java/com/google/android/libraries/feed/piet/ElementAdapterTest.java
+++ b/src/test/java/com/google/android/libraries/feed/piet/ElementAdapterTest.java
@@ -43,7 +43,6 @@
 import com.google.android.libraries.feed.piet.host.ActionHandler;
 import com.google.android.libraries.feed.piet.host.ActionHandler.ActionType;
 import com.google.android.libraries.feed.piet.host.LogDataCallback;
-import com.google.android.libraries.feed.piet.ui.BitmapMaskingRoundedCornerWrapperView;
 import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache;
 import com.google.android.libraries.feed.piet.ui.RoundedCornerWrapperView;
 import com.google.android.libraries.feed.testing.shadows.ExtendedShadowView;
@@ -172,7 +171,7 @@
 
   // TODO: remove Element gravity and roll this test into the above testGetters
   @Test
-  public void testGravityGetter() {
+  public void getGravity() {
     // Pre-creation
     assertThat(adapter.getHorizontalGravity(Gravity.CLIP_HORIZONTAL))
         .isEqualTo(Gravity.CLIP_HORIZONTAL);
@@ -203,7 +202,7 @@
   }
 
   @Test
-  public void testSetLayoutParams() {
+  public void setLayoutParams() {
     adapter.setLayoutParams(new LayoutParams(WIDTH, HEIGHT));
 
     assertThat(view.getLayoutParams().width).isEqualTo(WIDTH);
@@ -211,7 +210,7 @@
   }
 
   @Test
-  public void testSetLayoutParams_overlay() {
+  public void setLayoutParams_overlay() {
     Element elementWithOverlays =
         Element.newBuilder()
             .addOverlays(
@@ -240,7 +239,7 @@
   }
 
   @Test
-  public void testSetLayoutParams_resetsMarginsWithOverlay() {
+  public void setLayoutParams_resetsMarginsWithOverlay() {
     Element elementWithOverlays =
         Element.newBuilder()
             .addOverlays(
@@ -268,13 +267,13 @@
   }
 
   @Test
-  public void testGetComputedDimensions_defaults() {
+  public void getComputedDimensions_defaults() {
     assertThat(adapter.getComputedWidthPx()).isEqualTo(DIMENSION_NOT_SET);
     assertThat(adapter.getComputedHeightPx()).isEqualTo(DIMENSION_NOT_SET);
   }
 
   @Test
-  public void testGetComputedDimensions_explicit() {
+  public void getComputedDimensions_explicit() {
     adapter.setDims(WIDTH, HEIGHT);
 
     assertThat(adapter.getComputedWidthPx()).isEqualTo(WIDTH);
@@ -282,7 +281,7 @@
   }
 
   @Test
-  public void testCreateAdapter_callsOnCreateAdapter() {
+  public void createAdapter_callsOnCreateAdapter() {
     Element defaultElement = Element.getDefaultInstance();
 
     adapter.createAdapter(defaultElement, defaultElement, frameContext);
@@ -291,7 +290,7 @@
   }
 
   @Test
-  public void testCreateAdapter_extractsModelFromElement() {
+  public void createAdapter_extractsModelFromElement() {
     Element element = Element.getDefaultInstance();
 
     adapter.createAdapter(element, frameContext);
@@ -300,7 +299,7 @@
   }
 
   @Test
-  public void testCreateAdapter_createsOverlays() {
+  public void createAdapter_createsOverlays() {
     Element elementWithOverlays =
         Element.newBuilder()
             .addOverlays(
@@ -317,7 +316,7 @@
   }
 
   @Test
-  public void testCreateAdapter_setsOverlayLayoutParams() {
+  public void createAdapter_setsOverlayLayoutParams() {
     Content overlayContent =
         Content.newBuilder()
             .setElement(Element.newBuilder().setElementList(ElementList.getDefaultInstance()))
@@ -344,8 +343,8 @@
     when(styleProvider.createWrapperView(
             context,
             parameters.roundedCornerMaskCache,
-            parameters.useLegacyRoundedCornerImpl,
-            parameters.useOutlineRoundedCornerImpl))
+            parameters.allowLegacyRoundedCornerImpl,
+            parameters.allowOutlineRoundedCornerImpl))
         .thenReturn(new FrameLayout(context));
     adapter = new TestElementAdapter(context, parameters, view);
 
@@ -359,7 +358,7 @@
   }
 
   @Test
-  public void testCreateAdapter_doesNothingForBoundOverlays() {
+  public void createAdapter_doesNothingForBoundOverlays() {
     ElementBindingRef bindingRef = ElementBindingRef.newBuilder().setBindingId("overlay").build();
     BindingValue bindingValue =
         BindingValue.newBuilder()
@@ -376,7 +375,7 @@
   }
 
   @Test
-  public void testCreateAdapter_addsBorders() {
+  public void createAdapter_addsBorders() {
     when(styleProvider.hasBorders()).thenReturn(true);
 
     adapter.createAdapter(Element.getDefaultInstance(), Element.getDefaultInstance(), frameContext);
@@ -385,7 +384,7 @@
   }
 
   @Test
-  public void testCreateAdapter_appliesVisibility() {
+  public void createAdapter_appliesVisibility() {
     Element defaultElement =
         Element.newBuilder()
             .setVisibilityState(
@@ -398,7 +397,7 @@
   }
 
   @Test
-  public void testCreateAdapter_doesNothingWithVisibilityGone() {
+  public void createAdapter_doesNothingWithVisibilityGone() {
     Element defaultElement =
         Element.newBuilder()
             .setVisibilityState(VisibilityState.newBuilder().setDefaultVisibility(Visibility.GONE))
@@ -411,7 +410,7 @@
   }
 
   @Test
-  public void testCreateAdapter_ignoresBoundVisibility() {
+  public void createAdapter_ignoresBoundVisibility() {
     String visibilityBindingId = "invisible";
     VisibilityBindingRef visibilityBinding =
         VisibilityBindingRef.newBuilder().setBindingId(visibilityBindingId).build();
@@ -431,7 +430,7 @@
   }
 
   @Test
-  public void testCreateAdapter_setsDimensions() {
+  public void createAdapter_setsDimensions() {
     int width = 10;
     int height = 20;
 
@@ -447,7 +446,7 @@
   }
 
   @Test
-  public void testCreateAdapter_resetsUnsetDimensions() {
+  public void createAdapter_resetsUnsetDimensions() {
     // Set some dimensions first
     int width = 10;
     int height = 20;
@@ -473,7 +472,7 @@
   }
 
   @Test
-  public void testCreateAdapter_dimensionsDontOverrideSubclass() {
+  public void createAdapter_dimensionsDontOverrideSubclass() {
     int width = 10;
     int height = 20;
 
@@ -502,7 +501,7 @@
   }
 
   @Test
-  public void testBindModel_callsOnBindModel() {
+  public void bindModel_callsOnBindModel() {
     Element defaultElement = Element.getDefaultInstance();
 
     adapter.bindModel(defaultElement, defaultElement, frameContext);
@@ -520,7 +519,7 @@
   }
 
   @Test
-  public void testBindModel_extractsModelFromElement() {
+  public void bindModel_extractsModelFromElement() {
     Element element = Element.getDefaultInstance();
 
     adapter.bindModel(element, frameContext);
@@ -529,7 +528,7 @@
   }
 
   @Test
-  public void testBindModel_bindsOverlays() {
+  public void bindModel_bindsOverlays() {
     Content overlayContent =
         Content.newBuilder()
             .setElement(
@@ -556,7 +555,7 @@
   }
 
   @Test
-  public void testBindModel_bindsBoundOverlays() {
+  public void bindModel_bindsBoundOverlays() {
     ElementBindingRef bindingRef = ElementBindingRef.newBuilder().setBindingId("overlay").build();
     BindingValue bindingValue =
         BindingValue.newBuilder()
@@ -573,7 +572,7 @@
   }
 
   @Test
-  public void testBindModel_setsActions() {
+  public void bindModel_setsActions() {
     Actions actions = Actions.newBuilder().setOnClickAction(Action.getDefaultInstance()).build();
     Element elementWithActions = Element.newBuilder().setActions(actions).build();
 
@@ -583,7 +582,7 @@
   }
 
   @Test
-  public void testBindModel_setsActionsOnBaseView() {
+  public void bindModel_setsActionsOnBaseView() {
     Actions actions = Actions.newBuilder().setOnClickAction(Action.getDefaultInstance()).build();
     Element elementWithActions =
         Element.newBuilder()
@@ -602,7 +601,7 @@
   }
 
   @Test
-  public void testBindModel_setsActionsWithBinding() {
+  public void bindModel_setsActionsWithBinding() {
     Actions actions = Actions.newBuilder().setOnClickAction(Action.getDefaultInstance()).build();
     ActionsBindingRef actionsBinding =
         ActionsBindingRef.newBuilder().setBindingId("ACTION!").build();
@@ -615,7 +614,7 @@
   }
 
   @Test
-  public void testBindModel_unsetsActions() {
+  public void bindModel_unsetsActions() {
     Actions actions = Actions.newBuilder().setOnClickAction(Action.getDefaultInstance()).build();
     Element elementWithActions = Element.newBuilder().setActions(actions).build();
     Element elementWithoutActions = Element.getDefaultInstance();
@@ -630,7 +629,7 @@
   }
 
   @Test
-  public void testBindModel_unsetsActionsOnBaseView() {
+  public void bindModel_unsetsActionsOnBaseView() {
     Actions actions = Actions.newBuilder().setOnClickAction(Action.getDefaultInstance()).build();
     Element elementWithActions =
         Element.newBuilder()
@@ -656,7 +655,7 @@
   }
 
   @Test
-  public void testBindModel_failsWithIncompatibleOverlays() {
+  public void bindModel_failsWithIncompatibleOverlays() {
     Content overlayContent =
         Content.newBuilder()
             .setElement(
@@ -680,7 +679,7 @@
   }
 
   @Test
-  public void testBindModel_setsVisibility() {
+  public void bindModel_setsVisibility() {
     Element visibleElement =
         Element.newBuilder()
             .setVisibilityState(
@@ -700,7 +699,7 @@
   }
 
   @Test
-  public void testBindModel_doesNothingWhenVisibilityIsGone() {
+  public void bindModel_doesNothingWhenVisibilityIsGone() {
     Element visibleElement =
         Element.newBuilder()
             .setVisibilityState(
@@ -719,7 +718,7 @@
   }
 
   @Test
-  public void testBindModel_recreatesWhenVisibilityWasGone() {
+  public void bindModel_recreatesWhenVisibilityWasGone() {
     Element goneElement =
         Element.newBuilder()
             .setVisibilityState(VisibilityState.newBuilder().setDefaultVisibility(Visibility.GONE))
@@ -739,7 +738,7 @@
   }
 
   @Test
-  public void testBindModel_recreatesWhenVisibilityWasGone_bound() {
+  public void bindModel_recreatesWhenVisibilityWasGone_bound() {
     VisibilityBindingRef visibilityBinding =
         VisibilityBindingRef.newBuilder().setBindingId("camo").build();
     Element elementWithVisibilityBinding =
@@ -769,7 +768,7 @@
   }
 
   @Test
-  public void testBindModel_doesntRecreateWhenWasCreatedButVisibilityGone() {
+  public void bindModel_doesntRecreateWhenWasCreatedButVisibilityGone() {
     // Create a real adapter for a visible element.
     Element visibileElement =
         Element.newBuilder()
@@ -804,7 +803,7 @@
   }
 
   @Test
-  public void testBindModel_accessibility() {
+  public void bindModel_accessibility() {
     Element accessibilityElement =
         Element.newBuilder()
             .setAccessibility(
@@ -821,7 +820,7 @@
   }
 
   @Test
-  public void testBindModel_accessibilityBinding() {
+  public void bindModel_accessibilityBinding() {
     ParameterizedTextBindingRef textBinding =
         ParameterizedTextBindingRef.newBuilder().setBindingId("binding").build();
     Element accessibilityElement =
@@ -840,7 +839,7 @@
   }
 
   @Test
-  public void testBindModel_accessibilityReset() {
+  public void bindModel_accessibilityReset() {
     adapter.getView().setContentDescription("OLD CONTENT DESCRIPTION");
 
     adapter.bindModel(Element.getDefaultInstance(), frameContext);
@@ -849,7 +848,7 @@
   }
 
   @Test
-  public void testUnbindModel_callsOnUnbindModel() {
+  public void unbindModel_callsOnUnbindModel() {
     Element defaultElement = Element.getDefaultInstance();
 
     adapter.createAdapter(defaultElement, defaultElement, frameContext);
@@ -870,7 +869,7 @@
   }
 
   @Test
-  public void testUnbindModel_unsetsModel() {
+  public void unbindModel_unsetsModel() {
     Element element = Element.getDefaultInstance();
 
     adapter.createAdapter(element, element, frameContext);
@@ -883,7 +882,7 @@
 
   // If onBindModel was never called, onUnbindModel should not be called.
   @Test
-  public void testUnbindModel_visibilityWasGone() {
+  public void unbindModel_visibilityWasGone() {
     Element goneElement =
         Element.newBuilder()
             .setVisibilityState(VisibilityState.newBuilder().setDefaultVisibility(Visibility.GONE))
@@ -905,7 +904,7 @@
   }
 
   @Test
-  public void testUnbindModel_unsetsActions() {
+  public void unbindModel_unsetsActions() {
     Element elementWithOverlaysAndActions =
         Element.newBuilder()
             .addOverlays(
@@ -931,7 +930,7 @@
   }
 
   @Test
-  public void testUnbindModel_unbindsOverlayAdapters() {
+  public void unbindModel_unbindsOverlayAdapters() {
     Content overlayContent =
         Content.newBuilder()
             .setElement(
@@ -949,7 +948,7 @@
   }
 
   @Test
-  public void testUnbindModel_releasesBoundOverlays() {
+  public void unbindModel_releasesBoundOverlays() {
     Element overlayElement =
         Element.newBuilder()
             .setGravityVertical(GravityVertical.GRAVITY_MIDDLE)
@@ -981,7 +980,7 @@
   }
 
   @Test
-  public void testUnbindModel_affectsVisibilityCalculations() {
+  public void unbindModel_affectsVisibilityCalculations() {
     VisibilityBindingRef visibilityBinding =
         VisibilityBindingRef.newBuilder().setBindingId("vis").build();
     Element element =
@@ -1007,7 +1006,7 @@
   }
 
   @Test
-  public void testReleaseAdapter_callsOnReleaseAdapter() {
+  public void releaseAdapter_callsOnReleaseAdapter() {
     Element defaultElement = Element.getDefaultInstance();
 
     adapter.createAdapter(defaultElement, defaultElement, frameContext);
@@ -1018,7 +1017,7 @@
   }
 
   @Test
-  public void testReleaseAdapter_removesOverlays() {
+  public void releaseAdapter_removesOverlays() {
     Element elementWithOverlays =
         Element.newBuilder()
             .addOverlays(
@@ -1046,7 +1045,7 @@
   }
 
   @Test
-  public void testReleaseAdapter_resetsVisibility() {
+  public void releaseAdapter_resetsVisibility() {
     Element defaultElement = Element.getDefaultInstance();
 
     adapter.createAdapter(defaultElement, defaultElement, frameContext);
@@ -1058,7 +1057,7 @@
   }
 
   @Test
-  public void testReleaseAdapter_resetsDimensions() {
+  public void releaseAdapter_resetsDimensions() {
     Element defaultElement = Element.getDefaultInstance();
 
     adapter.createAdapter(defaultElement, defaultElement, frameContext);
@@ -1071,7 +1070,7 @@
   }
 
   @Test
-  public void testSetVisibility() {
+  public void setVisibility() {
     adapter.createAdapter(Element.getDefaultInstance(), Element.getDefaultInstance(), frameContext);
     adapter.getBaseView().setVisibility(View.GONE);
     adapter.setVisibilityOnView(Visibility.VISIBLE);
@@ -1085,7 +1084,7 @@
   }
 
   @Test
-  public void testGetVisibilityForElement() {
+  public void getVisibilityForElement() {
     VisibilityBindingRef bindingRef =
         VisibilityBindingRef.newBuilder().setBindingId("binding").build();
     Element elementWithDefaultVisibility =
@@ -1125,7 +1124,7 @@
 
   @Config(shadows = {ExtendedShadowView.class})
   @Test
-  public void testGetOnFullViewActions() {
+  public void getOnFullViewActions() {
     Frame frame = Frame.newBuilder().setTag("FRAME").build();
     when(frameContext.getFrame()).thenReturn(frame);
     View viewport = new View(context);
@@ -1188,7 +1187,7 @@
 
   @Config(shadows = {ExtendedShadowView.class})
   @Test
-  public void testGetOnPartialViewActions() {
+  public void getOnPartialViewActions() {
     Frame frame = Frame.newBuilder().setTag("FRAME").build();
     when(frameContext.getFrame()).thenReturn(frame);
     View viewport = new View(context);
@@ -1269,7 +1268,7 @@
 
   @Config(shadows = {ExtendedShadowView.class})
   @Test
-  public void testTriggerHideActions() {
+  public void triggerHideActions() {
     Frame frame = Frame.newBuilder().setTag("FRAME").build();
     when(frameContext.getFrame()).thenReturn(frame);
     View viewport = new View(context);
@@ -1339,16 +1338,18 @@
   }
 
   @Test
-  public void testRoundedCorners() {
+  public void createRoundedCorners() {
     RoundedCorners roundedCorners = RoundedCorners.newBuilder().setRadiusDp(10).build();
     RoundedCornerWrapperView roundedCornerWrapperView =
-        new BitmapMaskingRoundedCornerWrapperView(
+        new RoundedCornerWrapperView(
             context,
             roundedCorners,
             maskCache,
             Suppliers.of(false),
             /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+            Borders.getDefaultInstance(),
+            LEGACY_CORNERS_FLAG,
+            OUTLINE_CORNERS_FLAG);
     when(styleProvider.hasRoundedCorners()).thenReturn(true);
     when(styleProvider.createWrapperView(
             context, maskCache, LEGACY_CORNERS_FLAG, OUTLINE_CORNERS_FLAG))
@@ -1360,16 +1361,18 @@
   }
 
   @Test
-  public void testRoundedCornersWithVisibility() {
+  public void setVisibilityWithRoundedCorners() {
     RoundedCorners roundedCorners = RoundedCorners.newBuilder().setRadiusDp(10).build();
     RoundedCornerWrapperView roundedCornerWrapperView =
-        new BitmapMaskingRoundedCornerWrapperView(
+        new RoundedCornerWrapperView(
             context,
             roundedCorners,
             maskCache,
             Suppliers.of(false),
             /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+            Borders.getDefaultInstance(),
+            LEGACY_CORNERS_FLAG,
+            OUTLINE_CORNERS_FLAG);
     when(styleProvider.hasRoundedCorners()).thenReturn(true);
     when(styleProvider.createWrapperView(
             context, maskCache, LEGACY_CORNERS_FLAG, OUTLINE_CORNERS_FLAG))
@@ -1398,7 +1401,7 @@
   }
 
   @Test
-  public void testGetStyleIdsStack() {
+  public void getStyleIdsStack() {
     StyleIdsStack style = StyleIdsStack.newBuilder().addStyleIds("style").build();
     Element elementWithStyle = Element.newBuilder().setStyleReferences(style).build();
 
diff --git a/src/test/java/com/google/android/libraries/feed/piet/ImageElementAdapterTest.java b/src/test/java/com/google/android/libraries/feed/piet/ImageElementAdapterTest.java
index b162690..a9ab42a 100644
--- a/src/test/java/com/google/android/libraries/feed/piet/ImageElementAdapterTest.java
+++ b/src/test/java/com/google/android/libraries/feed/piet/ImageElementAdapterTest.java
@@ -41,8 +41,8 @@
 import com.google.android.libraries.feed.common.ui.LayoutUtils;
 import com.google.android.libraries.feed.piet.PietStylesHelper.PietStylesHelperFactory;
 import com.google.android.libraries.feed.piet.host.AssetProvider;
-import com.google.android.libraries.feed.piet.ui.BitmapMaskingRoundedCornerWrapperView;
 import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache;
+import com.google.android.libraries.feed.piet.ui.RoundedCornerWrapperView;
 import com.google.search.now.ui.piet.BindingRefsProto.ImageBindingRef;
 import com.google.search.now.ui.piet.BindingRefsProto.StyleBindingRef;
 import com.google.search.now.ui.piet.ElementsProto.BindingValue;
@@ -132,13 +132,15 @@
     when(styleProvider.createWrapperView(
             context, maskCache, LEGACY_CORNERS_FLAG, OUTLINE_CORNERS_FLAG))
         .thenReturn(
-            new BitmapMaskingRoundedCornerWrapperView(
+            new RoundedCornerWrapperView(
                 context,
                 CORNERS,
                 maskCache,
                 Suppliers.of(false),
                 /*radiusOverride= */ 0,
-                /* borders= */ null));
+                /* borders= */ null,
+                /* allowClipPath= */ false,
+                /* allowOutlineRounding= */ false));
     setStyle(null, null);
 
     adapter = new ImageElementAdapterForTest(context, parameters);
diff --git a/src/test/java/com/google/android/libraries/feed/piet/StyleProviderTest.java b/src/test/java/com/google/android/libraries/feed/piet/StyleProviderTest.java
index 8a7409c..4dd7acf 100644
--- a/src/test/java/com/google/android/libraries/feed/piet/StyleProviderTest.java
+++ b/src/test/java/com/google/android/libraries/feed/piet/StyleProviderTest.java
@@ -26,7 +26,6 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.ShapeDrawable;
-import android.os.Build;
 import android.os.Build.VERSION_CODES;
 import android.view.Gravity;
 import android.view.View;
@@ -39,11 +38,8 @@
 import com.google.android.libraries.feed.common.functional.Suppliers;
 import com.google.android.libraries.feed.common.ui.LayoutUtils;
 import com.google.android.libraries.feed.piet.host.AssetProvider;
-import com.google.android.libraries.feed.piet.ui.BitmapMaskingRoundedCornerWrapperView;
 import com.google.android.libraries.feed.piet.ui.BorderDrawable;
 import com.google.android.libraries.feed.piet.ui.GradientDrawable;
-import com.google.android.libraries.feed.piet.ui.LegacyRoundedCornerWrapperView;
-import com.google.android.libraries.feed.piet.ui.OutlineRoundedCornerWrapperView;
 import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache;
 import com.google.android.libraries.feed.piet.ui.RoundedCornerWrapperView;
 import com.google.search.now.ui.piet.GradientsProto.ColorStop;
@@ -400,11 +396,12 @@
     StyleProvider styleProvider =
         new StyleProvider(
             Style.newBuilder()
-                .setRoundedCorners(RoundedCorners.newBuilder().setRadius(0).setBitmask(4))
+                .setRoundedCorners(RoundedCorners.newBuilder().setRadiusDp(0).setBitmask(4))
                 .build(),
             mockAssetProvider);
 
     FrameLayout wrapperView = styleProvider.createWrapperView(context, maskCache, false, false);
+
     assertThat(wrapperView).isNotInstanceOf(RoundedCornerWrapperView.class);
     assertThat(wrapperView.getOutlineProvider()).isEqualTo(ViewOutlineProvider.BOUNDS);
   }
@@ -414,83 +411,14 @@
     StyleProvider styleProvider =
         new StyleProvider(
             Style.newBuilder()
-                .setRoundedCorners(RoundedCorners.newBuilder().setRadius(9).setBitmask(4))
+                .setRoundedCorners(RoundedCorners.newBuilder().setRadiusDp(16).setBitmask(4))
                 .build(),
             mockAssetProvider);
 
     FrameLayout wrapperView = styleProvider.createWrapperView(context, maskCache, false, false);
-    assertThat(wrapperView).isInstanceOf(BitmapMaskingRoundedCornerWrapperView.class);
-    assertThat(wrapperView.getOutlineProvider()).isNotNull();
-  }
 
-  @Test
-  public void testCreateWrapperView_roundedCornersOutline() {
-    StyleProvider styleProvider =
-        new StyleProvider(
-            Style.newBuilder()
-                .setRoundedCorners(RoundedCorners.newBuilder().setRadius(9).setBitmask(15))
-                .build(),
-            mockAssetProvider);
-
-    FrameLayout wrapperView = styleProvider.createWrapperView(context, maskCache, false, true);
-    assertThat(wrapperView).isInstanceOf(OutlineRoundedCornerWrapperView.class);
-    assertThat(wrapperView.getOutlineProvider()).isNotNull();
-  }
-
-  @Test
-  public void testCreateWrapperView_roundedCornersOutlineButNotFourCorners() {
-    StyleProvider styleProvider =
-        new StyleProvider(
-            Style.newBuilder()
-                .setRoundedCorners(RoundedCorners.newBuilder().setRadius(9).setBitmask(4))
-                .build(),
-            mockAssetProvider);
-
-    FrameLayout wrapperView = styleProvider.createWrapperView(context, maskCache, false, true);
-    assertThat(wrapperView).isInstanceOf(BitmapMaskingRoundedCornerWrapperView.class);
-    assertThat(wrapperView.getOutlineProvider()).isNotNull();
-  }
-
-  @Test
-  @Config(sdk = {Build.VERSION_CODES.KITKAT})
-  public void testCreateWrapperView_legacyButFlagOff() {
-    StyleProvider styleProvider =
-        new StyleProvider(
-            Style.newBuilder()
-                .setRoundedCorners(RoundedCorners.newBuilder().setRadius(9).setBitmask(4))
-                .build(),
-            mockAssetProvider);
-
-    assertThat(styleProvider.createWrapperView(context, maskCache, false, false))
-        .isInstanceOf(BitmapMaskingRoundedCornerWrapperView.class);
-  }
-
-  @Test
-  @Config(sdk = {Build.VERSION_CODES.KITKAT})
-  public void testCreateWrapperView_legacyWithFlagOn() {
-    StyleProvider styleProvider =
-        new StyleProvider(
-            Style.newBuilder()
-                .setRoundedCorners(RoundedCorners.newBuilder().setRadius(9).setBitmask(4))
-                .build(),
-            mockAssetProvider);
-
-    assertThat(styleProvider.createWrapperView(context, maskCache, true, false))
-        .isInstanceOf(LegacyRoundedCornerWrapperView.class);
-  }
-
-  @Test
-  @Config(sdk = {Build.VERSION_CODES.KITKAT})
-  public void testCreateWrapperView_legacyWithBothFlagsOn() {
-    StyleProvider styleProvider =
-        new StyleProvider(
-            Style.newBuilder()
-                .setRoundedCorners(RoundedCorners.newBuilder().setRadius(9).setBitmask(15))
-                .build(),
-            mockAssetProvider);
-
-    assertThat(styleProvider.createWrapperView(context, maskCache, true, true))
-        .isInstanceOf(LegacyRoundedCornerWrapperView.class);
+    assertThat(wrapperView).isInstanceOf(RoundedCornerWrapperView.class);
+    assertThat(wrapperView.getOutlineProvider()).isNotEqualTo(ViewOutlineProvider.BOUNDS);
   }
 
   @Test
diff --git a/src/test/java/com/google/android/libraries/feed/piet/ui/BUILD b/src/test/java/com/google/android/libraries/feed/piet/ui/BUILD
index b5cebf4..de5e954 100644
--- a/src/test/java/com/google/android/libraries/feed/piet/ui/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/piet/ui/BUILD
@@ -19,6 +19,24 @@
 )
 
 android_local_test(
+    name = "BitmapMaskingRoundedCornerDelegateTest",
+    size = "small",
+    timeout = "moderate",
+    srcs = ["BitmapMaskingRoundedCornerDelegateTest.java"],
+    aapt_version = "aapt2",
+    manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
+    deps = [
+        "//src/main/java/com/google/android/libraries/feed/common/functional",
+        "//src/main/java/com/google/android/libraries/feed/piet/ui",
+        "//src/main/proto/search/now/ui/piet:piet_java_proto_lite",
+        "//third_party:robolectric",
+        "@com_google_protobuf_javalite//:protobuf_java_lite",
+        "@maven//:org_mockito_mockito_core",
+        "@robolectric//bazel:android-all",
+    ],
+)
+
+android_local_test(
     name = "BorderDrawableTest",
     size = "small",
     timeout = "moderate",
@@ -138,7 +156,6 @@
         "//third_party:robolectric",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
         "@maven//:com_google_truth_truth",
-        "@maven//:org_mockito_mockito_core",
         "@robolectric//bazel:android-all",
     ],
 )
diff --git a/src/test/java/com/google/android/libraries/feed/piet/ui/BitmapMaskingRoundedCornerDelegateTest.java b/src/test/java/com/google/android/libraries/feed/piet/ui/BitmapMaskingRoundedCornerDelegateTest.java
new file mode 100644
index 0000000..03c72ee
--- /dev/null
+++ b/src/test/java/com/google/android/libraries/feed/piet/ui/BitmapMaskingRoundedCornerDelegateTest.java
@@ -0,0 +1,161 @@
+// Copyright 2019 The Feed Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.libraries.feed.piet.ui;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import com.google.android.libraries.feed.common.functional.Supplier;
+import com.google.android.libraries.feed.common.functional.Suppliers;
+import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache.Corner;
+import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache.RoundedCornerBitmaps;
+import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners;
+import com.google.search.now.ui.piet.StylesProto.Borders;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for the {@link BitmapMaskingRoundedCornerDelegate}. */
+@RunWith(RobolectricTestRunner.class)
+public class BitmapMaskingRoundedCornerDelegateTest {
+
+  @Mock private Canvas mockCanvas;
+  @Mock private RoundedCornerMaskCache cache;
+
+  private static final Supplier<Boolean> IS_RTL_SUPPLIER = Suppliers.of(false);
+  private RoundedCornerWrapperView roundedCornerWrapperView;
+  private Context context;
+  private static final int RADIUS = 10;
+
+  private Bitmap topLeft;
+  private Bitmap topRight;
+  private Bitmap bottomLeft;
+  private Bitmap bottomRight;
+  private RoundedCornerBitmaps bitmaps;
+
+  @Before
+  public void setUp() {
+    initMocks(this);
+    context = Robolectric.buildActivity(Activity.class).get();
+    roundedCornerWrapperView =
+        new RoundedCornerWrapperView(
+            context,
+            RoundedCorners.getDefaultInstance(),
+            cache,
+            IS_RTL_SUPPLIER,
+            0,
+            Borders.getDefaultInstance(),
+            true,
+            true);
+
+    RoundedCornerMaskCache realCache = new RoundedCornerMaskCache();
+    bitmaps = realCache.getMasks(16);
+    topLeft = realCache.getMasks(16).get(Corner.TOP_LEFT);
+    topRight = realCache.getMasks(16).get(Corner.TOP_RIGHT);
+    bottomLeft = realCache.getMasks(16).get(Corner.BOTTOM_LEFT);
+    bottomRight = realCache.getMasks(16).get(Corner.BOTTOM_RIGHT);
+
+    when(cache.getMasks(anyInt())).thenReturn(bitmaps);
+    when(cache.getMaskPaint()).thenReturn(new Paint());
+  }
+
+  @Test
+  public void maskCorners_radiusZero() {
+    BitmapMaskingRoundedCornerDelegate bitmapMaskingDelegate =
+        new BitmapMaskingRoundedCornerDelegate(
+            cache, /* bitmask= */ 15, /* isRtL= */ false, mockCanvas);
+
+    roundedCornerWrapperView.layout(0, 0, 100, 100);
+
+    bitmapMaskingDelegate.onLayout(/* radius= */ 0, /* isRtL= */ false, 100, 100);
+    bitmapMaskingDelegate.draw(roundedCornerWrapperView, new Canvas());
+
+    verify(mockCanvas, never())
+        .drawBitmap(any(Bitmap.class), anyFloat(), anyFloat(), any(Paint.class));
+  }
+
+  @Test
+  public void maskAndDrawCorners_allCorners() {
+    int all_corner_bitmask = 15;
+    boolean isRtL = false;
+    BitmapMaskingRoundedCornerDelegate bitmapMaskingDelegate =
+        new BitmapMaskingRoundedCornerDelegate(cache, all_corner_bitmask, isRtL, mockCanvas);
+
+    roundedCornerWrapperView.layout(0, 0, 100, 100);
+
+    bitmapMaskingDelegate.onLayout(RADIUS, isRtL, 100, 100);
+    bitmapMaskingDelegate.draw(roundedCornerWrapperView, new Canvas());
+
+    verify(mockCanvas).drawBitmap(eq(topLeft), anyFloat(), anyFloat(), any(Paint.class));
+    verify(mockCanvas).drawBitmap(eq(topRight), anyFloat(), anyFloat(), any(Paint.class));
+    verify(mockCanvas).drawBitmap(eq(bottomRight), anyFloat(), anyFloat(), any(Paint.class));
+    verify(mockCanvas).drawBitmap(eq(bottomLeft), anyFloat(), anyFloat(), any(Paint.class));
+  }
+
+  @Test
+  public void maskCorners_topStart_bottomEnd() {
+    int topStart_bottomEnd_bitmask = 5;
+    boolean isRtL = false;
+
+    BitmapMaskingRoundedCornerDelegate bitmapMaskingDelegate =
+        new BitmapMaskingRoundedCornerDelegate(
+            cache, topStart_bottomEnd_bitmask, isRtL, mockCanvas);
+
+    roundedCornerWrapperView.layout(0, 0, 100, 100);
+
+    bitmapMaskingDelegate.onLayout(RADIUS, isRtL, 100, 100);
+    bitmapMaskingDelegate.draw(roundedCornerWrapperView, new Canvas());
+
+    verify(mockCanvas).drawBitmap(eq(topLeft), anyFloat(), anyFloat(), any(Paint.class));
+    verify(mockCanvas, never()).drawBitmap(eq(topRight), anyFloat(), anyFloat(), any(Paint.class));
+    verify(mockCanvas).drawBitmap(eq(bottomRight), anyFloat(), anyFloat(), any(Paint.class));
+    verify(mockCanvas, never())
+        .drawBitmap(eq(bottomLeft), anyFloat(), anyFloat(), any(Paint.class));
+  }
+
+  @Test
+  public void maskCorners_topStart_bottomEnd_rtl() {
+    int topStart_bottomEnd_bitmask = 5;
+    boolean isRtL = true;
+    BitmapMaskingRoundedCornerDelegate bitmapMaskingDelegate =
+        new BitmapMaskingRoundedCornerDelegate(
+            cache, topStart_bottomEnd_bitmask, isRtL, mockCanvas);
+
+    roundedCornerWrapperView.layout(0, 0, 100, 100);
+
+    bitmapMaskingDelegate.onLayout(RADIUS, isRtL, 100, 100);
+    bitmapMaskingDelegate.draw(roundedCornerWrapperView, new Canvas());
+
+    verify(mockCanvas, never()).drawBitmap(eq(topLeft), anyFloat(), anyFloat(), any(Paint.class));
+    verify(mockCanvas).drawBitmap(eq(topRight), anyFloat(), anyFloat(), any(Paint.class));
+    verify(mockCanvas, never())
+        .drawBitmap(eq(bottomRight), anyFloat(), anyFloat(), any(Paint.class));
+    verify(mockCanvas).drawBitmap(eq(bottomLeft), anyFloat(), anyFloat(), any(Paint.class));
+  }
+}
diff --git a/src/test/java/com/google/android/libraries/feed/piet/ui/RoundedCornerWrapperViewTest.java b/src/test/java/com/google/android/libraries/feed/piet/ui/RoundedCornerWrapperViewTest.java
index 32c518e..e29e129 100644
--- a/src/test/java/com/google/android/libraries/feed/piet/ui/RoundedCornerWrapperViewTest.java
+++ b/src/test/java/com/google/android/libraries/feed/piet/ui/RoundedCornerWrapperViewTest.java
@@ -15,20 +15,15 @@
 package com.google.android.libraries.feed.piet.ui;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
 
 import android.app.Activity;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.os.Build.VERSION_CODES;
 import com.google.android.libraries.feed.common.functional.Supplier;
 import com.google.android.libraries.feed.common.functional.Suppliers;
-import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache.Corner;
-import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache.RoundedCornerBitmaps;
+import com.google.android.libraries.feed.piet.ui.RoundedCornerDelegateFactory.RoundingStrategy;
 import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners;
 import com.google.search.now.ui.piet.StylesProto.Borders;
 import org.junit.Before;
@@ -36,38 +31,28 @@
 import org.junit.runner.RunWith;
 import org.robolectric.Robolectric;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
 
 /** Tests for the {@link RoundedCornerWrapperView}. */
 @RunWith(RobolectricTestRunner.class)
 public class RoundedCornerWrapperViewTest {
 
-  private static final Supplier<Boolean> LEFT_TO_RIGHT = Suppliers.of(false);
   private static final int TOP_CORNERS_BITMASK = 3;
   private static final int LEFT_CORNERS_BITMASK = 9;
+  private static final int ALL_CORNERS_BITMASK = 15;
 
-  private Context context;
   private Canvas canvas;
   private Bitmap bitmap;
-  private RoundedCornerMaskCache maskCache;
 
   @Before
   public void setUp() {
-    context = Robolectric.buildActivity(Activity.class).get();
     bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
     canvas = new Canvas(bitmap);
-    maskCache = new RoundedCornerMaskCache();
   }
 
   @Test
-  public void testZeroDimensions_notRounded() {
-    RoundedCornerWrapperView view =
-        new BitmapMaskingRoundedCornerWrapperView(
-            context,
-            RoundedCorners.getDefaultInstance(),
-            maskCache,
-            LEFT_TO_RIGHT,
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+  public void layoutZeroDimensions_notRounded() {
+    RoundedCornerWrapperView view = CommonRoundedCornerWrapperView.getDefaultInstance();
 
     view.layout(0, 0, 0, 100);
     assertThat(view.getWidth()).isEqualTo(0);
@@ -87,17 +72,11 @@
   }
 
   @Test
-  public void testZeroDimensions_rounded() {
+  public void layoutZeroDimensions_rounded() {
     RoundedCorners roundedCorners =
         RoundedCorners.newBuilder().setBitmask(7).setRadiusDp(10).build();
     RoundedCornerWrapperView view =
-        new BitmapMaskingRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            maskCache,
-            LEFT_TO_RIGHT,
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     view.layout(0, 0, 0, 100);
     assertThat(view.getWidth()).isEqualTo(0);
@@ -117,15 +96,17 @@
   }
 
   @Test
-  public void testRoundedCorners_bordersAdded() {
+  public void roundCorners_bordersAdded() {
     // The width, height, and radius don't really matter for this test. As long as they exist, a
     // BorderDrawable should be set on the view.
     Borders borders = Borders.newBuilder().setWidth(10).build();
 
     RoundedCorners roundedCorners = RoundedCorners.newBuilder().setRadiusDp(10).build();
     RoundedCornerWrapperView view =
-        new BitmapMaskingRoundedCornerWrapperView(
-            context, roundedCorners, maskCache, Suppliers.of(false), 0, borders);
+        CommonRoundedCornerWrapperView.builder()
+            .setRoundedCorners(roundedCorners)
+            .setBorders(borders)
+            .build();
 
     view.layout(0, 0, 100, 100);
     view.draw(canvas);
@@ -137,13 +118,15 @@
   }
 
   @Test
-  public void testRoundedCorners_bordersWidthZero() {
+  public void setRoundedCorners_bordersWidthZero() {
     Borders borders = Borders.newBuilder().setWidth(0).build();
 
     RoundedCorners roundedCorners = RoundedCorners.newBuilder().setRadiusDp(10).build();
     RoundedCornerWrapperView view =
-        new BitmapMaskingRoundedCornerWrapperView(
-            context, roundedCorners, maskCache, Suppliers.of(false), 0, borders);
+        CommonRoundedCornerWrapperView.builder()
+            .setRoundedCorners(roundedCorners)
+            .setBorders(borders)
+            .build();
 
     // Set a width and height on the view so that the only thing stopping borders from being created
     // is the border width of 0.
@@ -155,19 +138,17 @@
   }
 
   @Test
-  public void testGetRadius_usesOverride() {
+  public void getRadius_usesOverride() {
     int viewWidth = 100;
     int viewHeight = 100;
     int radiusToSet = 10;
     int radiusOverride = 20;
     RoundedCorners roundedCorners = RoundedCorners.newBuilder().setRadiusDp(radiusToSet).build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            radiusOverride,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder()
+            .setRoundedCorners(roundedCorners)
+            .setRadiusOverride(radiusOverride)
+            .build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -176,18 +157,13 @@
   }
 
   @Test
-  public void testGetRadius_radiusDp() {
+  public void getRadius_radiusDp() {
     int viewWidth = 100;
     int viewHeight = 100;
     int radiusToSet = 10;
     RoundedCorners roundedCorners = RoundedCorners.newBuilder().setRadiusDp(radiusToSet).build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -195,19 +171,14 @@
   }
 
   @Test
-  public void testGetRadius_radiusPercentageOfHeight() {
+  public void getRadius_radiusPercentageOfHeight() {
     int viewWidth = 60;
     int viewHeight = 100;
     int radiusPercentageOfHeight = 25;
     RoundedCorners roundedCorners =
         RoundedCorners.newBuilder().setRadiusPercentageOfHeight(radiusPercentageOfHeight).build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -216,19 +187,14 @@
   }
 
   @Test
-  public void testGetRadius_radiusPercentageOfWidth() {
+  public void getRadius_radiusPercentageOfWidth() {
     int viewWidth = 60;
     int viewHeight = 100;
     int radiusPercentageOfWidth = 25;
     RoundedCorners roundedCorners =
         RoundedCorners.newBuilder().setRadiusPercentageOfWidth(radiusPercentageOfWidth).build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -237,18 +203,13 @@
   }
 
   @Test
-  public void testGetRadius_smallWidth_RadiusDpShouldAdjust() {
+  public void getRadius_smallWidth_RadiusDpShouldAdjust() {
     int viewWidth = 30;
     int viewHeight = 40;
     int radiusToSet = 20;
     RoundedCorners roundedCorners = RoundedCorners.newBuilder().setRadiusDp(radiusToSet).build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -258,7 +219,7 @@
   }
 
   @Test
-  public void testGetRadius_smallWidth_RadiusDpShouldNotAdjust() {
+  public void getRadius_smallWidth_RadiusDpShouldNotAdjust() {
     int viewWidth = 30;
     int viewHeight = 40;
     int radiusToSet = 20;
@@ -268,12 +229,7 @@
             .setRadiusDp(radiusToSet)
             .build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -283,18 +239,13 @@
   }
 
   @Test
-  public void testGetRadius_smallHeight_RadiusDpShouldAdjust() {
+  public void getRadius_smallHeight_RadiusDpShouldAdjust() {
     int viewWidth = 40;
     int viewHeight = 30;
     int radiusToSet = 20;
     RoundedCorners roundedCorners = RoundedCorners.newBuilder().setRadiusDp(radiusToSet).build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -304,7 +255,7 @@
   }
 
   @Test
-  public void testGetRadius_smallHeight_RadiusDpShouldNotAdjust() {
+  public void getRadius_smallHeight_RadiusDpShouldNotAdjust() {
     int viewWidth = 40;
     int viewHeight = 30;
     int radiusToSet = 20;
@@ -314,12 +265,7 @@
             .setRadiusDp(radiusToSet)
             .build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -329,19 +275,14 @@
   }
 
   @Test
-  public void testGetRadius_radiusPercentageOfHeight_100Percent_shouldAdjust() {
+  public void getRadius_radiusPercentageOfHeight_100Percent_shouldAdjust() {
     int viewWidth = 60;
     int viewHeight = 100;
     int radiusPercentageOfHeight = 100;
     RoundedCorners roundedCorners =
         RoundedCorners.newBuilder().setRadiusPercentageOfHeight(radiusPercentageOfHeight).build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -350,7 +291,7 @@
   }
 
   @Test
-  public void testGetRadius_radiusPercentageOfHeight_100Percent_topCorners_shouldAdjust() {
+  public void getRadius_radiusPercentageOfHeight_100Percent_topCorners_shouldAdjust() {
     int viewWidth = 60;
     int viewHeight = 100;
     int radiusPercentageOfHeight = 100;
@@ -360,12 +301,7 @@
             .setRadiusPercentageOfHeight(radiusPercentageOfHeight)
             .build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -375,7 +311,7 @@
   }
 
   @Test
-  public void testGetRadius_radiusPercentageOfHeight_100Percent_topCorners_shouldNotAdjust() {
+  public void getRadius_radiusPercentageOfHeight_100Percent_topCorners_shouldNotAdjust() {
     int viewWidth = 100;
     int viewHeight = 40;
     int radiusPercentageOfHeight = 100;
@@ -385,12 +321,7 @@
             .setRadiusPercentageOfHeight(radiusPercentageOfHeight)
             .build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -400,19 +331,14 @@
   }
 
   @Test
-  public void testGetRadius_radiusPercentageOfWidth_100Percent_shouldAdjust() {
+  public void getRadius_radiusPercentageOfWidth_100Percent_shouldAdjust() {
     int viewWidth = 100;
     int viewHeight = 60;
     int radiusPercentageOfWidth = 100;
     RoundedCorners roundedCorners =
         RoundedCorners.newBuilder().setRadiusPercentageOfWidth(radiusPercentageOfWidth).build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -422,7 +348,7 @@
   }
 
   @Test
-  public void testGetRadius_radiusPercentageOfWidth_100Percent_leftCorners_shouldAdjust() {
+  public void getRadius_radiusPercentageOfWidth_100Percent_leftCorners_shouldAdjust() {
     int viewWidth = 100;
     int viewHeight = 60;
     int radiusPercentageOfWidth = 100;
@@ -432,12 +358,7 @@
             .setRadiusPercentageOfWidth(radiusPercentageOfWidth)
             .build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -447,7 +368,7 @@
   }
 
   @Test
-  public void testGetRadius_radiusPercentageOfWidth_100Percent_topCorners_shouldNotAdjust() {
+  public void getRadius_radiusPercentageOfWidth_100Percent_topCorners_shouldNotAdjust() {
     int viewWidth = 40;
     int viewHeight = 100;
     int radiusPercentageOfWidth = 100;
@@ -457,12 +378,7 @@
             .setRadiusPercentageOfWidth(radiusPercentageOfWidth)
             .build();
     RoundedCornerWrapperView view =
-        new CommonRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
     int calculatedRadius = view.getRadius(viewWidth, viewHeight);
 
@@ -472,42 +388,238 @@
   }
 
   @Test
-  public void testGetsMasksFromCache() {
+  public void useMaskingStrategy_notAllCornersRounded() {
+    int radius = 16;
     RoundedCorners roundedCorners =
-        RoundedCorners.newBuilder().setBitmask(LEFT_CORNERS_BITMASK).setRadiusDp(16).build();
-    RoundedCornerMaskCache mockMaskCache = mock(RoundedCornerMaskCache.class);
-    RoundedCornerBitmaps mockBitmaps = mock(RoundedCornerBitmaps.class);
-    Bitmap maskBitmap = maskCache.getMasks(16).get(Corner.TOP_LEFT);
-    when(mockMaskCache.getPaint()).thenReturn(maskCache.getPaint());
-    when(mockMaskCache.getMaskPaint()).thenReturn(maskCache.getMaskPaint());
-    when(mockMaskCache.getMasks(16)).thenReturn(mockBitmaps);
-    when(mockBitmaps.get(anyInt())).thenReturn(maskBitmap);
-    RoundedCornerWrapperView view =
-        new BitmapMaskingRoundedCornerWrapperView(
-            context,
-            roundedCorners,
-            mockMaskCache,
-            Suppliers.of(false),
-            /*radiusOverride= */ 0,
-            Borders.getDefaultInstance());
+        RoundedCorners.newBuilder().setBitmask(LEFT_CORNERS_BITMASK).setRadiusDp(radius).build();
+    CommonRoundedCornerWrapperView view =
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
 
-    view.layout(0, 0, 100, 100);
+    // When not all corners are rounded, the outline provider strategy doesn't work, so it should
+    // use bitmap masking
+    assertThat(view.getRoundingStrategy()).isEqualTo(RoundingStrategy.BITMAP_MASKING);
+  }
 
-    verify(mockMaskCache).getMasks(16);
+  @Test
+  public void useOutlineProviderStrategy_hardwareAcceleration() {
+    int width = 100;
+    int height = 100;
+    int radius = 16;
+    RoundedCorners roundedCorners =
+        RoundedCorners.newBuilder().setBitmask(ALL_CORNERS_BITMASK).setRadiusDp(radius).build();
+    CommonRoundedCornerWrapperView view =
+        CommonRoundedCornerWrapperView.builder()
+            .setRoundedCorners(roundedCorners)
+            .setHardwareAccelerated(true)
+            .build();
 
-    verify(mockBitmaps).get(Corner.TOP_LEFT);
-    verify(mockBitmaps).get(Corner.BOTTOM_LEFT);
-    verifyNoMoreInteractions(mockBitmaps);
+    // Lay out the view, because that's when the view would know if it's hardware accelerated.
+    view.layout(0, 0, width, height);
+
+    assertThat(view.getClipToOutline()).isTrue();
+    assertThat(view.getClipChildren()).isTrue();
+    assertThat(view.getRoundingStrategy()).isEqualTo(RoundingStrategy.OUTLINE_PROVIDER);
+  }
+
+  @Config(sdk = VERSION_CODES.KITKAT)
+  @Test
+  public void useClipPathStrategy_kitkat() {
+    int radius = 16;
+    RoundedCorners roundedCorners =
+        RoundedCorners.newBuilder().setBitmask(ALL_CORNERS_BITMASK).setRadiusDp(radius).build();
+    CommonRoundedCornerWrapperView view =
+        CommonRoundedCornerWrapperView.builder().setRoundedCorners(roundedCorners).build();
+
+    assertThat(view.getRoundingStrategy()).isEqualTo(RoundingStrategy.CLIP_PATH);
+  }
+
+  @Config(sdk = VERSION_CODES.KITKAT)
+  @Test
+  public void useMaskingStrategy_kitkat_noClipPathAllowed() {
+    int radius = 16;
+    RoundedCorners roundedCorners =
+        RoundedCorners.newBuilder().setBitmask(ALL_CORNERS_BITMASK).setRadiusDp(radius).build();
+    CommonRoundedCornerWrapperView view =
+        CommonRoundedCornerWrapperView.builder()
+            .setRoundedCorners(roundedCorners)
+            .setAllowClipPath(false)
+            .build();
+
+    assertThat(view.getRoundingStrategy()).isEqualTo(RoundingStrategy.BITMAP_MASKING);
+  }
+
+  @Test
+  public void useClipPathStrategy_noHardwareAcceleration() {
+    int width = 100;
+    int height = 100;
+    int radius = 16;
+    RoundedCorners roundedCorners =
+        RoundedCorners.newBuilder().setBitmask(ALL_CORNERS_BITMASK).setRadiusDp(radius).build();
+    CommonRoundedCornerWrapperView view =
+        CommonRoundedCornerWrapperView.builder()
+            .setRoundedCorners(roundedCorners)
+            .setHardwareAccelerated(false)
+            .build();
+
+    view.layout(0, 0, width, height);
+
+    assertThat(view.getClipToOutline()).isFalse();
+    assertThat(view.getClipChildren()).isFalse();
+
+    assertThat(view.getRoundingStrategy()).isEqualTo(RoundingStrategy.CLIP_PATH);
+  }
+
+  @Test
+  public void useMaskingStrategy_noHardwareAcceleration_noClipPathAllowed() {
+    int width = 100;
+    int height = 100;
+    int radius = 16;
+    RoundedCorners roundedCorners =
+        RoundedCorners.newBuilder().setBitmask(ALL_CORNERS_BITMASK).setRadiusDp(radius).build();
+    CommonRoundedCornerWrapperView view =
+        CommonRoundedCornerWrapperView.builder()
+            .setRoundedCorners(roundedCorners)
+            .setHardwareAccelerated(false)
+            .setAllowClipPath(false)
+            .build();
+
+    view.layout(0, 0, width, height);
+
+    assertThat(view.getRoundingStrategy()).isEqualTo(RoundingStrategy.BITMAP_MASKING);
+  }
+
+  @Test
+  public void useMaskingStrategy_withHwAcceleration_noClipPathAllowed_noOutlineProviderAllowed() {
+    int width = 100;
+    int height = 100;
+    int radius = 16;
+    RoundedCorners roundedCorners =
+        RoundedCorners.newBuilder().setBitmask(ALL_CORNERS_BITMASK).setRadiusDp(radius).build();
+    CommonRoundedCornerWrapperView view =
+        CommonRoundedCornerWrapperView.builder()
+            .setRoundedCorners(roundedCorners)
+            .setHardwareAccelerated(true)
+            .setAllowClipPath(false)
+            .setAllowOutlineRounding(false)
+            .build();
+
+    view.layout(0, 0, width, height);
+
+    assertThat(view.getRoundingStrategy()).isEqualTo(RoundingStrategy.BITMAP_MASKING);
+  }
+
+  @Test
+  public void allowAllRoundingStrategies_shouldChooseOutlineProvider() {
+    int width = 100;
+    int height = 100;
+    int radius = 16;
+    RoundedCorners roundedCorners =
+        RoundedCorners.newBuilder().setBitmask(ALL_CORNERS_BITMASK).setRadiusDp(radius).build();
+    CommonRoundedCornerWrapperView view =
+        CommonRoundedCornerWrapperView.builder()
+            .setRoundedCorners(roundedCorners)
+            .setHardwareAccelerated(true)
+            .setAllowClipPath(true)
+            .setAllowOutlineRounding(true)
+            .build();
+
+    view.layout(0, 0, width, height);
+
+    assertThat(view.getClipToOutline()).isTrue();
+    assertThat(view.getClipChildren()).isTrue();
+
+    assertThat(view.getRoundingStrategy()).isEqualTo(RoundingStrategy.OUTLINE_PROVIDER);
   }
 
   static class CommonRoundedCornerWrapperView extends RoundedCornerWrapperView {
-    CommonRoundedCornerWrapperView(
-        Context context,
-        RoundedCorners roundedCorners,
-        Supplier<Boolean> isRtLSupplier,
-        int radiusOverride,
-        Borders borders) {
-      super(context, roundedCorners, isRtLSupplier, radiusOverride, borders);
+    private static final Supplier<Boolean> IS_RTL_SUPPLIER = Suppliers.of(false);
+    private static final Context CONTEXT = Robolectric.buildActivity(Activity.class).get();
+    private final boolean hardwareAccelerated;
+    private RoundingStrategy roundingStrategy;
+
+    @Override
+    public boolean isHardwareAccelerated() {
+      return hardwareAccelerated;
+    }
+
+    @Override
+    public boolean isAttachedToWindow() {
+      return true;
+    }
+
+    @Override
+    void setRoundingDelegate(RoundingStrategy roundingStrategy) {
+      super.setRoundingDelegate(roundingStrategy);
+      this.roundingStrategy = roundingStrategy;
+    }
+
+    RoundingStrategy getRoundingStrategy() {
+      return roundingStrategy;
+    }
+
+    private CommonRoundedCornerWrapperView(Builder builder) {
+      super(
+          CONTEXT,
+          builder.roundedCorners,
+          new RoundedCornerMaskCache(),
+          IS_RTL_SUPPLIER,
+          builder.radiusOverride,
+          builder.borders,
+          builder.allowClipPath,
+          builder.allowOutlineRounding);
+      this.hardwareAccelerated = builder.hardwareAccelerated;
+    }
+
+    static CommonRoundedCornerWrapperView getDefaultInstance() {
+      return new CommonRoundedCornerWrapperView(new CommonRoundedCornerWrapperView.Builder());
+    }
+
+    static Builder builder() {
+      return new CommonRoundedCornerWrapperView.Builder();
+    }
+
+    static class Builder {
+      private boolean hardwareAccelerated = false;
+
+      RoundedCorners roundedCorners = RoundedCorners.getDefaultInstance();
+      int radiusOverride = 0;
+      Borders borders = Borders.getDefaultInstance();
+      boolean allowClipPath = true;
+      boolean allowOutlineRounding = true;
+
+      Builder setHardwareAccelerated(boolean value) {
+        hardwareAccelerated = value;
+        return this;
+      }
+
+      Builder setRoundedCorners(RoundedCorners value) {
+        roundedCorners = value;
+        return this;
+      }
+
+      Builder setRadiusOverride(int value) {
+        radiusOverride = value;
+        return this;
+      }
+
+      Builder setBorders(Borders value) {
+        borders = value;
+        return this;
+      }
+
+      Builder setAllowClipPath(boolean value) {
+        allowClipPath = value;
+        return this;
+      }
+
+      Builder setAllowOutlineRounding(boolean value) {
+        allowOutlineRounding = value;
+        return this;
+      }
+
+      CommonRoundedCornerWrapperView build() {
+        return new CommonRoundedCornerWrapperView(this);
+      }
     }
   }
 }