自定义布局:圆角化其内容

18

我想创建一个通用的ViewGroup,可以在XML布局中重复使用,以使放入其中的任何内容都可以圆角化。

不知为何canvas.clipPath()似乎没有效果。 我做错了什么吗?

这是Java代码:

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

在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>
7个回答

55
在编写代码时,如果想要创建一个可裁剪其子项的ViewGroup,则应该在dispatchDraw(Canvas)方法中实现。以下是一个示例,演示如何使用圆形对ViewGroup的任何子项进行裁剪:
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);
}

dispatchDraw方法用于裁剪子视图,如果您的布局只需要裁剪其子视图,则无需使用setWillNotDraw(false)

上面的代码获取了此图像,我只是扩展了Facebook的ProfilePictureView(它是一个包含带有Facebook个人资料图片的正方形ImageViewFrameLayout):

circle clipping

要实现圆角边框,您需要像这样做:

@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

你实际上可以创建任何复杂的路径 :)

记住,您可以多次调用clipPath并使用所需的“Op”操作来相交多个剪辑,以您喜欢的方式进行剪辑。

注意:我在onSizeChanged中创建了路径,因为在onDraw中这样做会影响性能。

注意2:对路径进行剪辑时没有抗锯齿:/,因此如果您想要平滑的边框,则需要以其他方式进行剪辑。我不知道现在有任何使剪辑使用抗锯齿的方法。

更新(轮廓)

自Android Lollipop(API 21)以来,可以将高程和阴影应用于视图。引入了一个名为Outline的新概念。这是一条路径,告诉框架要使用的视图形状来计算阴影和其他东西(如涟漪效果)。

默认情况下,视图的轮廓是与视图大小相同的矩形,但可以轻松地制成椭圆形/圆形或圆角矩形。要定义自定义轮廓,请在视图上使用方法 setOutlineProvider(),如果它是自定义视图,则可能需要在构造函数中设置它,并将您的自定义 ViewOutlineProvider 定义为自定义视图的内部类。您可以使用您选择的Path定义自己的Outline提供程序,只要它是凸路径(数学概念,意味着没有凹陷和孔洞的封闭路径,例如,星形或齿轮形状都不是凸形)。
你也可以使用方法 setClipToOutline(true) 来使轮廓也被裁剪(我认为这也适用于抗锯齿,有人可以在评论中确认/否认吗?),但这仅适用于非Path轮廓。

祝好运


1
谢谢!我已经不再是Android开发者了,这个问题已经有3年了,但这个解决方案看起来是正确的。Stackoverflow终于交出了答案 :) - ՕլՁՅԿ
很抱歉,您不再是一名Android开发者 :) 但我很好奇,为什么您将其他答案标记为“接受答案”,即使它并不正确? :) - Daniele Segato
我记得我根据那个答案设法让它工作了,但并不优雅。 - ՕլՁՅԿ
当我这样做时,没有出现高程或水波纹效果。我认为您需要定义一个自定义轮廓。 - Daniele Segato
2
@DanieleSegato setOutlineProvider()setClipToOutline(true) 确实应用了抗锯齿,我已经检查过了。 - ilyamuromets
显示剩余4条评论

23

你可以覆盖 draw(Canvas canvas) 方法:



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


1
哇,为什么这个答案被踩了。这是唯一对我有效的解决方案。如果可以的话,我会点赞两次。 - iamruskie
准确的,完美的答案 - Muhammad Umar
1
完美的答案,除了我的绘制方法没有被调用,除非我设置setWillNotDraw(false)。 - swooby
另一个答案不起作用,但这个可以。为什么这个没有被选为最佳答案?无论如何,谢谢。 - Iman Akbari
惊人的答案。完美运作。+1 - Gaurav Saluja

3

如果您的布局(Layout)没有设置背景,那么FrameLayout的onDraw方法不会被调用;

您应该重写dispatchDraw方法;


2
不要忘记使用setWillNotDraw(false)来调用onDraw,并设置mRadius的值,然后只需像这样做:
@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);
}

2

ViewGroup(及其子类)默认情况下设置一个标志,表明它不会进行任何绘制。在源代码中,它看起来有点像这样:

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

所以你的onDraw(...)可能根本没有被执行。如果你想进行任何手动绘制,请调用setWillNotDraw(false)

onDraw()确实被调用了(我进行了调试),只是像super.onDraw()重置画布的剪切区域和变换矩阵之类的操作。但如果这种方式不正确,那么正确的方式是什么? - ՕլՁՅԿ
willNotDraw与剪辑无关,他的问题在于他正在onDraw()方法中执行这些操作。 - Daniele Segato

-2
你需要重写drawChild()方法来裁剪子视图。
@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;
}

如果您想要剪裁ViewGroup的背景,可以重写draw()方法,像这样:
@Override
public void draw(Canvas canvas) {
    int flag = canvas.save();
    canvas.clipPath(pathToClip);
    super.draw(canvas);
    canvas.restoreToCount(flag);
}

-3
为什么不直接将ShapeDrawable定义为布局的背景,并在运行时更改可绘制对象的角半径呢?

2
那不会剪裁布局中包含的其他任何东西的角落,对吧? - ՕլՁՅԿ
不,虽然您可以设置填充以确保它始终具有圆角,但我想这不是您想要的。我认为问题不在于canvas.clipPath,而是因为它用于布局。不确定什么是正确的方式,甚至是否存在一种方式。 - Neil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接