Lollipop elevation on concave outline - android
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?
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?
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)
}
}
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.
Related
how to make dynamic diagonal view list
i m adding diagonal cut layout to RecyclerView, but i m not getting expected result. my second view start with end of first view, and thats obvious. but what i want is that each view is join with each-other like this. My Output: and this is what i wanted: CutLayout.class: public class CutLayout extends FrameLayout { private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private Xfermode pdMode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR); private Path path = new Path(); public CutLayout(Context context) { super(context); } public CutLayout(Context context, AttributeSet attrs) { super(context, attrs); } public CutLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } #TargetApi(Build.VERSION_CODES.LOLLIPOP) public CutLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } #Override protected void dispatchDraw(Canvas canvas) { int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG); super.dispatchDraw(canvas); paint.setXfermode(pdMode); path.reset(); path.moveTo(0, getHeight()); path.lineTo(getWidth(), getHeight()); path.lineTo(getWidth(), getHeight() - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, getResources().getDisplayMetrics())); path.close(); canvas.drawPath(path, paint); canvas.restoreToCount(saveCount); paint.setXfermode(null); } } item.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" app:layout_behavior="#string/appbar_scrolling_view_behavior" tools:context=".SampleActivity"> <com.yarolegovich.slidingrootnav.sample.helper.CutLayout android:layout_width="match_parent" android:layout_height="200dp"> <ImageView android:scaleType="centerCrop" android:layout_width="match_parent" android:layout_height="wrap_content" android:src="#drawable/homebulding" /> </com.yarolegovich.slidingrootnav.sample.helper.CutLayout> </LinearLayout>
Change your CutLayout to have the same path on top like so: public class CutLayout extends FrameLayout { private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private Xfermode pdMode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR); private Path path = new Path(); public CutLayout(Context context) { super(context); } public CutLayout(Context context, AttributeSet attrs) { super(context, attrs); } public CutLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } #TargetApi(Build.VERSION_CODES.LOLLIPOP) public CutLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } #Override protected void dispatchDraw(Canvas canvas) { int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG); super.dispatchDraw(canvas); paint.setXfermode(pdMode); path.reset(); path.moveTo(0, 0); path.lineTo(getWidth(), 0); path.lineTo(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, getResources().getDisplayMetrics())); path.close(); canvas.drawPath(path, paint); path.reset(); path.moveTo(0, getHeight()); path.lineTo(getWidth(), getHeight()); path.lineTo(getWidth(), getHeight() - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, getResources().getDisplayMetrics())); path.close(); canvas.drawPath(path, paint); canvas.restoreToCount(saveCount); paint.setXfermode(null); } } Then all you have to do is to create an item decorator for your Recycler view that can accept a negative top margin public class NegativeTopItemDecorator extends RecyclerView.ItemDecoration{ private final int mTopMargin; public NegativeTopItemDecorator(int topMargin) { this.mTopMargin = topMargin; } #Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { outRect.top = mTopMargin; } } And add it to your RecyclerView int topMargin = - (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, getResources().getDisplayMetrics()); NegativeTopItemDecorator itemDecorator = new NegativeTopItemDecorator(topMargin); mRecyclerView.addItemDecoration(itemDecorator); This will give you something like the following output
onDraw on View extends ImageButton not work
I'm trying to draw a custom text on an ImageButton. I have overwritten the method onDraw(Canvas) but it didn't work. This is the code: public class CustomImageButton extends ImageButton { private static final String TAG = "CustomImageButton"; private String text = ""; private float textSize; private int textColor; private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); public CustomImageButton(Context context) { super(context); initialize(null); } public CustomImageButton(Context context, AttributeSet attrs) { super(context, attrs); initialize(attrs); } public CustomImageButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialize(attrs); } private void initialize(AttributeSet attrs) { if (attrs != null) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CustomImageButton); text = a.getString(R.styleable.CustomImageButton_text); textSize = a.getDimension(R.styleable.CustomImageButton_textSize, 20); textColor = a.getColor(R.styleable.CustomImageButton_textColor, getResources().getColor(R.color.white)); //setBackgroundColor(getResources().getColor(R.color.transparent)); a.recycle(); invalidate(); } } #Override protected void onDraw(Canvas canvas) { Typeface font = Typefaces.get(getContext(), "fonts/my-font.ttf"); paint.setColor(Color.WHITE); paint.setTypeface(font); paint.setTextSize(100); paint.setTextAlign(Paint.Align.CENTER); int xPos = canvas.getWidth() / 2; int yPos = (int) ((canvas.getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2)); canvas.drawText(text, xPos, yPos, paint); } } And XML Layout: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#OOOOOO" xmlns:app="http://schemas.android.com/apk/res-auto"> <com.my.app.CustomImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" app:text="Hello!" android:layout_alignParentBottom="true" android:layout_alignParentRight="true"/> . . . </RelativeLayout> I used setWillNotDraw(false) but it seems that does not work. I have verified that my custom view has a few correct dimensions.
Custom View disappears when aligned within a RelativeLayout
I have a custom view that simply draws a circle onto a canvas (so far). Using it within a RelativeLayout is fine until I tell it specifically where to be aligned within the RelativeLayout. i.e. as soon as I add something like: android:layout_centerInParent="true" it will no longer appear. The following is the relevant code: public class CircleProgressBar extends ProgressBar { int radius; float centerX; float centerY; Paint progressPaint; public CircleProgressBar(Context context) { super(context); init(context); } public CircleProgressBar(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public CircleProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context) { progressPaint = new Paint(); progressPaint.setColor(ContextCompat.getColor(context, R.color.colorAccent)); } private void init(Context context, AttributeSet attributeSet) { progressPaint = new Paint(); progressPaint.setColor(ContextCompat.getColor(context, R.color.colorAccent)); } #Override protected synchronized void onDraw(Canvas canvas) { radius = Math.max(getMeasuredWidth(), getWidth()) / 2; centerX = getLeft() + radius; centerY = getTop() + radius; canvas.drawCircle(centerX, centerY, radius, progressPaint); } } <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="#+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.tomtaila.circleprogressbar.CircleProgressBar android:layout_centerInParent="true" android:layout_width="100dp" android:layout_height="100dp"/> </RelativeLayout>
Try like this. This will fix your problem Remove "android:layout_centerInParent="true"" from "CircleProgressBar" and set "android:gravity="center"" to the "activity_main" RelativeLayout OR #Override protected synchronized void onDraw(Canvas canvas) { radius = Math.max(getMeasuredWidth(), getWidth()) / 2; centerX = radius; centerY = radius; canvas.drawCircle(centerX, centerY, radius, progressPaint); } While CircleProgressBar tag without centerInParent attribute, Its position is Top left (0,0). So in "onDraw" method you are using getLeft() return 0 and getTop() return 0. But While CircleProgressBar tag with centerInParent attribute, Its position is center of the parent (parent_width/2,parent_height/2). So In "onDraw" method getLeft() will return parent_width/2() (if parent width is 480, then it will return 240) and simmilarlly getTop() will return parent_height/2. Now centerX, centerY values become too big.
Create Custom View
I am trying to achieve the effect of creating a view with a custom shape(almost rectangular). Here is what i tried to do: public class CustomHeaderview extends View { public CustomHeaderview(Context context) { super(context); } public CustomHeaderview(Context context, AttributeSet attrs) { super(context, attrs); } public CustomHeaderview(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } Paint paint = new Paint(); #Override public void draw(Canvas canvas) { paint.setColor(Color.GRAY); paint.setStyle(Style.FILL); Path wallpath = new Path(); wallpath.reset(); // only needed when reusing this path for a new build wallpath.moveTo(100, 100); // used for first point wallpath.lineTo(100, 200); wallpath.lineTo(200, 200); wallpath.lineTo(150, 100); wallpath.lineTo(100, 100);// there is a setLastPoint action but i found it not to work as expected canvas.drawPath(wallpath, paint); super.draw(canvas); } } and the XML: <CustomHeaderview android:layout_width="152dp" android:layout_height="152dp" /> EDIT Thanks Dmitry, it works perfectly now!
You providing fancy coordinates for you rectangle: x1: 100 y1: 100 x2: 100 y2: 120 So, you're getting rectangle with 0 width, thats why it is invisible.
Draw text on ImageView doesn't work
I have extended class from ImageView and want some text to be drawn on it. This doesn't work, do you know why? Thank you. #Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int imgWidth = getMeasuredWidth(); int imgHeight = getMeasuredHeight(); float txtWidth = mTextPaint.measureText("your text"); int x = Math.round(imgWidth/2 - txtWidth/2); int y = imgHeight/2 - 6; // 6 is half of the text size canvas.drawText("your text", x, y, mTextPaint); } private void init(Context context, AttributeSet attrs, int defStyle) { mTextPaint = new Paint(); mTextPaint.setColor(android.R.color.black); mTextPaint.setTextSize(12); mTextPaint.setTextAlign(Paint.Align.LEFT);}
I ran your code, and immediately got a Lint error. In init() you are setting your mTextPaint to android.R.color.black. Because it's a static value, I could immediately see that the actual int value of that variable is 0x0106000c, which is almost completely transparent. You should use getResources().getColor(android.R.color.black) or plain ol' Color.BLACK. Note that a textSize of 12 is very, very small. This code shows 12 (albeit very small). public class MyImageView extends ImageView { public MyImageView(Context context, AttributeSet attributeSet, int defStyle) { super(context, attributeSet, defStyle); init(); } public MyImageView(Context context, AttributeSet attributeSet) { super(context, attributeSet); init(); } public MyImageView(Context context) { super(context); init(); } Paint mTextPaint; private void init() { mTextPaint = new Paint(); mTextPaint.setColor(Color.BLACK); mTextPaint.setTextSize(12); mTextPaint.setTextAlign(Paint.Align.LEFT); } #Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int imgWidth = getMeasuredWidth(); int imgHeight = getMeasuredHeight(); float txtWidth = mTextPaint.measureText("your text"); int x = Math.round(imgWidth/2 - txtWidth/2); int y = imgHeight/2 - 6; canvas.drawText("12", x, y, mTextPaint); } } xml: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.mytest.MyImageView android:layout_width="100dp" android:layout_height="100dp"/> </RelativeLayout> Copy/paste, if problem persist, start logging. Again, this code works, I see 12 on my screen.
You should be able to accomplish the desire effect without creating your own class. Just set the background of a TextView with an image drawable. See my example with a text in a speech bubble. <TextView android:id="#+id/textOverImage" android:background="#drawable/speech_bubble" android:text="#string/hello_world" android:gravity="center" android:layout_width="..." android:layout_height="..." />