1

How to make the bottom navigation view to a specific shape?

I'd like to have a bottom navigation view of this shape:

Shape of my bottom nav view

I have tried setting it as background of my bottom nav view as:

<com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/navigationBottomView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="@drawable/bg_nav_bar"
        app:itemHorizontalTranslationEnabled="true"
        app:itemIconTint="@drawable/bottom_bar_selector"
        app:itemTextColor="@drawable/bottom_bar_selector"
        app:labelVisibilityMode="labeled"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/nav_menu"/>

But it doesn't seem to work.

Any help will be appreciated. Thanks!

Sparsh Dutta
  • 2,450
  • 4
  • 27
  • 54

1 Answers1

1

The BottomNavigationView by default has a background of MaterialShapeDrawable so you can change its shape using the ShapeAppearanceModel by defining a custom TopEdge EdgeTreatment to draw the half-circle above the BottomNavigationView. To be able to draw something above the BottomNavigationView you need to have a parent which has the below attributes:

android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="35dp"

An Xml sample will be like the below:

<?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"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black">

    <RelativeLayout
        android:id="@+id/bottomNavigationViewParentRL"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:clipChildren="false"
        android:clipToPadding="false"
        android:paddingTop="35dp"
        android:background="@android:color/transparent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/bottomNavigationView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:backgroundTint="@color/white"
            app:elevation="2dp"
            app:labelVisibilityMode="labeled"
            app:itemIconSize="25dp"
            app:itemIconTint="@color/item_icon_tint_selector"
            app:itemTextColor="@color/item_text_color_selector"
            app:menu="@menu/bottom_nav_menu" />

    </RelativeLayout>

    <fragment
        android:id="@+id/nav_host_fragment_activity_main"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/bottomNavigationViewParentRL"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>

Then draw the shape like the below:

val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
val materialShapeDrawable = bottomNavigationView.getBackground() as MaterialShapeDrawable
materialShapeDrawable.shapeAppearanceModel = materialShapeDrawable.shapeAppearanceModel
    .toBuilder()
    .setTopEdge(CutoutCircleEdgeTreatment(resources, 70.toFloat(), 10.toFloat()))
    .build()

where CutoutCircleEdgeTreatment is a subclass of EdgeTreatment to draw the half-circle at the top which is similar code like the build-in BottomAppBarTopEdgeTreatment class which draws a semi-circular cutout from the top edge to bottom:

class CutoutCircleEdgeTreatment(res: Resources, circleDiameterDp: Float, circleLeftRightOffsetDp: Float) : EdgeTreatment() {

    private val fabDiameter: Float
    private val offset: Float

    init {
        fabDiameter = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleDiameterDp, res.getDisplayMetrics())
        offset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleLeftRightOffsetDp, res.getDisplayMetrics())
    }

    override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath) {
        if (fabDiameter == 0f) {
            // There is no cutout to draw.
            shapePath.lineTo(length, 0f)
            return
        }
        val fabMargin = 0f
        val cradleDiameter = fabMargin * 2 + fabDiameter
        val cradleRadius = cradleDiameter / 2f
        val roundedCornerRadius = 0f
        val roundedCornerOffset = interpolation * roundedCornerRadius
        val horizontalOffset = 0f
        val middle = center + horizontalOffset

        // The center offset of the cutout tweens between the vertical offset when attached, and the
        // cradleRadius as it becomes detached.
        val cradleVerticalOffset = 0f
        val verticalOffset =
            interpolation * cradleVerticalOffset + (1 - interpolation) * cradleRadius
        val 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, 0f)
            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.
        val fabCornerSize = -1f
        val cornerSize = fabCornerSize * interpolation
        val arcOffset = 0f
        val distanceBetweenCenters = cradleRadius + roundedCornerOffset
        val distanceBetweenCentersSquared = distanceBetweenCenters * distanceBetweenCenters
        val distanceY = verticalOffset + roundedCornerOffset
        val distanceX =
            Math.sqrt((distanceBetweenCentersSquared - distanceY * distanceY).toDouble())
                .toFloat()

        // Calculate the x position of the rounded corner circles.
        val leftRoundedCornerCircleX = middle - distanceX
        val rightRoundedCornerCircleX = middle + distanceX

        // Calculate the arc between the center of the two circles.
        val cornerRadiusArcLength =
            Math.toDegrees(Math.atan((distanceX / distanceY).toDouble())).toFloat()
        val cutoutArcOffset = ARC_QUARTER - cornerRadiusArcLength + arcOffset

        // Draw the starting line up to the left rounded corner.
        shapePath.lineTo( /* x= */leftRoundedCornerCircleX, 0f)

        // 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, 0f,  /* right= */
            leftRoundedCornerCircleX + roundedCornerOffset,  /* bottom= */
            roundedCornerOffset * 2,  /* startAngle= */
            ANGLE_UP.toFloat(),  /* sweepAngle= */
            cornerRadiusArcLength
        )

        // Draw the cutout circle.
        shapePath.addArc( /* left= */
            middle - (cradleRadius + offset),  /* top= */
            -cradleRadius - verticalOffset,  /* right= */
            middle + (cradleRadius + offset),  /* bottom= */
            cradleRadius - verticalOffset,  /* startAngle= */
            ANGLE_LEFT - cutoutArcOffset,  /* sweepAngle= */
            cutoutArcOffset * 2 + ARC_HALF
        )

        // 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, 0f,  /* 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, 0f)
    }

    companion object {
        private const val ARC_QUARTER = 90
        private const val ARC_HALF = 180
        private const val ANGLE_UP = 270
        private const val ANGLE_LEFT = 180
    }
}

