This semi-circle cutout can be achieved using a MaterialShapeDrawable
with a custom ShapeAppearanceModel
. This cut-out is a similar behaviour used in BottomAppBar
which has this semi circle into its Top Edge side. The BottomAppBar
uses a BottomAppBarTopEdgeTreatment in its ShapeAppearanceModel
for the Top side only. What you need is a similar behaviour but instead of Top Edge side to be on the Bottom Edge side.
In the below example i have used the same code used in BottomAppBarTopEdgeTreatment with the only difference that you can move the semi-circle to a specific x position using the cutoutEndSpacingDp
used in the constructor of CutoutCircleEdgeTreatment
.
1.Get the latest Material Design Library ('com.google.android.material:material:1.4.0'
) and add it into your grandle depedencies.
2.Create a subclass of EdgeTreatment
with name CutoutCircleEdgeTreatment
like below:
public class CutoutCircleEdgeTreatment extends EdgeTreatment {
private static final int ARC_QUARTER = 90;
private static final int ARC_HALF = 180;
private static final int ANGLE_UP = 270;
private static final int ANGLE_LEFT = 180;
private static final float ROUNDED_CORNER_FAB_OFFSET = 1.75f;
private float roundedCornerRadius;
private float fabMargin;
private float fabDiameter;
private float cradleVerticalOffset;
private float horizontalOffset;
private float fabCornerSize = -1f;
private float cutoutEndSpacing = 0;
public CutoutCircleEdgeTreatment(Resources res, float fabDiameterDp, float cutoutEndSpacingDp){
fabMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, res.getDisplayMetrics());
roundedCornerRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, res.getDisplayMetrics());
cradleVerticalOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, res.getDisplayMetrics());
fabDiameter = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, fabDiameterDp, res.getDisplayMetrics());
cutoutEndSpacing = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, cutoutEndSpacingDp, res.getDisplayMetrics());
}
@Override
public void getEdgePath(float length, float center, float interpolation, @NonNull ShapePath shapePath) {
horizontalOffset = -center + ((fabMargin * 2 + fabDiameter)/2) + cutoutEndSpacing;
//below is the same code used in BottomAppBarTopEdgeTreatment getEdgePath()
if (fabDiameter == 0) {
// There is no cutout to draw.
shapePath.lineTo(length, 0);
return;
}
float cradleDiameter = fabMargin * 2 + fabDiameter;
float cradleRadius = cradleDiameter / 2f;
float roundedCornerOffset = interpolation * roundedCornerRadius;
float middle = center + horizontalOffset;
// The center offset of the cutout tweens between the vertical offset when attached, and the
// cradleRadius as it becomes detached.
float verticalOffset =
interpolation * cradleVerticalOffset + (1 - interpolation) * cradleRadius;
float verticalOffsetRatio = verticalOffset / cradleRadius;
if (verticalOffsetRatio >= 1.0f) {
// Vertical offset is so high that there's no curve to draw in the edge, i.e., the fab is
// actually above the edge so just draw a straight line.
shapePath.lineTo(length, 0);
return; // Early exit.
}
// Calculate the path of the cutout by calculating the location of two adjacent circles. One
// circle is for the rounded corner. If the rounded corner circle radius is 0 the corner will
// not be rounded. The other circle is the cutout.
// Calculate the X distance between the center of the two adjacent circles using pythagorean
// theorem.
float cornerSize = fabCornerSize * interpolation;
boolean useCircleCutout = fabCornerSize == -1 || java.lang.Math.abs(fabCornerSize * 2f - fabDiameter) < .1f;
float arcOffset = 0;
if (!useCircleCutout) {
verticalOffset = 0;
arcOffset = ROUNDED_CORNER_FAB_OFFSET;
}
float distanceBetweenCenters = cradleRadius + roundedCornerOffset;
float distanceBetweenCentersSquared = distanceBetweenCenters * distanceBetweenCenters;
float distanceY = verticalOffset + roundedCornerOffset;
float distanceX = (float) Math.sqrt(distanceBetweenCentersSquared - (distanceY * distanceY));
// Calculate the x position of the rounded corner circles.
float leftRoundedCornerCircleX = middle - distanceX;
float rightRoundedCornerCircleX = middle + distanceX;
// Calculate the arc between the center of the two circles.
float cornerRadiusArcLength = (float) Math.toDegrees(Math.atan(distanceX / distanceY));
float cutoutArcOffset = (ARC_QUARTER - cornerRadiusArcLength) + arcOffset;
// Draw the starting line up to the left rounded corner.
shapePath.lineTo(/* x= */ leftRoundedCornerCircleX, /* y= */ 0);
// Draw the arc for the left rounded corner circle. The bounding box is the area around the
// circle's center which is at `(leftRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc(
/* left= */ leftRoundedCornerCircleX - roundedCornerOffset,
/* top= */ 0,
/* right= */ leftRoundedCornerCircleX + roundedCornerOffset,
/* bottom= */ roundedCornerOffset * 2,
/* startAngle= */ ANGLE_UP,
/* sweepAngle= */ cornerRadiusArcLength);
if (useCircleCutout) {
// Draw the cutout circle.
shapePath.addArc(
/* left= */ middle - cradleRadius,
/* top= */ -cradleRadius - verticalOffset,
/* right= */ middle + cradleRadius,
/* bottom= */ cradleRadius - verticalOffset,
/* startAngle= */ ANGLE_LEFT - cutoutArcOffset,
/* sweepAngle= */ cutoutArcOffset * 2 - ARC_HALF);
} else {
float cutoutDiameter = fabMargin + cornerSize * 2f;
shapePath.addArc(
/* left= */ middle - cradleRadius,
/* top= */ -(cornerSize + fabMargin),
/* right= */ middle - cradleRadius + cutoutDiameter,
/* bottom= */ (fabMargin + cornerSize),
/* startAngle= */ ANGLE_LEFT - cutoutArcOffset,
/* sweepAngle= */ (cutoutArcOffset * 2 - ARC_HALF) / 2f);
shapePath.lineTo(middle + cradleRadius - (cornerSize + fabMargin / 2f), /* y= */
(cornerSize + fabMargin));
shapePath.addArc(
/* left= */ middle + cradleRadius - (cornerSize * 2f + fabMargin),
/* top= */ -(cornerSize + fabMargin),
/* right= */ middle + cradleRadius,
/* bottom= */ (fabMargin + cornerSize),
/* startAngle= */ 90,
/* sweepAngle= */ -90 + cutoutArcOffset);
}
// Draw an arc for the right rounded corner circle. The bounding box is the area around the
// circle's center which is at `(rightRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc(
/* left= */ rightRoundedCornerCircleX - roundedCornerOffset,
/* top= */ 0,
/* right= */ rightRoundedCornerCircleX + roundedCornerOffset,
/* bottom= */ roundedCornerOffset * 2,
/* startAngle= */ ANGLE_UP - cornerRadiusArcLength,
/* sweepAngle= */ cornerRadiusArcLength);
// Draw the ending line after the right rounded corner.
shapePath.lineTo(/* x= */ length, /* y= */ 0);
}
}
From the constructor above you can pass the fabDiameterDp
which is the circle diameter in dps and the cutoutEndSpacingDp
which is the end spacing in dps needed to move the semi-circle to a specific x position from the end. Of course you can make any other modifications you want based on your needs.
3.You can use the above CutoutCircleEdgeTreatment
like below:
For Material Components like MaterialCardView etc. which they have already a MaterialShapeDrawable as a background:
MaterialCardView materialCardView = findViewById(R.id.materialCardView);
materialCardView.setShapeAppearanceModel(materialCardView.getShapeAppearanceModel()
.toBuilder()
.setBottomEdge(new CutoutCircleEdgeTreatment(getResources(), 80, 20))
.build());
Xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
android:background="#81afdc"
android:padding="20dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/materialCardView"
android:layout_width="match_parent"
android:layout_height="200dp"
app:cardBackgroundColor="@android:color/white"
app:cardCornerRadius="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-35dp"
android:layout_marginEnd="35dp"
app:backgroundTint="@android:color/holo_orange_dark"
app:elevation="3dp"
app:fabCustomSize="70dp"
app:layout_constraintEnd_toEndOf="@+id/materialCardView"
app:layout_constraintTop_toBottomOf="@+id/materialCardView"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@android:color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
For Non-Material Components like ConstraintLayout, RelativeLayout etc. which they don't have a MaterialShapeDrawable:
RelativeLayout relativeLayout = findViewById(R.id.relativeLayout);
ShapeAppearanceModel shapeAppearanceModel = new ShapeAppearanceModel()
.toBuilder()
.setBottomEdge(new CutoutCircleEdgeTreatment(getResources(), 80, 20))
.build();
MaterialShapeDrawable shapeDrawable = new MaterialShapeDrawable(shapeAppearanceModel);
shapeDrawable.setFillColor(ContextCompat.getColorStateList(this, android.R.color.white));
ViewCompat.setBackground(relativeLayout, shapeDrawable);
Xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
android:background="#81afdc"
android:padding="20dp">
<RelativeLayout
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-35dp"
android:layout_marginEnd="25dp"
app:backgroundTint="@android:color/holo_orange_dark"
app:elevation="3dp"
app:fabCustomSize="70dp"
app:layout_constraintEnd_toEndOf="@+id/relativeLayout"
app:layout_constraintTop_toBottomOf="@+id/relativeLayout"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@android:color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
Result:
