10
votes

I'm creating a ScrollView with a FrameLayout inside. I want to design it so that only the top corners are rounded on the ScrollView. I've created a drawable shape as follows

<shape>
    <solid android:color="@color/white"/>
    <corners
        android:bottomLeftRadius="0dp"
        android:bottomRightRadius="0dp"
        android:topLeftRadius="16dp"
        android:topRightRadius="16dp"/>
    <padding android:padding="0dp"/>
</shape>

I've then set the following on the ScrollView

 scrollView.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
 scrollView.setClipToOutline(true);

When i try scrolling, the elements in my FrameLayout end up protruding through the outline of my scrollview

Excuse the drawing, but what i'm looking to achieve

However if i instead create a shape like so

<shape>
    <solid android:color="@color/white"/>
    <corners
        android:radius="16dp"/>
    <padding android:padding="0dp"/>
</shape> 

It clips it just fine.

So how would i clip it if i only want the top to be cornered.

3

3 Answers

17
votes

I've managed to get this working by creating a custom ViewOutlineProvider and using that instead of a background value

ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() {
    @Override
    public void getOutline(final View view, final Outline outline) {
        float cornerRadiusDP = 16f;
        float cornerRadius = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, cornerRadiusDP, getResources().getDisplayMetrics());
            outline.setRoundRect(0, 0, view.getWidth(), (int)(view.getHeight() + cornerRadius), cornerRadius);
        }
};
scrollView.setOutlineProvider(mViewOutlineProvider);
scrollView.setClipToOutline(true);
15
votes

This is Kotlin variation of @Jankers answer, and answer to @Tony's question also. Explains how you can place only one rounded corner or two adjacent ones.

  fun setCorners() {
        
        val outlineProvider = object : ViewOutlineProvider() {
            override fun getOutline(view: View, outline: Outline) {

                val left = 0
                val top = 0;
                val right = view.width
                val bottom = view.height
                val cornerRadiusDP = 16f
                val cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, cornerRadiusDP, resources.displayMetrics).toInt()

                // all corners
                outline.setRoundRect(left, top, right, bottom, cornerRadius.toFloat())

                /* top corners
                outline.setRoundRect(left, top, right, bottom+cornerRadius, cornerRadius.toFloat())*/

                /* bottom corners
                outline.setRoundRect(left, top - cornerRadius, right, bottom, cornerRadius.toFloat())*/

                /* left corners
                outline.setRoundRect(left, top, right + cornerRadius, bottom, cornerRadius.toFloat())*/

                /* right corners
                outline.setRoundRect(left - cornerRadius, top, right, bottom, cornerRadius.toFloat())*/

                /* top left corner
                outline.setRoundRect(left , top, right+ cornerRadius, bottom + cornerRadius, cornerRadius.toFloat())*/

                /* top right corner
                outline.setRoundRect(left - cornerRadius , top, right, bottom + cornerRadius, cornerRadius.toFloat())*/

                /* bottom left corner
                outline.setRoundRect(left, top - cornerRadius, right + cornerRadius, bottom, cornerRadius.toFloat())*/

                /* bottom right corner
                outline.setRoundRect(left - cornerRadius, top - cornerRadius, right, bottom, cornerRadius.toFloat())*/

            }
        }

        myView.outlineProvider = outlineProvider
        myView.clipToOutline = true

    }
1
votes

I took the solution from @Jankers and @Dragan and condensed it, and added data binding so that it can be done from xml.

NOTE: Clipping with outline does not support corners to be different sizes!

OutlineProviders:

class RoundedCornersOutlineProvider(
    val radius: Float? = null,
    val topLeft: Float? = null,
    val topRight: Float? = null,
    val bottomLeft: Float? = null,
    val bottomRight: Float? = null,
) : ViewOutlineProvider() {

    private val topCorners = topLeft != null && topLeft == topRight
    private val rightCorners = topRight != null && topRight == bottomRight
    private val bottomCorners = bottomLeft != null && bottomLeft == bottomRight
    private val leftCorners = topLeft != null && topLeft == bottomLeft
    private val topLeftCorner = topLeft != null
    private val topRightCorner = topRight != null
    private val bottomRightCorner = bottomRight != null
    private val bottomLeftCorner = bottomLeft != null

    override fun getOutline(view: View, outline: Outline) {
        val left = 0
        val top = 0
        val right = view.width
        val bottom = view.height

        if (radius != null) {
            val cornerRadius = radius //.typedValue(resources).toFloat()
            outline.setRoundRect(left, top, right, bottom, cornerRadius)
        } else {
            val cornerRadius = topLeft ?: topRight ?: bottomLeft ?: bottomRight ?: 0F

            when {
                topCorners -> outline.setRoundRect(left, top, right, bottom + cornerRadius.toInt(), cornerRadius)
                bottomCorners -> outline.setRoundRect(left, top - cornerRadius.toInt(), right, bottom, cornerRadius)
                leftCorners -> outline.setRoundRect(left, top, right + cornerRadius.toInt(), bottom, cornerRadius)
                rightCorners -> outline.setRoundRect(left - cornerRadius.toInt(), top, right, bottom, cornerRadius)
                topLeftCorner -> outline.setRoundRect(
                    left, top, right + cornerRadius.toInt(), bottom + cornerRadius.toInt(), cornerRadius
                )
                bottomLeftCorner -> outline.setRoundRect(
                    left, top - cornerRadius.toInt(), right + cornerRadius.toInt(), bottom, cornerRadius
                )
                topRightCorner -> outline.setRoundRect(
                    left - cornerRadius.toInt(), top, right, bottom + cornerRadius.toInt(), cornerRadius
                )
                bottomRightCorner -> outline.setRoundRect(
                    left - cornerRadius.toInt(), top - cornerRadius.toInt(), right, bottom, cornerRadius
                )
            }
        }
    }
}

class CircleOutlineProvider : ViewOutlineProvider() {
    override fun getOutline(view: View, outline: Outline) {
        val size = view.run { min(width, height) }
        outline.setRoundRect(0, 0, size, size, (size).toFloat())
    }
}

Data Binding (@BindingAdapter):

@BindingAdapter("clipCircle")
fun View.bindClipCircle(clipCircle: Boolean?) {
    outlineProvider = CircleOutlineProvider()
    clipToOutline = true
}

@BindingAdapter("clipRadius", "clipTopLeft", "clipTopRight", "clipBottomLeft", "clipBottomRight", requireAll = false)
fun View.bindClipCorners(radius: Float?, topLeft: Float?, topRight: Float?, bottomLeft: Float?, bottomRight: Float?) {
    this.outlineProvider = RoundedCornersOutlineProvider(radius, topLeft, topRight, bottomLeft, bottomRight)
    this.clipToOutline = true
}

Clipping in xml

<androidx.constraintlayout.widget.ConstraintLayout
    clipCircle="@{@bool/const_true}"
    ...

<ImageView
    clipBottomLeft="@{@dimen/green_tab_corner_radius}"
    clipBottomRight="@{@dimen/green_tab_corner_radius}"
    ...