From the above CutoutCircleEdgeTreatment constructor you can pass the circleDiameterDp which is the circle diameter in dp value (in the above example is set to 70dp so the parent RelativeLayout it should have paddingTop equal to the radius of the Circle which is 70/2 = 35dp) and the circleLeftRightOffsetDp is used to draw the circle with a left/right offset in dp value. Of Course you can modify further the code based on your needs.

Result:

navigation_bar

To overlap the semi circle with the fragment hosted

To make the semi circle overlap with the fragment hosted you have to change the order of fragment:nav_host_fragment_activity_main with the RelativeLayout bottomNavigationViewParentRL like in the below sample:

<?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"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black">

    <fragment
        android:id="@+id/nav_host_fragment_activity_main"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/bottomNavigationViewParentRL"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

    <RelativeLayout
        android:id="@+id/bottomNavigationViewParentRL"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:clipChildren="false"
        android:clipToPadding="false"
        android:paddingTop="35dp"
        android:background="@android:color/transparent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/bottomNavigationView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:backgroundTint="@color/white"
            app:elevation="2dp"
            app:labelVisibilityMode="labeled"
            app:itemIconSize="25dp"
            app:itemIconTint="@color/item_icon_tint_selector"
            app:itemTextColor="@color/item_text_color_selector"
            app:menu="@menu/bottom_nav_menu" />

    </RelativeLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

And also give in each of your fragments some bottom margin with the same height of the navigation bar to start at the point of semi circle like in the below sample:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    android:background="@android:color/transparent"
    tools:context=".ui.dashboard.DashboardFragment">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_green_dark"
        android:layout_marginBottom="55dp">

        <TextView
            android:id="@+id/text_dashboard"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="25dp"
            android:textAlignment="center"
            android:textColor="@color/black"
            android:text="This is dashboard Fragment"
            android:textSize="20sp"
            android:layout_alignParentBottom="true"/>

    </RelativeLayout>

</RelativeLayout>

Result:

overlap_navigation_bar

Another variation of CutoutCircleEdgeTreatment

class CutoutCircleEdgeTreatment(res: Resources, circleDiameterDp: Float, circleLeftRightOffsetDp: Float) : EdgeTreatment() {

    private val fabDiameter: Float
    private val offset: Float

