18

I would like to create a generic ViewGroup which can then be reused in XML layouts to round the corners of anything that is put into it.

For some reason canvas.clipPath() doesn't seem to have an effect. What am I doing wrong?

Here is the Java code:

package rounded;

import static android.graphics.Path.Direction.CCW;
public class RoundedView extends FrameLayout {
    private float radius;
    private Path path = new Path();
    private RectF rect = new RectF();

    public RoundedView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.radius = attrs.getAttributeFloatValue(null, "corner_radius", 0f);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int savedState = canvas.save();
        float w = getWidth();
        float h = getHeight();
        path.reset();
        rect.set(0, 0, w, h);
        path.addRoundRect(rect, radius, radius, CCW);
        path.close();
        boolean debug = canvas.clipPath(path);
        super.onDraw(canvas);
        canvas.restoreToCount(savedState);
    }
}

Usage in XML:

<?xml version="1.0" encoding="utf-8"?>
<rounded.RoundedView   xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" 
    corner_radius="40.0" >
    <RelativeLayout 
        android:id="@+id/RelativeLayout1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        ...
    </RelativeLayout>
</rounded.RoundedView>
P Varga
  • 19,174
  • 12
  • 70
  • 108

7 Answers7

55

The right way to create a ViewGroup that clip its children is to do it in the dispatchDraw(Canvas) method.

This is an example on how you can clip any children of a ViewGroup with a circle:

private Path path = new Path();

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    // compute the path
    float halfWidth = w / 2f;
    float halfHeight = h / 2f;
    float centerX = halfWidth;
    float centerY = halfHeight;
    path.reset();
    path.addCircle(centerX, centerY, Math.min(halfWidth, halfHeight), Path.Direction.CW);
    path.close();

}

@Override
protected void dispatchDraw(Canvas canvas) {
    int save = canvas.save();
    canvas.clipPath(circlePath);
    super.dispatchDraw(canvas);
    canvas.restoreToCount(save);
}

the dispatchDraw method is the one called to clip children. No need to setWillNotDraw(false) if your layout just clip its children.

This image is obtained with the code above, I just extended Facebook ProfilePictureView (which is a FrameLayout including a square ImageView with the facebook profile picture):

circle clipping

So to achieve a round border you do something like this:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    // compute the path
    path.reset();
    rect.set(0, 0, w, h);
    path.addRoundRect(rect, radius, radius, Path.Direction.CW);
    path.close();

}

@Override
protected void dispatchDraw(Canvas canvas) {
    int save = canvas.save();
    canvas.clipPath(path);
    super.dispatchDraw(canvas);
    canvas.restoreToCount(save);
}

round border clipping

You can actually create any complex path :)

Remember you can call clipPath multiple times with the "Op" operation you please to intersect multiple clipping in the way you like.

NOTE: I created the Path in the onSizeChanged because doing so in the onDraw is bad for performance.

NOTE2: clipping a Path is done without anti-aliasing :/ so if you want smooth borders you'll need to do it in some other way. I'm not aware of any way of making clipping use anti-aliasing right now.

UPDATE (Outline)

Since Android Lollipop (API 21) elevation and shadows can be applied to views. A new concept called Outline has been introduced. This is a path that tells the framework the shape of the view to be used to compute the shadow and other things (like ripple effects).

The default Outline of the view is a rectangular of the size of the view but can be easily made an oval/circle or a rounded rectangular. To define a custom Outline you have to use the method setOutlineProvider() on the view, if it's a custom View you may want to set it in the constructor with your custom ViewOutlineProvider defined as inner class of your custom View. You can define your own Outline provider using a Path of your choice, as long as it is a convex path (mathematical concept meaning a closed path with no recess and no holes, as an example neither a star shape nor a gear shape are convex).

You can also use the method setClipToOutline(true) to make the Outline also clip (and I think this also works with anti-aliasing, can someone confirm/refute in comments?), but this is only supported for non-Path Outline.

Good luck

