18

I have a custom view which displays a star shape by using a path. This view works as expected, but now I want to shift it's implementation to the new Google Material recommendation.

Unfortunately elevation depends on a convex outline, and I haven't found a solution yet.

Are there any known workarounds or any other creative solution that any of you know?

enter image description here

This is my concave path:

    double outerSize = w / 2;
    double innerSize = w / 5;
    double delta = 2.0*Math.PI/5.0;
    double rotation = Math.toRadians(-90);
    double xpos = w/2.0;
    double ypos = h/2.0;
    mPath = new Path();

    mPath.moveTo((float)(outerSize * Math.cos(delta + rotation) + xpos),
                 (float)(outerSize * Math.sin(delta + rotation) + ypos));

    for(int point= 0;point<6;point++)
    {
        mPath.lineTo((float) (innerSize * Math.cos(delta * (point + 0.5) + rotation) + xpos),
                (float) (innerSize * Math.sin(delta * (point + 0.5) + rotation) + ypos));
        mPath.lineTo((float) (outerSize * Math.cos(delta * (point + 1.0) + rotation) + xpos),
                (float) (outerSize * Math.sin(delta * (point + 1.0) + rotation) + ypos));
    }

    mPath.close();

I've tried this code, without success, which works fine on convex views.

@TargetApi(21)
private class StarOutline extends ViewOutlineProvider {

    @Override
    public void getOutline(View view, Outline outline) {
        StartView r = (StartView) view;
        // i know here say setConvexPath not setConcavePath
        outline.setConvexPath(r.mPath); 
    }
}

But as expected, I'm getting an exception:

java.lang.IllegalArgumentException: path must be convex
        at android.graphics.Outline.setConvexPath(Outline.java:216)

Any idea how to achieve this aim?

Jantzilla
  • 600
  • 1
  • 6
  • 19
rnrneverdies
  • 13,347
  • 9
  • 57
  • 89
  • Concave outlines aren't supported. You can either use a pre-generated shadow (e.g. PNG or bitmap) or use a convex approximation (e.g. a circle). – alanv Dec 22 '14 at 17:15
  • @alanv yes, aren't supported by default but, is it imposible to achieve? – rnrneverdies Dec 22 '14 at 17:20
  • 1
    The framework code responsible for generating the ambient/spot shadows is incapable of working with non-convex outlines. You won't be able to use elevation/translationZ with a non-convex outline. Using multiple convex outlines will give you overlapping shadows. As I mentioned, there are workarounds that don't involve a non-convex outline or framework-generated shadows. – alanv Dec 22 '14 at 17:38
  • @alanv another possibility is draw (using `onDraw`) the shape with a shadow... – Ixx Jul 21 '17 at 20:58

2 Answers2

5

As some of the comments and answer point out, native android shadow only works with convex outlines.

So you are left with either drawing a fake shadow manually on your own (via canvas, bitmap etc) or rely on someone else's library to draw fake shadow for you (Google's Material Components library etc).

Are there any known workarounds or any other creative solution that any of you know?

If you must rely on native android shadow, you can try to break down the shape into multiple convex shape and draw these individually.

Here is an example:

I broke down the star shape into 1 pentagon and 5 triangle polygons (all of them have convex outline) and draw them individually.

TriangleView:

public class TriangleView extends View {
    private final Path path = new Path();
    private final Paint paint = new Paint();

    public TriangleView(Context context) {
        super(context);
        init(context, null, 0,0);
    }

    public TriangleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0,0);
    }

    public TriangleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr, 0);
    }

    public TriangleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs, defStyleAttr, defStyleRes);
    }

    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes){
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.argb(255, 100, 100, 255));
        setOutlineProvider(new OutlineProvider());
    }

    public void setPoints(float x1, float y1, float x2, float y2, float x3, float y3){
        path.reset();
        path.moveTo(x1, y1);
        path.lineTo(x2, y2);
        path.lineTo(x3, y3);
        path.close();
        postInvalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawPath(path, paint);
    }

    private static class OutlineProvider extends ViewOutlineProvider{
        @Override
        public void getOutline(View view, Outline outline) {
            Path path = ((TriangleView)view).path;
            outline.setConvexPath(path);
        }
    }
}

PentagonView:

public class PentagonView extends View {
    private final Path path = new Path();
    private final Paint paint = new Paint();

    public PentagonView(Context context) {
        super(context);
        init(context, null, 0,0);
    }

    public PentagonView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0,0);
    }

    public PentagonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr, 0);
    }

    public PentagonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs, defStyleAttr, defStyleRes);
    }

    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes){
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.argb(255, 150, 150, 255));
        setOutlineProvider(new OutlineProvider());
    }

    public void setPoints(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float x5, float y5){
        path.reset();
        path.moveTo(x1, y1);
        path.lineTo(x2, y2);
        path.lineTo(x3, y3);
        path.lineTo(x4, y4);
        path.lineTo(x5, y5);
        path.close();
        postInvalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawPath(path, paint);
    }

    private static class OutlineProvider extends ViewOutlineProvider {
        @Override
        public void getOutline(View view, Outline outline) {
            Path path = ((PentagonView)view).path;
            outline.setConvexPath(path);
        }
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <app.eccweizhi.concaveshadow.PentagonView
        android:id="@+id/pentagonView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:elevation="4dp" />

    <app.eccweizhi.concaveshadow.TriangleView
        android:id="@+id/triangle1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:elevation="4dp" />

    <app.eccweizhi.concaveshadow.TriangleView
        android:id="@+id/triangle2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:elevation="4dp" />

    <app.eccweizhi.concaveshadow.TriangleView
        android:id="@+id/triangle3"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:elevation="4dp" />

    <app.eccweizhi.concaveshadow.TriangleView
        android:id="@+id/triangle4"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:elevation="4dp" />

    <app.eccweizhi.concaveshadow.TriangleView
        android:id="@+id/triangle5"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:elevation="4dp" />

</FrameLayout>

I then use them like this

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        pentagonView.setPoints(
            520f,
            520f,
            640f,
            520f,
            677.0818f,
            634.1266f,
            580f,
            704.6608f,
            482.9182f,
            634.1266f
        )
        triangle1.setPoints(520f, 520f, 640f, 520f, 580f, 400f)
        triangle2.setPoints(640f, 520f, 677.0818f, 634.1266f, 777f, 520f)
        triangle3.setPoints(677.0818f, 634.1266f, 580f, 704.6608f, 697f, 750f)
        triangle4.setPoints(580f, 704.6608f, 482.9182f, 634.1266f, 440f, 750f)
        triangle5.setPoints(482.9182f, 634.1266f, 520f, 520f, 400f, 520f)
    }
}
Weizhi
  • 977
  • 6
  • 20
0

There is a new drawable called MaterialShapeDrawable in AndroidX package. Given a path, it can render shadow to both concave and convex shapes.

https://developer.android.com/reference/com/google/android/material/shape/MaterialShapeDrawable

This is how you would provide shadow to your concave shape WITHOUT MaterialShapeDrawable:

  • Create a new bitmap
  • Modify the bitmap ( draw star shape path on it using a new Canvas object )
  • Blur the bitmap so it actually looks like a shadow. ( Blurring should be done with RenderScript for performance reasons )
  • Draw the bitmap on views Canvas.
Nezih Yılmaz
  • 596
  • 3
  • 8