    init {
        fabDiameter = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleDiameterDp, res.getDisplayMetrics())
        offset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleLeftRightOffsetDp, res.getDisplayMetrics())
    }

    override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath) {
        if (fabDiameter == 0f) {
            // There is no cutout to draw.
            shapePath.lineTo(length, 0f)
            return
        }
        val fabMargin = 0f
        val cradleDiameter = fabMargin * 2 + fabDiameter
        val cradleRadius = cradleDiameter / 2f
        val roundedCornerRadius = 0f
        val roundedCornerOffset = interpolation * roundedCornerRadius
        val horizontalOffset = 0f
        val middle = center + horizontalOffset

        // The center offset of the cutout tweens between the vertical offset when attached, and the
        // cradleRadius as it becomes detached.
        val cradleVerticalOffset = 0f
        val verticalOffset =
            interpolation * cradleVerticalOffset + (1 - interpolation) * cradleRadius
        val 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, 0f)
            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.
        val fabCornerSize = -1f
        val cornerSize = fabCornerSize * interpolation
        val arcOffset = 0f
        val distanceBetweenCenters = cradleRadius + roundedCornerOffset
        val distanceBetweenCentersSquared = distanceBetweenCenters * distanceBetweenCenters
        val distanceY = verticalOffset + roundedCornerOffset
        val distanceX =
            Math.sqrt((distanceBetweenCentersSquared - distanceY * distanceY).toDouble())
                .toFloat()

        // Calculate the x position of the rounded corner circles.
        val leftRoundedCornerCircleX = middle - distanceX
        val rightRoundedCornerCircleX = middle + distanceX

        // Calculate the arc between the center of the two circles.
        val cornerRadiusArcLength =
            Math.toDegrees(Math.atan((distanceX / distanceY).toDouble())).toFloat()
        val cutoutArcOffset = ARC_QUARTER - cornerRadiusArcLength + arcOffset

        // Draw the cutout circle.
        shapePath.addArc( /* left= */
            middle - (cradleRadius + offset),  /* top= */
            -cradleRadius - verticalOffset,  /* right= */
            middle + (cradleRadius + offset),  /* bottom= */
            (cradleRadius - verticalOffset) * 2,  /* startAngle= */
            ANGLE_LEFT + 20.0f,  /* sweepAngle= */
            ARC_HALF - 40.0f
        )
    }

    companion object {
        private const val ARC_QUARTER = 90
        private const val ARC_HALF = 180
        private const val ANGLE_UP = 270
        private const val ANGLE_LEFT = 180
    }
}

Usage:

val materialShapeDrawable = bottomNavigationView.getBackground() as MaterialShapeDrawable
materialShapeDrawable.shapeAppearanceModel = materialShapeDrawable.shapeAppearanceModel
    .toBuilder()
    .setTopEdge(CutoutCircleEdgeTreatment(resources, 70.toFloat(), 20.toFloat()))
    .build()

Result:

navigation_bar_change_v2

MariosP
  • 8,300
  • 1
  • 9
  • 30
  • Thanks a lot, but my requirement is that the semi circle of the bottom nav should overlap with the fragment hosted. In your solution the bottom of fragment ends ABOVE the semi circle of bottom nav, so the semi circle is not overlapping with fragment screen. I need the bottom of fragment to end at the point where the semi circle starts, such that the semi circle overlaps with fragment screen. Hope I was clear. – Sparsh Dutta Aug 16 '22 at 11:48
  • @SparshDutta check updated answer of how to achieve this requirement. – MariosP Aug 24 '22 at 18:05
  • Thanks @MariosP. Wanted to know if the 'semi circle' in the bottom nav view can be made to look exactly like the way it is in the image I have put in my question. – Sparsh Dutta Aug 25 '22 at 08:23
  • @SparshDutta i have added another variation of the CutoutCircleEdgeTreatment which i think its more close to your desired result but you can make further changes if needed. Also you can accept my answer if suit your case. Cheers. – MariosP Aug 25 '22 at 12:46
  • it would really be helpful if you could also suggest a way to make the center icon (Dashboard icon in your example) long so that it fills in the semi-circle. This is my question related to it: https://stackoverflow.com/questions/73384717/change-size-of-only-one-icon-of-bottom-navigation-view-android?noredirect=1#comment129602407_73384717 Requesting you to answer it. – Sparsh Dutta Aug 26 '22 at 09:42
  • @SparshDutta check my answer there. – MariosP Aug 26 '22 at 14:55
  • sorry just noticed that the left half of 2nd variation of semi circle is not the same as the right half. Can the right half be made as steep as the left half? – Sparsh Dutta Sep 21 '22 at 09:31
  • @SparshDutta i can't see any difference between the left and right half circles but you can change anything you want using the shapePath.addArc( float left, float top, float right, float bottom, float startAngle, float sweepAngle) in CutoutCircleEdgeTreatment. If you need a bit offset from the right half circle eg 20dp change the right parameter eg : middle + (cradleRadius + offset) - dpToPx(20) or if you need to change the sweepAngle a bit you can try something like ARC_HALF - 38.0f (38 is an angle). – MariosP Sep 23 '22 at 09:15