Daniele Segato
  • 12,314
  • 6
  • 62
  • 88
  • 1
    Thanks! I'm no longer an Android developer, this question is 3 years old, but this solution looks like the correct one. Stackoverflow delivers at last :) – P Varga Sep 24 '15 at 17:51
  • I'm sorry you are no longer an Android Developer :) I'm curious tough. Why you marked the other answer as accepted answer if it wasn't right? :) – Daniele Segato Sep 25 '15 at 11:48
  • IIRC I got it working somehow based on that answer, but it wasn't elegant – P Varga Sep 25 '15 at 11:52
  • Your solution work well to me, but when I apply ripple effect into the ViewGroup (foreground of FrameLayout), it doesn't hide the effect's background outside the path. Any idea for this, @DanieleSegato? – Tuan Chau Dec 20 '16 at 08:00
  • when I did this there was no elevation or Ripple effect. I think you need to define a custom Outline. – Daniele Segato Dec 20 '16 at 08:08
  • Look [here](https://developer.android.com/reference/android/graphics/Outline.html) and [here](https://developer.android.com/training/material/shadows-clipping.html#Shadows) for how to use Outlines – Daniele Segato Dec 20 '16 at 08:21
  • 2
    @DanieleSegato `setOutlineProvider()` and `setClipToOutline(true)` really apply anti-aliasing, I checked. – ilyamuromets Nov 15 '18 at 07:26
  • I think it's worth knowing that `setClipToOutline(true)` can cause massive performance hits - I used it on items in a RecyclerView, and the scrolling was incredibly laggy. Using the custom `ViewGroup` solution does not suffer from this issue, and performance is great – Luke Needham Jan 22 '20 at 12:37
  • Luke, you need to enable hardware acceleration for it to work performantly i think. But I never used it on a scrolling list so not sure – Daniele Segato Jan 24 '20 at 21:04
23

You can override the draw(Canvas canvas) method:



    public class RoundedLinearLayout extends LinearLayout {
        Path mPath;
        float mCornerRadius;

        public RoundedLinearLayout(Context context) {
            super(context);
        }

        public RoundedLinearLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        public RoundedLinearLayout(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }

        @Override
        public void draw(Canvas canvas) {
            canvas.save();
            canvas.clipPath(mPath);
            super.draw(canvas);
            canvas.restore();
        }

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            RectF r = new RectF(0, 0, w, h);
            mPath = new Path();
            mPath.addRoundRect(r, mCornerRadius, mCornerRadius, Direction.CW);
            mPath.close();
        }

        public void setCornerRadius(int radius) {
            mCornerRadius = radius;
            invalidate();
        }
      }

Danil Ternovikh
  • 352
  • 2
  • 4
3

onDraw of FrameLayout is not called if Ur Layout's background not set;

You should override dispatchDraw;

xihua
  • 147
  • 1
  • 5
2

ViewGroup (and hence its subclasses) sets a flag indicating that it doesn't do any drawing by default. In source it looks somewhat like this:

// ViewGroup doesn't draw by default
if (!debugDraw()) {
    setFlags(WILL_NOT_DRAW, DRAW_MASK);
}

So your onDraw(...) probably doesn't get hit at all right now. If you want to do any manual drawing, call setWillNotDraw(false).

MH.
  • 45,303
  • 10
  • 103
  • 116
  • `onDraw()` does get hit (I debugged), it is just something like `super.onDraw()` resetting the canvas's clip regions and tranformation matrix, or something... But if this is the wrong approach, what would be the right one? – P Varga Nov 22 '12 at 21:16
  • willNotDraw has nothing to do with clipping, the problem he has is that he is doing that stuff in the onDraw() method. – Daniele Segato Sep 24 '15 at 15:44
2

Don't forget to ask for onDraw to be called with setWillNotDraw(false); and set a value to mRadius then just do something like that :

@Override
protected void onDraw(Canvas canvas) {
    mPath.reset();
    mRect.set(0, 0, canvas.getWidth(), canvas.getHeight());
    mPath.addRoundRect(mRect, mRadius, mRadius, Direction.CCW);
    mPath.close();
    canvas.clipPath(mPath);
}
gleroyDroid
  • 451
  • 6
  • 13
-2

U need to override the drawChild() method to clip childViews.

@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    int flag = canvas.save();
    canvas.clipPath(pathToClip);
    boolean result=super.drawChild(canvas, child, drawingTime);
    canvas.restoreToCount(flag);
    return result;
}

If u want to clip the background of the ViewGroup too,override draw() instead.like this

@Override
public void draw(Canvas canvas) {
    int flag = canvas.save();
    canvas.clipPath(pathToClip);
    super.draw(canvas);
    canvas.restoreToCount(flag);
}
-3

Why not just define a ShapeDrawable as the background of your layout and just change the corner radius of the drawable at runtime.

Neil
  • 448
  • 2
  • 8
  • 2
    That wouldn't clip the corners of anything else that is contained within the layout... would it? – P Varga Nov 22 '12 at 17:23
  • No, although you can set a padding to make sure it always has the rounded corner, I guess that's not what you want.I think the problem is not on canvas.clipPath but because it's used on a layout. Not sure what's the right way or even if there is one. – Neil Nov 23 '12 at 00:20