androidandroid-custom-viewandroid-5.0-lollipopmaterial-designandroid-elevation

Lollipop elevation on concave outline


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?


Solution

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